myspace / CommonLua /Editor /XEditor /XSelectObjectsTool.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
17.7 kB
-- dummy placement helper for the default "Select" mode
DefineClass.XSelectObjectsHelper = {
__parents = { "XEditorPlacementHelper" },
InXSelectObjectsTool = true,
HasSnapSetting = true,
Title = "Edit objects (Q)",
ActionIcon = "CommonAssets/UI/Editor/Tools/SelectObjects.tga",
ActionShortcut = "Escape",
ActionShortcut2 = "Q",
}
DefineClass.XSelectObjectsTool = {
__parents = { "XEditorTool", "XEditorPlacementHelperHost", "XEditorRotateLogic", "XSelectObjectsToolCustomFilter" },
ToolTitle = "Edit objects",
ToolSection = "Objects",
Description = function(self)
local descr = self and self.placement_helper:GetDescription()
if descr then return { descr } end
return { "(hold <style GedHighlight>Ctrl</style> to clone, <style GedHighlight>Alt</style> to rotate, <style GedHighlight>Shift</style> to scale)\n(use <style GedHighlight>[</style> and <style GedHighlight>]</style> to cycle between object variants)\n(<style GedHighlight>Alt-DblClick</style> to select/filter by class)" }
end,
ActionIcon = "CommonAssets/UI/Editor/Tools/SelectObjects.tga",
ActionSortKey = "01",
ActionShortcut = "Q",
ToolKeepSelection = true,
helper_class = "XSelectObjectsHelper",
edit_operation = false,
highlighted_objs = false,
selection_box = false,
selection_box_mesh = false,
selection_box_enable = false,
editing_line_mesh = false,
init_selection = false,
init_mouse_pos = false,
init_move_positions = false,
init_rotate_data = false,
init_scales = false,
last_mouse_pos = false,
last_mouse_obj = false,
last_mouse_click = false,
}
function XSelectObjectsTool:Init()
self:CreateThread("fixup_hovered_object", self.FixupHoveredObject, self)
end
function XSelectObjectsTool:Done()
self.desktop:SetMouseCapture() -- finalize pending operation
self:HighlightObjects(false)
end
function XSelectObjectsTool:OnEditorSetProperty(prop_id, old_value, ged)
if prop_id == "WireCurve" then
Msg("WireCurveTypeChanged", self:GetProperty("WireCurve"), old_value)
end
end
function XSelectObjectsTool:UpdatePlacementHelper()
local helper = self.placement_helper
helper.local_cs = helper.HasLocalCSSetting and GetLocalCS()
helper.snap = helper.HasSnapSetting and XEditorSettings:GetSnapEnabled()
XEditorUpdateToolbars()
end
function XSelectObjectsTool:CantSnapObjects()
return not g_Classes[self:GetHelperClass()].HasSnapSetting and "This mode does not support snapping."
end
function XSelectObjectsTool:HighlightObjects(objs)
objs = XEditorSettings:GetHighlightOnHover() and objs
local highlighted = {}
if objs then
objs = editor.SelectionPropagate(objs, "for_rollover")
for _, obj in ipairs(objs) do
if IsValid(obj) then
if IsKindOf(obj, "CollideLuaObject") then
obj:SetHighlighted(true)
else
obj:SetHierarchyGameFlags(const.gofEditorHighlight)
end
highlighted[obj] = true
end
end
end
for _, obj in ipairs(self.highlighted_objs) do
if IsValid(obj) and not highlighted[obj] then
if IsKindOf(obj, "CollideLuaObject") then
obj:SetHighlighted(false)
else
obj:ClearHierarchyGameFlags(const.gofEditorHighlight)
end
end
end
self.highlighted_objs = objs and table.copy(objs)
end
function XSelectObjectsTool:StartEditOperation(operation)
if not self.edit_operation then
XEditorUndo:BeginOp({
name = operation == "PlacementHelper" and
string.format(self.placement_helper.UndoOpName, #editor.GetSel()) or
string.format("%sd %d object(s)", operation, #editor.GetSel()),
objects = operation == "Clone" and empty_table or editor.GetSel(),
edit_op = true,
})
SuspendPassEditsForEditOp()
self.edit_operation = operation
end
end
function XSelectObjectsTool:EndEditOperation()
if self.edit_operation then
ResumePassEditsForEditOp()
local sel = editor.GetSel()
editor.SetSel(editor.SelectionPropagate(sel))
XEditorUndo:EndOp(sel)
self.edit_operation = false
end
end
function XSelectObjectsTool:SelectNextObjectAtCursor()
XEditorUndo:BeginOp()
local obj = XEditorSelectSingleObjects == 1 and
GetNextObjectAtScreenPos(CanSelect, "topmost", selo()) or
GetNextObjectAtScreenPos(CanSelect, "topmost", "collection", selo())
editor.SetSel(editor.SelectionPropagate({obj}))
XEditorUndo:EndOp()
end
function XSelectObjectsTool:OnMouseButtonDoubleClick(pt, button)
local obj = GetObjectAtCursor()
if button == "L" and obj then
if terminal.IsKeyPressed(const.vkRalt) then
self:SelectNextObjectAtCursor()
return "break"
elseif terminal.IsKeyPressed(const.vkAlt) then
local sel = table.copy(editor.GetSel())
XEditorUndo:BeginOp()
-- if selection is a single object, select objects of that class on the screen; otherwise, filter current selection
if #sel == 1 or terminal.IsKeyPressed(const.vkShift) then
if not terminal.IsKeyPressed(const.vkShift) then
editor.ClearSel()
end
local locked = Collection.GetLockedCollection()
editor.AddToSel(XEditorGetVisibleObjects(function(o)
return o.class == obj.class and (not locked or o:GetRootCollection() == locked)
end))
else
for i = #sel, 1, -1 do
if sel[i].class ~= obj.class then
table.remove(sel, i)
end
end
editor.ClearSel()
editor.AddToSel(sel)
end
XEditorUndo:EndOp()
return "break"
end
end
end
function XSelectObjectsTool:OnMouseButtonDown(pt, button)
self:SetFocus()
if XEditorPlacementHelperHost.OnMouseButtonDown(self, pt, button) then
XPopupMenu.ClosePopupMenus()
return "break"
end
if button == "L" then
XPopupMenu.ClosePopupMenus()
self.desktop:SetMouseCapture(self)
local obj = GetObjectAtCursor()
local terrain_pos = GetTerrainCursor()
self.init_mouse_pos = { terrain = terrain_pos, screen = pt, time = GetPreciseTicks() }
if obj then -- prepare data for a move operation
local ptBase = obj:GetPos():SetTerrainZ()
local _, ptScreenAtBase = GameToScreen(ptBase)
self.init_mouse_pos.mouse_offset = ptScreenAtBase - pt
self.init_mouse_pos.terrain_base = ptBase
end
self.last_mouse_click = terrain_pos
if obj and terminal.IsKeyPressed(const.vkRalt) then
self:SelectNextObjectAtCursor()
elseif not (selo() and terminal.IsKeyPressed(const.vkAlt)) then
if obj and terminal.IsKeyPressed(const.vkRshift) then
XEditorUndo:BeginOp()
if editor.IsSelected(obj) then
editor.RemoveFromSel(editor.SelectionPropagate({obj}))
else
editor.AddToSel(editor.SelectionPropagate({obj}))
end
XEditorUndo:EndOp()
return "break"
end
if not obj or terminal.IsKeyPressed(const.vkShift) and not editor.IsSelected(obj) then
XEditorUndo:BeginOp()
if not terminal.IsKeyPressed(const.vkShift) then
editor.ClearSel()
elseif obj then
editor.AddToSel(editor.SelectionPropagate({obj}))
end
self.init_selection = table.copy(editor.GetSel())
self.selection_box_enable = true
return "break"
end
if not (obj and editor.IsSelected(obj)) then
editor.ChangeSelWithUndoRedo(editor.SelectionPropagate({obj}))
end
XEditorPlacementHelperHost.OnMouseButtonDown(self, pt, button) -- try again after object is selected
end
return "break"
elseif button == "R" then
if XEditorIsContextMenuOpen() and #editor.GetSel() > 0 then
editor.ClearSelWithUndoRedo()
end
XPopupMenu.ClosePopupMenus()
end
return XEditorTool.OnMouseButtonDown(self, pt, button)
end
function XSelectObjectsTool:OnMousePos(pt)
local obj = GetObjectAtCursor()
self.last_mouse_pos = pt
self.last_mouse_obj = obj
XEditorRemoveFocusFromToolbars()
local operation = self.edit_operation or
terminal.IsKeyPressed(const.vkControl) and "Clone" or
terminal.IsKeyPressed(const.vkAlt) and "Rotate" or
terminal.IsKeyPressed(const.vkShift) and "Scale"
if self.placement_helper.operation_started then
if operation == "Clone" then
self.placement_helper:EndOperation()
self:StartEditOperation("Clone")
XEditorPlacementHelperHost.OnMousePos(self, pt)
self:Clone()
self.placement_helper:StartOperation(pt, editor.GetSel())
self.edit_operation = "PlacementHelper"
else
self:StartEditOperation("PlacementHelper")
XEditorPlacementHelperHost.OnMousePos(self, pt)
end
self:HighlightObjects(false)
return "break"
end
if self.init_mouse_pos then
if self.selection_box_enable then
self:SelectWithSelectionBox()
self:HighlightObjects(false)
elseif selo() then
-- call StartEditOperation only when objects are actually modified to prevent empty undo operations
local mouse_moved = self.init_mouse_pos.screen:Dist(pt) >= 7
if operation == "Clone" and obj and editor.IsSelected(obj) and mouse_moved then
self:StartEditOperation("Clone")
self:Clone()
self:Move(pt)
self.edit_operation = "Move"
elseif (operation == "Move" or not operation) and GetPreciseTicks() - self.init_mouse_pos.time > 70 then
self:StartEditOperation("Move")
self:Move(pt)
elseif operation == "Rotate" then
self:StartEditOperation("Rotate")
self:CreateEditingLine()
if not self.init_rotate_data then
self:InitRotation(editor.GetSel())
else
self:Rotate(editor.GetSel(), not terminal.IsKeyPressed(const.vkShift))
end
elseif operation == "Scale" then
self:StartEditOperation("Scale")
self:Scale(pt)
end
self:HighlightObjects(editor.GetSel())
end
return "break"
end
if not terminal.IsKeyPressed(const.vkMbutton) then -- camera orbit not active
local op_check = self.placement_helper:CheckStartOperation(pt, not "btn_pressed")
if op_check or obj then
local two_pt = self.placement_helper:IsKindOf("XTwoPointAttachHelper")
local objects = (not two_pt and (op_check or obj and editor.IsSelected(obj))) and editor.GetSel() or {obj}
self:HighlightObjects(objects)
else
self:HighlightObjects(false)
end
return "break"
end
self:HighlightObjects(false)
return "break"
end
function XSelectObjectsTool:FixupHoveredObject()
-- GetObjectAtCursor uses GetPreciseCursorObj, which needs several frames to get updated,
-- so call OnMousePos for the next several frames after the mouse stopped moving
while true do
if terminal.GetMousePos() == self.last_mouse_pos then
local obj = GetObjectAtCursor() or false
if obj ~= self.last_mouse_obj then
self:OnMousePos(self.last_mouse_pos)
self.last_mouse_obj = obj
end
end
WaitNextFrame()
end
end
function XSelectObjectsTool:OnMouseButtonUp(pt, button)
if XEditorPlacementHelperHost.OnMouseButtonUp(self, pt, button) then
return "break"
elseif self.init_mouse_pos then
self.desktop:SetMouseCapture()
return "break"
end
end
function XSelectObjectsTool:OnCaptureLost()
self.init_mouse_pos = false
self.init_move_positions = false
self.init_scales = false
if self.selection_box_enable then
self.selection_box_enable = false
if self.selection_box_mesh then
self.selection_box_mesh:delete()
self.selection_box_mesh = false
end
XEditorUndo:EndOp()
editor.SelectionChanged()
end
if self.editing_line_mesh then
self.editing_line_mesh:delete()
self.editing_line_mesh = false
end
self:CleanupRotation()
XEditorPlacementHelperHost.OnCaptureLost(self)
self:EndEditOperation()
end
function XSelectObjectsTool:CreateSelectionBox()
local ptOne = self.init_mouse_pos.terrain:SetInvalidZ()
local ptThree = GetTerrainCursor():SetInvalidZ()
local localY = camera.GetDirection()
local localX = Normalize(Cross(axis_z, localY):SetInvalidZ())
local diagonalNorm = Normalize(ptOne - ptThree)
localX = Dot(diagonalNorm, localX) > 0 and localX or -localX
local angle = diagonalNorm:Len() ~= 0 and Angle3dVectors(diagonalNorm, localX) or 0
local sin, cos = sincos(angle)
local diagonal = ptOne - ptThree
local localWidth = MulDivRound(diagonal, cos, 4096):Len()
local ptTwo = ptThree + MulDivRound(localX, localWidth, 4096)
local ptFour = ptOne - MulDivRound(localX, localWidth, 4096)
return {ptOne, ptTwo, ptThree, ptFour}
end
function XSelectObjectsTool:SelectWithSelectionBox()
local selection_box = self:CreateSelectionBox()
local selection_box_mesh = self.selection_box_mesh
if not selection_box_mesh then
selection_box_mesh = Mesh:new()
selection_box_mesh:SetShader(ProceduralMeshShaders.default_polyline)
selection_box_mesh:SetMeshFlags(const.mfWorldSpace + const.mfTerrainDistorted)
selection_box_mesh:SetDepthTest(false)
self.selection_box_mesh = selection_box_mesh
end
local minX, maxX = MinMax(selection_box[1]:x(), selection_box[2]:x(), selection_box[3]:x(), selection_box[4]:x())
local minY, maxY = MinMax(selection_box[1]:y(), selection_box[2]:y(), selection_box[3]:y(), selection_box[4]:y())
local box = box(minX, minY, maxX, maxY)
local w, h = box:sizexyz()
local p, tile = (w + h) / guim, const.HeightTileSize
local step = Max(p, 50) * tile / 100
PlaceTerrainPoly(selection_box, RGB(255, 255, 255), step, 10, selection_box_mesh)
PauseInfiniteLoopDetection("SelectWithSelectionBox")
local objects = MapGet(box, "attached", false, "CObject", function(o)
return IsPointInsidePoly2D(o, selection_box) and CanSelect(o)
end)
local sel = editor.SelectionPropagate(objects)
if terminal.IsKeyPressed(const.vkShift) then
table.iappend(sel, self.init_selection)
end
editor.SetSel(sel, "dont_notify")
ResumeInfiniteLoopDetection("SelectWithSelectionBox")
end
function XSelectObjectsTool:Clone()
local objs = editor.GetSel("permanent")
local clones = XEditorClone(objs)
Msg("EditorCallback", "EditorCallbackClone", clones, objs)
editor.SetSel(clones)
end
function XSelectObjectsTool:Move(pt)
local objs = editor.GetSel()
if not self.init_move_positions then
self.init_move_positions = {}
for i, o in ipairs(objs) do
self.init_move_positions[i] = o:GetPos()
end
end
local data = self.init_mouse_pos
local vMove = (ScreenToTerrainPoint(pt + data.mouse_offset) - data.terrain_base):SetZ(0)
local snapBySlabs = HasAlignedObjs(objs)
for i, obj in ipairs(objs) do
XEditorSnapPos(obj, self.init_move_positions[i], vMove, snapBySlabs)
end
Msg("EditorCallback", "EditorCallbackMove", objs)
end
function XSelectObjectsTool:Scale(pt)
self:CreateEditingLine()
local objs = editor.GetSel()
if not self.init_scales then
self.init_scales = {}
for i, obj in ipairs(objs) do
self.init_scales[i] = obj:GetScale()
end
end
local screenHeight = UIL.GetScreenSize():y()
local mouseY = 4096 * (pt:y() - screenHeight / 2) / screenHeight
local initY = 4096 * (self.init_mouse_pos.screen:y() - screenHeight / 2) / screenHeight
local scale
if mouseY < initY then
scale = 100 * (mouseY + 4096)/(initY + 4096) + 300 * (initY - mouseY)/(initY + 4096)
else
scale = 100 * (4096 - mouseY)/(4096 - initY) + 30 * (mouseY - initY)/(4096 - initY)
end
for i, obj in ipairs(objs) do
obj:SetScaleClamped(self.init_scales[i] * scale / 100)
end
Msg("EditorCallback", "EditorCallbackScale", objs)
end
function XSelectObjectsTool:CreateEditingLine()
local vpstr = pstr("")
local pt = CenterOfMasses(editor.GetSel())
vpstr:AppendVertex(pt, RGB(255, 255, 255))
vpstr:AppendVertex(GetTerrainCursor():SetZ(pt:z()))
if not self.editing_line_mesh then self.editing_line_mesh = PlaceObject("Polyline") end
self.editing_line_mesh:SetMesh(vpstr)
self.editing_line_mesh:SetPos(pt)
self.editing_line_mesh:AddMeshFlags(const.mfWorldSpace)
end
function XSelectObjectsTool:GetRotateAngle()
local _, pt1 = GameToScreen(self.init_rotate_center)
local _, pt2 = GameToScreen(GetTerrainCursor())
return CalcOrientation(pt1, pt2)
end
function XSelectObjectsTool:OnShortcut(shortcut, source, ...)
if shortcut == "Escape" and self:GetHelperClass() == "XSelectObjectsHelper" and #editor.GetSel() > 0 then
editor.ClearSelWithUndoRedo()
return "break"
end
if XEditorPlacementHelperHost.OnShortcut(self, shortcut, source, ...) == "break" then
return "break"
end
-- don't change tool modes, allow undo, etc. while in the process of dragging
if terminal.desktop:GetMouseCapture() and shortcut ~= "Ctrl-F1" then
return "break"
end
if shortcut == "Delete" then
CreateRealTimeThread(function()
if self:PreDeleteConfirmation() then
editor.DelSelWithUndoRedo()
end
end)
return "break"
elseif shortcut == "[" or shortcut == "]" then
local dir = shortcut == "[" and -1 or 1
-- cycle selected objects among available variants
local sel = editor.GetSel()
if sel and #sel > 0 and not self.edit_operation then
local dir = shortcut == "[" and -1 or 1
XEditorUndo:BeginOp{ objects = sel, name = string.format("Cycled %d objects", #sel) }
SuspendPassEditsForEditOp()
local newsel = {}
for _, obj in ipairs(sel) do
table.insert(newsel, CycleObjSubvariant(obj, dir)) -- produces an undo op for obj
end
ResumePassEditsForEditOp()
XEditorUndo:EndOp()
editor.SetSel(newsel) -- must be AFTER the editor op
end
return "break"
elseif shortcut == "Pageup" or shortcut == "Pagedown" or shortcut == "Shift-Pageup" or shortcut == "Shift-Pagedown" then
local sel = editor.GetSel()
local down = shortcut:ends_with("down")
local dir = (down and point(0, 0, -1) or point(0, 0, 1)) * (terminal.IsKeyPressed(const.vkShift) and guic or 1)
XEditorUndo:BeginOp{ objects = sel, name = string.format("Moved %d objects %s", #sel, down and "down" or "up") }
for _, obj in ipairs(sel) do
obj:SetPos(obj:GetVisualPos() + dir)
end
XEditorUndo:EndOp(sel)
return "break"
end
return XEditorSettings.OnShortcut(self, shortcut, source, ...)
end
function XSelectObjectsTool:PreDeleteConfirmation()
return true
end
function XSelectObjectsTool:GetToolTitle()
return XEditorShowCustomFilters and "Custom Selection Filter" or XEditorPlacementHelperHost.GetToolTitle(self)
end