local Utility = script.Parent
local BoundingBox = require(Utility.BoundingBox)
local shouldDragAsFace = require(Utility.shouldDragAsFace)

local DraggerFramework = Utility.Parent
local Implementation = DraggerFramework.Implementation

local DraggerContext = require(Implementation.DraggerContext)
type DraggerContext = DraggerContext.Class

type SelectionInfo = {
	basisCFrame: CFrame,
	basisObject: PVInstance?,
	boundingBoxSize: Vector3,
	boundingBoxOffset: Vector3,
	localBasisCFrame: CFrame,
	localBoundingBoxSize: Vector3,
	localBoundingBoxOffset: Vector3,
	parts: { BasePart },
	partSet: { [BasePart]: true },
	attachments: { Attachment },
	models: { Model },
	allAttachments: { Attachment },
	originalCFrameMap: { [BasePart]: CFrame },
	hasPhysics: boolean,
	instancesWithConfigurableFace: { Instance },
}

--[[
	Find all of the "roots" bones in a list of Bones. One of the bones in the
	list is a root iff it is not a descendant of any of the other root bones.
]]
local function markBoneRecursive(bone: Bone, movedSet: { [Bone]: true }, rootBoneMap: { [Bone]: BasePart })
	if movedSet[bone] then
		rootBoneMap[bone] = nil
	else
		movedSet[bone] = true

		for _, ch in ipairs(bone:GetChildren()) do
			if ch:IsA("Bone") then
				markBoneRecursive(ch, movedSet, rootBoneMap)
			end
		end
	end
end

local function findRootBoneMap(boneList: { Bone })
	local rootBoneMap = {}
	local movedSet = {}

	for _, bone in ipairs(boneList) do
		if not movedSet[bone] then
			local part = bone:FindFirstAncestorWhichIsA("BasePart")

			if part then
				rootBoneMap[bone] = part
			end

			markBoneRecursive(bone, movedSet, rootBoneMap)
		end
	end

	return rootBoneMap
end

local function computeInfo(draggerContext: DraggerContext, selectedObjects: { Instance }): SelectionInfo
	local isSimulating = draggerContext:isSimulating()
	local useLocalSpace = draggerContext:shouldUseLocalSpace()

	local allParts = table.create<<BasePart>>(64)
	local allPartSet = {} :: { [BasePart]: true }

	local allAttachments = {} :: { Attachment }
	local allBones = {} :: { Bone }
	local allModels = {} :: { Model }

	local allInstancesWithConfigurableFace = {} :: { Instance }
	local basisObject = nil

	local terrain = workspace.Terrain
	local selectedPVInstanceCount = 0

	for _, instance in ipairs(selectedObjects) do
		if instance:IsA("Model") then
			selectedPVInstanceCount += 1
			table.insert(allModels, instance)
			basisObject = instance
		elseif instance:IsA("BasePart") then
			selectedPVInstanceCount += 1
			if not allPartSet[instance] and instance ~= terrain then
				table.insert(allParts, instance)
				allPartSet[instance] = true
				basisObject = instance
			end
		elseif shouldDragAsFace(instance) then
			table.insert(allInstancesWithConfigurableFace, instance)
		elseif instance:IsA("Attachment") then
			if instance:IsA("Bone") then
				table.insert(allBones, instance)
			else
				table.insert(allAttachments, instance)
			end
		end
		-- It is possible to place parts inside of other parts, so this isn't an else on the prior if.
		for _, descendant in ipairs(instance:GetDescendants()) do
			if descendant:IsA("BasePart") and not allPartSet[descendant] and descendant ~= terrain then
				table.insert(allParts, descendant)
				allPartSet[descendant] = true
				-- Note: This is a helpful feature to still allow people to drag
				-- non-Model objects that still typically have parts inside of
				-- them like Folders and Tools.
				basisObject = basisObject or descendant
			elseif descendant:IsA("Model") then
				basisObject = basisObject or descendant
				table.insert(allModels, descendant)
			end
		end
	end

	-- Build a table of only the "interesting" attachments, that is, those
	-- which aren't underneath one of the selected parts.
	local interestingAttachments = {}

	for _, attachment in ipairs(allAttachments) do
		local parent = attachment:FindFirstAncestorWhichIsA("BasePart")

		if parent and parent:IsA("BasePart") and not allPartSet[parent] then
			table.insert(interestingAttachments, attachment)
		end
	end

	-- Figure out what bones in the list of bones are "roots" that are not
	-- relative to another bone we are already moving. Then add those bones to
	-- the above list of "interesting"a attachments.
	local rootBoneMap = findRootBoneMap(allBones)

	for attachment, ancestorPart in pairs(rootBoneMap) do
		if not allPartSet[ancestorPart] then
			table.insert(interestingAttachments, attachment)
		end
	end

	-- Look for a pivot:
	-- * If there are any PVInstances in the selection, use the pivot of the
	--   last one in the selection (the most recently selected one)
	-- * As a fallback when no PVInstances are selected, try to use the
	--   WorldCFrame of an Attachment in the selection.
	local isMultiSelection
	local basisCFrame = nil

	if basisObject then
		basisCFrame = basisObject:GetPivot()
		isMultiSelection = selectedPVInstanceCount ~= 1
	elseif #interestingAttachments > 0 then
		basisCFrame = interestingAttachments[1].WorldCFrame

		-- We use interestingAttachments here because if you box selected
		-- to select multiple bones here, if there's only one effective
		-- root bone to be moved, you still want to use that bone as the
		-- pivot, but if multiple root bones were selected then we have to
		-- fall back to pivoting around the center of the bounding box.
		-- (Setting isMultiSelection = true causes the pivot to be the
		-- center of the bounding box)
		if #interestingAttachments > 1 then
			isMultiSelection = true
		end
	else
		basisCFrame = CFrame.identity
		isMultiSelection = false
	end

	-- Store original CFrames so while moving parts we don't run into floating
	-- point error problems by using relative moves.
	local originalCFrameMap = {}

	for _, part in ipairs(allParts) do
		originalCFrameMap[part] = part.CFrame
	end

	-- Figure out if any of the objects in the selection are physically free
	-- moving parts. If there are, we will need to update the selection info
	-- every frame because those parts may have moved.
	local selectionHasPhysics = false

	if isSimulating then
		for _, part in ipairs(allParts) do
			if not part:IsGrounded() then
				selectionHasPhysics = true
				break
			end
		end
		if not selectionHasPhysics then
			for _, attachment in ipairs(allAttachments) do
				local parentPart = attachment:FindFirstAncestorWhichIsA("BasePart")

				if parentPart and not parentPart:IsGrounded() then
					selectionHasPhysics = true
					break
				end
			end
			for attachment, ancestorPart in pairs(rootBoneMap) do
				if not ancestorPart:IsGrounded() then
					selectionHasPhysics = true
				end
			end
		end
	end

	-- Finally add all the bones to the list of all attachments, because we want
	-- to pass back a list of all the selected things that inherit Attachment.
	-- We don't do this earlier because for the preceeding code we want to
	-- treat Bones and non-Bone Attachments slightily differently.
	-- We re-use the allAttachments table for this purpose to avoid allocating
	-- another table of both attachments and bones.
	for _, bone in ipairs(allBones) do
		table.insert(allAttachments, bone)
	end

	-- Build the basisCFrame aligned bounding box.
	-- Local = always the local space. Needed because freeform dragging always
	-- uses the local space bounding box.
	local localBasisCFrame = basisCFrame
	local localBoundingBoxOffset, localBoundingBoxSize

	-- Chosen = local or global depending on the UseLocalSpace setting
	local chosenBasisCFrame
	local chosenBoundingBoxOffset, chosenBoundingBoxSize

	if useLocalSpace then
		localBoundingBoxOffset, localBoundingBoxSize =
			BoundingBox.fromPartsAndAttachments(allParts, allAttachments, localBasisCFrame)

		chosenBasisCFrame = localBasisCFrame
		chosenBoundingBoxOffset, chosenBoundingBoxSize =
			localBoundingBoxOffset, localBoundingBoxSize
	else
		localBoundingBoxOffset, localBoundingBoxSize,
			chosenBoundingBoxOffset, chosenBoundingBoxSize =
				BoundingBox.computeTwoBoundingBoxes(allParts, allAttachments, localBasisCFrame)

		chosenBasisCFrame = CFrame.new(basisCFrame.Position)
	end

	-- If we have a multi-selection, the pivot should be moved to the center
	-- of the bounding box.
	if isMultiSelection then
		localBasisCFrame *= CFrame.new(localBoundingBoxOffset)
		localBoundingBoxOffset = Vector3.new()
		chosenBasisCFrame *= CFrame.new(chosenBoundingBoxOffset)
		chosenBoundingBoxOffset = Vector3.new()
	end

	return {
		-- basisCFrame is the CFrame which the selection will be transformed
		-- relative to when transforming it.
		basisCFrame = chosenBasisCFrame,

		-- If a PVInstance is selected, the PVInstance that the pivot was
		-- read from.
		basisObject = basisObject,

		-- boundingBoxSize is the size of the bounding box, as oriented around
		-- the basisCFrame.
		boundingBoxSize = chosenBoundingBoxSize,

		-- boundingBoxOffset is the offset of the center of the bounding box
		-- from the basisCFrame's position. basisCFrame will always be
		-- somewhere in the bounding box, but not necessarily at the center.
		boundingBoxOffset = chosenBoundingBoxOffset,

		-- This is a local space variant of the above set of properties. If the
		-- user is working in local space, then basisCFrame = localBasisCFrame,
		-- but they will differ if they have chosen a different frame of
		-- reference to work in.
		localBasisCFrame = localBasisCFrame,
		localBoundingBoxSize = localBoundingBoxSize,
		localBoundingBoxOffset = localBoundingBoxOffset,

		-- Parts and Attachments are the arrays of parts and attachments that
		-- need to be updated when transforming the selection.
		parts = allParts,
		partSet = allPartSet,
		attachments = interestingAttachments,
		models = allModels,

		-- All the attachments, including ones under parts in the list of parts
		-- to move. We still need these for some edge case behavior.
		allAttachments = allAttachments,

		-- OriginalCFrameMap stores the cframes of the parts before any
		-- transformation was applied to the selection.
		originalCFrameMap = originalCFrameMap,

		-- Are any of the parts in the selection physically free moving
		hasPhysics = selectionHasPhysics,

		-- Instances with configurable faces
		instancesWithConfigurableFace = allInstancesWithConfigurableFace,
	}
end

local SelectionInfo = {}
SelectionInfo.__index = SelectionInfo

export type Class = setmetatable<SelectionInfo, typeof(SelectionInfo)>

function SelectionInfo.new(draggerContext: DraggerContext, selection: { Instance }): Class
	return setmetatable(computeInfo(draggerContext, selection), SelectionInfo)
end

--[[
	Core schema specific
	Used to optimize dragger performance, returns a new SelectionInfo which
	is the same as this SelectionInfo, but transformed by a world transform.
]]
function SelectionInfo.getTransformedCopy(self: Class, globalTransform: CFrame)
	local transformedOriginalCFrames = {}
	local copy = table.clone(self)

	copy.basisCFrame = globalTransform * copy.basisCFrame
	copy.localBasisCFrame = globalTransform * copy.localBasisCFrame

	for k, cframe in pairs(copy.originalCFrameMap) do
		transformedOriginalCFrames[k] = globalTransform * cframe
	end

	copy.originalCFrameMap = transformedOriginalCFrames
	return setmetatable(copy, SelectionInfo)
end

function SelectionInfo.isEmpty(self: Class)
	return #self.parts == 0 and #self.attachments == 0
end

function SelectionInfo.getBoundingBox(self: Class)
	return self.basisCFrame, self.boundingBoxOffset, self.boundingBoxSize
end

function SelectionInfo.doesContainItem(self: Class, item: BasePart?)
	return (item and self.partSet[item]) ~= nil
end

function SelectionInfo.isDynamic(self: Class)
	return self.hasPhysics
end

--[[
	Core schema specific
	Returns: A map of where selected parts originally were before we started
	moving them.
]]
function SelectionInfo.getOriginalCFrameMap(self: Class)
	return self.originalCFrameMap
end

--[[
	Core schema specific
	Returns: The local space bounding box even when Use Local Space is false.
]]
function SelectionInfo.getLocalBoundingBox(self: Class)
	return self.localBasisCFrame, self.localBoundingBoxOffset, self.localBoundingBoxSize
end

-- Core schema specific
function SelectionInfo.getObjectsToTransform(self: Class)
	return self.parts, self.attachments, self.models
end

-- Core schema specific
function SelectionInfo.getBasisObject(self: Class)
	return self.basisObject
end

--[[
	Core schema specific
	The attachments returned by getObjectsToTransform only includes the
	attachments which are not underneath one of the parts to transform. This
	method on the other hand, returns all of the selected attachments regardless
	of parent.
]]
function SelectionInfo.getAllAttachments(self: Class)
	return self.allAttachments
end

return SelectionInfo


--!strict

