From bda1c9460fc1e419b279967d3b8f9c78c34e63d2 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Wed, 19 Oct 2022 01:00:51 -0700 Subject: [PATCH] Update F3X to use PivotTo where possible * Previously, F3X always individually moved all of the parts in the selection even if they were under models. This is not optimal, if all the parts are under a single model then it suffices to make a single PivotTo call transforming the model. * Change SyncRotate/SyncMove into SyncRootTransform/SyncPartTransform which are both used by Rotate and Move. In the case where the "Local" mode is used, each part is moved indiviudally, and synced via SyncPartTransform. In the case where "Center" or "Last" is used, the set of rootmost PVInstances is moved via PivotTo and synced via SyncRootTransform. * The code specific to updating pivots has been removed, as PivotTo automatically updates WorldPivots when moving the hierarchy. * One minor behavior change here is that when using "Local" mode with the Rotate tool, the rotation of the model's pivot is no longer updated. Maintaining that behavior under this new syncing scheme added unnecessary complexity and it was probably a mistake to include that behavior in the first place when updating F3X to respect pivots. --- Core/Selection.lua | 39 +++++ SyncAPI.lua | 151 ++++++++---------- Tools/Move/FreeDragging.lua | 46 ++++-- Tools/Move/HandleDragging.lua | 14 +- Tools/Move/init.lua | 217 +++++++++++++++----------- Tools/Rotate.lua | 283 ++++++++++++++++++---------------- 6 files changed, 418 insertions(+), 332 deletions(-) diff --git a/Core/Selection.lua b/Core/Selection.lua index cc70044..ca317b3 100644 --- a/Core/Selection.lua +++ b/Core/Selection.lua @@ -65,6 +65,45 @@ local function CollectPartsAndModels(Item, PartTable, ModelTable) end end +local function CollectTransformablesRecursive(PotentialRoot: Instance, Seen: {PVInstance: boolean}, Roots: {PVInstance}) + if PotentialRoot:IsA("PVInstance") then + -- Once we hit a PVInstance, if it's already been seen, this heirarchy + -- has already been traversed down, bail out. + if Seen[PotentialRoot] then + return + end + + -- Mark this as seen and insert it into the set of roots + Seen[PotentialRoot] = true + table.insert(Roots, PotentialRoot) + + -- At this point, we know we haven't seen the descendants, go and mark + -- all the descendants as seen. + for _, Descendant in PotentialRoot:GetDescendants() do + if Descendant:IsA("PVInstance") then + Seen[Descendant] = true + end + end + else + -- Traverse down hierarchy looking for roots + for _, Child in PotentialRoot:GetChildren() do + CollectTransformablesRecursive(Child, Seen, Roots) + end + end +end + +-- Get all the PVInstances in the selection which are NOT inside of another +-- PVInstance in the selection. Those are the PVInstances which need to be +-- transformed via PivotTo. +function Selection.GetRootPVInstances(Items: {Instance}): {PVInstance} + local Seen = {} :: {PVInstance: boolean} + local Results = {} :: {PVInstance} + for _, Instance in Items do + CollectTransformablesRecursive(Instance, Seen, Results) + end + return Results +end + function Selection.Add(Items, RegisterHistory) -- Adds the given items to the selection diff --git a/SyncAPI.lua b/SyncAPI.lua index 4db97e9..6f3cc13 100644 --- a/SyncAPI.lua +++ b/SyncAPI.lua @@ -415,22 +415,19 @@ Actions = { end; - ['SyncMove'] = function (Changes) - -- Updates parts server-side given their new CFrames + ['SyncResize'] = function (Changes) + -- Updates parts server-side given their new sizes and CFrames -- Grab a list of every part we're attempting to modify local Parts = {}; - local Models = {} for _, Change in pairs(Changes) do if Change.Part then table.insert(Parts, Change.Part); - elseif Change.Model then - table.insert(Models, Change.Model) - end + end; end; -- Ensure parts are selectable - if not (CanModifyItems(Parts) and CanModifyItems(Models)) then + if not CanModifyItems(Parts) then return; end; @@ -443,27 +440,16 @@ Actions = { end; -- Reorganize the changes - local PartChangeSet = {} - local ModelChangeSet = {} + local ChangeSet = {}; for _, Change in pairs(Changes) do if Change.Part then - Change.InitialState = { - Anchored = Change.Part.Anchored; - CFrame = Change.Part.CFrame; - } - PartChangeSet[Change.Part] = Change - elseif Change.Model then - ModelChangeSet[Change.Model] = Change.Pivot - end - end; - - -- Preserve joints - for Part, Change in pairs(PartChangeSet) do - Change.Joints = PreserveJoints(Part, PartChangeSet) + Change.InitialState = { Anchored = Change.Part.Anchored, Size = Change.Part.Size, CFrame = Change.Part.CFrame }; + ChangeSet[Change.Part] = Change; + end; end; -- Perform each change - for Part, Change in pairs(PartChangeSet) do + for Part, Change in pairs(ChangeSet) do -- Stabilize the parts and maintain the original anchor state Part.Anchored = true; @@ -471,46 +457,42 @@ Actions = { Part.Velocity = Vector3.new(); Part.RotVelocity = Vector3.new(); - -- Set the part's CFrame + -- Set the part's size and CFrame + Part.Size = Change.Size; Part.CFrame = Change.CFrame; end; - for Model, Pivot in pairs(ModelChangeSet) do - Model.WorldPivot = Pivot - end -- Make sure the player is authorized to move parts into this area if Security.ArePartsViolatingAreas(Parts, Player, false, AreaPermissions) then -- Revert changes if unauthorized destination - for Part, Change in pairs(PartChangeSet) do + for Part, Change in pairs(ChangeSet) do + Part.Size = Change.InitialState.Size; Part.CFrame = Change.InitialState.CFrame; end; end; -- Restore the parts' original states - for Part, Change in pairs(PartChangeSet) do + for Part, Change in pairs(ChangeSet) do Part:MakeJoints(); - RestoreJoints(Change.Joints); Part.Anchored = Change.InitialState.Anchored; end; end; - ['SyncResize'] = function (Changes) - -- Updates parts server-side given their new sizes and CFrames + ['SyncPartTransform'] = function (Changes) + -- Updates parts server-side given their new CFrames - -- Grab a list of every part we're attempting to modify + -- Grab a list of every part and model we're attempting to modify local Parts = {}; for _, Change in pairs(Changes) do - if Change.Part then - table.insert(Parts, Change.Part); - end; + table.insert(Parts, Change.Part); end; -- Ensure parts are selectable - if not CanModifyItems(Parts) then + if not (CanModifyItems(Parts)) then return; end; @@ -523,16 +505,24 @@ Actions = { end; -- Reorganize the changes - local ChangeSet = {}; + local PartChangeSet = {} for _, Change in pairs(Changes) do if Change.Part then - Change.InitialState = { Anchored = Change.Part.Anchored, Size = Change.Part.Size, CFrame = Change.Part.CFrame }; - ChangeSet[Change.Part] = Change; - end; + Change.InitialState = { + Anchored = Change.Part.Anchored; + CFrame = Change.Part.CFrame; + } + PartChangeSet[Change.Part] = Change + end + end; + + -- Preserve joints + for Part, Change in pairs(PartChangeSet) do + Change.Joints = PreserveJoints(Part, PartChangeSet) end; -- Perform each change - for Part, Change in pairs(ChangeSet) do + for Part, Change in pairs(PartChangeSet) do -- Stabilize the parts and maintain the original anchor state Part.Anchored = true; @@ -540,8 +530,7 @@ Actions = { Part.Velocity = Vector3.new(); Part.RotVelocity = Vector3.new(); - -- Set the part's size and CFrame - Part.Size = Change.Size; + -- Set the part's CFrame Part.CFrame = Change.CFrame; end; @@ -550,37 +539,43 @@ Actions = { if Security.ArePartsViolatingAreas(Parts, Player, false, AreaPermissions) then -- Revert changes if unauthorized destination - for Part, Change in pairs(ChangeSet) do - Part.Size = Change.InitialState.Size; + for Part, Change in pairs(PartChangeSet) do Part.CFrame = Change.InitialState.CFrame; end; end; -- Restore the parts' original states - for Part, Change in pairs(ChangeSet) do + for Part, Change in pairs(PartChangeSet) do Part:MakeJoints(); + RestoreJoints(Change.Joints); Part.Anchored = Change.InitialState.Anchored; end; end; - - ['SyncRotate'] = function (Changes) + + ['SyncRootTransform'] = function (Changes) -- Updates parts server-side given their new CFrames - -- Grab a list of every part and model we're attempting to modify + -- Grab a list of every root we're attempting to modify local Parts = {}; - local Models = {} + local PartChangeSet = {}; for _, Change in pairs(Changes) do - if Change.Part then - table.insert(Parts, Change.Part); - elseif Change.Model then - table.insert(Models, Change.Model) + local Root = Change.Root + if Root:IsA("BasePart") then + table.insert(Parts, Root) + PartChangeSet[Root] = {} + end + for _, Descendant in Root:GetDescendants() do + if Descendant:IsA("BasePart") then + table.insert(Parts, Descendant) + PartChangeSet[Descendant] = {} + end end end; -- Ensure parts are selectable - if not (CanModifyItems(Parts) and CanModifyItems(Models)) then + if not (CanModifyItems(Parts)) then return; end; @@ -593,57 +588,39 @@ Actions = { end; -- Reorganize the changes - local PartChangeSet = {} - local ModelChangeSet = {} - for _, Change in pairs(Changes) do - if Change.Part then - Change.InitialState = { - Anchored = Change.Part.Anchored; - CFrame = Change.Part.CFrame; - } - PartChangeSet[Change.Part] = Change - elseif Change.Model then - ModelChangeSet[Change.Model] = Change.Pivot - end + local RootChangeSet = {} + local InitialPivot = {} + for _, Change in Changes do + RootChangeSet[Change.Root] = Change.Pivot + InitialPivot[Change.Root] = Change.Root:GetPivot() end; -- Preserve joints - for Part, Change in pairs(PartChangeSet) do + for Part, Change in PartChangeSet do Change.Joints = PreserveJoints(Part, PartChangeSet) end; - -- Perform each change - for Part, Change in pairs(PartChangeSet) do - - -- Stabilize the parts and maintain the original anchor state - Part.Anchored = true; + -- Stabilize parts + for Part, Change in PartChangeSet do Part:BreakJoints(); Part.Velocity = Vector3.new(); Part.RotVelocity = Vector3.new(); - - -- Set the part's CFrame - Part.CFrame = Change.CFrame; - end; - for Model, Pivot in pairs(ModelChangeSet) do - Model.WorldPivot = Pivot + for Root, Pivot in RootChangeSet do + Root:PivotTo(Pivot) end -- Make sure the player is authorized to move parts into this area if Security.ArePartsViolatingAreas(Parts, Player, false, AreaPermissions) then - - -- Revert changes if unauthorized destination - for Part, Change in pairs(PartChangeSet) do - Part.CFrame = Change.InitialState.CFrame; - end; - + for Root, Pivot in RootChangeSet do + Root:PivotTo(InitialPivot[Root]) + end end; -- Restore the parts' original states for Part, Change in pairs(PartChangeSet) do Part:MakeJoints(); RestoreJoints(Change.Joints); - Part.Anchored = Change.InitialState.Anchored; end; end; diff --git a/Tools/Move/FreeDragging.lua b/Tools/Move/FreeDragging.lua index dbee6db..a8f30e5 100644 --- a/Tools/Move/FreeDragging.lua +++ b/Tools/Move/FreeDragging.lua @@ -129,12 +129,12 @@ function FreeDragging:SetUpDragging(BasePart, BasePoint) Core.Targeting.CancelSelecting() -- Prepare parts, and start dragging - self.InitialPartStates, self.InitialModelStates = self.Tool:PrepareSelectionForDragging() - self:StartDragging(BasePart, self.InitialPartStates, self.InitialModelStates, BasePoint) + self.InitialPartStates, self.InitialRootStates = self.Tool:PrepareSelectionForDragging() + self:StartDragging(BasePart, self.InitialPartStates, self.InitialRootStates, BasePoint) end -function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelStates, BasePoint) +function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialRootStates, BasePoint) -- Begins dragging the selection -- Ensure dragging is not already ongoing @@ -146,7 +146,7 @@ function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelSta self.IsDragging = true -- Track changes - self.Tool:TrackChange() + self.Tool:TrackChange(InitialRootStates) -- Disable bounding box calculation BoundingBox.ClearBoundingBox() @@ -185,14 +185,17 @@ function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelSta self.Tool.Maid.DragSnapping = self.Tool.PointSnapped:Connect(function (SnappedPoint) -- Align the selection's base point to the snapped point - local Rotation = self.SurfaceAlignment or (InitialPartStates[BasePart].CFrame - InitialPartStates[BasePart].CFrame.p) - BasePart.CFrame = CFrame.new(SnappedPoint) * Rotation * CFrame.new(BasePartOffset) - MoveUtil.TranslatePartsRelativeToPart(BasePart, InitialPartStates, InitialModelStates) + local Rotation = self.SurfaceAlignment or InitialPartStates[BasePart].CFrame.Rotation + local TargetCFrame = CFrame.new(SnappedPoint) * Rotation * CFrame.new(BasePartOffset) + + -- Move the parts + self:MoveBasePartAndRoots(BasePart, TargetCFrame, InitialPartStates, InitialRootStates) -- Make sure we're not entering any unauthorized private areas if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - BasePart.CFrame = InitialPartStates[BasePart].CFrame - MoveUtil.TranslatePartsRelativeToPart(BasePart, InitialPartStates, InitialModelStates) + for Root, InitialPivot in InitialRootStates do + Root:PivotTo(InitialPivot) + end end end) @@ -204,7 +207,7 @@ function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelSta self.TriggerAlignment = function () -- Trigger drag recalculation - self:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialModelStates, AreaPermissions) + self:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialRootStates, AreaPermissions) -- Trigger snapping recalculation if SnapTracking.Enabled then @@ -215,7 +218,7 @@ function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelSta local function HandleDragChange(Action, State, Input) if State.Name == 'Change' then - self:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialModelStates, AreaPermissions) + self:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialRootStates, AreaPermissions) end return Enum.ContextActionResult.Pass end @@ -228,7 +231,17 @@ function FreeDragging:StartDragging(BasePart, InitialPartStates, InitialModelSta end -function FreeDragging:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialModelStates, AreaPermissions) +function FreeDragging:MoveBasePartAndRoots(BasePart, TargetCFrame, InitialPartStates, InitialRootStates) + local OldBasePartCFrame = InitialPartStates[BasePart].CFrame + local GlobalTransform = TargetCFrame * OldBasePartCFrame:Inverse() + + -- Apply the global transform to each root + for Root, InitialPivot in InitialRootStates do + Root:PivotTo(GlobalTransform * InitialPivot) + end +end + +function FreeDragging:DragToMouse(BasePart, BasePartOffset, InitialPartStates, InitialRootStates, AreaPermissions) -- Drags the selection by `BasePart`, judging area authorization from `AreaPermissions` ---------------------------------------------- @@ -311,8 +324,8 @@ function FreeDragging:DragToMouse(BasePart, BasePartOffset, InitialPartStates, I end -- Move the selection, retracted by the max. crossthrough amount - BasePart.CFrame = TargetCFrame - (self.TargetNormal * self.CrossthroughCorrection) - MoveUtil.TranslatePartsRelativeToPart(BasePart, InitialPartStates, InitialModelStates) + local FinalCFrame = TargetCFrame - (self.TargetNormal * self.CrossthroughCorrection) + self:MoveBasePartAndRoots(BasePart, FinalCFrame, InitialPartStates, InitialRootStates) ---------------------------------------- -- Check for relevant area authorization @@ -320,8 +333,9 @@ function FreeDragging:DragToMouse(BasePart, BasePartOffset, InitialPartStates, I -- Make sure we're not entering any unauthorized private areas if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - BasePart.CFrame = InitialPartStates[BasePart].CFrame - MoveUtil.TranslatePartsRelativeToPart(BasePart, InitialPartStates, InitialModelStates) + for Root, InitialPivot in InitialRootStates do + Root:PivotTo(InitialPivot) + end end end diff --git a/Tools/Move/HandleDragging.lua b/Tools/Move/HandleDragging.lua index f346d4a..f27eca9 100644 --- a/Tools/Move/HandleDragging.lua +++ b/Tools/Move/HandleDragging.lua @@ -83,13 +83,13 @@ function HandleDragging:AttachHandles(Part, Autofocus) end -- Stop parts from moving, and capture the initial state of the parts - local InitialPartStates, InitialModelStates, InitialFocusCFrame = self.Tool:PrepareSelectionForDragging() + local InitialPartStates, InitialRootStates, InitialFocusCFrame = self.Tool:PrepareSelectionForDragging() self.InitialPartStates = InitialPartStates - self.InitialModelStates = InitialModelStates + self.InitialRootStates = InitialRootStates self.InitialFocusCFrame = InitialFocusCFrame -- Track the change - self.Tool:TrackChange() + self.Tool:TrackChange(InitialRootStates) -- Cache area permissions information if Core.Mode == 'Tool' then @@ -110,13 +110,13 @@ function HandleDragging:AttachHandles(Part, Autofocus) Distance = MoveUtil.GetIncrementMultiple(Distance, self.Tool.Increment) -- Move the parts along the selected axes by the calculated distance - self.Tool:MovePartsAlongAxesByFace(Face, Distance, self.InitialPartStates, self.InitialModelStates, self.InitialFocusCFrame) + self.Tool:MovePartsAlongAxesByFace(Face, Distance, self.InitialPartStates, self.InitialRootStates, self.InitialFocusCFrame) -- Make sure we're not entering any unauthorized private areas if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - local Part, InitialPartState = next(self.InitialPartStates) - Part.CFrame = InitialPartState.CFrame - MoveUtil.TranslatePartsRelativeToPart(Part, self.InitialPartStates, self.InitialModelStates) + for Root, InitialPivot in self.InitialRootStates do + Root:PivotTo(InitialPivot) + end Distance = 0 end diff --git a/Tools/Move/init.lua b/Tools/Move/init.lua index 67d84a8..c754aa6 100644 --- a/Tools/Move/init.lua +++ b/Tools/Move/init.lua @@ -6,6 +6,7 @@ local BoundingBox = require(Tool.Core.BoundingBox) -- Services local ContextActionService = game:GetService 'ContextActionService' local UserInputService = game:GetService 'UserInputService' +local RunService = game:GetService 'RunService' -- Libraries local Libraries = Tool:WaitForChild 'Libraries' @@ -141,18 +142,15 @@ function MoveTool:SetAxes(AxisMode) end --- Moves the given parts in `InitialStates`, along the given axis mode, in the given face direction, by the given distance. -function MoveTool:MovePartsAlongAxesByFace(Face, Distance, InitialPartStates, InitialModelStates, InitialFocusCFrame) +function MoveTool:MovePartsAlongAxesByFace(Face, Distance, InitialPartStates, InitialRootStates, InitialFocusCFrame) -- Calculate the shift along the direction of the face local Shift = Vector3.FromNormalId(Face) * Distance -- Move along global axes if self.Axes == 'Global' then - for Part, InitialState in pairs(InitialPartStates) do - Part.CFrame = InitialState.CFrame + Shift - end - for Model, InitialState in pairs(InitialModelStates) do - Model.WorldPivot = InitialState.Pivot + Shift + for Root, InitialPivot in pairs(InitialRootStates) do + Root:PivotTo(InitialPivot + Shift) end -- Move along individual items' axes @@ -160,9 +158,6 @@ function MoveTool:MovePartsAlongAxesByFace(Face, Distance, InitialPartStates, In for Part, InitialState in pairs(InitialPartStates) do Part.CFrame = InitialState.CFrame * CFrame.new(Shift) end - -- for Model, InitialState in pairs(InitialModelStates) do - -- Model.WorldPivot = InitialState.Pivot * CFrame.new(Shift) - -- end -- Move along focused item's axes elseif self.Axes == 'Last' then @@ -170,14 +165,10 @@ function MoveTool:MovePartsAlongAxesByFace(Face, Distance, InitialPartStates, In -- Calculate focused item's position local FocusCFrame = InitialFocusCFrame * CFrame.new(Shift) - -- Move parts based on initial offset from focus - for Part, InitialState in pairs(InitialPartStates) do - local FocusOffset = InitialFocusCFrame:toObjectSpace(InitialState.CFrame) - Part.CFrame = FocusCFrame * FocusOffset - end - for Model, InitialState in pairs(InitialModelStates) do - local FocusOffset = InitialFocusCFrame:ToObjectSpace(InitialState.Pivot) - Model.WorldPivot = FocusCFrame * FocusOffset + -- Move roots based on initial offset from focus + for Root, InitialPivot in pairs(InitialRootStates) do + local FocusOffset = InitialFocusCFrame:toObjectSpace(InitialPivot) + Root:PivotTo(FocusCFrame * FocusOffset) end end @@ -314,11 +305,11 @@ end function MoveTool:SetAxisPosition(Axis, Position) -- Sets the selection's position on axis `Axis` to `Position` - -- Track this change - self:TrackChange() - -- Prepare parts to be moved - local InitialPartStates = self:PrepareSelectionForDragging() + local InitialPartStates, InitialRootStates = self:PrepareSelectionForDragging() + + -- Track this change + self:TrackChange(InitialRootStates) -- Update each part for Part in pairs(InitialPartStates) do @@ -337,8 +328,8 @@ function MoveTool:SetAxisPosition(Axis, Position) -- Revert changes if player is not authorized to move parts to target destination if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - for Part, State in pairs(InitialPartStates) do - Part.CFrame = State.CFrame; + for Root, InitialPivot in InitialRootStates do + Root:PivotTo(InitialPivot) end; end; @@ -367,14 +358,14 @@ function MoveTool:NudgeSelectionByFace(Face) NudgeAmount = -NudgeAmount; end; - -- Track this change - self:TrackChange() - -- Prepare parts to be moved - local InitialPartStates, InitialModelStates, InitialFocusCFrame = self:PrepareSelectionForDragging() + local InitialPartStates, InitialRootStates, InitialFocusCFrame = self:PrepareSelectionForDragging() + -- Track this change + self:TrackChange(InitialRootStates) + -- Perform the movement - self:MovePartsAlongAxesByFace(Face, NudgeAmount, InitialPartStates, InitialModelStates, InitialFocusCFrame) + self:MovePartsAlongAxesByFace(Face, NudgeAmount, InitialPartStates, InitialRootStates, InitialFocusCFrame) -- Indicate updated drag distance self.DragChanged:Fire(NudgeAmount) @@ -384,9 +375,9 @@ function MoveTool:NudgeSelectionByFace(Face) -- Revert changes if player is not authorized to move parts to target destination if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - for Part, State in pairs(InitialPartStates) do - Part.CFrame = State.CFrame; - end; + for Root, InitialPivot in InitialRootStates do + Root:SetPivot(InitialPivot) + end end; -- Restore the parts' original states @@ -402,12 +393,10 @@ function MoveTool:NudgeSelectionByFace(Face) end -function MoveTool:TrackChange() - +function MoveTool:TrackChangeParts() -- Start the record self.HistoryRecord = { Parts = Support.CloneTable(Selection.Parts); - Models = Support.CloneTable(Selection.Models); BeforeCFrame = {}; AfterCFrame = {}; Selection = Selection.Items; @@ -426,15 +415,9 @@ function MoveTool:TrackChange() CFrame = Record.BeforeCFrame[Part]; }) end - for _, Model in ipairs(Record.Models) do - table.insert(Changes, { - Model = Model; - Pivot = Record.BeforeCFrame[Model]; - }) - end -- Send the change request - Core.SyncAPI:Invoke('SyncMove', Changes); + Core.SyncAPI:Invoke('SyncPartTransform', Changes); end; @@ -452,30 +435,87 @@ function MoveTool:TrackChange() CFrame = Record.AfterCFrame[Part]; }) end - for _, Model in ipairs(Record.Models) do + + -- Send the change request + Core.SyncAPI:Invoke('SyncPartTransform', Changes); + + end; + + }; + + -- Collect the selection's initial state + for _, Part in pairs(self.HistoryRecord.Parts) do + self.HistoryRecord.BeforeCFrame[Part] = Part.CFrame + end +end + +function MoveTool:TrackChangeRoots(RootMapping) + local Roots = {} + for Root, _ in RootMapping do + table.insert(Roots, Root) + end + + -- Start the record + self.HistoryRecord = { + Roots = Roots, + BeforePivot = {}; + AfterPivot = {}; + Selection = Selection.Items; + + Unapply = function (Record) + -- Reverts this change + + -- Select the changed parts + Selection.Replace(Record.Selection) + + -- Put together the change request + local Changes = {} + for _, Root in Record.Roots do + table.insert(Changes, { + Root = Root; + Pivot = Record.BeforePivot[Root]; + }) + end + + -- Send the change request + Core.SyncAPI:Invoke('SyncPartTransform', Changes); + + end; + + Apply = function (Record) + -- Applies this change + + -- Select the changed parts + Selection.Replace(Record.Selection) + + -- Put together the change request + local Changes = {}; + for _, Root in Record.Roots do table.insert(Changes, { - Model = Model; - Pivot = Record.AfterCFrame[Model]; + Root = Root; + Pivot = Record.AfterPivot[Root]; }) end -- Send the change request - Core.SyncAPI:Invoke('SyncMove', Changes); + Core.SyncAPI:Invoke('SyncPartTransform', Changes); end; }; -- Collect the selection's initial state - for _, Part in pairs(self.HistoryRecord.Parts) do - self.HistoryRecord.BeforeCFrame[Part] = Part.CFrame + for _, Root in self.HistoryRecord.Roots do + self.HistoryRecord.BeforePivot[Root] = Root:GetPivot() end - pcall(function () - for _, Model in ipairs(self.HistoryRecord.Models) do - self.HistoryRecord.BeforeCFrame[Model] = Model:GetPivot() - end - end) +end +function MoveTool:TrackChange(RootMapping) + if self.Axes == 'Local' then + self:TrackChangeParts() + else + self:TrackChangeRoots(RootMapping) + end end function MoveTool:RegisterChange() @@ -488,25 +528,29 @@ function MoveTool:RegisterChange() -- Collect the selection's final state local Changes = {} - for _, Part in pairs(self.HistoryRecord.Parts) do - self.HistoryRecord.AfterCFrame[Part] = Part.CFrame - table.insert(Changes, { - Part = Part; - CFrame = Part.CFrame; - }) - end; - pcall(function () - for _, Model in pairs(self.HistoryRecord.Models) do - self.HistoryRecord.AfterCFrame[Model] = Model:GetPivot() + if self.HistoryRecord.Roots then + for _, Root in self.HistoryRecord.Roots do + self.HistoryRecord.AfterPivot[Root] = Root:GetPivot() table.insert(Changes, { - Model = Model; - Pivot = Model:GetPivot(); + Root = Root; + Pivot = Root:GetPivot(); }) - end - end) + end; - -- Send the change to the server - Core.SyncAPI:Invoke('SyncMove', Changes); + -- Send the change to the server + Core.SyncAPI:Invoke('SyncRootTransform', Changes); + else + for _, Part in pairs(self.HistoryRecord.Parts) do + self.HistoryRecord.AfterCFrame[Part] = Part.CFrame + table.insert(Changes, { + Part = Part; + CFrame = Part.CFrame; + }) + end; + + -- Send the change to the server + Core.SyncAPI:Invoke('SyncPartTransform', Changes); + end -- Register the record and clear the staging Core.History.Add(self.HistoryRecord) @@ -517,7 +561,7 @@ end --- Prepares selection for dragging, and returns the initial state of the selection. function MoveTool:PrepareSelectionForDragging() local InitialPartStates = {} - local InitialModelStates = {} + local InitialRootStates = {} -- Get index of parts local PartIndex = Support.FlipTable(Selection.Parts) @@ -529,38 +573,33 @@ function MoveTool:PrepareSelectionForDragging() CanCollide = Part.CanCollide; CFrame = Part.CFrame; } - Part.Anchored = true; - Part.CanCollide = false; + -- Only do this at runtime because it is not needed at Edit time and + -- may disrupt packages. + if RunService:IsRunning() then + Part.Anchored = true; + Part.CanCollide = false; + Part.Velocity = Vector3.new(); + Part.RotVelocity = Vector3.new(); + end InitialPartStates[Part].Joints = Core.PreserveJoints(Part, PartIndex) Part:BreakJoints(); - Part.Velocity = Vector3.new(); - Part.RotVelocity = Vector3.new(); end; - -- Get initial model states (temporarily pcalled due to pivot API being in beta) - pcall(function () - for _, Model in ipairs(Selection.Models) do - InitialModelStates[Model] = { - Pivot = Model:GetPivot(); - } - end - end) + -- Record the initial position of each root PVInstance for movement + for _, PVInstance in Selection.GetRootPVInstances(Selection.Items) do + InitialRootStates[PVInstance] = PVInstance:GetPivot() + end; -- Get initial state of focused item local InitialFocusCFrame local Focus = Selection.Focus if not Focus then InitialFocusCFrame = nil - elseif Focus:IsA 'BasePart' then - InitialFocusCFrame = Focus.CFrame - elseif Focus:IsA 'Model' then - InitialFocusCFrame = Focus:GetModelCFrame() - pcall(function () - InitialFocusCFrame = Focus:GetPivot() - end) + elseif Focus:IsA 'PVInstance' then + InitialFocusCFrame = Focus:GetPivot() end - return InitialPartStates, InitialModelStates, InitialFocusCFrame + return InitialPartStates, InitialRootStates, InitialFocusCFrame end; -- Return the tool diff --git a/Tools/Rotate.lua b/Tools/Rotate.lua index 328c73b..f14e4e7 100644 --- a/Tools/Rotate.lua +++ b/Tools/Rotate.lua @@ -7,6 +7,7 @@ BoundingBox = require(Tool.Core.BoundingBox); local ContextActionService = game:GetService 'ContextActionService' local Workspace = game:GetService 'Workspace' local UserInputService = game:GetService('UserInputService') +local RunService = game:GetService 'RunService' -- Libraries local Libraries = Tool:WaitForChild 'Libraries' @@ -314,10 +315,10 @@ function AttachHandles(Part, Autofocus) end; -- Stop parts from moving, and capture the initial state of the parts - InitialPartStates, InitialModelStates = PrepareSelectionForRotating() + InitialPartStates, InitialRootStates = PrepareSelectionForRotating() -- Track the change - TrackChange(); + TrackChange(InitialRootStates); -- Cache area permissions information if Core.Mode == 'Tool' then @@ -328,15 +329,10 @@ function AttachHandles(Part, Autofocus) if RotateTool.Pivot == 'Center' then PivotPoint = BoundingBox.GetBoundingBox().CFrame; - -- Set the pivot point to the center of the focused part if in Last mode + -- Set the pivot point to the pivot of the focused part if in Last mode elseif RotateTool.Pivot == 'Last' and not CustomPivotPoint then - if Selection.Focus:IsA 'BasePart' then - PivotPoint = Selection.Focus.CFrame - elseif Selection.Focus:IsA 'Model' then - PivotPoint = Selection.Focus:GetModelCFrame() - pcall(function () - PivotPoint = Selection.Focus:GetPivot() - end) + if Selection.Focus:IsA 'PVInstance' then + PivotPoint = Selection.Focus:GetPivot() end end; @@ -360,15 +356,12 @@ function AttachHandles(Part, Autofocus) local DisplayedRotation = GetHandleDisplayDelta(Rotation); -- Perform the rotation - RotateSelectionAroundPivot(RotateTool.Pivot, PivotPoint, Axis, Rotation, InitialPartStates, InitialModelStates) + RotateSelectionAroundPivot(RotateTool.Pivot, PivotPoint, Axis, Rotation, InitialPartStates, InitialRootStates) -- Make sure we're not entering any unauthorized private areas if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - for Part, State in pairs(InitialPartStates) do - Part.CFrame = State.CFrame; - end; - for Model, State in pairs(InitialModelStates) do - Model.WorldPivot = State.Pivot + for PVInstance, InitialPivot in pairs(InitialRootStates) do + PVInstance:PivotTo(InitialPivot) end -- Reset displayed rotation delta @@ -447,52 +440,30 @@ function HideHandles() end; -function RotateSelectionAroundPivot(PivotMode, PivotPoint, Axis, Rotation, InitialPartStates, InitialModelStates) +function RotateSelectionAroundPivot(PivotMode, PivotPoint, Axis, Rotation, InitialPartStates, InitialRootStates) -- Rotates the given selection around `PivotMode` (using `PivotPoint` if applicable)'s `Axis` by `Rotation` -- Create a CFrame that increments rotation by `Rotation` around `Axis` local RotationCFrame = CFrame.fromAxisAngle(Vector3.FromAxis(Axis), math.rad(Rotation)); - - -- Rotate each part - for Part, InitialState in pairs(InitialPartStates) do - - -- Rotate around the selection's center, or the currently focused part - if PivotMode == 'Center' or PivotMode == 'Last' then - - -- Calculate the focused part's rotation - local RelativeTo = PivotPoint * RotationCFrame; - - -- Calculate this part's offset from the focused part's rotation - local Offset = PivotPoint:toObjectSpace(InitialState.CFrame); - - -- Rotate relative to the focused part by this part's offset from it - Part.CFrame = RelativeTo * Offset; - - -- Rotate around the part's center - elseif RotateTool.Pivot == 'Local' then + + if PivotMode == 'Center' or PivotMode == 'Last' then + -- Call PivotTo on each root to rotate the selection relative to the pivot + for PVInstance, InitialPivot in pairs(InitialRootStates) do + -- Rotate around the selection's center, or the currently focused part + if PivotMode == 'Center' or PivotMode == 'Last' then + -- Calculate the offset from the old pivot, and apply that + -- offset relative to the new one. + local Offset = PivotPoint:ToObjectSpace(InitialPivot) + local NewPivot = PivotPoint * RotationCFrame + PVInstance:PivotTo(NewPivot * Offset) + end + end + elseif PivotMode == 'Local' then + -- Rotate each part around its center (do not modify any pivots) + for Part, InitialState in pairs(InitialPartStates) do Part.CFrame = InitialState.CFrame * RotationCFrame; - end; - end; - - -- Rotate each model's pivot - for Model, InitialState in pairs(InitialModelStates) do - - -- Rotate around the selection's center, or the currently focused part - if (PivotMode == 'Center') or (PivotMode == 'Last') then - - -- Calculate the focused part's rotation - local RelativeTo = PivotPoint * RotationCFrame - - -- Calculate this part's offset from the focused part's rotation - local Offset = PivotPoint:ToObjectSpace(InitialState.Pivot) - - -- Rotate relative to the focused part by this model's offset from it - Model.WorldPivot = RelativeTo * Offset - end - end - end; function GetHandleDisplayDelta(HandleRotation) @@ -693,11 +664,11 @@ function SetAxisAngle(Axis, Angle) -- Turn the given angle from degrees to radians local Angle = math.rad(Angle); - -- Track this change - TrackChange(); - -- Prepare parts to be moved - local InitialPartStates = PrepareSelectionForRotating() + local InitialPartStates, InitialRootStates = PrepareSelectionForRotating() + + -- Track this change + TrackChange(InitialRootStates); -- Update each part for Part, State in pairs(InitialPartStates) do @@ -716,8 +687,8 @@ function SetAxisAngle(Axis, Angle) -- Revert changes if player is not authorized to move parts to target destination if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - for Part, State in pairs(InitialPartStates) do - Part.CFrame = State.CFrame; + for PVInstance, InitialPivot in pairs(InitialRootStates) do + PVInstance:PivotTo(InitialPivot) end; end; @@ -751,12 +722,12 @@ function NudgeSelectionByAxis(Axis, Direction) NudgeAmount = -NudgeAmount; end; - -- Track the change - TrackChange(); - -- Stop parts from moving, and capture the initial state of the parts - local InitialPartStates, InitialModelStates = PrepareSelectionForRotating() - + local InitialPartStates, InitialRootStates = PrepareSelectionForRotating() + + -- Track the change + TrackChange(InitialRootStates); + -- Set the pivot point to the center of the selection if in Center mode if RotateTool.Pivot == 'Center' then local BoundingBoxSize, BoundingBoxCFrame = BoundingBox.CalculateExtents(Selection.Parts); @@ -764,18 +735,13 @@ function NudgeSelectionByAxis(Axis, Direction) -- Set the pivot point to the center of the focused part if in Last mode elseif RotateTool.Pivot == 'Last' and not CustomPivotPoint then - if Selection.Focus:IsA 'BasePart' then - PivotPoint = Selection.Focus.CFrame - elseif Selection.Focus:IsA 'Model' then - PivotPoint = Selection.Focus:GetModelCFrame() - pcall(function () - PivotPoint = Selection.Focus:GetPivot() - end) + if Selection.Focus:IsA 'PVInstance' then + PivotPoint = Selection.Focus:GetPivot() end end; -- Perform the rotation - RotateSelectionAroundPivot(RotateTool.Pivot, PivotPoint, Axis, NudgeAmount * (Direction or 1), InitialPartStates, InitialModelStates) + RotateSelectionAroundPivot(RotateTool.Pivot, PivotPoint, Axis, NudgeAmount * (Direction or 1), InitialPartStates, InitialRootStates) -- Update the "degrees rotated" indicator if RotateTool.UI then @@ -787,11 +753,8 @@ function NudgeSelectionByAxis(Axis, Direction) -- Make sure we're not entering any unauthorized private areas if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then - for Part, State in pairs(InitialPartStates) do - Part.CFrame = State.CFrame; - end; - for Model, State in pairs(InitialModelStates) do - Model.WorldPivot = State.Pivot + for PVInstance, InitialPivot in pairs(InitialRootStates) do + PVInstance:PivotTo(InitialPivot) end end; @@ -808,12 +771,19 @@ function NudgeSelectionByAxis(Axis, Direction) end; -function TrackChange() +function TrackChange(RootMapping) + if RotateTool.Pivot == 'Local' then + TrackChangeParts() + else + TrackChangeRoots(RootMapping) + end +end + +function TrackChangeParts() -- Start the record HistoryRecord = { Parts = Support.CloneTable(Selection.Parts); - Models = Support.CloneTable(Selection.Models); BeforeCFrame = {}; AfterCFrame = {}; Selection = Selection.Items; @@ -832,15 +802,9 @@ function TrackChange() CFrame = Record.BeforeCFrame[Part]; }) end; - for _, Model in pairs(Record.Models) do - table.insert(Changes, { - Model = Model; - Pivot = Record.BeforeCFrame[Model]; - }) - end -- Send the change request - Core.SyncAPI:Invoke('SyncRotate', Changes); + Core.SyncAPI:Invoke('SyncPartTransform', Changes); end; @@ -858,15 +822,9 @@ function TrackChange() CFrame = Record.AfterCFrame[Part]; }) end; - for _, Model in pairs(Record.Models) do - table.insert(Changes, { - Model = Model; - Pivot = Record.AfterCFrame[Model]; - }) - end -- Send the change request - Core.SyncAPI:Invoke('SyncRotate', Changes); + Core.SyncAPI:Invoke('SyncPartTransform', Changes); end; @@ -876,11 +834,67 @@ function TrackChange() for _, Part in pairs(HistoryRecord.Parts) do HistoryRecord.BeforeCFrame[Part] = Part.CFrame; end; - pcall(function () - for _, Model in pairs(HistoryRecord.Models) do - HistoryRecord.BeforeCFrame[Model] = Model:GetPivot() - end - end) +end; + +function TrackChangeRoots(RootMap) + local Roots = {} + for Root, _ in RootMap do + table.insert(Roots, Root) + end + + -- Start the record + HistoryRecord = { + Roots = Roots; + BeforePivot = {}; + AfterPivot = {}; + Selection = Selection.Items; + + Unapply = function (Record) + -- Reverts this change + + -- Select the changed parts + Selection.Replace(Record.Selection) + + -- Put together the change request + local Changes = {}; + for _, Root in Record.Roots do + table.insert(Changes, { + Root = Root; + Pivot = Record.BeforePivot[Root]; + }) + end; + + -- Send the change request + Core.SyncAPI:Invoke('SyncRootTransform', Changes); + + end; + + Apply = function (Record) + -- Applies this change + + -- Select the changed parts + Selection.Replace(Record.Selection) + + -- Put together the change request + local Changes = {}; + for _, Root in pairs(Record.Roots) do + table.insert(Changes, { + Root = Root; + Pivot = Record.AfterPivot[Root]; + }) + end; + + -- Send the change request + Core.SyncAPI:Invoke('SyncRootTransform', Changes); + + end; + + }; + + -- Collect the selection's initial state + for _, Root in HistoryRecord.Roots do + HistoryRecord.BeforePivot[Root] = Root:GetPivot(); + end; end; function RegisterChange() @@ -893,25 +907,29 @@ function RegisterChange() -- Collect the selection's final state local Changes = {}; - for _, Part in pairs(HistoryRecord.Parts) do - HistoryRecord.AfterCFrame[Part] = Part.CFrame; - table.insert(Changes, { - Part = Part; - CFrame = Part.CFrame; - }) - end; - pcall(function () - for _, Model in pairs(HistoryRecord.Models) do - HistoryRecord.AfterCFrame[Model] = Model:GetPivot() + if HistoryRecord.Roots then + for _, Root in pairs(HistoryRecord.Roots) do + HistoryRecord.AfterPivot[Root] = Root:GetPivot(); table.insert(Changes, { - Model = Model; - Pivot = Model:GetPivot(); + Root = Root; + Pivot = Root:GetPivot(); }) - end - end) - - -- Send the change to the server - Core.SyncAPI:Invoke('SyncRotate', Changes); + end; + + -- Send the change to the server + Core.SyncAPI:Invoke('SyncRootTransform', Changes); + else + for _, Part in pairs(HistoryRecord.Parts) do + HistoryRecord.AfterCFrame[Part] = Part.CFrame; + table.insert(Changes, { + Part = Part; + CFrame = Part.CFrame; + }) + end; + + -- Send the change to the server + Core.SyncAPI:Invoke('SyncPartTransform', Changes); + end -- Register the record and clear the staging Core.History.Add(HistoryRecord); @@ -923,7 +941,7 @@ function PrepareSelectionForRotating() -- Prepares parts for rotating and returns the initial state of the parts local InitialPartStates = {} - local InitialModelStates = {} + local InitialRootStates = {} -- Get index of parts local PartIndex = Support.FlipTable(Selection.Parts); @@ -935,25 +953,24 @@ function PrepareSelectionForRotating() CanCollide = Part.CanCollide; CFrame = Part.CFrame; } - Part.Anchored = true; - Part.CanCollide = false; + -- Only do this at runtime because it is not needed at Edit time and + -- may disrupt packages. + if RunService:IsRunning() then + Part.Anchored = true; + Part.CanCollide = false; + Part.Velocity = Vector3.new(); + Part.RotVelocity = Vector3.new(); + end InitialPartStates[Part].Joints = Core.PreserveJoints(Part, PartIndex); Part:BreakJoints(); - Part.Velocity = Vector3.new(); - Part.RotVelocity = Vector3.new(); + end; + + -- Record the initial position of each root PVInstance for movement + for _, PVInstance in Selection.GetRootPVInstances(Selection.Items) do + InitialRootStates[PVInstance] = PVInstance:GetPivot() end; - -- Record model pivots - -- (temporarily pcalled due to pivot API being in beta) - pcall(function () - for _, Model in pairs(Selection.Models) do - InitialModelStates[Model] = { - Pivot = Model:GetPivot(); - } - end - end) - - return InitialPartStates, InitialModelStates + return InitialPartStates, InitialRootStates; end; function GetIncrementMultiple(Number, Increment)