local Array = require('Array') local EditorLane = require('EditorLane') ---@class EditorTabLanes ---@field editor EditorMain local EditorTabLanes = class('EditorTabLanes') ---@param editor EditorMain ---@return EditorTabLanes function EditorTabLanes:initialize(editor) self.editor = editor self.oldSelection = nil end local minItemHeight = vec2(10, 98) ---@param self EditorTabLanes ---@param filename string local function loadFromOBJ(self, filename) local vertices = {} local nextName = nil local lines = self.editor.lanesList lines:clear() for _, line in ipairs(io.load(filename, ''):split('\n', nil, true, true)) do local b = line:byte(1) if b == 118 then -- v local x, y, z = line:numbers(3) vertices[#vertices + 1] = vec3(x, z, -y) elseif b == 111 then -- o nextName = line:sub(3):trim() elseif b == 108 then -- l local points = table.map({ line:numbers() }, function (i, _, data) return data[i] end, vertices) if #points > 1 then lines:push(EditorLane{ points = points, name = nextName or error('No next name found') }) end nextName = nil end end self.editor:onChange() end function EditorTabLanes:doUI() if ui.button('Replace lines with OBJ file', vec2(ui.availableSpaceX(), 0)) then os.openFileDialog({ title = 'Open', defaultFolder = ac.getFolder(ac.FolderID.ContentTracks)..'/'..ac.getTrackID(), fileTypes = { { name = 'OBJ Models', mask = '*.obj' } }, }, function (err, filename) if not err and filename then loadFromOBJ(self, filename) end end) end ui.offsetCursorY(12) ui.header('Created lanes') ui.childWindow('##lanesList', vec2(0, -50), function () local lanesList = self.editor.lanesList local lanesLen = #lanesList if lanesLen == 0 then ui.text('No lanes were created yet.') end local scroll = self.editor.selectedLane ~= self.oldSelection if scroll then self.oldSelection = self.editor.selectedLane if self.oldSelection and not ui.mouseBusy() then ui.setScrollY(minItemHeight.y * (lanesList:indexOf(self.oldSelection) - 2)) end end local toRemoveIndex = 0 for i = 1, lanesLen do if self:laneItem(lanesList[i]) then toRemoveIndex = i end end if toRemoveIndex ~= 0 then local toRemove = lanesList[toRemoveIndex] if self.editor.selectedLane == toRemove then self.editor.selectedLane = nil end lanesList:removeAt(toRemoveIndex) self.editor:onChange() ui.toast(ui.Icons.Delete, 'Traffic lane “'..toRemove.name..'” removed', function () lanesList:insert(math.min(toRemoveIndex, #lanesList + 1), toRemove) self.editor:onChange() end) end end) ui.offsetCursorY(12) self.editor:laneWorldEditor() end local _slider = refnumber() local _exportWidth = refnumber(4) local _verticalOffset = refnumber(0.5) local _cloneOffset = refnumber(0) ---@param lane EditorLane function EditorTabLanes:laneItem(lane) if not ui.areaVisible(minItemHeight) then -- simple trick for super-fast lists of items ui.offsetCursorY(minItemHeight.y) return end local c = ui.getCursorY() local toRemove = false ui.pushID(lane.uniqueID) if ui.checkbox('##' .. lane.name, lane == self.editor.selectedLane) then self.editor:select(lane ~= self.editor.selectedLane and lane or nil) end ui.sameLine() ui.setNextItemWidth(ui.availableSpaceX() - 40) local newName, _c = ui.inputText('##name', lane.name) if _c then lane.name, _c = newName, true end ui.sameLine(ui.availableSpaceX() - 32) ui.button('…', vec2(32, 0)) ui.itemPopup('cfg', ui.MouseButton.Left, function () if ui.checkbox("Loop", lane.loop) then lane.loop, _c = not lane.loop, true end if ui.selectable('Reverse direction', false, 1) then lane.points:reverse(lane.points) _c = true end ui.separator() if ui.selectable('Clone with offset…') then ui.modalDialog('Clone lane', function () ui.slider('##offset', _cloneOffset, -20, 20, 'Offset: %.1f m') ui.newLine() ui.offsetCursorY(4) if ui.modernButton('OK', vec2(ui.availableSpaceX() / 2 - 4, 40), ui.ButtonFlags.Confirm, ui.Icons.Confirm) or ui.keyPressed(ui.Key.Enter) then self.editor:cloneLane(lane, _cloneOffset.value) return true end ui.sameLine(0, 8) return ui.modernButton('Cancel', vec2(-0.1, 40), ui.ButtonFlags.Cancel, ui.Icons.Cancel) end, true) end toRemove = ui.selectable('Delete lane') ui.separator() if ui.selectable('Export as AI spline…') then ui.modalDialog('Export AI spline', function () ui.slider('##width', _exportWidth, 2, 20, 'Lane width: %.1f m') ui.slider('##offset', _verticalOffset, -2, 2, 'Vertical offset: %.2f m') ui.newLine() ui.offsetCursorY(4) if ui.modernButton('OK', vec2(ui.availableSpaceX() / 2 - 4, 40), ui.ButtonFlags.Confirm, ui.Icons.Confirm) or ui.keyPressed(ui.Key.Enter) then os.saveFileDialog({ defaultFolder = ac.getFolder(ac.FolderID.ContentTracks), fileTypes = {{name = 'AI splines', mask = '*.ai'}}, fileName = 'fast_lane', defaultExtension = '.ai', addAllFilesFileType = true, flags = bit.bor(os.DialogFlags.PathMustExist, os.DialogFlags.OverwritePrompt, os.DialogFlags.NoReadonlyReturn) }, function (err, filename) if filename then require('EditorLaneExporter')(lane, filename, _exportWidth.value, _verticalOffset.value) end end) return true end ui.sameLine(0, 8) return ui.modernButton('Cancel', vec2(-0.1, 40), ui.ButtonFlags.Cancel, ui.Icons.Cancel) end, true) end end) ui.pushFont(ui.Font.Small) ui.offsetCursorX(30) ui.setNextItemWidth(120) local r = self.editor.rules.laneRoles[lane.role or 1] ui.combo('##role', string.format('Role: %s', r and r.name or tostring(lane.role or 1)), ui.ComboFlags.None, function () for i, v in ipairs(self.editor.rules.laneRoles) do if ui.selectable(v.name, v == r) then lane.role, _c = i, true end if ui.itemHovered() then ui.setTooltip('Priority: '..tostring(v.priority)) end end end) ui.sameLine(0, 4) ui.setNextItemWidth(ui.availableSpaceX()) if ui.slider('##po', _slider:set(lane.priorityOffset), -10, 10, 'Priority offset: %.0f') then lane.priorityOffset, _c = _slider.value, true end local _p = lane.params ui.offsetCursorX(30) if ui.checkbox('Allow U-turns', _p.allowUTurns == true) then _p.allowUTurns, _c = not _p.allowUTurns, true end if ui.itemHovered() then ui.setTooltip('Allow turns to nearby lanes running in different directions') end ui.sameLine() if ui.checkbox('Allow lane changes', _p.allowLaneChanges ~= false) then _p.allowLaneChanges, _c = not _p.allowLaneChanges, true end if ui.itemHovered() then ui.setTooltip('Allow changes to nearby lanes running in similar direction') end ui.offsetCursorX(30) ui.text(string.format('Length: %.1f m, points: %d%s', lane.length, #lane.points, lane.loop and ', loop' or '')) ui.popFont() ui.popID() ui.offsetCursorY(8) if _c then lane:recalculate() self.editor.onChange() end if ui.getCursorY() - c ~= minItemHeight.y then ac.debug('Correct area item height', ui.getCursorY() - c) end return toRemove end return class.emmy(EditorTabLanes, EditorTabLanes.initialize)