--[[
	A wrapper around a Selection object (anything with the same API the Roblox
	SelectionService has), which disambiguates selection changed events which
	were caused by something else setting the selection, vs selection changed
	events which were caused by calling :Set on the Selection object.

	Also caches the selection between changes, so that repeated calls to
	the get method do not need to check what the underlying selection is.
]]

local Utility = script.Parent
local DraggerFramework = Utility.Parent

local Root = DraggerFramework.Parent
local Signal = require(Root.Signal)

local SelectionWrapper = {}
SelectionWrapper.__index = SelectionWrapper

type Signal<T...> = Signal.Typed<T...>

type SelectionWrapper = {
	_selectionObject: any,
	_selection: {Instance},
	_isSettingSelection: boolean,
	_destroyed: boolean,
	onSelectionExternallyChanged: Signal<>,
	_selectionChangedConnection: RBXScriptConnection?,
}

type Proto = typeof(SelectionWrapper)
export type Class = setmetatable<SelectionWrapper, Proto>

local WRAPPER_COUNT = 0

function SelectionWrapper.new(selectionObject: any): Class
	local self: Class = setmetatable({
		_selectionObject = selectionObject,
		_selection = selectionObject:Get(),
		_isSettingSelection = false,
		_destroyed = false,
	}, SelectionWrapper)

	self.onSelectionExternallyChanged = Signal.new()
	self._selectionChangedConnection =
		selectionObject.SelectionChanged:Connect(function()
			self:_handleSelectionChanged()
		end)

	WRAPPER_COUNT += 1

	if WRAPPER_COUNT > 1 then
		warn("More than one SelectionWrapper created at once, this is probably a mistake!")
	end

	return self
end

function SelectionWrapper.get(self: Class): {Instance}
	return self._selection
end

function SelectionWrapper.set(self: Class, selection: {Instance}, hint: any)
	self._selection = selection
	self._isSettingSelection = true
	self._selectionObject:Set(selection, hint)
	self._isSettingSelection = false
end

function SelectionWrapper.destroy(self: Class)
	assert(not self._destroyed)

	if self._selectionChangedConnection then
		self._selectionChangedConnection:Disconnect()
		self._selectionChangedConnection = nil
	end

	WRAPPER_COUNT -= 1
	self._destroyed = true
end

function SelectionWrapper._handleSelectionChanged(self: Class)
	if not self._isSettingSelection then
		self._selection = self._selectionObject:Get()
		self.onSelectionExternallyChanged:Fire()
	end
end

