ArcService

Run Settings
LanguageLua
Language Version
Run Command
local Util do Util = {}; function Util:getBoundingBox(object: PVInstance) if object:IsA("Model") then return object:GetBoundingBox(); end return object.CFrame, object.Size; end end local Settings = { Axis = "Z"; RotationType = "Yaw"; ResizeMethod = "OuterTouch"; Alignment = "Inside"; WindowMode = "ScreenGui"; Angle = 10; RenderAmount = 1; FlipAxis = false; SwapSides = false; Handles = false; AutoResize = false; FixNeighbors = true; ShowOverlap = true; UsePrimaryPart = false; Enabled = true; Alternate = false; Multidirectional = false; WidgetOpened = true; ResizeWarningPromptOpened = false; Offset = Vector3.new(); Orientation = Vector3.new() }; local ResizeAlign = {}; function ResizeAlign:clearFolder() end function ResizeAlign:getNormal(target: PVInstance, normalId: Enum.NormalId) local cframe, size = Util:getBoundingBox(target); return (cframe * (Vector3.FromNormalId(normalId) * size/2)); end function ResizeAlign:isIntersecting(point, part) return table.find(workspace:GetPartBoundsInBox(CFrame.new(point), Vector3.new(1, .1, 1)), part); end function ResizeAlign:getClosestFace(part: BasePart, position: Vector3) local closest = -math.huge; local closestNormalId; for _, normalId in ipairs(Enum.NormalId:GetEnumItems()) do local normal = Vector3.FromNormalId(normalId); local offset = part.CFrame:VectorToWorldSpace(normal); local point = part.CFrame:PointToWorldSpace(normal); local dot = (position-point):Dot(offset); if dot>closest then closest = dot; closestNormalId = normalId; end end return closestNormalId, closest; end function ResizeAlign:isOverlappingPart(a: BasePart, b:BasePart) return table.find(workspace:GetPartBoundsInBox(a.CFrame, a.Size), b); end local function createSurface(data) end function ResizeAlign:getOppositeNormalId(normalId: Enum.NormalId) local vector = Vector3.FromNormalId(normalId) * -1; for _,v in ipairs(Enum.NormalId:GetEnumItems()) do if Vector3.FromNormalId(v)==vector then return v; end end end local thickness = 1; function ResizeAlign:getIntersectingParts(instance: PVInstance, instance2: PVInstance, normalId: Enum.NormalId, getAll: boolean) if instance and instance2 then if instance:IsA("BasePart") and instance2:IsA("BasePart") then local list = {}; list[instance] = { TargetPart = instance2; TargetFace = self:getOppositeNormalId(normalId); FromFace = normalId; }; return list; end local edgeA = self:getNormal(instance, normalId); local edgeB = self:getNormal(instance2, self:getOppositeNormalId(normalId)); thickness = math.clamp((edgeA-edgeB).Magnitude*2, 1, math.huge); end local normal = Vector3.FromNormalId(normalId); local positive = if normal.X<0 or normal.Y<0 or normal.Z<0 then normal*-1 else normal; local cframe, size = Util:getBoundingBox(instance); local offset = normal * size * 0.5; --cframe = cframe:ToWorldSpace(CFrame.new(offset)-(normal*thickness/2.25)); cframe = cframe:ToWorldSpace(CFrame.new(offset)); size = (positive*thickness)+(size*-positive+size); local parts = workspace:GetPartBoundsInBox(cframe, size); --local p = Instance.new("Part"); --p.CFrame = cframe; --p.Size = size; --p.Transparency = 1; --local sb = Instance.new("SelectionBox"); --sb.Adornee = p; --sb.LineThickness = 0.025; --sb.Color3 = Color3.new(1, 0, 0); --sb.SurfaceColor3 = Color3.new(1, 0, 0); --sb.SurfaceTransparency = .8; --sb.Parent = debugFolder; --p.Parent = debugFolder; local mappedList = {}; local count = 0; if getAll then for _,v in ipairs(parts) do if not v:IsDescendantOf(instance) then table.insert(mappedList, v); end end return mappedList; end for _,v in ipairs(parts) do if v:IsDescendantOf(instance) then local vId = v:GetAttribute("ArchimedesID"); for _,p in ipairs(parts) do local closestFace = self:getClosestFace(v, p.Position); local overlapPos = self:getNormal(v, closestFace); local matchingId = vId and v~=p and p:GetAttribute("ArchimedesID")==vId; local isDesc = if instance2 then p:IsDescendantOf(instance2) else not p:IsDescendantOf(instance); if v~=p and (not mappedList[p] or matchingId) and isDesc then --if v~=p and not mappedList[p] and isDesc then local intersects = self:isIntersecting(overlapPos, p); if intersects then local cf = self:getClosestFace(p, overlapPos); if cf then count += 1; mappedList[p] = { TargetPart = v; TargetFace = closestFace; FromFace = cf; }; end end end end end end return mappedList, count; end function ResizeAlign:mapSurfaces(a: PVInstance, b: PVInstance, face: Enum.NormalId, showSurfaces: boolean) local list, total = self:getIntersectingParts(a, b, face); if showSurfaces then local count = 0; for i,v in pairs(list) do local color = nil; createSurface({ Part = i; Color = color or Color3.new(0, 1, 0); Face = v.FromFace; }); createSurface({ Part = v.TargetPart; Color = color or Color3.new(1, 0, 0); Face = v.TargetFace; }); count+=1; end end return list; end function resizePart(part, normal, delta) local axis = Vector3.FromNormalId(normal) local cf = part.CFrame local targetSize = part.Size + Vector3.new(math.abs(axis.X), math.abs(axis.Y), math.abs(axis.Z))*delta if not part:IsA('FormFactorPart') then -- Nothing to do, can't modify formfactor anyways elseif part.FormFactor == Enum.FormFactor.Brick then if targetSize.X % 1 ~= 0 or targetSize.Y % 1.2 ~= 0 or targetSize.Z % 1 ~= 0 then part.FormFactor = 'Custom' end elseif part.FormFactor == Enum.FormFactor.Symmetric then if targetSize.X % 1 ~= 0 or targetSize.Y % 1 ~= 0 or targetSize.Z % 1 ~= 0 then part.FormFactor = 'Custom' end elseif part.FormFactor == Enum.FormFactor.Plate then if targetSize.X % 1 ~= 0 or targetSize.Y % 0.4 ~= 0 or targetSize.Z % 1 ~= 0 then part.FormFactor = 'Custom' end else -- nothing to do, is custom end part:BreakJoints() part.Size = targetSize part:BreakJoints() part.CFrame = cf * CFrame.new(axis * (delta/2)) end function getPositivePointToFace(face, points) local hsize = face.Object.Size / 2 local faceDir = Vector3.FromNormalId(face.Normal) local faceNormal = face.Object.CFrame:vectorToWorldSpace(faceDir) local facePoint = face.Object.CFrame:pointToWorldSpace(faceDir * hsize) -- local maxDist = -math.huge local maxPoint = nil for _, point in pairs(points) do local dist = (point - facePoint):Dot(faceNormal) if dist > maxDist then maxDist = dist maxPoint = point end end return maxPoint end function getNegativePointToFace(face, points) local hsize = face.Object.Size / 2 local faceDir = Vector3.FromNormalId(face.Normal) local faceNormal = face.Object.CFrame:vectorToWorldSpace(faceDir) local facePoint = face.Object.CFrame:pointToWorldSpace(faceDir * hsize) -- local minDist = math.huge local minPoint = nil for _, point in pairs(points) do local dist = (point - facePoint):Dot(faceNormal) if dist < minDist then minDist = dist minPoint = point end end return minPoint end local function otherNormals(dir) if math.abs(dir.X) > 0 then return Vector3.new(0, 1, 0), Vector3.new(0, 0, 1) elseif math.abs(dir.Y) > 0 then return Vector3.new(1, 0, 0), Vector3.new(0, 0, 1) else return Vector3.new(1, 0, 0), Vector3.new(0, 1, 0) end end local function getFacePoints(face) local hsize = face.Object.Size / 2 local faceDir = Vector3.FromNormalId(face.Normal) local faceA, faceB = otherNormals(faceDir) faceDir, faceA, faceB = faceDir*hsize, faceA*hsize, faceB*hsize -- local function sp(offset) return (face.Object.CFrame * CFrame.new(offset)).p end -- return { sp(faceDir + faceA + faceB); sp(faceDir + faceA - faceB); sp(faceDir - faceA - faceB); sp(faceDir - faceA + faceB); } end local function getDimension(face) local dir = Vector3.FromNormalId(face.Normal) return Vector3.new(math.abs(dir.X), math.abs(dir.Y), math.abs(dir.Z)) end local function getNormal(face) return face.Object.CFrame:vectorToWorldSpace(Vector3.FromNormalId(face.Normal)) end local function getPoints(part) local hsize = part.Size / 2 local cf = part.CFrame local points = {} for i = -1, 1, 2 do for j = -1, 1, 2 do for k = -1, 1, 2 do table.insert(points, cf:pointToWorldSpace(Vector3.new(i, j, k) * hsize)) end end end return points end function ResizeAlign:doExtend(faceA, faceB, mode) -- local pointsA = getFacePoints(faceA) local pointsB = getFacePoints(faceB) -- local extendPointA, extendPointB; if mode == 'ExtendInto' or mode == 'OuterTouch' or mode == 'ButtJoint' then extendPointA = getPositivePointToFace(faceB, pointsA) extendPointB = getPositivePointToFace(faceA, pointsB) elseif mode == 'ExtendUpto' or mode == 'InnerTouch' then extendPointA = getNegativePointToFace(faceB, pointsA) extendPointB = getNegativePointToFace(faceA, pointsB) else assert(false, "unreachable") end local startSep = extendPointB - extendPointA -- local localDimensionA = getDimension(faceA) local localDimensionB = getDimension(faceB) local dirA = getNormal(faceA) local dirB = getNormal(faceB) -- -- Find the closest distance between the rays (extendPointA, dirA) and (extendPointB, dirB): -- See: http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment local a, b, c, d, e = dirA:Dot(dirA), dirA:Dot(dirB), dirB:Dot(dirB), dirA:Dot(startSep), dirB:Dot(startSep) local denom = a*c - b*b -- Is this a degenerate case? if math.abs(denom) < 0.001 then -- Parts are parallel, extend faceA to faceB local lenA = (extendPointA - extendPointB):Dot(getNormal(faceB)) local extendableA = (localDimensionA * faceA.Object.Size).magnitude if getNormal(faceA):Dot(getNormal(faceB)) > 0 then lenA = -lenA end if lenA < -extendableA then return end resizePart(faceA.Object, faceA.Normal, lenA) return end -- Get the distances to extend by local lenA = -(b*e - c*d) / denom local lenB = -(a*e - b*d) / denom if mode == 'ExtendInto' or mode == 'ExtendUpto' then -- We need to find a different lenA, which is the intersection of -- extendPointA to the plane faceB: -- dist to plane (point, normal) = - (ray_dir . normal) / ((ray_origin - point) . normal) local denom2 = dirA:Dot(dirB) --if math.abs(denom2) > 0.0001 then -- lenA = - (extendPointA - extendPointB):Dot(dirB) / denom2 -- lenB = 0 --else -- Perpendicular -- Project all points of faceB onto faceA and extend by that much local points = getPoints(faceB.Object) if mode == 'ExtendUpto' then local smallestLen = math.huge for _, v in pairs(points) do local dist = (v - extendPointA):Dot(getNormal(faceA)) if dist < smallestLen then smallestLen = dist end end lenA = smallestLen elseif mode == 'ExtendInto' then local largestLen = -math.huge for _, v in pairs(points) do local dist = (v - extendPointA):Dot(getNormal(faceA)) if dist > largestLen then largestLen = dist end end lenA = largestLen end lenB = 0 --end end -- Are both extents doable? -- Note: Negative amounts to extend by *are* allowed, but only -- up to the size of the part on the dimension being extended on. local extendableA = (localDimensionA * faceA.Object.Size).magnitude local extendableB = (localDimensionB * faceB.Object.Size).magnitude if lenA < -extendableA then return end if lenB < -extendableB then return end -- Both are doable, execute: resizePart(faceA.Object, faceA.Normal, lenA) resizePart(faceB.Object, faceB.Normal, lenB) -- For a butt joint, we want to resize back one of the parts by the thickness -- of the other part on that axis. Renize the first part (A), such that it -- "butts up against" the second part (B). if mode == 'ButtJoint' then -- Find the width of B on the axis A, which is the amount to resize by local points = getPoints(faceB.Object) local minV = math.huge local maxV = -math.huge for _, v in pairs(points) do local proj = (v - extendPointA):Dot(dirA) if proj < minV then minV = proj end if proj > maxV then maxV = proj end end resizePart(faceA.Object, faceA.Normal, -(maxV - minV)) end end local HttpService = game:GetService("HttpService"); local ServerStorage = game:GetService("ServerStorage"); local ArcService = {}; ArcService.Settings = Settings; ArcService.Previews = {}; local mappedFaces = { X = Enum.NormalId.Right; Y = Enum.NormalId.Top; Z = Enum.NormalId.Front; }; local mappedSwappedFaces = { X = Enum.NormalId.Left; Y = Enum.NormalId.Bottom; Z = Enum.NormalId.Back; }; local mappedSurfaces = {}; local normals = {}; for _,enum in ipairs(Enum.NormalId:GetEnumItems()) do normals[enum] = Vector3.FromNormalId(enum); end local rotations = { X = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Back], normals[mappedFaces.X]); Y = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Right], normals[mappedFaces.Y]); Z = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Top], normals[mappedFaces.Z]); }; local rotationsSwapped = { X = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Back]*-1, normals[mappedFaces.X]*-1); Y = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Right]*-1, normals[mappedFaces.Y]*-1); Z = CFrame.fromMatrix(Vector3.new(), normals[Enum.NormalId.Top]*-1, normals[mappedFaces.Z]*-1); }; local alignmentOffsets = { Inside = 0.5; Middle = 0; Outside = -0.5; }; local pluginStorage = ServerStorage:FindFirstChild("Archimedes_Storage") function ArcService:saveSettings() -- do nothing because no plugin yippee end function ArcService:set(index, value, skipPreview) if typeof(index)=="table" then for i,v in pairs(index) do self:set(i, v, true); end else Settings[index] = value; end if index~="Angle" then mappedSurfaces = {}; if index=="AutoResize" or index=="Enabled" and not value then ResizeAlign:clearFolder(); end self:saveSettings(); end if not skipPreview then self:preview(); end end function ArcService:canRotate(object) return not object:IsA("Terrain") and not object:IsA("WorldRoot") and (object:IsA("BasePart") or object:IsA("Model")); end local function getPivotOffset(target) if target:IsA("BasePart") then return target.PivotOffset; elseif target.PrimaryPart then return target.PrimaryPart.PivotOffset; elseif target:IsA("Model") then local cframe = Util:getBoundingBox(target); return target:GetPivot():ToObjectSpace(cframe); else return CFrame.new(); end end local function addToStorage(object) if not object:IsA("Model") then return end object:SetAttribute("ArchimedesID", HttpService:GenerateGUID(false)); for _,v in ipairs(object:GetDescendants()) do if v:IsA("BasePart") then v:SetAttribute("ArchimedesID", HttpService:GenerateGUID(false)); end end if not pluginStorage then pluginStorage = Instance.new("Folder"); pluginStorage.Name = "Archimedes_Storage"; pluginStorage.Parent = ServerStorage; end object:Clone().Parent = pluginStorage; end local function getOriginalModel(object) local id = object:GetAttribute("ArchimedesID"); if not id then return end if pluginStorage then for _,v in ipairs(pluginStorage:GetChildren()) do if v:GetAttribute("ArchimedesID")==id then return v; end end end ArcService:clearAttributes(object); end local function toggleJoin(object, state) if state then workspace:JoinToOutsiders({object}, Enum.JointCreationMode.None); else workspace:UnjoinFromOutsiders({object}); end end local function clone(object) object = getOriginalModel(object) or object; return object:Clone(); end function ArcService:transform(fromObject, toCFrame, object) local offset, size = Util:getBoundingBox(fromObject); local original = getOriginalModel(fromObject); if original then local _, originalSize = Util:getBoundingBox(original); offset = offset:ToWorldSpace(CFrame.new(self:getNormal()*(originalSize-size)*0.5)); end self:setCFrame(object, toCFrame*offset); end local function getRoots(object, list) list = list or {}; local rootPart = object:IsA("BasePart") and object:GetRootPart(); if rootPart then list[rootPart] = true; end for _,v in ipairs(object:GetChildren()) do list = getRoots(v, list); end return list; end local function forcePrimaryPart(object) local primaryPart = object.PrimaryPart; if primaryPart then return end local largestSize, largestPart = -math.huge, nil; for _, v in ipairs(object:GetDescendants()) do if v:IsA("BasePart") then local size = v.Size.Magnitude; if size>largestSize then largestSize = size; largestPart = v; end end end if largestPart then object.PrimaryPart = largestPart; end end --local function transformModel(object, finalCF) -- forcePrimaryPart(object); -- local originalCF = object:GetModelCFrame(); -- for part, _ in pairs(getRoots(object)) do -- part.CFrame = finalCF:ToWorldSpace(originalCF:ToObjectSpace(part.CFrame)); -- end --end function ArcService:setCFrame(object, cframe, ignoreSettings) local objectCFrame = Util:getBoundingBox(object) if not ignoreSettings then local offset = Settings.Offset offset = offset and CFrame.new(offset) local offsetOrientation = Settings.Orientation if offsetOrientation then offset *= CFrame.fromOrientation(math.rad(offsetOrientation.X), math.rad(offsetOrientation.Y), math.rad(offsetOrientation.Z)) end --if Settings.Pivot then -- offset *= getPivotOffset(object) --end if offset then cframe = cframe:ToWorldSpace(offset) end end if object:IsA("BasePart") then object.CFrame = cframe; elseif Settings.Pivot then object:PivotTo(cframe); else object:PivotTo(cframe:ToWorldSpace(objectCFrame:ToObjectSpace(object:GetPivot()))) end end function ArcService:push(object) local preview, isNew = self:addPreview(object); local original = getOriginalModel(object); local cframe, size = Util:getBoundingBox(object); local offset = cframe:ToWorldSpace(CFrame.new(size*self:getNormal())); if original then local _, originalSize = Util:getBoundingBox(original); offset = offset:ToWorldSpace(CFrame.new(self:getNormal()*(originalSize-size)*0.5)); end self:setCFrame(preview, offset, true); if isNew then preview.Parent = object.Parent; end return preview; end function ArcService:filterSelection(sel) local list = {}; for _,v in ipairs(sel) do if not v:GetAttribute("ArchimedesPreview") and self:canRotate(v) then table.insert(list, v); end end self.Selection = list; return list; end function ArcService:getSelection(sel) if Settings.Enabled then return self.Selection; end return self:filterSelection(sel); end function ArcService:clearAttributes(object) local objects = object and {object} for _,object in ipairs(objects) do local list = object:GetDescendants(); table.insert(list, object); for _,v in ipairs(list) do if v:IsA("BasePart") or v==object then local attributes = v:GetAttributes(); for index, value in pairs(attributes) do if string.find(string.lower(index), "archimedes") then v:SetAttribute(index, nil); end end end end end end local function globalTransform(faceCFrame, localTransform) return faceCFrame * localTransform * faceCFrame:Inverse(); end function ArcService:getAngles(localSize, faceCFrame, size) local dir = alignmentOffsets[Settings.Alignment]; local currentOffsetAngle = math.rad(math.abs(Settings.Angle)); local halfOffset = CFrame.new(0,(0.5 * localSize.Y), 0); local multiplier = Settings.FlipAxis and -1 or 1; if Settings.Angle<0 then multiplier*=-1; end local x = globalTransform(faceCFrame, halfOffset * CFrame.new(multiplier*dir*localSize.X, 0, 0) * CFrame.Angles(0, 0, multiplier*currentOffsetAngle) * CFrame.new(-multiplier*dir*localSize.X, 0, 0) * halfOffset); local z = globalTransform(faceCFrame, halfOffset * CFrame.new(0, 0, -multiplier*dir*localSize.Z) * CFrame.Angles(multiplier*currentOffsetAngle, 0, 0) * CFrame.new(0, 0, multiplier*dir*localSize.Z) * halfOffset); local axis = Settings.Axis; return { Yaw = (axis=="X" or axis=="Y") and x or z; Pitch = (axis=="X" or axis=="Y") and z or x; Roll = axis=="Y" and x; }; end function ArcService:getTransform(object) local angles = {}; local cframe, size = Util:getBoundingBox(object); for axis, rot in pairs(rotations) do if Settings.SwapSides then rot = rotationsSwapped[axis]; end local localSize = rot:VectorToObjectSpace(size); localSize = Vector3.new(math.abs(localSize.X), math.abs(localSize.Y), math.abs(localSize.Z)); angles[axis] = self:getAngles(localSize, cframe*rot, size); end return angles; end function ArcService:getFaceFromNormal(vector) for face, normal in pairs(normals) do if normal==vector then return face; end end end function ArcService:getAxisFromNormalId(normalId) for axis, face in pairs(mappedFaces) do if face==normalId then return axis, false; end end for axis, face in pairs(mappedSwappedFaces) do if face==normalId then return axis, true; end end end function ArcService:getFace(axis) local face = mappedFaces[axis or Settings.Axis]; return Settings.SwapSides and self:getFaceFromNormal(normals[face]*-1) or face; end function ArcService:getNormal(axis) return normals[self:getFace(axis)]; end function ArcService:selectFace(object, axis) local existing = object:FindFirstChild("Archimedes_SurfaceSelection"); if existing then existing.TargetSurface = self:getFace(axis); return end local objectRep if object:IsA("Model") then local camera = workspace.CurrentCamera local cframe, size = Util:getBoundingBox(object) object = Instance.new("Part") object.Archivable = false object.Anchored = true object.Size = size object.Transparency = 1 object.CFrame = cframe objectRep = object end local surfaceSelection = Instance.new("SurfaceSelection"); surfaceSelection.Name = "Archimedes_SurfaceSelection"; surfaceSelection.TargetSurface = self:getFace(axis); surfaceSelection.Adornee = object; surfaceSelection.Parent = object; if objectRep then objectRep.Parent = workspace.CurrentCamera end return surfaceSelection, objectRep; end ArcService.SurfaceSelections = {}; function ArcService:addSelectionFaces(axis, selected) self:removeSelectionFaces() axis = axis or Settings.Axis; local selected = self:filterSelection(selected); for _,v in ipairs(selected) do local surfaceSelection, objectRep = self:selectFace(v, axis); if surfaceSelection then table.insert(self.SurfaceSelections, {SurfaceSelection = surfaceSelection, Rep = objectRep}); end end end function ArcService:removeSelectionFaces() for _,v in ipairs(self.SurfaceSelections) do v.SurfaceSelection:Destroy() local rep = v.Rep if rep then rep:Destroy() end end self.SurfaceSelections = {}; end --function ArcService:getEdge(object) -- local cframe, size = Util:getBoundingBox(object); -- return cframe:ToWorldSpace(CFrame.new()+normals[mappedFaces[Settings.Axis]]*size); --end local function lock(object, val) val = if val~=nil then val else true; if object:IsA("BasePart") then object.Locked = val; else for _,v in ipairs(object:GetDescendants()) do if v:IsA("BasePart") then v.Locked = val; end end end end function ArcService:resetModels(object) local objects = object and {object} for _,v in ipairs(objects) do local original = getOriginalModel(v); if original then local cframe = Util:getBoundingBox(v); local dupe = original:Clone(); self:setCFrame(dupe, cframe, true); dupe.Parent = v.Parent; v:Destroy(); end end end function ArcService:addPreview(object) local existing = self.Previews[object]; if existing then if existing:IsA("BasePart") then if existing.PivotOffset~=object.PivotOffset then existing.PivotOffset = object.PivotOffset; end end return existing; end if object:IsA("Model") then if not getOriginalModel(object) and Settings.AutoResize then addToStorage(object); end end mappedSurfaces[object] = nil; toggleJoin(object, false); local dupe = clone(object); dupe.Archivable = false; self.Previews[object] = dupe; local selectionBox = Instance.new("SelectionBox"); selectionBox.Name = "Archimedes_SelectionBox"; selectionBox.Adornee = dupe; selectionBox.LineThickness = .01; selectionBox.SurfaceTransparency = .75; selectionBox.Parent = dupe; dupe:SetAttribute("ArchimedesPreview", true); lock(dupe); return dupe, true; end local signals = {}; local function clearSignals() for _,list in pairs(signals) do for _, signal in pairs(list) do signal:Disconnect(); end end signals = {}; end function ArcService:clearPreviews(filter) local previews = self.Previews for i,v in pairs(previews) do if not filter or not table.find(filter, i) then v:Destroy(); previews[i] = nil; self.PreviewCount -= 1 end end if self.PreviewCount < 0 then self.PreviewCount = 0 end if not filter then self.Previews = {} end end ArcService.PreviewCount = 0; function ArcService:getResizeMap(object) if not Settings.AutoResize then return end local existing = mappedSurfaces[object]; if existing then return existing; end ResizeAlign:clearFolder(); local map = ResizeAlign:mapSurfaces(object, self:push(object), self:getFace(), Settings.ShowOverlap); mappedSurfaces[object] = map; return map; end function ArcService:autoResize(object, target) local surfaceMap = self:getResizeMap(object); if surfaceMap then for part, data in pairs(surfaceMap) do ResizeAlign:doExtend({Object = data.TargetPart, Normal = data.TargetFace}, {Object = part, Normal = data.FromFace}, Settings.ResizeMethod); end end end function ArcService:preview(objects) if not Settings.Enabled or not Settings.WidgetOpened then if self.PreviewCount>0 then self:clearPreviews(); end clearSignals(); return end local selected = objects; self:clearPreviews(selected); local amount = self:getRenderAmount()-1; local showMore = amount<100; local currentAngle = Settings.Angle; if #selected==0 then ResizeAlign:clearFolder(); end for _,v in ipairs(selected) do if not signals[v] then local list = {}; local lastPivot = v:GetPivot() local lastPreview = tick() local changeSignal = v.Changed:Connect(function(property) local timeNow = tick() lastPreview = timeNow task.defer(function() if lastPreview~=timeNow then return end local newPivot = v:GetPivot() if newPivot~=lastPivot then lastPivot = newPivot else self:clearPreviews() end self:preview() end) end) table.insert(list, changeSignal) --local pivotSignal = v:GetPropertyChangedSignal(v:IsA("BasePart") and "PivotOffset" or "WorldPivot"):Connect(function() -- self:preview(); --end) --table.insert(list, pivotSignal); --if v:IsA("BasePart") then -- local cframeSignal = v:GetPropertyChangedSignal("CFrame"):Connect(function() -- self:preview(); -- end) -- local sizeSignal = v:GetPropertyChangedSignal("Size"):Connect(function() -- self:clearPreviews(); -- self:preview(); -- end) -- table.insert(list, cframeSignal); -- table.insert(list, sizeSignal); --end signals[v] = list; end self.PreviewCount += 1; local prev = self:addPreview(v); local push = currentAngle==0; if push then self:push(v); end if Settings.AutoResize then self:getResizeMap(v); end if not push then self:transform(v, self:getTransform(v)[Settings.Axis][Settings.RotationType], prev); end end end function ArcService:getRenderAmount() return (360/math.abs(Settings.Angle)); end function ArcService:alternate() if Settings.Alternate then Settings.RotationType = (Settings.RotationType=="Pitch" and "Yaw") or "Pitch"; end end local function tag(...) local face = ArcService:getFace(); for _, object in ipairs({...}) do object:SetAttribute("ArchimedesNormalID", face.Name); end end local function hasMatchingTags(a, b) local id = a:GetAttribute("ArchimedesID"); return id and b:GetAttribute("ArchimedesID")==id and a:GetAttribute("ArchimedesNormalID")==b:GetAttribute("ArchimedesNormalID"); end local function getFirstMatchingAncestor(object, matchId) if object==workspace then return end return (object:GetAttribute("ArchimedesID")==matchId and object) or getFirstMatchingAncestor(object.Parent, matchId); end function ArcService:getMatchingNeighbors(object, face) local id = object:GetAttribute("ArchimedesID"); if not id then return end local neighbors = {}; local numNeighbors = 0; local normalIds = {face, ResizeAlign:getOppositeNormalId(face)}; for _,normalId in ipairs(normalIds) do local intersection = ResizeAlign:getIntersectingParts(object, nil, normalId, true); for _,v in pairs(intersection) do local firstMatch = getFirstMatchingAncestor(v, id); if firstMatch and hasMatchingTags(object, firstMatch) then neighbors[firstMatch] = normalId; --ResizeAlign:getOppositeNormalId(normalId); numNeighbors +=1; break end end end return neighbors, numNeighbors; end function ArcService:fixNeighbors(object) local face = object:GetAttribute("ArchimedesNormalID"); face = face and Enum.NormalId[face]; if not face then return end local neighbors, numNeighbors = self:getMatchingNeighbors(object, face); if numNeighbors==0 then return end local showOverlap = Settings.ShowOverlap; for neighbor, fromFace in pairs(neighbors) do local oppositeNormalId = ResizeAlign:getOppositeNormalId(fromFace); local intersecting, count = ResizeAlign:getIntersectingParts(neighbor, object, oppositeNormalId); if count==0 then continue end local cframe, size = Util:getBoundingBox(object); local faceVector = Vector3.FromNormalId(fromFace); local p = Instance.new("Part"); p.Name = "NeighborPart"; p.CFrame = cframe:ToWorldSpace(CFrame.new(faceVector*size/2))-faceVector*.5; p.Size = Vector3.new(1, 1, 1); for fromPart, data in pairs(intersecting) do --local ss = Instance.new("SurfaceSelection"); --ss.TargetSurface = data.TargetFace; --ss.Adornee = data.TargetPart; --ss.Parent = ResizeAlign.DebugFolder; ResizeAlign:doExtend({Object = data.TargetPart, Normal = data.TargetFace}, {Object = p, Normal = Enum.NormalId.Top}, "ExtendUpto"); end p:Destroy(); end end function ArcService:render(blockWaypoint) if not Settings.Enabled then return end clearSignals(); local targets = {}; local autoResize = Settings.AutoResize; for i,v in pairs(self.Previews) do local box = v:FindFirstChild("Archimedes_SelectionBox"); if box then box:Destroy(); end v.Parent = i.Parent; v.Archivable = true; table.insert(targets, v); self:autoResize(i, v); v:SetAttribute("ArchimedesPreview", nil); lock(v, false); if autoResize then tag(i, v); end toggleJoin(i, true); toggleJoin(v, true); -- ugly fix for ChangeHistoryService to work properly here v:SetAttribute("ArchimedesIgnore", true); v.Parent = nil; v:SetAttribute("ArchimedesIgnore"); v.Parent = i.Parent; end mappedSurfaces = {}; self.Previews = {}; self:alternate(); return targets end function ArcService:SetSetting(i, v) Settings[i] = v end function ArcService:GetSetting(i) return Settings[i] end function ArcService:renderMultiple(amount: number) local lastTargets = self.Previews for i = 1, amount do lastTargets = ArcService:render(false); ArcService:preview(lastTargets) task.wait(); end end function ArcService:load() self.descendantRemovingSignal = workspace.DescendantRemoving:Connect(function(object) if not Settings.AutoResize or not Settings.FixNeighbors then return end if object:GetAttribute("ArchimedesIgnore") then return end if object:IsA("Model") and object:GetAttribute("ArchimedesID") then if object:GetAttribute("ArchimedesPreview") then return end ArcService:fixNeighbors(object); ResizeAlign:clearFolder(); end end) end function ArcService:unload() local descendantRemovingSignal = self.descendantRemovingSignal; if descendantRemovingSignal then descendantRemovingSignal:Disconnect(); self.descendantRemovingSignal = nil; end clearSignals(); ResizeAlign:clearFolder(); self:clearPreviews(); end return ArcService;
Editor Settings
Theme
Key bindings
Full width
Lines