Quick actions

cmd+k|ctrl+k

Navigation

Languages

ArcService

Snippet info

Language

Lua

Visibility

public

Author

thosewhoknow

Created

2023-09-25T15:55:17.253525Z

Updated

2023-09-25T16:10:59.742988Z

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;
INFO