function SelectionWrapper.getActiveSelectable(self: Class): Instance?
	return self._selection[#self._selection]
end

return SelectionWrapper


--[[
This file is a workaround for STUDIOCORE-22344

We would like to just use the system cursors for the draggers, however, due to
the above unresolved issue, the system cursors do not work in Play Solo mode.

To work around this, use the old set of dragger cursors instead while in Play
Solo mode.
]]

local RunService = game:GetService("RunService")

local StandardCursor = {}

local function isPlaySolo()
	return RunService:IsRunning() and not RunService:IsRunMode()
end

function StandardCursor.getArrow()
	if isPlaySolo() then
		return "rbxasset://textures/advCursor-default.png"
	else
		return "rbxasset://SystemCursors/Arrow"
	end
end

function StandardCursor.getOpenHand()
	if isPlaySolo() then
		return "rbxasset://textures/advCursor-openedHand.png"
	else
		return "rbxasset://SystemCursors/OpenHand"
	end
end

function StandardCursor.getClosedHand()
	if isPlaySolo() then
		return "rbxasset://textures/advClosed-hand.png"
	else
		return "rbxasset://SystemCursors/ClosedHand"
	end
end

return StandardCursor


local NO_COLLISIONS_TRANSPARENCY = 0.4

local TemporaryTransparency = {}
TemporaryTransparency.__index = TemporaryTransparency

type TemporaryTransparency = {
	_draggingModifiedParts: { BasePart },
}

type Proto = typeof(TemporaryTransparency)
export type Class = setmetatable<TemporaryTransparency, Proto>

function TemporaryTransparency.new(parts: { BasePart }): Class
	local self = setmetatable({
		_draggingModifiedParts = {},
	}, TemporaryTransparency)

	for _, part in ipairs(parts) do
		if part:IsA("BasePart") then
			part.LocalTransparencyModifier = NO_COLLISIONS_TRANSPARENCY
			table.insert(self._draggingModifiedParts, part)
		end
	end

	return self
end

function TemporaryTransparency.destroy(self: Class)
	for _, part in ipairs(self._draggingModifiedParts) do
		part.LocalTransparencyModifier = 0
	end
end

return TemporaryTransparency


--[[
	Check that a CFrame has "good" values. That is:
	* None of the values are NaN
	* The position values are not particularly large
	* The rotation matrix is orthonormal

	This is a general catchall to be used for DEBUGGING ONLY to investigate
	CFrames becoming corrupted in some way. There are situations where a user
	might legitimately want to have a CFrame with position values larger than
	the TEST_AREA_RADIUS, so this should not be called in production code.
]]

-- How big is the test place you're testing in?
local TEST_AREA_RADIUS = 10000

-- How close to orthonormal does the CFrame need to be?
local ORTHONORMAL_EPSILON = 0.001

return function(cframe: CFrame)
	local x, y, z,
		d, e, f,
		g, h, i,
		j, k, l = cframe:GetComponents()

	-- Note: For IEEE floating point NaNs, NaN == NaN is false.
	-- We use that here to check for NaNs.
	if x ~= x or y ~= y or z ~= z or
		d ~= d or e ~= e or f ~= f or
		g ~= g or h ~= h or i ~= i or
		j ~= j or k ~= k or l ~= l then
		error("Bad CFrame: "..tostring(cframe))
	end

	if math.abs(x) + math.abs(y) + math.abs(z) > TEST_AREA_RADIUS * 3 then
		error("Big CFrame: "..tostring(cframe))
	end

	local right = cframe.RightVector
	local top = cframe.UpVector
	local back = cframe.LookVector

	if math.abs(right:Dot(top)) > ORTHONORMAL_EPSILON or
		math.abs(top:Dot(back)) > ORTHONORMAL_EPSILON or
		math.abs(back:Dot(right)) > ORTHONORMAL_EPSILON then
		error("Non orthogonal CFrame: "..tostring(cframe))
	end

	if math.abs(1 - right.Magnitude) > ORTHONORMAL_EPSILON or
		math.abs(1 - top.Magnitude) > ORTHONORMAL_EPSILON or
		math.abs(1 - back.Magnitude) > ORTHONORMAL_EPSILON then
		error("Non unitary units: "..tostring(cframe))
	end
end


const PIVOT_NEAR_EDGE_THRESHOLD = 0.01

export type PivotType =
	| "None"
	| "Center"
	| "Surface"
	| "Inside"
	| "Outside"
	| "Far"

return function(cframe: CFrame, offset: Vector3, size: Vector3): PivotType
	if offset:FuzzyEq(Vector3.zero, PIVOT_NEAR_EDGE_THRESHOLD) then
		return "Center"
	end

	local absOffset = Vector3.new(
		math.abs(offset.X),
		math.abs(offset.Y),
		math.abs(offset.Z)
	)

	local halfSize = size / 2
	local howFarOutside = absOffset - halfSize

	local isInside =
		howFarOutside.X < PIVOT_NEAR_EDGE_THRESHOLD and
		howFarOutside.Y < PIVOT_NEAR_EDGE_THRESHOLD and
		howFarOutside.Z < PIVOT_NEAR_EDGE_THRESHOLD

	if isInside then
		if math.abs(howFarOutside.X) < PIVOT_NEAR_EDGE_THRESHOLD or
			math.abs(howFarOutside.Y) < PIVOT_NEAR_EDGE_THRESHOLD or
			math.abs(howFarOutside.Z) < PIVOT_NEAR_EDGE_THRESHOLD then
			return "Surface"
		else
			return "Inside"
		end
	else
		local fractionOutVector = howFarOutside / size
		local fractionOut = math.max(fractionOutVector.X, fractionOutVector.Y, fractionOutVector.Z)

		if fractionOut > 1 then
			return "Far"
		else
			return "Outside"
		end
	end
end


local Utility = script.Parent
local Math = require(Utility.Math)

--[[
	Returns the distance the mouse cursor was dragged from dragStartPosition
    along dragDirection

	This is non-trivial when the cursor gets away from the axis on the screen.
	Let p be the start of the handle, u be the direction of the handle, and eye
	be the origin of the mouse ray (eye of the camera).

	Let v be the unit vector from p pointing to the eye, and let w = v x u.

	Then v is normal to plane defined by points eye, p and the vector u.
	|v| = 0 if and only if the handle projects to a dot on the screen.

	Let cur be where the mouse ray intersects the plane at p spanned by u, v.
	Find the normal projection of cur onto the handle ray, and use that to
	compute the dragged distance.
]]
local function computeDraggedDistance(dragStartPosition: Vector3, dragDirection: Vector3, mouseRay: Ray)
    local eye = mouseRay.Origin
    local ray = mouseRay.Direction.Unit

	local handleToEyeDirection = (eye - dragStartPosition).Unit
	local eyePlaneNormal = dragDirection:Cross(handleToEyeDirection)

	-- the handle axis projects to a point, can't compute drag distance
	if eyePlaneNormal:Dot(eyePlaneNormal) < 0.0001 then
		return false, 0
	end

	local dragPlaneNormal = dragDirection:Cross(eyePlaneNormal).Unit

    -- mouse ray nearly parallel to drag axis, halt drag before sending part to infinity
    if ray:Dot(dragPlaneNormal) < 0.0001 then
        return false, 0
    end

	local cursorPosition = Math.intersectRayPlanePoint(eye, ray, dragStartPosition, dragPlaneNormal)
	local draggedDistance = (cursorPosition - dragStartPosition):Dot(dragDirection)

	return true, draggedDistance
end

return computeDraggedDistance


local BoundingBoxCorners = {
	Vector3.new(0.5, 0.5, 0.5),
	Vector3.new(-0.5, 0.5, 0.5),
	Vector3.new(0.5, -0.5, 0.5),
	Vector3.new(-0.5, -0.5, 0.5),
	Vector3.new(0.5, 0.5, -0.5),
	Vector3.new(-0.5, 0.5, -0.5),
	Vector3.new(0.5, -0.5, -0.5),
	Vector3.new(-0.5, -0.5, -0.5),
}

local DraggerFramework = script.Parent.Parent
local Implementation = DraggerFramework.Implementation

local DraggerContext = require(Implementation.DraggerContext)
type DraggerContext = DraggerContext.Class

local function getBoundingBoxScale(draggerContext: DraggerContext, cframe: CFrame, size: Vector3)
	local minScale = math.huge

	for _, relativeCorner in ipairs(BoundingBoxCorners) do
		local globalCorner = cframe:PointToWorldSpace(size * relativeCorner)
		minScale = math.min(minScale, draggerContext:getHandleScale(globalCorner))
	end

	return minScale
end

return getBoundingBoxScale


--[[
	Get the FaceInstance (Decal or Texture) on a given part closest to a given
	position.
]]

local function getNormalId(normalizedPosition: Vector3): Enum.NormalId
	local x = math.abs(normalizedPosition.X)
	local y = math.abs(normalizedPosition.Y)
	local z = math.abs(normalizedPosition.Z)

	if x > y and x > z then
		return normalizedPosition.X > 0 and Enum.NormalId.Right or Enum.NormalId.Left
	elseif y > z then
		return normalizedPosition.Y > 0 and Enum.NormalId.Top or Enum.NormalId.Bottom
	else
		return normalizedPosition.Z > 0 and Enum.NormalId.Back or Enum.NormalId.Front
	end
end

local function getFaceInstance(part: BasePart, position: Vector3): FaceInstance?
	local localPosition = part.CFrame:PointToObjectSpace(position)
	local normalizedPosition = localPosition / part.Size * 2
	local face = getNormalId(normalizedPosition)

	for _, child in pairs(part:GetChildren()) do
		if child:IsA("FaceInstance") and child.Face == face then
			return child
		end
	end

	return nil
end

return getFaceInstance


export type Shape =
	| "Brick"
	| "Sphere"
	| "Cylinder"
	| "Wedge"
	| "CornerWedge"
	| "Terrain"
	| "Mesh"

export type Surface =
	| "RightSurface"
	| "LeftSurface"
	| "TopSurface"
	| "BottomSurface"
	| "FrontSurface"
	| "BackSurface"

type IGeometry<T> = {
	type: T,
	id: number?,
	part: BasePart?,
}

export type Face = IGeometry<"Face"> & {
	point: Vector3,
	normal: Vector3,
	direction: Vector3,
	vertices: { Vector3 },
	surface: Surface?,
	center: Vector3?,
}

export type Vertex = IGeometry<"Vertex"> & {
	position: Vector3,
}

export type Edge = IGeometry<"Edge"> & {
	a: Vector3,
	b: Vector3,
	length: number,
	direction: Vector3?,
	edgeMargin: number,
	vertexMargin: number?,
}

export type Geometry = {
	part: BasePart,
	shape: Shape,
	vertices: { Vertex },
	edges: { Edge },
	faces: { Face },
	vertexMargin: number,
}

local function getShape(part: BasePart): (Shape, Vector3)
	if part:IsA("WedgePart") then
		return "Wedge", Vector3.one
	elseif part:IsA("CornerWedgePart") then
		return "CornerWedge", Vector3.one
	elseif part:IsA("Terrain") then
		return "Terrain", Vector3.one
	elseif part:IsA("UnionOperation") then
		return "Brick", Vector3.one
	elseif part:IsA("MeshPart") then
		return "Brick", Vector3.one
	elseif part:IsA("Part") then
		-- BasePart
		if part.Shape == Enum.PartType.Ball then
			return "Sphere", Vector3.one
		elseif part.Shape == Enum.PartType.Cylinder then
			return "Cylinder", Vector3.one
		elseif part.Shape == Enum.PartType.Block then
			return "Brick", Vector3.one
		elseif part.Shape == Enum.PartType.CornerWedge then
			return "CornerWedge", Vector3.one
		elseif part.Shape == Enum.PartType.Wedge then
			return "Wedge", Vector3.one
		else
			assert(false, "Unreachable")
		end
	else
		return "Brick", Vector3.one
	end
end

return function(part: BasePart, hit: Vector3?, assumedCFrame: CFrame?): Geometry
	local cf = assumedCFrame or part.CFrame
	local pos = cf.Position

	local sx = part.Size.X / 2
	local sy = part.Size.Y / 2
	local sz = part.Size.Z / 2

	local xvec = cf.XVector
	local yvec = cf.YVector
	local zvec = cf.ZVector

	local shape, scale = getShape(part)
	local verts = {} :: { Vector3 }
	local edges = {} :: { Edge }
	local faces = {} :: { Face }

	sx = sx * scale.X
	sy = sy * scale.Y
	sz = sz * scale.Z

	if shape == "Brick" or shape == "Sphere" or shape == "Cylinder" then
		--8 vertices
		verts = {
			pos +xvec*sx  +yvec*sy  +zvec*sz, --top 4
			pos +xvec*sx  +yvec*sy  -zvec*sz,
			pos -xvec*sx  +yvec*sy  +zvec*sz,
			pos -xvec*sx  +yvec*sy  -zvec*sz,
			--
			pos +xvec*sx  -yvec*sy  +zvec*sz, --bottom 4
			pos +xvec*sx  -yvec*sy  -zvec*sz,
			pos -xvec*sx  -yvec*sy  +zvec*sz,
			pos -xvec*sx  -yvec*sy  -zvec*sz,
		}

		--12 edges
		edges = {
			{a = verts[1], b = verts[2], length = math.min(2*sx, 2*sy)}, --top 4
			{a = verts[3], b = verts[4], length = math.min(2*sx, 2*sy)},
			{a = verts[1], b = verts[3], length = math.min(2*sy, 2*sz)},
			{a = verts[2], b = verts[4], length = math.min(2*sy, 2*sz)},
			--
			{a = verts[5], b = verts[6], length = math.min(2*sx, 2*sy)}, --bottom 4
			{a = verts[7], b = verts[8], length = math.min(2*sx, 2*sy)},
			{a = verts[5], b = verts[7], length = math.min(2*sy, 2*sz)},
			{a = verts[6], b = verts[8], length = math.min(2*sy, 2*sz)},
			--
			{a = verts[1], b = verts[5], length = math.min(2*sx, 2*sz)}, --verticals
			{a = verts[2], b = verts[6], length = math.min(2*sx, 2*sz)},
			{a = verts[3], b = verts[7], length = math.min(2*sx, 2*sz)},
			{a = verts[4], b = verts[8], length = math.min(2*sx, 2*sz)},
		} :: { Edge }

		--6 faces
		faces = {
			RightSurface = {
				point = verts[1],
				normal = xvec,
				direction = zvec,
				vertices = { verts[5], verts[6], verts[2], verts[1] }
			},

			LeftSurface = {
				point = verts[3],
				normal = -xvec,
				direction = zvec,
				vertices = {verts[3], verts[4], verts[8], verts[7] }
			},

			TopSurface = {
				point = verts[1],
				normal = yvec,
				direction = xvec,
				vertices = {verts[1], verts[2], verts[4], verts[3] }
			},

			BottomSurface = {
				point = verts[5],
				normal = -yvec,
				direction = xvec,
				vertices = {verts[7], verts[8], verts[6], verts[5] }
			},

			BackSurface = {
				point = verts[1],
				normal = zvec,
				direction = xvec,
				vertices = {verts[1], verts[3], verts[7], verts[5] }
			},

			FrontSurface = {
				point = verts[2],
				normal = -zvec,
				direction = xvec,
				vertices = {verts[6], verts[8], verts[4], verts[2] }
			},
		}
	elseif (shape == "Sphere" or shape == "Cylinder") and hit then
		-- Just have one face and vertex, at the hit pos
		verts = { hit }
		edges = {} --edge can be selected as the normal of the face if the user needs it

		local norm = (hit - pos).Unit
		local norm2 = norm:Cross(Vector3.yAxis).Unit
		local surfaceName: Surface

		if math.abs(norm.X) > math.abs(norm.Y) and math.abs(norm.X) > math.abs(norm.Z) then
			surfaceName = (if (norm.X > 0) then "RightSurface" else "LeftSurface") :: Surface
		elseif math.abs(norm.Y) > math.abs(norm.Z) then
			surfaceName = (if (norm.Y > 0) then "TopSurface" else "BottomSurface") :: Surface
		else
			surfaceName = (if (norm.Z > 0) then "BackSurface" else "FrontSurface") :: Surface
		end

		faces = {
			[surfaceName] = {
				point = hit,
				normal = norm,
				direction = norm2,
				vertices = {},
			} :: Face
		}
	elseif shape == "CornerWedge" then
		local slantVec1 = ( zvec*sy + yvec*sz).Unit
		local slantVec2 = (-xvec*sy + yvec*sx).Unit

		-- 5 verts
		verts = {
			pos +xvec*sx  +yvec*sy  -zvec*sz, --top 1
			--
			pos +xvec*sx  -yvec*sy  +zvec*sz, --bottom 4
			pos +xvec*sx  -yvec*sy  -zvec*sz,
			pos -xvec*sx  -yvec*sy  +zvec*sz,
			pos -xvec*sx  -yvec*sy  -zvec*sz,
		}

		-- 8 edges
		edges = {
			{a = verts[2], b = verts[3], length = 0}, -- bottom 4
			{a = verts[3], b = verts[5], length = 0},
			{a = verts[5], b = verts[4], length = 0},
			{a = verts[4], b = verts[2], length = 0},
			--
			{a = verts[1], b = verts[3], length = 0}, -- vertical
			--
			{a = verts[1], b = verts[2], length = 0}, -- side diagonals
			{a = verts[1], b = verts[5], length = 0},
			--
			{a = verts[1], b = verts[4], length = 0}, -- middle diagonal
		} :: { Edge }

		-- 5 faces
		faces = {
			-- bottom
			BottomSurface = {
				point = verts[2],
				normal = -yvec,
				direction = xvec,
				vertices = {verts[2], verts[3], verts[5], verts[4]},
			},

			-- sides
			RightSurface = {
				point = verts[1],
				normal = xvec,
				direction = -yvec,
				vertices = {verts[1], verts[3], verts[2]},
			},

			FrontSurface = {
				point = verts[1],
				normal = -zvec,
				direction = -yvec,
				vertices = {verts[1], verts[3], verts[5]},
			},

			-- tops
			BackSurface = {
				point = verts[1],
				normal = slantVec1,
				direction = xvec,
				vertices = {verts[1], verts[2], verts[4]},
			},

			LeftSurface = {
				point = verts[1],
				normal = slantVec2,
				direction = zvec,
				vertices = {verts[1], verts[5], verts[4]},
			},
		}

	elseif shape == "Wedge" then
		local slantVec = (-zvec*sy + yvec*sz).Unit

		--6 vertices
		verts = {
			pos +xvec*sx  +yvec*sy  +zvec*sz, --top 2
			pos -xvec*sx  +yvec*sy  +zvec*sz,
			--
			pos +xvec*sx  -yvec*sy  +zvec*sz, --bottom 4
			pos +xvec*sx  -yvec*sy  -zvec*sz,
			pos -xvec*sx  -yvec*sy  +zvec*sz,
			pos -xvec*sx  -yvec*sy  -zvec*sz,
		}

		--9 edges
		edges = {
			{a = verts[1], b = verts[2], length = math.min(2*sy, 2*sz)}, --top 1
			--
			{a = verts[1], b = verts[4], length = math.min(2*sy, 2*sz)}, --slanted 2
			{a = verts[2], b = verts[6], length = math.min(2*sy, 2*sz)},
			--
			{a = verts[3], b = verts[4], length = math.min(2*sx, 2*sy)}, --bottom 4
			{a = verts[5], b = verts[6], length = math.min(2*sx, 2*sy)},
			{a = verts[3], b = verts[5], length = math.min(2*sy, 2*sz)},
			{a = verts[4], b = verts[6], length = math.min(2*sy, 2*sz)},
			--
			{a = verts[1], b = verts[3], length = math.min(2*sx, 2*sz)}, --vertical 2
			{a = verts[2], b = verts[5], length = math.min(2*sx, 2*sz)},
		} :: { Edge }

		--5 faces
		faces = {
			-- right
			RightSurface = {
				point = verts[1],
				normal = xvec,
				direction = zvec,
				vertices = {verts[1], verts[4], verts[3], verts[2]},
			},

			-- left
			LeftSurface = {
				point = verts[2],
				normal = -xvec,
				direction = zvec,
				vertices = {verts[2], verts[3], verts[4], verts[1]},
			},

			-- bottom
			BottomSurface = {
				point = verts[3],
				normal = -yvec,
				direction = xvec,
				vertices = {verts[3], verts[4], verts[6], verts[5]},
			},

			-- back
			BackSurface = {
				point = verts[1],
				normal = zvec,
				direction = xvec,
				vertices = {verts[1], verts[2], verts[5], verts[3]},
			},

			-- slanted
			FrontSurface = {
				point = verts[4],
				normal = -slantVec,
				direction = slantVec:Cross(xvec),
				vertices = {verts[4], verts[1], verts[2], verts[6]},
			},
		}
	elseif shape == "Terrain" then
		assert(false, "Called GetGeometry on Terrain")
	else
		assert(false, "Bad shape: " .. shape)
	end

	local vertData = {} :: { Vertex }
	local faceData = {} :: { Face }
	local geomId = 0

	local geometry: Geometry = {
		part = part;
		shape = ((shape == "Sphere" or shape == "Cylinder") and shape or "Mesh") :: Shape;
		vertices = vertData;
		faces = faceData;
		edges = edges;
		vertexMargin = math.min(sx, sy, sz) * 2;
	}

	for surfaceType, dat in pairs(faces) do
		geomId += 1
		dat.id = geomId
		dat.part = part
		dat.type = "Face"
		dat.surface = surfaceType
		table.insert(faceData, dat)
	end

	for _, dat in ipairs(edges) do
		geomId += 1
		dat.id = geomId

		dat.direction = (dat.b - dat.a).Unit
		dat.length = (dat.b - dat.a).Magnitude
		dat.vertexMargin = geometry.vertexMargin
		dat.part = part
		dat.type = "Edge"
	end

	for i, dat in ipairs(verts) do
		geomId += 1

		vertData[i] = {
			position = dat;
			id = geomId;
			type = "Vertex";
		} :: Vertex
	end

	return geometry
end


--[[
	isProtectedInstance( instance ): Return true for instances which are
	"protected" by Roblox. Accessing any method or property on these instances
	will raise an error.

	Internally use a weak hash map to cache the protection status of the
	instances, as determining the protection status is somewhat expensive.
]]

local WeakIsProtectedCache = setmetatable({} :: {
	[Instance]: boolean
}, {
	__mode = "k"
})

local function emptyErrorHandler()
end

local function safetyCheckerFunction(instance: Instance)
	assert(instance.Name)
end

local function isProtectedInstance(instance: Instance): boolean
	local isProtected = WeakIsProtectedCache[instance]

	if isProtected == nil then
		-- Use xpcall even though we don't need the error handler because
		-- xpcall is much faster than pcall on Roblox.
		isProtected = not xpcall(safetyCheckerFunction, emptyErrorHandler, instance)
		WeakIsProtectedCache[instance] = isProtected
	end

	return isProtected
end

return isProtectedInstance


--[[
	roundRotation(CFrame) -> CFrame

	Round a rotation CFrame which is approximately primary axis aligned to be
	exactly primary axis aligned instead (such that all of its components are
	-1, 0, or 1).

	Will not work on an arbitrary CFrame! This function is intended to be used
	on CFrames which within a small amount of floating point error of already
	being aligned.
]]
return function(cframe: CFrame)
	local x, y, z,
		r0, r1, r2,
		r3, r4, r5,
		r6, r7, r8 = cframe:components()
	assert(x == 0 and y == 0 and z == 0)
	return CFrame.new(0, 0, 0,
		math.floor(r0 + 0.5), math.floor(r1 + 0.5), math.floor(r2 + 0.5),
		math.floor(r3 + 0.5), math.floor(r4 + 0.5), math.floor(r5 + 0.5),
		math.floor(r6 + 0.5), math.floor(r7 + 0.5), math.floor(r8 + 0.5))
end


-- Wrapper around WorldRoot::SetInsertPoint, since that function is Roblox only,
-- but we want the DraggerFramework to be forkable.

return function(insertPoint: Vector3)
	pcall(function()
		local setInsertPoint = (workspace :: any).SetInsertPoint
		setInsertPoint(workspace, insertPoint, true)
	end)
end


return function(instance: Instance)
	return instance:IsA("FaceInstance") or instance:IsA("VideoFrame") or instance:IsA("SurfaceGui")
end


local PrimaryDirections = {
	Vector3.xAxis
	-Vector3.xAxis,

	Vector3.yAxis,
	-Vector3.yAxis,

	Vector3.zAxis,
	-Vector3.zAxis,
}

local function largestComponent(vector: Vector3)
	return math.max(math.abs(vector.X), math.abs(vector.Y), math.abs(vector.Z))
end

local function snapVectorToPrimaryDirection(direction: Vector3)
	local largestDot = -math.huge
	local closestDirection = direction

	for _, target in ipairs(PrimaryDirections) do
		local dot = direction:Dot(target)
		if dot > largestDot then
			largestDot = dot
			closestDirection = target
		end
	end

	return closestDirection
end

return function(cframe: CFrame)
	local right = cframe.RightVector
	local top = cframe.UpVector
	local front = -cframe.LookVector

	local largestRight = largestComponent(right)
	local largestTop = largestComponent(top)
	local largestFront = largestComponent(front)

	if largestRight > largestTop and largestRight > largestFront then
		-- Most aligned axis is X, the right, preserve that
		right = snapVectorToPrimaryDirection(right)

		if largestTop > largestFront then
			top = snapVectorToPrimaryDirection(top)
		else
			local front = snapVectorToPrimaryDirection(front)
			top = front:Cross(right).Unit
		end
	elseif largestTop > largestFront then
		-- Most aligned axis is Y, the top, preserve that
		top = snapVectorToPrimaryDirection(top)

		if largestRight > largestFront then
			right = snapVectorToPrimaryDirection(right)
		else
			local front = snapVectorToPrimaryDirection(front)
			right = top:Cross(front).Unit
		end
	else
		-- Most aligned axis is Z, the front, preserve that
		local front = snapVectorToPrimaryDirection(front)

		if largestRight > largestTop then
			right = snapVectorToPrimaryDirection(right)
			top = front:Cross(right).Unit
		else
			top = snapVectorToPrimaryDirection(top)
			right = top:Cross(front).Unit
		end
	end

	return CFrame.fromMatrix(Vector3.zero, right, top)
end


local Implementation = script.Implementation
local Components = script.Components
local Handles = script.Handles
local Utility = script.Utility

local DraggerTool = require(Components.DraggerTool)
local DraggedPivot = require(Components.DraggedPivot)
local SelectionDot = require(Components.SelectionDot)
local MoveHandleView = require(Components.MoveHandleView)
local ScaleHandleView = require(Components.ScaleHandleView)
local RotateHandleView = require(Components.RotateHandleView)
local AnimatedHoverBox = require(Components.AnimatedHoverBox)
local SummonHandlesNote = require(Components.SummonHandlesNote)
local DragSelectionView = require(Components.DragSelectionView)
local SummonHandlesHider = require(Components.SummonHandlesHider)
local LocalSpaceIndicator = require(Components.LocalSpaceIndicator)
local StandaloneSelectionBox = require(Components.StandaloneSelectionBox)

local ExtrudeHandles = require(Handles.ExtrudeHandles)
local RotateHandles = require(Handles.RotateHandles)
local MoveHandles = require(Handles.MoveHandles)

local HoverTracker = require(Implementation.HoverTracker)
local DraggerContext = require(Implementation.DraggerContext)
local DraggerStateType = require(Implementation.DraggerStateType)
local DraggerToolModel = require(Implementation.DraggerToolModel)

local Math = require(Utility.Math)
local Colors = require(Utility.Colors)
local JointUtil = require(Utility.JointUtil)
local PartMover = require(Utility.PartMover)
local DragHelper = require(Utility.DragHelper)
local JointMaker = require(Utility.JointMaker)
local JointPairs = require(Utility.JointPairs)
local BoundingBox = require(Utility.BoundingBox)
local DragSelector = require(Utility.DragSelector)
local SelectionInfo = require(Utility.SelectionInfo)
local StandardCursor = require(Utility.StandardCursor)
local AttachmentMover = require(Utility.AttachmentMover)
local SelectionHelper = require(Utility.SelectionHelper)
local SelectionWrapper = require(Utility.SelectionWrapper)
local ViewChangeDetector = require(Utility.ViewChangeDetector)
local BoundsChangedTracker = require(Utility.BoundsChangedTracker)
local TemporaryTransparency = require(Utility.TemporaryTransparency)

local getGeometry = require(Utility.getGeometry)
local classifyPivot = require(Utility.classifyPivot)
local roundRotation = require(Utility.roundRotation)
local setInsertPoint = require(Utility.setInsertPoint)
local getFaceInstance = require(Utility.getFaceInstance)
local assertGoodCFrame = require(Utility.assertGoodCFrame)
local shouldDragAsFace = require(Utility.shouldDragAsFace)
local getBoundingBoxScale = require(Utility.getBoundingBoxScale)
local isProtectedInstance = require(Utility.isProtectedInstance)
local computeDraggedDistance = require(Utility.computeDraggedDistance)
local snapRotationToPrimaryDirection = require(Utility.snapRotationToPrimaryDirection)

export type DraggerStateType = DraggerStateType.StateType
export type DragTargetType = DragHelper.DragTargetType
export type DragTarget = DragHelper.DragTarget
export type Gizmo = DraggerContext.Gizmo

export type PivotType = classifyPivot.PivotType
export type Geometry = getGeometry.Geometry
export type Surface = getGeometry.Surface
export type Vertex = getGeometry.Vertex
export type Shape = getGeometry.Shape
export type Edge = getGeometry.Edge
export type Face = getGeometry.Face

export type MoveHandles = MoveHandles.Class
export type RotateHandles = RotateHandles.Class
export type ExtrudeHandles = ExtrudeHandles.Class

export type TemporaryTransparency = TemporaryTransparency.Class
export type BoundsChangedTracker = BoundsChangedTracker.Class
export type ViewChangeDetector = ViewChangeDetector.Class
export type SelectionWrapper = SelectionWrapper.Class
export type DraggerToolModel = DraggerToolModel.Class
export type AttachmentMover = AttachmentMover.Class
export type DraggerContext = DraggerContext.Class
export type SelectionInfo = SelectionInfo.Class
export type HoverTracker = HoverTracker.Class
export type DragSelector = DragSelector.Class
export type JointMaker = JointMaker.Class
export type JointPairs = JointPairs.Class
export type PartMover = PartMover.Class

return table.freeze({
    DraggerTool = DraggerTool,
    DraggerContext = DraggerContext,

    Components = table.freeze({
        DraggerTool = DraggerTool,
        DraggedPivot = DraggedPivot,
        SelectionDot = SelectionDot,

        MoveHandleView = MoveHandleView,
        ScaleHandleView = ScaleHandleView,
        RotateHandleView = RotateHandleView,

        AnimatedHoverBox = AnimatedHoverBox,
        DragSelectionView = DragSelectionView,
        LocalSpaceIndicator = LocalSpaceIndicator,

        StandaloneSelectionBox = StandaloneSelectionBox,
        SummonHandlesHider = SummonHandlesHider,
        SummonHandlesNote = SummonHandlesNote,
    }),

    Handles = table.freeze({
        ExtrudeHandles = ExtrudeHandles,
        RotateHandles = RotateHandles,
        MoveHandles = MoveHandles,
    }),

    Implementation = table.freeze({
        HoverTracker = HoverTracker,
        DraggerContext = DraggerContext,
        DraggerStateType = DraggerStateType,
        DraggerToolModel = DraggerToolModel,
    }),

    Utility = table.freeze({
        Math = Math,
        Colors = Colors,
        PartMover = PartMover,
        JointUtil = JointUtil,
        DragHelper = DragHelper,
        JointMaker = JointMaker,
        JointPairs = JointPairs,
        BoundingBox = BoundingBox,
        DragSelector = DragSelector,
        SelectionInfo = SelectionInfo,
        StandardCursor = StandardCursor,
        AttachmentMover = AttachmentMover,
        SelectionHelper = SelectionHelper,
        SelectionWrapper = SelectionWrapper,
        ViewChangeDetector = ViewChangeDetector,
        BoundsChangedTracker = BoundsChangedTracker,
        TemporaryTransparency = TemporaryTransparency,

        getGeometry = getGeometry,
        classifyPivot = classifyPivot,
        roundRotation = roundRotation,
        setInsertPoint = setInsertPoint,
        getFaceInstance = getFaceInstance,
        assertGoodCFrame = assertGoodCFrame,
        shouldDragAsFace = shouldDragAsFace,
        getBoundingBoxScale = getBoundingBoxScale,
        isProtectedInstance = isProtectedInstance,
        computeDraggedDistance = computeDraggedDistance,
        snapRotationToPrimaryDirection = snapRotationToPrimaryDirection,
    }),
})


-- Baseline wrapper around the default BoundsChangedTracker implementation provided in DraggerFramework.
-- This provides backwards compatibility for its previous place of residence, while still allowing
-- consumers to extend it as needed.

local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
local BoundsChangedTracker = DraggerFramework.Utility.BoundsChangedTracker

export type Class = DraggerFramework.BoundsChangedTracker
return BoundsChangedTracker


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent
local DraggerFramework = require(Packages.DraggerFramework)

local Utility = DraggerFramework.Utility
local JointMaker = Utility.JointMaker
local getBoundingBoxScale = Utility.getBoundingBoxScale
local TemporaryTransparency = Utility.TemporaryTransparency
local Math = Utility.Math

type JointMaker = DraggerFramework.JointMaker
type JointPairs = DraggerFramework.JointPairs
type SelectionInfo = DraggerFramework.SelectionInfo
type DraggerContext = DraggerFramework.DraggerContext
type TemporaryTransparency = DraggerFramework.TemporaryTransparency

local EMPTY_CFRAME = CFrame.identity
local ZERO_VECTOR = Vector3.zero
local ONES_VECTOR = Vector3.one
local HUGE_VECTOR = ONES_VECTOR * math.huge

-- controls how large/small a part may be on any dimension
local MIN_PART_SIZE = 0.001
local MAX_PART_SIZE = 2048.0

local ExtrudeHandlesImplementation = {}
ExtrudeHandlesImplementation.__index = ExtrudeHandlesImplementation

export type ExtrudeHandlesImplementation = {
	_draggerContext: DraggerContext,
	_jointMaker: JointMaker,
	_jointPairs: JointPairs?,
	_originalBoundingBoxSize: Vector3,
	_originalBoundingBoxCFrame: CFrame,
	_originalCFrameMap: { [BasePart]: CFrame },
	_partsToResize: { BasePart },
	_attachmentsToMove: { Attachment },
	_modelsToResize: { Model },
	_originalSizeMap: { [BasePart]: Vector3 },
	_lastDeltaSize: Vector3,
	_temporaryTransparency: TemporaryTransparency?,
	_fixupAttachments: { [Attachment]: Vector3 },
	_fixupNontrivialAttachments: {{
		Attachment: Attachment,
		RelativeTo: BasePart,
		LocalRotation: CFrame,
		LocalPosition: Vector3,
	}},
	_fixupOffsets: { [DataModelMesh]: Vector3 },
	_fixupScales: { [DataModelMesh]: Vector3 },
	_fixupPartPivot: { [BasePart]: CFrame },
	_fixupModelPivot: { [Model]: CFrame },
}

type Proto = typeof(ExtrudeHandlesImplementation)
export type Class = setmetatable<ExtrudeHandlesImplementation, Proto>

function ExtrudeHandlesImplementation.new(draggerContext: DraggerContext): Class
	return setmetatable({
		_draggerContext = draggerContext,
		_jointMaker = JointMaker.new(),
		_originalBoundingBoxSize = Vector3.zero,
		_originalBoundingBoxCFrame = CFrame.identity,
		_originalCFrameMap = {},
		_partsToResize = {},
		_attachmentsToMove = {},
		_modelsToResize = {},
		_originalSizeMap = {},
		_lastDeltaSize = Vector3.zero,
		_fixupAttachments = {},
		_fixupNontrivialAttachments = {},
		_fixupOffsets = {},
		_fixupScales = {},
		_fixupPartPivot = {},
		_fixupModelPivot = {},
	}, ExtrudeHandlesImplementation) :: Class
end

-- The difference from exactly touching to try to bring a pair of parts within
-- when collisions are enabled.
-- After resizing one part into a collision with another one, the closest pair
-- of points P1 on the first part and P2 on the second part should satisfy:
-- (P1 - P2).Magnitude < RESIZE_COLLISION_THRESHOLD
-- Note: This is a deliberately looser threshold than the transform one, so that
-- after resizing parts into a collision, they can still be moved.
local RESIZE_COLLISION_THRESHOLD = 0.001

-- This function can just be `return selectionInfo:getBoundingBox()` unless you
-- need some special behavior.
function ExtrudeHandlesImplementation.getBoundingBox(self: Class, selection: { Instance }, selectionInfo: SelectionInfo)
	-- When we only have one part selected, we're in extrude scaling mode,
	-- which can only be done in local space.
	local partsToResize, _ = selectionInfo:getObjectsToTransform()

	if #partsToResize == 1 then
		return selectionInfo:getLocalBoundingBox()
	else
		return selectionInfo:getBoundingBox()
	end
end

-- Should the tool act as an actual scale tool (scaling around the basis
-- CFrame), or scale by moving the edge of the bounding box?
function ExtrudeHandlesImplementation.shouldScaleFromCenter(self: Class, selection: { Instance }, selectionInfo: SelectionInfo, normalId: number)
	return self._draggerContext:isCtrlKeyDown()
end

-- If FFlagLimitScaling is enabled, this function is no longer used
-- as it is replaced by :axesToScale(...)
function ExtrudeHandlesImplementation.shouldKeepAspectRatio(self: Class, selection: { Instance }, selectionInfo: SelectionInfo, normalId: number)
	if self._draggerContext:isShiftKeyDown() then
		return true
	end

	local partsToResize = selectionInfo:getObjectsToTransform()

	if #partsToResize == 1 then
		local part = partsToResize[1]

		if part:IsA("Part") and part.Shape == Enum.PartType.Ball then
			return true
		end

		if part:IsA("UnionOperation") or part:IsA("NegateOperation") then
			return true
		end

		return false
	else
		return true
	end
end

function ExtrudeHandlesImplementation.axesToScale(self: Class, selectionInfo: SelectionInfo, normalId: number)
	local ALL_AXES = { X = true, Y = true, Z = true }
	-- UX: shift key forces equal aspect ratio scaling

	if self._draggerContext:isShiftKeyDown() then
		return ALL_AXES
	end

	local partsToResize = selectionInfo:getObjectsToTransform()

	-- multiple parts are only scaled with equal aspect ratio
	if #partsToResize ~= 1 then
		return ALL_AXES
	end

	local part = partsToResize[1]

	if part:IsA("Part") and part.Shape == Enum.PartType.Ball then
		-- special case: sphere only scales uniformly
		return ALL_AXES
	elseif part:IsA("UnionOperation") or part:IsA("NegateOperation") then
		-- special case: CSG operations are treated the same way as multiple parts
		return ALL_AXES
	end

	-- everything else is considered a regular single part, and can be scaled along one axis
	-- without keeping aspect ratio
	local axes = {
		{ X = true,  Y = false, Z = false },
		{ X = false, Y = true,  Z = false },
		{ X = false, Y = false, Z = true  },
	}

	return axes[normalId]
end

-- If FFlagLimitScaling is enabled, this function is no longer used
-- as it is replaced by :getMinMaxSizes()
--[[
	Find the smallest extents size that assures that all parts are larger
	than the minimum allowed part size.

	Resizing Cases:
	Suppose you have a part 5.5 studs long and a grid snap of 2
	 * You should be able to resize it down to 1.5 (5.5 % 2)
	Suppose you have a part 4 studs long and a grid snap of 2
	 * You should be able to resize it down to 2
	 * You should NOT be able to resize it down to 0 or MIN_PART_SIZE (It should
	   stop resizing at 2)
	Suppose you have a part 2.02 studs long and grid snap of 2
	 * You should NOT be able to resize it down to 0.02 (< MIN_PART_SIZE)
]]
function ExtrudeHandlesImplementation.getMinimumSize(self: Class, selection: { Instance }, selectionInfo: SelectionInfo, normalId: number)
	local partsToResize = selectionInfo:getObjectsToTransform()
	local _, _, boundsSize = self:getBoundingBox(selection, selectionInfo)

	if #partsToResize == 1 then
		-- The modulo covers resizing down to smaller than the initial size in
		-- gridSize increments.
		local gridSizeValue = self._draggerContext:getGridSize()
		local partSize = partsToResize[1].Size

		local xmod = partSize.X % gridSizeValue
		local ymod = partSize.Y % gridSizeValue
		local zmod = partSize.Z % gridSizeValue

		-- This handles the edge case of trying to resize down to zero from an
		-- even multiple of the grid size.
		local modSize = Vector3.new(
			xmod > 0 and xmod or gridSizeValue,
			ymod > 0 and ymod or gridSizeValue,
			zmod > 0 and zmod or gridSizeValue)

		-- This Max call handles the resizing down to less than MIN_PART_SIZE
		-- but still greater than zero edge case.
		local hardMinSize = Vector3.new(MIN_PART_SIZE, MIN_PART_SIZE, MIN_PART_SIZE)
		hardMinSize = hardMinSize:Max(modSize)

		return hardMinSize
	else
		local smallest = math.huge
		local minimumSize = HUGE_VECTOR

		for _, part in ipairs(partsToResize) do
			local size = part.Size
			local minLength = math.min(size.X, size.Y, size.Z)
			if minLength < smallest then
				smallest = minLength
				local sizeRatio = minLength / MIN_PART_SIZE
				minimumSize = boundsSize / sizeRatio
			end
		end

		-- Make sure that the resized normal of the part is at least as
		-- large as the grid size.
		local minSizeComponents =
			{minimumSize.X, minimumSize.Y, minimumSize.Z}
		local sizeRatio = minSizeComponents[normalId] / self._draggerContext:getGridSize()
		if sizeRatio < 1 then
			minimumSize = minimumSize / sizeRatio
		end

		-- Finally, we CAN still resize down to less than the grid size if
		-- we started out smaller than the grid size.
		return minimumSize:Min(boundsSize)
	end
end

--[[
	Find the min/max sizes the selection can be scaled to that ensure
	that all parts have sizes between MIN_PART_SIZE and MAX_PART_SIZE in all dimensions.
]]
function ExtrudeHandlesImplementation.getMinMaxSizes(self: Class, selectionInfo: SelectionInfo, axesToScale: { X: boolean, Y: boolean, Z: boolean }, bounds: Vector3)
	local axesVec = Math.setToVector3(axesToScale)

	-- this vector has 0 for axes along which we scale, and a huge number elsewhere
	local ignoredAxesVec = (ONES_VECTOR - axesVec) * MAX_PART_SIZE * 10
	local partsToResize = selectionInfo:getObjectsToTransform()

	local sizeMax = ZERO_VECTOR
	local sizeMin = HUGE_VECTOR

	local LOCAL_MAX_PART_SIZE = MAX_PART_SIZE
	local LOCAL_MIN_PART_SIZE = MIN_PART_SIZE

	-- special behavior for scaling a single truss part :(
	-- if trusses are a part of a group, ignore them
	if #partsToResize == 1 and partsToResize[1]:IsA("TrussPart") then
		LOCAL_MAX_PART_SIZE = 64
		LOCAL_MIN_PART_SIZE = 2
	end

	for _, part in ipairs(partsToResize) do
		sizeMax = sizeMax:Max(part.Size)
		sizeMin = sizeMin:Min(part.Size)
	end

	-- ignore axes that are not scaled by making them +/- huge
	local largestScaledDimension = Math.maxComponent(sizeMax - ignoredAxesVec)
	local smallestScaledDimension = Math.minComponent(sizeMin + ignoredAxesVec)

	local maxRatio = LOCAL_MAX_PART_SIZE / largestScaledDimension
	local minRatio = LOCAL_MIN_PART_SIZE / smallestScaledDimension

	-- Make sure the largest dimension of ANY part is between MIN_PART_SIZE and MAX_PART_SIZE after scaling along the given axes
	-- Leave other axes as they are.
	local maximumAllowableSize = bounds:Max(maxRatio * bounds - ignoredAxesVec)
	local minimumAllowableSize = bounds:Min(minRatio * bounds + ignoredAxesVec)

	return minimumAllowableSize, maximumAllowableSize
end

function ExtrudeHandlesImplementation.beginScale(self: Class, selection: { Instance }, initialSelectionInfo: SelectionInfo, normalId: number)
	if self._draggerContext.SetDraggingFunction then
		self._draggerContext.SetDraggingFunction(true)
	end

	local boundsCFrame, boundsOffset, boundsSize =
		self:getBoundingBox(selection, initialSelectionInfo)

	self._originalBoundingBoxSize = boundsSize
	self._originalBoundingBoxCFrame = boundsCFrame * CFrame.new(boundsOffset)

	self._originalCFrameMap = initialSelectionInfo:getOriginalCFrameMap()
	self._partsToResize, self._attachmentsToMove, self._modelsToResize =
		initialSelectionInfo:getObjectsToTransform()

	self._originalSizeMap = {}

	for _, part in ipairs(self._partsToResize) do
		self._originalSizeMap[part] = part.Size
	end

	self._lastDeltaSize = Vector3.zero
	self._jointMaker:pickUpParts(self._partsToResize)
	self._jointMaker:anchorParts()
	self._jointMaker:breakJointsToOutsiders()
	self._jointMaker:disconnectInternalJoints()

	self:_recordItemsToFixup(self._partsToResize, self._modelsToResize)

	if not self._draggerContext:areCollisionsEnabled() then
		self._temporaryTransparency = TemporaryTransparency.new(self._partsToResize)
	end
end

local function roundDownIfClose(value: number, gridSize: number, threshold: number)
	local snapped = math.floor(value / gridSize) * gridSize

	if value - snapped < threshold then
		return snapped
	else
		return value
	end
end

-- If the part size is almost (within RESIZE_COLLISION_THRESHOLD) aligned to the
-- grid size, then snap it exactly back to the grid size, to allow parts which
-- somehow gained a floating point error in their size to return to being sized
-- exactly on grid. Note: We can only round down because rounding up may bring
-- the part into collision with something it wasn't before.
function ExtrudeHandlesImplementation._maybeSnapPartSizeBackDownToGrid(self: Class, size: Vector3): Vector3
	local gridSize = self._draggerContext:getGridSize()

	return Vector3.new(
		roundDownIfClose(size.X, gridSize, RESIZE_COLLISION_THRESHOLD),
		roundDownIfClose(size.Y, gridSize, RESIZE_COLLISION_THRESHOLD),
		roundDownIfClose(size.Z, gridSize, RESIZE_COLLISION_THRESHOLD)
	)
end

local function largestComponent(vector: Vector3): number
	return math.min(math.abs(vector.X), math.abs(vector.Y), math.abs(vector.Z))
end

local function getFraction(lastSizeDelta: Vector3, thisSizeDelta: Vector3): number
	local maxLast = largestComponent(lastSizeDelta)
	local maxThis = largestComponent(thisSizeDelta)

	if maxThis == 0 then
		return 0
	else
		return maxLast / maxThis
	end
end

-- Scale / offset the selection by some local offset relative to the
-- initialSelectionInfo's basisCFrame. Also takes the new size of the bounding
-- box for convenience, though you may not need this.
function ExtrudeHandlesImplementation.updateScale(self: Class, localDeltaSize: Vector3, localOffset: Vector3)
	local doResize: (fraction: number) -> ()
	local lastOffset = localOffset
	local partsToResize = self._partsToResize

	if partsToResize and #partsToResize == 1 then
		local part = partsToResize[1]

		local originalSizeMap = self._originalSizeMap
		local originalSize = originalSizeMap and originalSizeMap[part]

		local originalCFrameMap = self._originalCFrameMap
		local originalCFrame = originalCFrameMap and originalCFrameMap[part]

		doResize = function(frac: number)
			self._lastDeltaSize = localDeltaSize * frac
			part.Size = originalSize + self._lastDeltaSize
			lastOffset = localOffset * frac
			part.CFrame = originalCFrame * CFrame.new(lastOffset)
		end
	else
		local originalCFrame = self._originalBoundingBoxCFrame
		local originalSize = self._originalBoundingBoxSize
		local center = originalCFrame.Position

		local globalOffset = originalCFrame:VectorToWorldSpace(localOffset)
		local totalScale =
			(originalSize + localDeltaSize).Magnitude / originalSize.Magnitude

		-- Precalculate the offsets to apply while scaling
		local totalOffsetForPartMap = {} :: {
			[BasePart]: Vector3,
		}

		for _, part in ipairs(self._partsToResize) do
			local originalPartPosition = self._originalCFrameMap[part].Position
			local offsetFromCenter = originalPartPosition - center
			totalOffsetForPartMap[part] = globalOffset + offsetFromCenter * (totalScale - 1)
		end

		doResize = function (frac: number)
			self._lastDeltaSize = localDeltaSize * frac
			lastOffset = localOffset * frac
			local scale = 1 + frac * (totalScale - 1)
			local scaleVector = Vector3.new(scale, scale, scale)

			for part, totalOffset in totalOffsetForPartMap do
				part.CFrame = self._originalCFrameMap[part] + totalOffset * frac
				part.Size = self._originalSizeMap[part] * scaleVector
			end
		end
	end

	if self._draggerContext:areCollisionsEnabled() then
		local initialFraction = getFraction(self._lastDeltaSize, localDeltaSize)
		local targetFraction = 1.0
		local includeInitiallyTouching = true
		local threshold = RESIZE_COLLISION_THRESHOLD / math.max(largestComponent(localDeltaSize), 1)

		self:_binarySearchForGoodPartSize(doResize,
			initialFraction,
			targetFraction,
			includeInitiallyTouching,
			threshold)
	else
		doResize(1.0)
	end

	-- Try to fix up floating point error on parts which are very close to the
	-- grid size.
	if #self._partsToResize == 1 then
		local thePart = self._partsToResize[1]
		thePart.Size = self:_maybeSnapPartSizeBackDownToGrid(thePart.Size)
	end

	-- Fixup stuff like meshes / constraint lengths
	local appliedScale =
		(self._originalBoundingBoxSize + self._lastDeltaSize) / self._originalBoundingBoxSize

	self:_applyFixup(appliedScale, lastOffset)
	self._jointMaker:fixupConstraintLengths()

	if self._draggerContext:shouldJoinSurfaces() then
		self._jointPairs = self._jointMaker:computeJointPairs()
	end

	return self._lastDeltaSize, lastOffset
end

function ExtrudeHandlesImplementation.endScale(self: Class)
	self._originalSizeMap = {}

	if self._jointPairs then
		self._jointPairs:createJoints()
		self._jointPairs = nil
	end

	local appliedScale = getFraction(
		self._originalBoundingBoxSize + self._lastDeltaSize,
		self._originalBoundingBoxSize)

	self._jointMaker:reconnectInternalJointsWithScale(appliedScale)
	self._jointMaker:restoreAnchored()
	self._jointMaker:putDownParts()

	if self._temporaryTransparency then
		self._temporaryTransparency:destroy()
		self._temporaryTransparency = nil
	end

	if self._draggerContext.SetDraggingFunction then
		self._draggerContext.SetDraggingFunction(false)
	end
end

function ExtrudeHandlesImplementation.render(self: Class, localDeltaSize: Vector3, localOffset: Vector3)
	if self._draggerContext:shouldJoinSurfaces() and self._jointPairs then
		local cframe = self._originalBoundingBoxCFrame * CFrame.new(localOffset)
		local size = self._originalBoundingBoxSize + localDeltaSize
		local scale = getBoundingBoxScale(self._draggerContext, cframe, size)
		return self._jointPairs:renderJoints(scale)
	else
		return nil
	end
end

--[[
	Finds the maximum value resizeFunction can be called with (called with a
	fraction between initialFraction and targetFraction) without any of the
	_jointMaker's parts colliding with other parts.
	If includeInitiallyTouching is false then collisions with parts it was
	colliding with to begin with are ignored.
]]
function ExtrudeHandlesImplementation._binarySearchForGoodPartSize(
	self: Class,
	resizeFunction: (number) -> (),
	initialFraction: number,
	targetFraction: number,
	includeInitiallyTouching: boolean,
	threshold: number
): number?
	-- Early out, can we resize right to the target?
	resizeFunction(targetFraction)
	local lastIterationHadCollision = self._jointMaker:isColliding(includeInitiallyTouching)

	if not lastIterationHadCollision then
		return nil
	end

	local collides = 0
	local min = initialFraction
	local max = targetFraction

	for _ = 1, 32 do
		local mid = 0.5 * (min + max)
		resizeFunction(mid)
		lastIterationHadCollision = self._jointMaker:isColliding(includeInitiallyTouching)

		if lastIterationHadCollision then
			collides = collides + 1
			max = mid
		else
			min = mid
		end
	end

	-- Don't allow slightly resizing away from the last value (which would
	-- introduce floating point error but no useful changes)
	if math.abs(min - initialFraction) < threshold then
		lastIterationHadCollision = true
		min = initialFraction
	end

	-- Ensure we stop in a non-colliding state
	if lastIterationHadCollision then
		resizeFunction(min)
	end

	return min
end

function ExtrudeHandlesImplementation._recordItemsToFixup(self: Class, parts: { BasePart }, models: { Model })
	self._fixupAttachments = {}
	self._fixupNontrivialAttachments = {}
	self._fixupOffsets = {}
	self._fixupScales = {}
	self._fixupPartPivot = {}
	self._fixupModelPivot = {}

	local pivotBasis = self._originalBoundingBoxCFrame:Inverse()

	for _, model in ipairs(models) do
		self._fixupModelPivot[model] = pivotBasis * model:GetPivot()
	end

	for _, part in ipairs(parts) do
		local inverseCFrame = part.CFrame:Inverse()
		local pivot = part.PivotOffset

		if pivot ~= EMPTY_CFRAME then
			self._fixupPartPivot[part] = pivot
		end

		for _, descendant in ipairs(part:GetDescendants()) do
			if descendant:IsA("Attachment") then
				if descendant.Parent ~= part then
					-- Note: We need this for bones, which may be under another
					-- bone rather than directly under the part.
					local cframeRelativeToPart = inverseCFrame * descendant.WorldCFrame
					local localPosition = cframeRelativeToPart.Position
					local localRotation = cframeRelativeToPart - localPosition
					table.insert(self._fixupNontrivialAttachments, {
						Attachment = descendant,
						RelativeTo = part,
						LocalRotation = localRotation,
						LocalPosition = localPosition,
					})
				else
					self._fixupAttachments[descendant] = descendant.Position
				end
			elseif descendant:IsA("DataModelMesh") then
				self._fixupOffsets[descendant] = descendant.Offset
				if descendant.ClassName == "FileMesh" or
					(descendant:IsA("SpecialMesh") and descendant.MeshType == Enum.MeshType.FileMesh) then
					self._fixupScales[descendant] = descendant.Scale
				end
			end
		end
	end
end

function ExtrudeHandlesImplementation._applyFixup(self: Class, scaledBy: Vector3, translatedBy: Vector3)
	for attachment, position in pairs(self._fixupAttachments) do
		attachment.Position = position * scaledBy
	end

	-- Note: The forwards iteration is important here. Parents must have
	-- their CFrames updated first for things to work out as intended, as
	-- we're assigning WorldCFrame, which has a dependency on the CFrames
	-- of the ancestor attachments.
	for _, data in ipairs(self._fixupNontrivialAttachments) do
		data.Attachment.WorldCFrame =
			data.RelativeTo.CFrame * CFrame.new(data.LocalPosition * scaledBy) * data.LocalRotation
	end

	for offsetItem, offset in pairs(self._fixupOffsets) do
		offsetItem.Offset = offset * scaledBy
	end

	for scaleItem, scale in pairs(self._fixupScales) do
		scaleItem.Scale = scale * scaledBy
	end

	for part, localPivot in pairs(self._fixupPartPivot) do
		local position = localPivot.Position
		local rotation = localPivot - position
		part.PivotOffset = rotation + position * scaledBy
	end

	for model, localPivot in pairs(self._fixupModelPivot) do
		local position = localPivot.Position
		local rotation = localPivot - position
		model.WorldPivot = self._originalBoundingBoxCFrame * CFrame.new(translatedBy + position * scaledBy) * rotation
	end
end

return ExtrudeHandlesImplementation


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
local Utility = DraggerFramework.Utility

local AttachmentMover = Utility.AttachmentMover
local DragHelper = Utility.DragHelper
local PartMover = Utility.PartMover

local FreeformDragger = {}
FreeformDragger.__index = FreeformDragger

type DraggerContext = DraggerFramework.DraggerContext
type DraggerToolModel = DraggerFramework.DraggerToolModel
type AttachmentMover = DraggerFramework.AttachmentMover
type DragTarget = DraggerFramework.DragTarget
type JointPairs = DraggerFramework.JointPairs
type PartMover = DraggerFramework.PartMover

type DragInfo = {
	clickPoint: Vector3?,
	basisPoint: Vector3,
	attachmentBeingDragged: Attachment?,
}

export type FreeformDragInfo = {
	basisPoint: Vector3,
	clickPoint: Vector3,
	clickedFaceInstance: Instance?,
	attachmentBeingDragged: Attachment?,
}

type FreeformDragger = {
	_draggerContext: DraggerContext,
	_draggerToolModel: DraggerToolModel,
	_dragInfo: FreeformDragInfo,

	_partMover: PartMover,
	_attachmentMover: AttachmentMover,

	_tiltRotate: CFrame,
	_lastAppliedTransform: CFrame,
	_jointPairs: JointPairs?,

	_lastDragTarget: DragTarget?,
	_raycastFilter: { BasePart },
}

type Proto = typeof(FreeformDragger)
export type Class = setmetatable<FreeformDragger, Proto>

function FreeformDragger.new(draggerContext: DraggerContext, draggerToolModel: DraggerToolModel, dragInfo: FreeformDragInfo)
	local self = setmetatable({
		_draggerContext = draggerContext,
		_draggerToolModel = draggerToolModel,
		_dragInfo = dragInfo,

		_partMover = PartMover.new(),
		_attachmentMover = AttachmentMover.new(),
		_tiltRotate = CFrame.identity,
		_lastAppliedTransform = CFrame.identity,

		_raycastFilter = {},
	}, FreeformDragger) :: Class

	self:_init()
	return self
end

function FreeformDragger._init(self: Class)
	self:_initIgnoreList(self._draggerToolModel._selectionInfo:getObjectsToTransform())
	self:_initMovers()
end

function FreeformDragger._initMovers(self: Class)
	local breakJointsToOutsiders = not self._draggerToolModel._draggerContext:areConstraintsEnabled()

	local partsToMove, attachmentsToMove, modelsToMove =
		self._draggerToolModel._selectionInfo:getObjectsToTransform()

	self._partMover:setDragged(
		partsToMove,
		self._draggerToolModel._selectionInfo:getOriginalCFrameMap(),
		breakJointsToOutsiders,
		self._draggerToolModel._selectionInfo:getBoundingBox().Position,
		self._draggerToolModel:getSelectionWrapper():get(),
		modelsToMove)

	self._attachmentMover:setDragged(attachmentsToMove)
end

function FreeformDragger._initIgnoreList(self: Class, parts: { BasePart })
	local filter = table.create<<BasePart>>(#parts + 1)

	for i, part in ipairs(parts) do
		filter[i] = part
	end

	local ignorePart = self._partMover:getIgnorePart()

	if ignorePart then
		table.insert(filter, ignorePart)
	end

	self._raycastFilter = filter
end

function FreeformDragger.render(self: Class)
	if self._draggerToolModel._draggerContext:shouldJoinSurfaces() and self._jointPairs then
		local cframe, offset = self._draggerToolModel._selectionInfo:getBoundingBox()
		local focus = cframe * offset
		return self._jointPairs:renderJoints(self._draggerToolModel._draggerContext:getHandleScale(focus))
	end
end

function FreeformDragger.rotate(self: Class, axis: Vector3)
	if self._draggerToolModel._draggerContext:areConstraintsEnabled() then
		return
	end

	local mainCFrame = self._draggerToolModel._selectionInfo:getLocalBoundingBox()
	local lastTargetMatrix = CFrame.identity

	if self._lastDragTarget then
		lastTargetMatrix = self._lastDragTarget.targetMatrix or CFrame.identity
	end

	self._tiltRotate = DragHelper.updateTiltRotate(
		workspace.CurrentCamera.CFrame,
		self._draggerToolModel._draggerContext:getMouseRay(),
		self._raycastFilter :: { Instance }, mainCFrame, lastTargetMatrix,
		self._tiltRotate, axis, self._draggerToolModel:shouldAlignDraggedObjects())
end

function FreeformDragger._updateGeometric(self: Class)
	local lastTargetMatrix = nil

	if self._lastDragTarget then
		lastTargetMatrix = self._lastDragTarget.targetMatrix
	end

	local function snapFunction(distance: number): number
		return self._draggerContext:snapToGridSize(distance)
	end

	local localBoundingBoxCFrame, localBoundingBoxOffset, localBoundingBoxSize =
		self._draggerToolModel._selectionInfo:getLocalBoundingBox()

	local dragTarget = DragHelper.getDragTarget(
		self._draggerToolModel._draggerContext:getMouseRay(),
		snapFunction,
		self._dragInfo.clickPoint,
		self._raycastFilter :: { Instance },
		localBoundingBoxCFrame,
		self._dragInfo.basisPoint,
		localBoundingBoxSize,
		localBoundingBoxOffset,
		self._tiltRotate,
		lastTargetMatrix or CFrame.identity,
		self._draggerToolModel:shouldAlignDraggedObjects(),
		localBoundingBoxSize
	)

	if dragTarget then
		self._lastDragTarget = dragTarget
		local originalCFrame = localBoundingBoxCFrame
		local newCFrame = dragTarget.mainCFrame
		local globalTransform = newCFrame * originalCFrame:Inverse()
		self._partMover:transformTo(globalTransform)
		self._attachmentMover:transformTo(globalTransform)
		self._lastAppliedTransform = globalTransform

		if self._draggerToolModel._draggerContext:shouldJoinSurfaces() then
			self._jointPairs = self._partMover:computeJointPairs(globalTransform)
		end
	end
end

function FreeformDragger._updatePhysical(self: Class)
	local localBoundingBoxCFrame =
		self._draggerToolModel._selectionInfo:getLocalBoundingBox()

	if not self._dragInfo.clickPoint then
		return
	end

	local dragTarget = DragHelper.getCameraPlaneDragTarget(
		self._draggerToolModel._draggerContext:getMouseRay(),
		workspace.CurrentCamera.CFrame.LookVector,
		localBoundingBoxCFrame:pointToWorldSpace(self._dragInfo.clickPoint))

	if dragTarget then
		local collisionsMode =
			self._draggerToolModel._draggerContext:areCollisionsEnabled() and
			Enum.IKCollisionsMode.IncludeContactedMechanisms or
			Enum.IKCollisionsMode.NoCollisions
		local actualGlobalTransformUsed =
			self._partMover:moveToWithIk(dragTarget.mainCFrame, collisionsMode)
		self._attachmentMover:transformTo(actualGlobalTransformUsed)
		self._lastAppliedTransform = actualGlobalTransformUsed
	end
end

function FreeformDragger.update(self: Class)
	if self._draggerToolModel._draggerContext:areConstraintsEnabled() then
		self:_updatePhysical()
	else
		self:_updateGeometric()
	end
end

function FreeformDragger.destroy(self: Class)
	local attachment = self._dragInfo.attachmentBeingDragged
	if attachment then
		-- Single attachment being dragged case -- In that case we need to
		-- potentially reparent the attachment to the part it was dragged onto.
		local mouseRay = self._draggerToolModel._draggerContext:getMouseRay()
		local worldResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction)

		if worldResult then
			local worldHit = worldResult.Instance
			local worldCFrame = attachment.WorldCFrame

			if attachment.Parent ~= worldHit then
				-- pcall as the dragged attachment may have been Destroyed.
				pcall(function()
					attachment.Parent = worldHit
				end)

				attachment.WorldCFrame = worldCFrame
			end
		else
			-- When there's no valid target, reset the AttachmentMover so we
			-- don't move the attachment.
			self._attachmentMover:transformTo(CFrame.identity)
		end
	end

	if self._draggerToolModel._draggerContext:shouldJoinSurfaces() and self._jointPairs then
		self._jointPairs:createJoints()
	end

	self._jointPairs = nil
	self._partMover:commit()
	self._attachmentMover:commit()

	-- Return an updated SelectionInfo to prevent the DraggerFramework from
	-- computing fresh one from scratch (it would do that by default if we
	-- did not return anything).
	-- The additional info we have lets us compute the new one more efficiently
	-- by deriving it from the old one based on the operation we did.
	return self._draggerToolModel._selectionInfo:getTransformedCopy(self._lastAppliedTransform)
end

return FreeformDragger


--[[
	class HoverEscapeDetector

	Constructed with a given selectable object and a given callback. Calls the
	callback whenever selectable object changes in some way that could make it
	no longer hovered thanks to factors external to the dragger (such as when
	it is deleted through a hotkey).
]]

local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

local HoverEscapeDetector = {}
HoverEscapeDetector.__index = HoverEscapeDetector

export type Class = setmetatable<{
	_destroyed: boolean,
	_hoverInstanceEscapedConnection: RBXScriptConnection,
	_hoverInstanceContentsChangedConnection: RBXScriptConnection,
}, typeof(HoverEscapeDetector)>

function HoverEscapeDetector.new(draggerContext: DraggerContext, hoveredItem: Instance, onEscaped: () -> ()): Class
	local firedFlag = false
	local signalsAreDeferred = false
	local temporaryEvent = Instance.new("BindableEvent")

	temporaryEvent.Event:Once(function()
		firedFlag = true
	end)

	temporaryEvent:Fire()
	signalsAreDeferred = not firedFlag
	temporaryEvent:Destroy()

	local self = setmetatable({
		_destroyed = false,
	}, HoverEscapeDetector)

	self._hoverInstanceEscapedConnection =
		hoveredItem.AncestryChanged:Connect(function(child, newParent)
			if not signalsAreDeferred then
				-- Waiting on the parent changing here is done to give
				-- the engine a chance to update the physics state.
				-- Engine physics state updates happen on hierarchy
				-- changed events too, so they may not have happened
				-- yet when this event is fired, which would cause raycasts
				-- to return wrong results and other oddities like that.
				-- If we wait for the associated parent property changed
				-- signal to fire, then we can be sure the physics state
				-- updates have already ocurred.
				child:GetPropertyChangedSignal("Parent"):Wait()
			end

			if not self._destroyed then
				onEscaped()
			end
		end)
	self._hoverInstanceContentsChangedConnection =
		hoveredItem.DescendantRemoving:Connect(function(descendant)
			-- Ignore other junk like temporary movement welds being
			-- removed from the hover selectable.
			-- For example if we end the drag over one of the
			-- dragged parts this would otherwise pick up the temp
			-- movement weld being removed from under it.
			if descendant:IsA("BasePart") or descendant:IsA("Attachment") then
				descendant:GetPropertyChangedSignal("Parent"):Wait()

				if not self._destroyed then
					onEscaped()
				end
			end
		end)

	return self
end

function HoverEscapeDetector.destroy(self: Class)
	assert(not self._destroyed)
	self._destroyed = true

	self._hoverInstanceEscapedConnection:Disconnect()
	self._hoverInstanceContentsChangedConnection:Disconnect()

	self._destroyed = true
end

return HoverEscapeDetector


local SelectionService = game:GetService("Selection")

local Packages = script.Parent.Parent
local DraggerFramework = require(Packages.DraggerFramework)
local isProtectedInstance = DraggerFramework.Utility.isProtectedInstance

local Selection = {}
Selection.__index = Selection

export type Class = setmetatable<{
	SelectionChanged: RBXScriptSignal,
}, typeof(Selection)>

export type Hint = {
	Added: { Instance },
	Removed: { Instance },
}

function Selection.new(): Class
	local self = setmetatable({}, Selection)
	self.SelectionChanged = SelectionService.SelectionChanged
	return self
end

--[[
	Note: The fact that the processing of hint _first_ removes and _then_ adds
	items to the selection is important, all Selection implementations should
	handle things this way.
]]
function Selection.Set(self: Class, objects: { Instance }, hint: Hint?)
	if hint then
		SelectionService:Remove(hint.Removed)
		SelectionService:Add(hint.Added)
	else
		SelectionService:Set(objects)
	end
end

function Selection.Get(self: Class): { Instance }
	local unsafeSelection = SelectionService:Get()
	local safeSelection = {}

	for _, unsafeInstance in ipairs(unsafeSelection) do
		if not isProtectedInstance(unsafeInstance) then
			table.insert(safeSelection, unsafeInstance)
		end
	end

	return safeSelection
end

function Selection.GetActiveInstance(self: Class): Instance?
	local success, activeInstance = pcall(function()
		return (SelectionService :: any).ActiveInstance
	end)

	if success then
		return activeInstance
	else
		return nil
	end
end

return Selection


-- Baseline wrapper around the default SelectionInfo implementation provided in DraggerFramework.
-- This provides backwards compatibility for its previous place of residence, while still allowing
-- consumers to extend it as needed.

local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
local SelectionInfo = DraggerFramework.Utility.SelectionInfo

export type Class = DraggerFramework.SelectionInfo
return SelectionInfo


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type TemporaryTransparency = DraggerFramework.TemporaryTransparency
type AttachmentMover = DraggerFramework.AttachmentMover
type DraggerContext = DraggerFramework.DraggerContext
type JointPairs = DraggerFramework.JointPairs
type PartMover = DraggerFramework.PartMover

local Utility = DraggerFramework.Utility
local TemporaryTransparency = Utility.TemporaryTransparency
local AttachmentMover = Utility.AttachmentMover
local PartMover = Utility.PartMover

local getBoundingBoxScale = Utility.getBoundingBoxScale
local getGeometry = Utility.getGeometry

local SelectionInfo = require(DraggerSchemaCore.SelectionInfo)
type SelectionInfo = SelectionInfo.Class

local TransformHandlesImplementation = {}
TransformHandlesImplementation.__index = TransformHandlesImplementation

-- The difference from exactly touching to try to bring a pair of parts within
-- when collisions are enabled.
-- After dragging one part into a collision with another one, the closest pair
-- of points P1 on the first part and P2 on the second part should satisfy:
-- (P1 - P2).Magnitude < TRANSFORM_COLLISION_THRESHOLD
local TRANSFORM_COLLISION_THRESHOLD = 0.0005

-- How far to snap the pivot when summoning the handles
local SUMMON_SNAP_THRESHOLD_PIXELS = 16

-- The center snap threshold is more generous since you can't easily tell where
-- the center is trying to snap to it blind (you don't see it highlighted while
-- attempting to snap to it)
local SUMMON_SNAP_CENTER_THRESHOLD_PIXELS = 20

type IKTransformFunction = (
	partMover: PartMover,
	transform: CFrame,
	collisionsMode: Enum.IKCollisionsMode
) -> CFrame

type TransformHandlesImplementation = {
	_draggerContext: DraggerContext,
	_ikTransformFunction: IKTransformFunction,
	_partMover: PartMover,
	_attachmentMover: AttachmentMover,

	_initialSelectionInfo: SelectionInfo?,
	_lastGoodGeometricTransform: CFrame?,
	_lastAppliedTransform: CFrame?,
	_hasPartsToMove: boolean?,
	_centerPoint: CFrame,
	_boundingBoxSize: Vector3?,
	_temporaryTransparency: TemporaryTransparency?,
	_jointPairs: JointPairs?,
}

type Proto = typeof(TransformHandlesImplementation)
export type Class = setmetatable<TransformHandlesImplementation, Proto>

local function ikTransformNoOp(partMover: PartMover, transform: CFrame, collisionsMode: Enum.IKCollisionsMode): CFrame
	partMover:transformTo(transform)
	return transform
end

function TransformHandlesImplementation.new(draggerContext: DraggerContext, ikTransformFunction: IKTransformFunction?): Class
	return setmetatable({
		_draggerContext = draggerContext,
		_ikTransformFunction = ikTransformFunction or ikTransformNoOp,
		_centerPoint = CFrame.identity,
		_partMover = PartMover.new(),
		_attachmentMover = AttachmentMover.new(),
	}, TransformHandlesImplementation) :: Class
end

--[[
	Start dragging the items in initialSelectionInfo.
]]
function TransformHandlesImplementation.beginDrag(self: Class, selection: { Instance }, initialSelectionInfo: SelectionInfo)
	if self._draggerContext.SetDraggingFunction then
		self._draggerContext.SetDraggingFunction(true)
	end

	local partsToMove, attachmentsToMove, modelsToMove =
		initialSelectionInfo:getObjectsToTransform()

	self._initialSelectionInfo = initialSelectionInfo

	self._lastGoodGeometricTransform = CFrame.identity
	self._lastAppliedTransform = CFrame.identity
	self._hasPartsToMove = #partsToMove > 0

	local basisCFrame, offset
	basisCFrame, offset, self._boundingBoxSize =
		initialSelectionInfo:getBoundingBox()

	local centerPoint = basisCFrame * CFrame.new(offset)
	self._centerPoint = centerPoint

	local breakJoints = not self:_shouldSolveConstraints()

	self._partMover:setDragged(
		partsToMove,
		initialSelectionInfo:getOriginalCFrameMap(),
		breakJoints,
		centerPoint.Position,
		selection,
		modelsToMove)

	self._attachmentMover:setDragged(
		attachmentsToMove)

	if not self._draggerContext:areCollisionsEnabled() then
		self._temporaryTransparency = TemporaryTransparency.new(partsToMove)
	end
end

--[[
	Try to move the selection passed to beginDrag by a global transform relative
	to where it started, that is for each point p in the selection:
	  p' = globalTransform * p
	Then return that global transform that was actually applied.
]]
function TransformHandlesImplementation.updateDrag(self: Class, globalTransform: CFrame): CFrame
	local ret;

	if self:_shouldSolveConstraints() then
		ret = self:_transformInverseKinematics(globalTransform)
	else
		ret = self:_transformGeometric(globalTransform)
	end

	local draggerContext: DraggerContext = self._draggerContext

	if draggerContext.DragUpdatedSignal then
		draggerContext.DragUpdatedSignal:Fire(self._initialSelectionInfo)
	end

	return ret
end

--[[
	Finish dragging the items.
]]
function TransformHandlesImplementation.endDrag(self: Class)
	local initialSelectionInfo = assert(self._initialSelectionInfo, "You must call beginDrag before endDrag")
	local draggerContext: DraggerContext = self._draggerContext

	if draggerContext.SetDraggingFunction then
		draggerContext.SetDraggingFunction(false)
	end

	if draggerContext:shouldJoinSurfaces() and self._jointPairs then
		self._jointPairs:createJoints()
	end

	self._jointPairs = nil
	self._partMover:commit()
	self._attachmentMover:commit()
	if self._temporaryTransparency then
		self._temporaryTransparency:destroy()
		self._temporaryTransparency = nil
	end

	-- Return an updated SelectionInfo to prevent the DraggerFramework from
	-- computing fresh one from scratch (it would do that by default if we
	-- did not return anything).
	-- The additional info we have lets us compute the new one more efficiently
	-- by deriving it from the old one based on the operation we did.

	return initialSelectionInfo:getTransformedCopy(self._lastAppliedTransform)
end

--[[
	Renders any snapping, joint, etc widgets that should show up while dragging.
	Returns: A Roact element.
]]
function TransformHandlesImplementation.render(self: Class, globalTransform: CFrame)
	local centerPoint = self._centerPoint or CFrame.identity
	local newCenterPoint = globalTransform * centerPoint

	if self._draggerContext:shouldJoinSurfaces() and self._jointPairs then
		local scale = getBoundingBoxScale(
			self._draggerContext, newCenterPoint, self._boundingBoxSize)
		return self._jointPairs:renderJoints(scale)
	else
		-- Don't render anything
		return nil
	end
end

--[[
	For direct geometric movement, move as a delta from the position the
	part started at on mouse down to reduce floating point error.
]]
function TransformHandlesImplementation._transformGeometric(self: Class, globalTransform: CFrame)
	if globalTransform == self._lastGoodGeometricTransform then
		return globalTransform
	end

	-- Transform the parts, resolving collisions if needed
	local appliedTransform = self:_safelyTransformParts(
		self._lastGoodGeometricTransform, globalTransform)
	if self._draggerContext:shouldJoinSurfaces() then
		self._jointPairs = self._partMover:computeJointPairs(appliedTransform)
	end

	-- Attachments follow what the parts did
	self._attachmentMover:transformTo(appliedTransform)
	self._lastGoodGeometricTransform = appliedTransform
	self._lastAppliedTransform = appliedTransform

	return appliedTransform
end

--[[
	For inverse kinematics movement, always move in incremental steps
]]
function TransformHandlesImplementation._transformInverseKinematics(self: Class, globalTransform: CFrame)
	local collisionsMode =
		self._draggerContext:areCollisionsEnabled() and
		Enum.IKCollisionsMode.IncludeContactedMechanisms or
		Enum.IKCollisionsMode.NoCollisions

	-- Transform the parts using IK
	local actualGlobalTransformUsed =
		self._ikTransformFunction(self._partMover, globalTransform, collisionsMode)
	if self._draggerContext:shouldJoinSurfaces() then
		self._jointPairs = self._partMover:computeJointPairs(actualGlobalTransformUsed)
	end

	-- Attachments follow what the parts did
	self._attachmentMover:transformTo(actualGlobalTransformUsed)
	self._lastAppliedTransform = actualGlobalTransformUsed

	return actualGlobalTransformUsed
end

local function cframesAreWithinThreshold(a: CFrame, b: CFrame, threshold: number)
	local dist = (a.Position - b.Position).Magnitude

	local rotation = math.max(
		(a.XVector - b.XVector).Magnitude,
		(a.YVector - b.YVector).Magnitude,
		(a.ZVector - b.ZVector).Magnitude)

	return dist < threshold and rotation < threshold, dist, rotation
end

function TransformHandlesImplementation._toLocalTransform(self: Class, globalTransform: CFrame)
	return self._centerPoint:Inverse() * globalTransform * self._centerPoint
end

function TransformHandlesImplementation._toGlobalTransform(self: Class, localTransform: CFrame)
	return self._centerPoint * localTransform * self._centerPoint:Inverse()
end

--[[
	Resolve collisions via a binary search between the last good delta and the
	currently desired delta. Assume that the last good delta is free, the
	desired delta is blocked, and there is a single point between the two where
	it switches between blocked and free.
	Return the good delta that we moved the parts to.
]]
function TransformHandlesImplementation._safelyTransformParts(self: Class, fromTransform: CFrame, goalTransform: CFrame)
	self._partMover:transformTo(goalTransform)

	-- Easy path if there are no collisions to resolve
	if not self._draggerContext:areCollisionsEnabled() or
		not self._partMover:isIntersectingOthers() then
		return goalTransform
	end

	-- We need the transforms in local space for them to lerp nicely
	local start = self:_toLocalTransform(fromTransform)
	local goal = self:_toLocalTransform(goalTransform)
	local isIntersecting = true
	local iterations = 0

	while not cframesAreWithinThreshold(start, goal, TRANSFORM_COLLISION_THRESHOLD) do
		local mid = start:Lerp(goal, 0.5)
		self._partMover:transformTo(self:_toGlobalTransform(mid))
		isIntersecting = self._partMover:isIntersectingOthers()

		if isIntersecting then
			goal = mid
		else
			start = mid
		end

		iterations += 1

		-- We should never need more than 32 iterations
		-- if we do it probably means there was a floating point error
		-- The safest thing to do in this case is to reset to the
		-- original position

		if iterations > 32 then
			start = self:_toLocalTransform(fromTransform)
			break
		end
	end

	-- Special case to not let us move very slightly and then stop. If we allow
	-- that then a translate which collides almost right away will produce no
	-- visible changes but introduce floating point error.
	-- The 2x is not a random fudge factor, it is precisely chosen: `goal` and
	-- `start` always bound the true collision point. After the loop, they are
	-- guaranteed to be within the threshold of eachother. We want to test
	-- whether the the move is within the threshold of zero. That means, at the
	-- very worst, the true collision point is at +threshold, and the loop
	-- exited at +threshold of the collision point, for a total of 2x threshold
	-- away from zero.

	if cframesAreWithinThreshold(start, CFrame.identity, TRANSFORM_COLLISION_THRESHOLD * 2) then
		start = CFrame.identity
		isIntersecting = true
	end

	local globalStart = self:_toGlobalTransform(start)

	-- Also don't allow us to move very slightly from the last move for the
	-- same reason as above, a very small move means we were already stopped at
	-- a good location but then overshot it.
	if cframesAreWithinThreshold(globalStart, fromTransform, TRANSFORM_COLLISION_THRESHOLD * 2) then
		globalStart = fromTransform
		isIntersecting = true
	end

	-- Have to make sure that we end on a non-intersection. The invariant is
	-- that start is the best safe position we've found, so we can move it.
	if isIntersecting then
		self._partMover:transformTo(globalStart)
	end

	-- Either we ended the loop on an intersection, and the above code moved us
	-- to start, or we ended on a non-intersect, in which case start is an
	-- up to date representation of the mid we last moved the parts to.
	return globalStart
end

function TransformHandlesImplementation._shouldSolveConstraints(self: Class)
	return self._draggerContext:areConstraintsEnabled() and self._hasPartsToMove
end

local function toVector2(vector3: Vector3): Vector2
	return Vector2.new(vector3.X, vector3.Y)
end

-- When summoning the rotate, find a point to snap the temporary pivot to given
-- the hit geometry under the cursor.
function TransformHandlesImplementation.findSummonSnap(self: Class, hitPoint: Vector3, hitItem: BasePart): (CFrame?, boolean)
	if hitItem:IsA("Terrain") then
		return nil, false
	end

	local geometry = getGeometry(hitItem)

	if geometry.shape ~= "Mesh" then
		return nil, false
	end

	-- Find the face we hit
	local hitFace
	local hitDistance = math.huge

	for _, face in ipairs(geometry.faces) do
		local projection = math.abs((hitPoint - face.vertices[1]).Unit:Dot(face.normal))
		if projection < hitDistance then
			hitDistance = projection
			hitFace = face
		end
	end

	if hitFace then -- Are we close to any of the verts? (in screen space)
		local screenPoint = toVector2(self._draggerContext:worldToViewportPoint(hitPoint))
		local closestVertexInThreshold
		local closestVertexDist = SUMMON_SNAP_THRESHOLD_PIXELS

		for _, vertex in ipairs(hitFace.vertices) do
			local vertexOnScreen = toVector2(self._draggerContext:worldToViewportPoint(vertex))
			local distance = (vertexOnScreen - screenPoint).Magnitude

			if distance < closestVertexDist then
				closestVertexDist = distance
				closestVertexInThreshold = vertex
			end
		end

		if closestVertexInThreshold then
			return CFrame.fromMatrix(closestVertexInThreshold, hitFace.direction, hitFace.normal), true
		end

		-- Are we close to any of the edges? (in screen space)
		local closestEdgeInThreshold
		local closestEdgeDirection

		local closestEdgeDist = SUMMON_SNAP_THRESHOLD_PIXELS
		local vertexCount = #hitFace.vertices

		for i = 1, vertexCount do
			local v0 = hitFace.vertices[i]
			local v1 = hitFace.vertices[i % vertexCount + 1]

			local s0 = toVector2(self._draggerContext:worldToViewportPoint(v0))
			local s1 = toVector2(self._draggerContext:worldToViewportPoint(v1))

			local edgeDirection2D = (s1 - s0).Unit
			local toHit2D = (screenPoint - s0)

			local toPointOnEdge2D = edgeDirection2D * toHit2D:Dot(edgeDirection2D)
			local distance2D = (toHit2D - toPointOnEdge2D).Magnitude

			if distance2D < closestEdgeDist then
				local edgeDirection = (v1 - v0).Unit
				local toHit = hitPoint - v0
				local toPointOnEdge = edgeDirection * toHit:Dot(edgeDirection)

				closestEdgeInThreshold = v0 + toPointOnEdge
				closestEdgeDirection = edgeDirection
				closestEdgeDist = distance2D
			end
		end

		if closestEdgeInThreshold and closestEdgeDirection then
			return CFrame.fromMatrix(closestEdgeInThreshold, closestEdgeDirection, hitFace.normal), true
		end

		-- Are we close to the center?
		local mouseAt = self._draggerContext:getMouseLocation()
		local centerPoint = toVector2(self._draggerContext:worldToViewportPoint(hitItem.Position))

		if (mouseAt - centerPoint).Magnitude < SUMMON_SNAP_CENTER_THRESHOLD_PIXELS then
			return hitItem.CFrame, false
		end
	end

	return nil, false
end

return TransformHandlesImplementation


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

return function(draggerContext: DraggerContext, waypointIdentifier: string, waypointText: string)
	draggerContext:addUndoWaypoint(waypointIdentifier, waypointText)
end


local DraggerSchemaCore = script.Parent
local Packages = DraggerSchemaCore.Parent

local DraggerFramework = require(Packages.DraggerFramework)
type DraggerContext = DraggerFramework.DraggerContext

local getSelectableWithCache = require(DraggerSchemaCore.getSelectableWithCache)
local shouldSelectSubPart = require(DraggerSchemaCore.shouldSelectSubPart)

type ISelectable =
	| PVInstance
	| Attachment

type DragCandidate = {
	Center: Vector3,
	Selectable: ISelectable,
}

return function(draggerContext: DraggerContext)
	local dragCandidates = {} :: { DragCandidate }

	local areConstraintDetailsShown = draggerContext:areConstraintDetailsShown()
	local shouldSelectIndividually = shouldSelectSubPart(draggerContext)

	local getSelectableCache = {}
	local alreadyAddedSet = {} :: { [Instance]: true }

	local descendants = workspace:GetDescendants()

	for _, object in ipairs(descendants) do
		-- TODO: Refactor this to not look through every BasePart
		-- unless shouldSelectIndividually is true
		-- (though it still has to look through every Attachment)
		if object:IsA("BasePart") then
			if not object.Locked then
				local selectable = getSelectableWithCache(object, shouldSelectIndividually, getSelectableCache)

				if selectable and not alreadyAddedSet[selectable] then
					local center = Vector3.zero
					local iselectable: ISelectable?

					if selectable:IsA("Model") then
						if selectable:IsA("Tool") then
							center = object.Position
							iselectable = selectable
						else
							center = selectable:GetBoundingBox().Position
							iselectable = selectable
						end
					elseif selectable:IsA("BasePart") then
						center = selectable.Position
						iselectable = selectable
					end

					alreadyAddedSet[selectable] = true

					if iselectable then
						table.insert(dragCandidates, {
							Center = center,
							Selectable = iselectable,
						})
					end
				end
			end
		elseif object:IsA("Attachment") then
			if object.Visible or areConstraintDetailsShown then
				table.insert(dragCandidates, {
					Center = object.WorldPosition,
					Selectable = object,
				})
			end
		end
	end

	return dragCandidates
end