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;