Download CommonLua/Editor/XEditor/XAreaCopyTool.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 33.5 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/XEditor/XAreaCopyTool.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Editor/XEditor/XAreaCopyTool.lua
-
curl -L -o XAreaCopyTool.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/XEditor/XAreaCopyTool.lua
33.5 kB
| if FirstLoad then | |
| g_TerrainAreaMeshes = {} | |
| g_AreaUndoQueue = false -- a separate undo queue, active while the area copy tool is active | |
| LocalStorage.XAreaCopyTool = LocalStorage.XAreaCopyTool or {} | |
| AreaCopyBrushGrid = false | |
| end | |
| local snap_size = Max(const.SlabSizeX or 0, const.HeightTileSize, const.TypeTileSize) | |
| local function SnapPt(pt) | |
| local x, y = pt:xy() | |
| return point(x / snap_size * snap_size, y / snap_size * snap_size) | |
| end | |
| DefineClass.XAreaCopyTool = { | |
| __parents = { "XEditorTool", "XMapGridAreaBrush" }, | |
| properties = { | |
| { id = "DrawingMode", name = "Drawing Mode", editor = "text_picker", name_on_top = true, | |
| max_rows = 2, default = "Box areas", items = { "Box areas", "Brush" }, | |
| }, | |
| -- Overridden brush properties | |
| { id = "Size", editor = "number", default = 3 * guim, scale = "m", min = const.HeightTileSize, | |
| max = 300 * guim, step = guim / 10, slider = true, persisted_setting = true, auto_select_all = true, | |
| sort_order = -1, exponent = 3, no_edit = function(self) return self.DrawingMode ~= "Brush" end | |
| }, | |
| { id = "TerrainDebugAlphaPerc", name = "Opacity", editor = "number", | |
| default = 50, min = 0, max = 100, slider = true, no_edit = function(self) return self.DrawingMode ~= "Brush" end | |
| }, | |
| { id = "WriteValue", name = "Value", editor = "texture_picker", default = 1, | |
| thumb_width = 101, thumb_height = 35, small_font = true, items = function(self) return self:GetGridPaletteItems() end, | |
| max_rows = 2, no_edit = function(self) return self.DrawingMode ~= "Brush" end | |
| }, | |
| }, | |
| ToolTitle = "Copy terrain & objects", | |
| ToolSection = "Misc", | |
| Description = { "Copies an entire area of a map.\n\nDrag to define selection areas, then\nuse <style GedHighlight>Ctrl-C</style> to copy and <style GedHighlight>Ctrl-V</style> twice to paste." }, | |
| ActionIcon = "CommonAssets/UI/Editor/Tools/EnrichTerrain.tga", | |
| ActionShortcut = "O", | |
| ActionSortKey = "5", | |
| UsesCodeRenderables = true, | |
| GridName = "AreaCopyBrushGrid", | |
| GridTileSize = const.SlabSizeX or const.HeightTileSize, | |
| brush_area_boxes = false, | |
| old_undo = false, | |
| start_pos = false, | |
| operation = false, | |
| current_box = false, | |
| highlighted_objset = false, | |
| drag_area = false, | |
| drag_helper_id = false, | |
| filter_roofs = false, | |
| filter_floor = false, | |
| } | |
| function XAreaCopyTool:Init() | |
| -- make areas visible and highlight selected objects | |
| Collection.UnlockAll() | |
| for _, a in ipairs(self:GetAreas()) do | |
| a:SetVisible(true) | |
| a:Setbox(a.box) -- update according to current terrain height | |
| end | |
| -- set default roof visibility settings | |
| self.filter_floor = LocalStorage.FilteredCategories["HideFloor"] | |
| self.filter_roofs = LocalStorage.FilteredCategories["Roofs"] | |
| LocalStorage.FilteredCategories["HideFloor"] = 0 | |
| LocalStorage.FilteredCategories["Roofs"] = true | |
| XEditorFilters:UpdateHiddenRoofsAndFloors() | |
| XEditorFilters:SuspendHighlights() | |
| -- highlight objects; this makes all non-highlighted objects have gofWhiteColored | |
| local objset = {} | |
| MapGet(true, function(obj) objset[obj] = true end) | |
| self.highlighted_objset = objset | |
| self:UpdateHighlights(true) | |
| -- replace undo queue | |
| self.old_undo = XEditorUndo | |
| g_AreaUndoQueue = g_AreaUndoQueue or XEditorUndoQueue:new() | |
| XEditorUndo = g_AreaUndoQueue | |
| XEditorUpdateToolbars() | |
| -- brush mode | |
| self.brush_area_boxes = {} | |
| end | |
| function XAreaCopyTool:Done() | |
| -- make areas invisible, remove highlights | |
| for _, a in ipairs(self:GetAreas()) do | |
| a:SetVisible(false) | |
| end | |
| MapGet(true, function(obj) obj:ClearGameFlags(const.gofWhiteColored) end) | |
| self.highlighted_objset = false | |
| -- restore filters | |
| LocalStorage.FilteredCategories["HideFloor"] = self.filter_floor | |
| LocalStorage.FilteredCategories["Roofs"] = self.filter_roofs | |
| XEditorFilters:UpdateHiddenRoofsAndFloors() | |
| XEditorFilters:ResumeHighlights() | |
| -- restore undo queue | |
| XEditorUndo = self.old_undo | |
| if GetDialog("XEditor") then | |
| XEditorUpdateToolbars() | |
| end | |
| -- free allocated grids | |
| if self.Grid then | |
| self.Grid:free() | |
| end | |
| -- delete brush areas | |
| for _, a in ipairs(self.brush_area_boxes) do | |
| a:delete() | |
| end | |
| end | |
| function OnMsg.OnMapPatchBegin() | |
| local editor_tool = XEditorGetCurrentTool() | |
| if IsKindOf(editor_tool, "XAreaCopyTool") then | |
| XEditorUndo = editor_tool.old_undo | |
| end | |
| end | |
| function OnMsg.OnMapPatchEnd() | |
| if IsKindOf(XEditorGetCurrentTool(), "XAreaCopyTool") then | |
| XEditorUndo = g_AreaUndoQueue | |
| end | |
| end | |
| function XAreaCopyTool:CreateBrushGrid() | |
| if self.Grid then | |
| return self.Grid | |
| end | |
| local map_data = mapdata or _G.mapdata | |
| local map_size = point(map_data.Width - 1, map_data.Height - 1) * const.HeightTileSize | |
| local width, height = (map_size / self.GridTileSize):xy() | |
| -- brush grid to be used by XMapGridAreaBrush | |
| self.Grid = NewHierarchicalGrid(width, height, 64, 1) -- patch_size = 64 | |
| return self.Grid | |
| end | |
| function XAreaCopyTool:OnEditorSetProperty(prop_id, old_value, ged) | |
| if prop_id == "DrawingMode" then | |
| if self.DrawingMode == "Brush" then | |
| -- Hide previously created area meshes | |
| for _, a in ipairs(g_TerrainAreaMeshes) do | |
| a:SetVisible(false) | |
| end | |
| self.WriteValue = 1 -- select the "Copy Area" brush value | |
| -- Show debug overlay | |
| hr.TerrainDebugDraw = 1 | |
| -- Create the brush cursor | |
| self:CreateCursor() | |
| return XMapGridAreaBrush.OnEditorSetProperty(prop_id, old_value, ged) | |
| elseif self.DrawingMode == "Box areas" then | |
| -- Show previously create area meshes | |
| for _, a in ipairs(g_TerrainAreaMeshes) do | |
| a:SetVisible(true) | |
| end | |
| -- Delete existing area boxes | |
| for _, a in ipairs(self.brush_area_boxes) do | |
| a:delete() | |
| end | |
| -- Hide debug overlay | |
| hr.TerrainDebugDraw = 0 | |
| -- Destroy the brush cursor | |
| self:DestroyCursor() | |
| end | |
| end | |
| end | |
| function XAreaCopyTool:GetGridPaletteItems() | |
| local white = "CommonAssets/System/white.dds" | |
| local items = {} | |
| table.insert(items, { text = "Blank", value = 0, image = white, color = RGB(0, 0, 0) }) | |
| table.insert(items, { text = "Copy Area", value = 1, image = white, color = RGB(10, 10, 150) }) | |
| return items | |
| end | |
| function XAreaCopyTool:GetPalette() | |
| local palette = { | |
| [0] = RGB(0, 0, 0), | |
| [1] = RGB(10, 10, 150) | |
| } | |
| return palette | |
| end | |
| function XAreaCopyTool:OnCursorCreate(cursor_mesh) | |
| -- Destroy the brush cursor if we're not in brush mode | |
| if self.DrawingMode ~= "Brush" then | |
| self:DestroyCursor() | |
| end | |
| end | |
| function XAreaCopyTool:GetAreas() | |
| local area_list | |
| if self.DrawingMode == "Box areas" then | |
| area_list = g_TerrainAreaMeshes | |
| elseif self.DrawingMode == "Brush" then | |
| area_list = self.brush_area_boxes | |
| end | |
| for i = #area_list, 1, -1 do | |
| local area = area_list[i] | |
| if not IsValid(area) or area.box:IsEmpty() then | |
| table.remove(area_list, i) | |
| end | |
| end | |
| return area_list | |
| end | |
| function XAreaCopyTool:UpdateBrushAreaBoxes() | |
| if self.DrawingMode ~= "Brush" then return end | |
| -- Delete existing area boxes | |
| for _, a in ipairs(self.brush_area_boxes) do | |
| a:delete() | |
| end | |
| -- Create areas on the non-zero boxes of the brush grid | |
| local non_zero_boxes = editor.GetNonZeroInvalidationBoxes(self.Grid) | |
| for idx, bx in ipairs(non_zero_boxes) do | |
| local area = XEditableTerrainAreaMesh:new() | |
| local no_mesh = self.DrawingMode == "Brush" | |
| area:Setbox(bx * self.GridTileSize, "force_setpos", no_mesh) | |
| self.brush_area_boxes[idx] = area | |
| end | |
| self:UpdateHighlights(true) | |
| end | |
| function CanSelectWithMaskGrid(obj) | |
| if not obj then return CanSelect(obj) end | |
| if obj and mask_grid and mask_grid_tile and obj:IsValidPos() then | |
| local pos = obj:GetPos() | |
| if mask_grid:get(pos / mask_grid_tile) then | |
| return CanSelect(obj) | |
| end | |
| end | |
| return false | |
| end | |
| function XAreaCopyTool:GetBrushObjectSelector() | |
| local CanSelectWithMaskGrid = function(obj) | |
| if not obj then return CanSelect(obj) end | |
| if obj and self.Grid and self.GridTileSize and obj:IsValidPos() then | |
| local pos = obj:GetPos() | |
| if self.Grid:get(pos / self.GridTileSize) > 0 then | |
| return CanSelect(obj) | |
| end | |
| end | |
| return false | |
| end | |
| return CanSelectWithMaskGrid | |
| end | |
| function XAreaCopyTool:GetObjects(box_list) | |
| local selector_fn = self.DrawingMode == "Brush" and self:GetBrushObjectSelector() or CanSelect | |
| local objset = {} | |
| for _, b in ipairs(box_list) do | |
| b = IsKindOf(b, "XTerrainAreaMesh") and b.box or b | |
| for _, obj in ipairs(MapGet(b, "attached", false, selector_fn)) do | |
| objset[obj] = obj:GetGameFlags(const.gofPermanent) ~= 0 and not IsKindOf(obj, "XTerrainAreaMesh") or nil | |
| end | |
| end | |
| return XEditorPropagateParentAndChildObjects(table.keys(objset)) | |
| end | |
| function XAreaCopyTool:UpdateHighlights(highlight) | |
| PauseInfiniteLoopDetection("XAreaCopyTool:UpdateHighlights") | |
| local new = highlight and self:GetObjects(self:GetAreas()) or empty_table | |
| local old_set = self.highlighted_objset or empty_table | |
| local new_set = {} | |
| for _, obj in ipairs(new) do | |
| if not old_set[obj] then | |
| obj:ClearHierarchyGameFlags(const.gofWhiteColored) | |
| else | |
| old_set[obj] = nil | |
| end | |
| new_set[obj] = true | |
| end | |
| for obj in pairs(old_set) do | |
| obj:SetHierarchyGameFlags(const.gofWhiteColored) | |
| end | |
| self.highlighted_objset = new_set | |
| ResumeInfiniteLoopDetection("XAreaCopyTool:UpdateHighlights") | |
| end | |
| function XAreaCopyTool:EndDraw(pt1, pt2, invalid_box) | |
| XMapGridAreaBrush.EndDraw(self, pt1, pt2, invalid_box) | |
| self:UpdateBrushAreaBoxes() | |
| end | |
| function XAreaCopyTool:OnMouseButtonDown(pt, button) | |
| if self.DrawingMode == "Box areas" then | |
| if button == "L" then | |
| self.desktop:SetMouseCapture(self) | |
| self.start_pos = SnapPt(GetTerrainCursor()) | |
| -- are we starting a drag to move/resize an area? | |
| for _, a in ipairs(self:GetAreas()) do | |
| local helper_id = a:UpdateHelpers(pt) | |
| if helper_id then | |
| self.operation = "movesize" | |
| self.drag_area = a | |
| self.drag_helper_id = helper_id | |
| XEditorUndo:BeginOp{ name = "Moves/sized area", objects = { self.drag_area } } | |
| self.drag_area:DragStart(self.drag_helper_id, self.start_pos) | |
| return "break" | |
| end | |
| end | |
| self.operation = "place" | |
| self.current_box = XEditableTerrainAreaMesh:new() | |
| g_TerrainAreaMeshes[#g_TerrainAreaMeshes + 1] = self.current_box | |
| return "break" | |
| end | |
| if button == "R" then | |
| for _, a in ipairs(self:GetAreas()) do | |
| a:delete() | |
| end | |
| self:UpdateHighlights(true) | |
| return "break" | |
| end | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnMouseButtonDown(self, pt, button) | |
| end | |
| end | |
| local function MinMaxPtXY(f, p1, p2) | |
| return point(f(p1:x(), p2:x()), f(p1:y(), p2:y())) | |
| end | |
| function XAreaCopyTool:OnMousePos(pt) | |
| if self.DrawingMode == "Box areas" then | |
| XEditorRemoveFocusFromToolbars() | |
| if self.operation == "place" then | |
| local pt1, pt2 = self.start_pos, SnapPt(GetTerrainCursor()) | |
| local new_box = box(MinMaxPtXY(Min, pt1, pt2), MinMaxPtXY(Max, pt1, pt2) + point(snap_size, snap_size)) | |
| local old_box = self.current_box.box | |
| local no_mesh = self.DrawingMode == "Brush" | |
| self.current_box:Setbox(new_box, "force_setpos", no_mesh) | |
| self:UpdateHighlights(true) | |
| return "break" | |
| end | |
| if self.operation == "movesize" then | |
| self.drag_area:DragMove(self.drag_helper_id, SnapPt(GetTerrainCursor())) | |
| self:UpdateHighlights(true) | |
| return "break" | |
| end | |
| local areas = self:GetAreas() | |
| for _, a in ipairs(areas) do | |
| a:UpdateHelpers(pt) | |
| end | |
| local hovered | |
| for _, a in ipairs(areas) do | |
| a:UpdateHover(hovered) | |
| hovered = hovered or a.hovered | |
| end | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnMousePos(self, pt) | |
| end | |
| end | |
| function XAreaCopyTool:OnMouseButtonUp(pt, button) | |
| if self.DrawingMode == "Box areas" then | |
| if self.operation then | |
| self.desktop:SetMouseCapture() | |
| return "break" | |
| end | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnMouseButtonUp(self, pt, button) | |
| end | |
| end | |
| function XAreaCopyTool:OnCaptureLost() | |
| if self.DrawingMode == "Box areas" then | |
| if self.operation == "place" then | |
| XEditorUndo:BeginOp{ name = "Added area" } | |
| XEditorUndo:EndOp{ self.current_box } | |
| end | |
| if self.operation == "movesize" then | |
| XEditorUndo:EndOp{ self.drag_area } | |
| end | |
| self.start_pos = nil | |
| self.operation = nil | |
| self.current_box = nil | |
| self.drag_area = nil | |
| self.drag_helper_id = nil | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnCaptureLost(self) | |
| end | |
| end | |
| function XAreaCopyTool:OnShortcut(shortcut, source, ...) | |
| -- don't change tool modes, allow undo, etc. while in the process of dragging | |
| if terminal.desktop:GetMouseCapture() and shortcut ~= "Ctrl-F1" and shortcut ~= "Escape" then | |
| return "break" | |
| end | |
| if shortcut == "Ctrl-C" then | |
| ExecuteWithStatusUI("Copying terrain & objects...", function() self:CopyToClipboard() end) | |
| return "break" | |
| end | |
| if self.DrawingMode == "Box areas" then | |
| if shortcut == "Delete" then | |
| for _, a in ipairs(self:GetAreas()) do | |
| if a.hovered then | |
| XEditorUndo:BeginOp{ name = "Deleted area", objects = { a } } | |
| a:delete() | |
| XEditorUndo:EndOp() | |
| self:UpdateHighlights(true) | |
| return "break" | |
| end | |
| end | |
| end | |
| return XEditorTool.OnShortcut(self, shortcut, source, ...) | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnShortcut(self, shortcut, source, ...) | |
| end | |
| end | |
| function XAreaCopyTool:OnKbdKeyDown(vkey) | |
| if self.DrawingMode == "Box areas" then | |
| return XEditorTool.OnKbdKeyDown(self, vkey) | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnKbdKeyDown(self, vkey) | |
| end | |
| end | |
| function XAreaCopyTool:OnKbdKeyUp(vkey) | |
| if self.DrawingMode == "Box areas" then | |
| return XEditorTool.OnKbdKeyUp(self, vkey) | |
| elseif self.DrawingMode == "Brush" then | |
| return XMapGridAreaBrush.OnKbdKeyUp(self, vkey) | |
| end | |
| end | |
| function XAreaCopyTool:CopyToClipboard() | |
| self:UpdateBrushAreaBoxes() | |
| local areas = self:GetAreas() | |
| if #areas == 0 then return end | |
| -- create XTerrainGridData for each area to capture all grids | |
| local area_datas = {} | |
| for _, a in ipairs(areas) do | |
| local data = XTerrainGridData:new() | |
| local mask = self.DrawingMode == "Brush" and self.Grid or nil | |
| data:CaptureData(a.box, mask, mask and self.GridTileSize or nil) | |
| area_datas[#area_datas + 1] = data | |
| end | |
| -- copy area data and objects to clipboard with a custom PasteTerrainAndObjects paste function | |
| local data = XEditorSerialize(area_datas) | |
| data.objs = XEditorSerialize(XEditorCollapseChildObjects(self:GetObjects(areas))) | |
| data.pivot = CenterPointOnBase(areas) | |
| data.paste_fn = "PasteTerrainAndObjects" | |
| data.mask_grid_tile_size = self.GridTileSize | |
| CopyToClipboard(XEditorToClipboardFormat(data)) | |
| -- delete areas and select default editor tool | |
| XEditorUndo:BeginOp{ objects = table.copy(areas), name = "Copied terrain & objects" } | |
| for _, a in ipairs(areas) do | |
| a:delete() | |
| end | |
| for _, a in ipairs(area_datas) do | |
| a:delete() | |
| end | |
| XEditorUndo:EndOp() | |
| XEditorSetDefaultTool() | |
| end | |
| ----- XTerrainAreaMesh | |
| function OnMsg.PreSaveMap() MapForEach("map", "XTerrainAreaMesh", function(obj) obj:ClearGameFlags(const.gofPermanent) end) end | |
| function OnMsg.PostSaveMap() MapForEach("map", "XTerrainAreaMesh", function(obj) obj: SetGameFlags(const.gofPermanent) end) end | |
| DefineClass.XTerrainAreaMesh = { | |
| __parents = { "Mesh", "EditorCallbackObject" }, | |
| properties = { | |
| { id = "box", editor = "box" }, | |
| }, | |
| outer_color = RGB(255, 255, 255), | |
| inner_color = RGBA(255, 255, 255, 80), | |
| outer_border = 6 * guic, | |
| inner_border = 4 * guic, | |
| box = empty_box, | |
| } | |
| function XTerrainAreaMesh:Init() | |
| self:SetGameFlags(const.gofPermanent) -- so it can be copied by XEditorSerialize | |
| self:SetShader(ProceduralMeshShaders.default_mesh) | |
| self:SetDepthTest(true) | |
| end | |
| function XTerrainAreaMesh:GetPivot() | |
| local pivot = self.box:Center() | |
| return pivot:SetZ(self:GetHeight(pivot)) | |
| end | |
| function XTerrainAreaMesh:GetHeight(pt) | |
| return terrain.GetHeight(pt) | |
| end | |
| function XTerrainAreaMesh:AddQuad(v_pstr, pivot, pt1, pt2, pt3, pt4, color) | |
| local offs = 30 * guic | |
| pt1 = (pt1 - pivot):SetZ(self:GetHeight(pt1) - pivot:z() + offs) | |
| pt2 = (pt2 - pivot):SetZ(self:GetHeight(pt2) - pivot:z() + offs) | |
| pt3 = (pt3 - pivot):SetZ(self:GetHeight(pt3) - pivot:z() + offs) | |
| pt4 = (pt4 - pivot):SetZ(self:GetHeight(pt4) - pivot:z() + offs) | |
| v_pstr:AppendVertex(pt1, color) | |
| v_pstr:AppendVertex(pt2) | |
| v_pstr:AppendVertex(pt3) | |
| v_pstr:AppendVertex(pt2) | |
| v_pstr:AppendVertex(pt3) | |
| v_pstr:AppendVertex(pt4) | |
| end | |
| function XTerrainAreaMesh:AddTriangle(v_pstr, pivot, pt1, pt2, pt3, color) | |
| local offs = 30 * guic | |
| pt1 = (pt1 - pivot):SetZ(self:GetHeight(pt1) - pivot:z() + offs) | |
| pt2 = (pt2 - pivot):SetZ(self:GetHeight(pt2) - pivot:z() + offs) | |
| pt3 = (pt3 - pivot):SetZ(self:GetHeight(pt3) - pivot:z() + offs) | |
| v_pstr:AppendVertex(pt1, color) | |
| v_pstr:AppendVertex(pt2) | |
| v_pstr:AppendVertex(pt3) | |
| end | |
| function XTerrainAreaMesh:Setbox(bbox, force_setpos, no_mesh) | |
| self.box = bbox | |
| if no_mesh then | |
| if force_setpos or self:GetPos() == InvalidPos() then | |
| self:SetPos(self:GetPivot()) | |
| end | |
| return | |
| end | |
| -- for too large areas, we make the lines sparser for better performance | |
| -- 'n' below is the n in the statement "we will only draw every nth line" | |
| local treshold_size = snap_size * 32 | |
| local n = Max(bbox:sizex(), bbox:sizey()) / treshold_size + 1 | |
| local inner_border = self.inner_border + self.inner_border * (n - 1) / 2 | |
| local outer_border = self.outer_border + self.outer_border * (n - 1) / 2 | |
| -- generate mesh | |
| local step = snap_size | |
| local v_pstr = pstr("", 65536) | |
| local pivot = self:GetPivot() | |
| for x = bbox:minx(), bbox:maxx(), step do | |
| for y = bbox:miny(), bbox:maxy(), step do | |
| if x + step <= bbox:maxx() then | |
| local outer = y == bbox:miny() or y + step > bbox:maxy() | |
| if outer or (y - bbox:miny()) / step % n == 0 then | |
| local d = outer and outer_border or inner_border | |
| local pt1, pt2 = point(x, y - d), point(x + step, y - d) | |
| local pt3, pt4 = point(x, y + d), point(x + step, y + d) | |
| self:AddQuad(v_pstr, pivot, pt1, pt2, pt3, pt4, outer and self.outer_color or self.inner_color) | |
| end | |
| end | |
| if y + step <= bbox:maxy() then | |
| local outer = x == bbox:minx() or x + step > bbox:maxx() | |
| if outer or (x - bbox:minx()) / step % n == 0 then | |
| local d = outer and outer_border or inner_border | |
| local pt1, pt2 = point(x - d, y), point(x - d, y + step) | |
| local pt3, pt4 = point(x + d, y), point(x + d, y + step) | |
| self:AddQuad(v_pstr, pivot, pt1, pt2, pt3, pt4, outer and self.outer_color or self.inner_color) | |
| end | |
| end | |
| end | |
| end | |
| if force_setpos or self:GetPos() == InvalidPos() then | |
| self:SetPos(pivot) | |
| end | |
| self:SetMesh(v_pstr) | |
| end | |
| function XTerrainAreaMesh:Getbox(bbox) | |
| return self.box | |
| end | |
| -- handles updating the area tool when undo/redo places or deletes an area | |
| local function UpdateAreas(self) | |
| if self then | |
| table.insert_unique(g_TerrainAreaMeshes, self) | |
| end | |
| if GetDialogMode("XEditor") == "XAreaCopyTool" then | |
| CreateRealTimeThread(function() XAreaCopyTool:UpdateHighlights(true) end) | |
| end | |
| end | |
| XTerrainAreaMesh.EditorCallbackPlace = UpdateAreas | |
| XTerrainAreaMesh.EditorCallbackDelete = UpdateAreas | |
| OnMsg.EditorFiltersChanged = UpdateAreas | |
| ----- XEditableTerrainAreaMesh | |
| local helpers_data = { | |
| { x = 0, y = 0, x1 = true, y1 = true, x2 = false, y2 = false, point(0, 0), point(3, 0), point(0, 3) }, | |
| { x = 1, y = 0, x1 = false, y1 = true, x2 = false, y2 = false, point(-2, 0), point(2, 0), point(0, 2), stretch_x = true }, | |
| { x = 2, y = 0, x1 = false, y1 = true, x2 = true, y2 = false, point(-3, 0), point(0, 0), point(0, 3) }, | |
| { x = 0, y = 1, x1 = true, y1 = false, x2 = false, y2 = false, point(0, -2), point(0, 2), point(2, 0), stretch_y = true }, | |
| { x = 1, y = 1, x1 = true, y1 = true, x2 = true, y2 = true, point(-3, 0), point(3, 0), point(0, 3), point(-3, 0), point(3, 0), point(0, -3) }, | |
| { x = 2, y = 1, x1 = false, y1 = false, x2 = true, y2 = false, point(0, -2), point(0, 2), point(-2, 0), stretch_y = true }, | |
| { x = 0, y = 2, x1 = true, y1 = false, x2 = false, y2 = true, point(0, 0), point(3, 0), point(0, -3) }, | |
| { x = 1, y = 2, x1 = false, y1 = false, x2 = false, y2 = true, point(-2, 0), point(2, 0), point(0, -2), stretch_x = true }, | |
| { x = 2, y = 2, x1 = false, y1 = false, x2 = true, y2 = true, point(-3, 0), point(0, 0), point(0, -3) }, | |
| } | |
| DefineClass.XEditableTerrainAreaMesh = { | |
| __parents = { "XTerrainAreaMesh" }, | |
| hover_color = RGBA(240, 230, 150, 100), | |
| helper_color = RGBA(255, 255, 255, 30), | |
| helper_size = 40 * guic, | |
| helpers = false, | |
| hovered = false, | |
| start_pt = false, | |
| start_box = false, | |
| last_delta = false, | |
| } | |
| function XEditableTerrainAreaMesh:Done() | |
| self:DoneHelpers() | |
| end | |
| function XEditableTerrainAreaMesh:DoneHelpers() | |
| for _, helper in ipairs(self.helpers) do | |
| helper:delete() | |
| end | |
| end | |
| function XEditableTerrainAreaMesh:SetVisible(value) | |
| for _, helper in ipairs(self.helpers) do | |
| helper:SetVisible(value) | |
| end | |
| XTerrainAreaMesh.SetVisible(self, value) | |
| end | |
| function XEditableTerrainAreaMesh:Setbox(bbox, force_setpos, no_mesh) | |
| XTerrainAreaMesh.Setbox(self, bbox, force_setpos, no_mesh) | |
| if no_mesh then | |
| return | |
| end | |
| self:UpdateHelpers() | |
| end | |
| function XEditableTerrainAreaMesh:UpdateHelpers(pt, active_idx) | |
| -- ray for checks whether helpers are under the mouse cursor | |
| local pt1, pt2 | |
| if pt then | |
| pt1, pt2 = camera.GetEye(), ScreenToGame(pt) | |
| end | |
| -- scale up for larger areas | |
| local treshold_size = snap_size * 32 | |
| local n = Max(self.box:sizex(), self.box:sizey()) / treshold_size + 1 | |
| local helper_size = self.helper_size + self.helper_size * (n - 1) / 2 | |
| if self.box:sizex() <= snap_size * 2 or self.box:sizey() <= snap_size * 2 then | |
| helper_size = helper_size / 2 | |
| end | |
| local pivot = self:GetPivot() | |
| self.helpers = self.helpers or {} | |
| for idx, data in ipairs(helpers_data) do | |
| local active = idx == active_idx or pt and self.helpers[idx] and IntersectRayMesh(self, pt1, pt2, self.helpers[idx].vertices_pstr) | |
| active_idx = active_idx or active and idx | |
| local color = active and self.hover_color or self.helper_color | |
| local helper = self.helpers[idx] or Mesh:new() | |
| local v_pstr = pstr("", 64) | |
| helper:SetShader(ProceduralMeshShaders.default_mesh) | |
| helper:SetDepthTest(false) | |
| for t = 1, #data, 3 do | |
| local function trans(pt) | |
| if data.stretch_x then | |
| pt = pt:SetX(pt:x() * self.box:sizex() / (helper_size * 6)) | |
| end | |
| if data.stretch_y then | |
| pt = pt:SetY(pt:y() * self.box:sizey() / (helper_size * 6)) | |
| end | |
| return pt * helper_size + point(self.box:minx() + data.x * self.box:sizex() / 2, self.box:miny() + data.y * self.box:sizey() / 2) | |
| end | |
| self:AddTriangle(v_pstr, pivot, trans(data[t]), trans(data[t + 1]), trans(data[t + 2]), color) | |
| end | |
| helper:SetMesh(v_pstr) | |
| helper:SetPos(self:GetPos()) | |
| self.helpers[idx] = helper | |
| end | |
| return active_idx | |
| end | |
| function XEditableTerrainAreaMesh:UpdateHover(unhover_only) | |
| local hovered = not unhover_only and GetTerrainCursor():InBox2D(self.box) | |
| if hovered ~= self.hovered then | |
| self.hovered = hovered | |
| self.outer_color = hovered and RGB(240, 220, 120) or nil | |
| XTerrainAreaMesh.Setbox(self, self.box) | |
| end | |
| return hovered | |
| end | |
| function XEditableTerrainAreaMesh:DragStart(idx, pt) | |
| self.start_pt = pt | |
| self.start_box = self.box | |
| self.last_delta = nil | |
| end | |
| function XEditableTerrainAreaMesh:DragMove(idx, pt) | |
| local data = helpers_data[idx] | |
| local x1, y1, x2, y2 = self.start_box:xyxy() | |
| local delta = pt - self.start_pt | |
| if delta ~= self.last_delta then | |
| if data.x1 then x1 = Min(x2 - snap_size, x1 + delta:x()) end | |
| if data.y1 then y1 = Min(y2 - snap_size, y1 + delta:y()) end | |
| if data.x2 then x2 = Max(x1 + snap_size, x2 + delta:x()) end | |
| if data.y2 then y2 = Max(y1 + snap_size, y2 + delta:y()) end | |
| self:Setbox(box(x1, y1, x2, y2), "force_setpos") | |
| self:UpdateHelpers(pt, idx) | |
| self.last_delta = delta | |
| end | |
| end | |
| ----- XTerrainGridData | |
| DefineClass.XTerrainGridData = { | |
| __parents = { "XTerrainAreaMesh", "AlignedObj" }, | |
| } | |
| function XTerrainGridData:Done() | |
| for _, grid in ipairs(editor.GetGridNames()) do | |
| local data = rawget(self, grid .. "_grid") | |
| if data then | |
| data:free() | |
| end | |
| local mask = rawget(self, grid .. "_grid_mask") | |
| if mask then | |
| mask:free() | |
| end | |
| end | |
| end | |
| function XTerrainGridData:AlignObj(pos, angle) | |
| -- keep a full slab offset from the original position to make sure aligned objects | |
| -- being pasted won't be displaced relative to the terrain and other objects | |
| local pivot = self:GetPivot() | |
| local offs = (pos or self:GetPos()) - pivot | |
| if const.SlabSizeX then | |
| local x = offs:x() / const.SlabSizeX * const.SlabSizeX | |
| local y = offs:y() / const.SlabSizeY * const.SlabSizeY | |
| local z = offs:z() and (offs:z() + const.SlabSizeZ / 2) / const.SlabSizeZ * const.SlabSizeZ | |
| offs = point(x, y, z) | |
| end | |
| if XEditorSettings:GetSnapMode() == "BuildLevel" and offs:z() then | |
| local step = const.BuildLevelHeight | |
| offs = offs:SetZ((offs:z() + step / 2) / step * step) | |
| end | |
| self:SetPosAngle(pivot + offs, angle or self:GetAngle()) | |
| end | |
| -- generated properties to persist all terrain grids | |
| function XTerrainGridData:GetProperties() | |
| local props = table.copy(XTerrainAreaMesh:GetProperties()) | |
| for _, grid in ipairs(editor.GetGridNames()) do | |
| props[#props + 1] = { id = grid .. "_grid", editor = "grid", default = false } | |
| props[#props + 1] = { id = grid .. "_grid_mask", editor = "grid", default = false } | |
| end | |
| return props | |
| end | |
| function XTerrainGridData:SetProperty(prop_id, value) | |
| if prop_id == "box" then | |
| self.box = value -- just store the value, as we need height_grid to update the mesh | |
| return | |
| end | |
| if prop_id:ends_with("_grid") then | |
| rawset(self, prop_id, value) | |
| return | |
| end | |
| PropertyObject.SetProperty(self, prop_id, value) | |
| end | |
| function XTerrainGridData:PostLoad(reason) | |
| self:Setbox(self.box) -- update the mesh after height_grid is restored | |
| end | |
| function XTerrainGridData:CaptureData(bbox, mask_grid, mask_grid_tile_size) | |
| for _, grid in ipairs(editor.GetGridNames()) do | |
| local copied_area, mask_area = editor.GetGrid(grid, bbox, nil, mask_grid or nil, mask_grid_tile_size or nil) | |
| rawset(self, grid .. "_grid", copied_area or false) | |
| rawset(self, grid .. "_grid_mask", mask_area) | |
| end | |
| self.box = bbox | |
| end | |
| function XTerrainGridData:RotateGrids() | |
| local angle = self:GetAngle() / 60 | |
| if angle == 0 then return end | |
| local transform, transpose | |
| if angle == 90 then | |
| transform = function(x, y, w, h) return y, w - x end | |
| transpose = true | |
| elseif angle == 180 then | |
| transform = function(x, y, w, h) return w - x, h - y end | |
| transpose = false | |
| elseif angle == 270 then | |
| transform = function(x, y, w, h) return h - y, x end | |
| transpose = true | |
| end | |
| for _, grid in ipairs(editor.GetGridNames()) do | |
| local old = rawget(self, grid .. "_grid") | |
| if old then | |
| local new = old:clone() | |
| local sx, sy = old:size() | |
| if transpose then | |
| sx, sy = sy, sx | |
| new:resize(sx, sy) | |
| end | |
| local sx1, sy1 = sx - 1, sy - 1 | |
| for x = 0, sx do | |
| for y = 0, sy do | |
| new:set(x, y, old:get(transform(x, y, sx1, sy1))) | |
| end | |
| end | |
| rawset(self, grid .. "_grid", new) | |
| end | |
| end | |
| if transpose then | |
| local b = self.box - self:GetPivot() | |
| b = box(b:miny(), b:minx(), b:maxy(), b:maxx()) | |
| self.box = b + self:GetPivot() | |
| end | |
| end | |
| function XTerrainGridData:ApplyData(paste_grids, mask_grid_tile_size) | |
| local pos = self:GetPos() | |
| if not pos:IsValidZ() then | |
| pos = pos:SetTerrainZ() | |
| end | |
| local offset = pos - self:GetPivot() | |
| for _, grid in ipairs(editor.GetGridNames()) do | |
| if paste_grids[grid] then | |
| local data = rawget(self, grid .. "_grid") | |
| if data then | |
| local mask_grid = rawget(self, grid .. "_grid_mask") | |
| if grid == "height" then | |
| data = data:clone() | |
| -- Calculate Z offset for the terrain height grid | |
| local offset_z_scaled = offset:z() / const.TerrainHeightScale | |
| local sx, sy = data:size() | |
| for x = 0, sx do | |
| for y = 0, sy do | |
| -- If there's a mask, offset z only for tiles where the mask is 1 | |
| if not mask_grid or mask_grid:get(x, y) ~= 0 then | |
| local new_z = data:get(x, y) + offset_z_scaled | |
| new_z = Clamp(new_z, 0, const.MaxTerrainHeight / const.TerrainHeightScale) | |
| data:set(x, y, new_z) | |
| end | |
| end | |
| end | |
| -- The mask slice for the height grid uses the height tile | |
| editor.SetGrid(grid, data, self.box + offset, mask_grid or nil, mask_grid and const.HeightTileSize or nil) | |
| data:free() | |
| else | |
| editor.SetGrid(grid, data, self.box + offset, mask_grid or nil, mask_grid and mask_grid_tile_size or nil) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function XTerrainGridData:GetHeight(pt) | |
| pt = (pt - self.box:min() + point(const.HeightTileSize / 2, const.HeightTileSize / 2)) / const.HeightTileSize | |
| return self.height_grid:get(pt) * const.TerrainHeightScale | |
| end | |
| XTerrainGridData.EditorCallbackPlace = UpdateAreas | |
| XTerrainGridData.EditorCallbackDelete = UpdateAreas | |
| ----- Two-step pasting logic | |
| -- a) paste the areas first and let the user move them with Move Gizmo | |
| -- b) the second Ctrl-V pastes the stored terrain and objects | |
| -- c) cancel the entire operation if the editor tool is changed from Move Gizmo | |
| local areas, undo_index, op_in_progress | |
| local function UpdatePasteOpState() | |
| if op_in_progress then return end | |
| op_in_progress = true | |
| if not areas then | |
| if IsKindOf(selo(), "XTerrainGridData") then | |
| local ops = XEditorUndo:GetOpNames() | |
| local index | |
| for i = #ops, 2, -1 do | |
| if string.find(ops[i], "Started pasting", 1, true) then | |
| index = i - 1 | |
| break | |
| end | |
| end | |
| undo_index = index or XEditorUndo:GetCurrentOpNameIdx() | |
| areas = editor.GetSel() | |
| XEditorSetDefaultTool("MoveGizmo", { | |
| rotation_arrows_on = true, | |
| rotation_arrows_z_only = true, | |
| }) | |
| end | |
| else | |
| if not XEditorUndo.undoredo_in_progress then | |
| XEditorUndo:RollToOpIndex(undo_index) | |
| end | |
| areas = nil | |
| undo_index = nil | |
| end | |
| op_in_progress = false | |
| end | |
| OnMsg.EditorToolChanged = UpdatePasteOpState | |
| OnMsg.EditorSelectionChanged = UpdatePasteOpState | |
| function calculate_center(objs, method) | |
| local pos = point30 | |
| for _, obj in ipairs(objs) do | |
| pos = pos + ValidateZ(obj[method](obj)) | |
| end | |
| return pos / #objs | |
| end | |
| function XEditorPasteFuncs.PasteTerrainAndObjects(clipboard_data, clipboard_text) | |
| CreateRealTimeThread(function() | |
| PauseInfiniteLoopDetection("PasteTerrainAndObjects") | |
| local grid_to_item_map = { | |
| BiomeGrid = "Biome", | |
| height = "Terrain height", | |
| terrain_type = "Terrain texture", | |
| grass_density = "Grass density", | |
| impassability = "Impassability", | |
| passability = "Passability", | |
| colorize = "Terrain colorization", | |
| --- | |
| ["Biome"] = "BiomeGrid", | |
| ["Terrain height"] = "height", | |
| ["Terrain texture"] = "terrain_type", | |
| ["Grass density"] = "grass_density", | |
| ["Impassability"] = "impassability", | |
| ["Passability"] = "passability", | |
| ["Terrain colorization"] = "colorize", | |
| } | |
| if const.CaveTileSize then | |
| grid_to_item_map.CaveGrid = "Caves" | |
| grid_to_item_map.Caves = "CaveGrid" | |
| end | |
| -- first step | |
| if not areas or #table.validate(areas) == 0 then | |
| op_in_progress = true | |
| XEditorUndo:BeginOp{ name = "Started pasting", clipboard = clipboard_text } | |
| local areas = XEditorDeserialize(clipboard_data) | |
| XEditorSelectAndMoveObjects(areas, editor.GetPlacementPoint(GetTerrainCursor()) - clipboard_data.pivot) | |
| XEditorUndo:EndOp(areas) | |
| op_in_progress = false | |
| UpdatePasteOpState() -- activates the Move Gizmo and starts the second paste step | |
| else -- second step | |
| op_in_progress = true | |
| XEditorSetDefaultTool() | |
| local op = {} | |
| local grids = {} | |
| local items = {} | |
| local starting_selection = {} | |
| for idx, grid in ipairs(editor.GetGridNames()) do | |
| op[grid] = true | |
| grids[idx] = grid | |
| -- Read last choice from local storage | |
| if LocalStorage.XAreaCopyTool[grid] then | |
| table.insert(starting_selection, idx) | |
| end | |
| -- Map from grid name to item name | |
| if grid_to_item_map[grid] then | |
| table.insert(items, grid_to_item_map[grid]) | |
| end | |
| end | |
| local result = WaitListMultipleChoice(nil, items, "Choose grids to paste:", starting_selection) | |
| if not result then | |
| -- Cancel | |
| for _, a in ipairs(areas) do | |
| a:delete() | |
| end | |
| areas = nil | |
| undo_index = nil | |
| op_in_progress = nil | |
| ResumeInfiniteLoopDetection("PasteTerrainAndObjects") | |
| return | |
| end | |
| if #result == 0 then | |
| -- Nothing chosen | |
| result = items | |
| end | |
| local paste_grids = {} | |
| for _, item in ipairs(result) do | |
| -- Map from item name to grid name | |
| if grid_to_item_map[item] then | |
| local grid = grid_to_item_map[item] | |
| paste_grids[grid] = true | |
| end | |
| end | |
| -- Save choice in local storage | |
| LocalStorage.XAreaCopyTool = table.copy(paste_grids) | |
| SaveLocalStorage() | |
| op.name = "Pasted terrain & objects" | |
| op.objects = areas | |
| op.clipboard = clipboard_text | |
| XEditorUndo:BeginOp(op) | |
| local offs = calculate_center(areas, "GetPos") - calculate_center(areas, "GetPivot") | |
| local angle = areas[1]:GetAngle() | |
| local center = CenterOfMasses(areas) | |
| for _, a in ipairs(areas) do | |
| a:RotateGrids() | |
| a:ApplyData(paste_grids, clipboard_data.mask_grid_tile_size) | |
| a:delete() | |
| end | |
| local objs = XEditorDeserialize(clipboard_data.objs, nil, "paste") | |
| objs = XEditorSelectAndMoveObjects(objs, offs) | |
| editor.ClearSel() | |
| if angle ~= 0 then | |
| local rotate_logic = XEditorRotateLogic:new() | |
| SuspendPassEditsForEditOp() | |
| rotate_logic:InitRotation(objs, center, 0) | |
| rotate_logic:Rotate(objs, "group_rotation", center, axis_z, angle) | |
| ResumePassEditsForEditOp() | |
| end | |
| XEditorUndo:EndOp(objs) | |
| areas = nil | |
| undo_index = nil | |
| op_in_progress = nil | |
| end | |
| ResumeInfiniteLoopDetection("PasteTerrainAndObjects") | |
| end) | |
| end | |
| function OnMsg.ChangeMap() | |
| areas = nil | |
| undo_index = nil | |
| op_in_progress = nil | |
| end | |