diff --git a/Core/Selection.lua b/Core/Selection.lua index cc70044e..ca317b37 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 4db97e95..6f3cc13d 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 dbee6dbb..a8f30e5e 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 f346d4ab..f27eca95 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 67d84a81..c754aa63 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 328c73be..f14e4e71 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)