--!strict

local CaptureService = game:GetService("CaptureService") :: CaptureService
local ServerStorage = game:GetService("ServerStorage") :: ServerStorage

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Trove = require(pluginRoot.Packages.Trove)

local State = require(pluginRoot.Main.State)

local Bindings = {}
local trove = Trove.new()

-- Private

type Invocation = {
	identifier: string,
	callback: (...any) -> ...any,
}

local invocationByIdentifier = {}
for _, invocationModule in script.Invocations:GetDescendants() do
	if invocationModule:IsA("ModuleScript") then
		local invocation = require(invocationModule) :: Invocation
		assert(
			not invocationByIdentifier[invocation.identifier],
			`Duplicate invocation identifier: {invocation.identifier}`
		)
		invocationByIdentifier[invocation.identifier] = invocation
	end
end

local function connectBindings(plugin: Plugin, interface: Instance)
	local binding = interface:FindFirstChild("Binding") :: ModuleScript?
	local bindable = binding and binding:FindFirstChild("Bindable") :: BindableFunction?

	if bindable then
		bindable.OnInvoke = function(identifier: string, ...: any)
			local results
			local packedArgs = table.pack(...)
			local success, err = pcall(function()
				local invocation = assert(invocationByIdentifier[identifier], `No invocation found for: {identifier}`)
				results = table.pack(invocation.callback(table.unpack(packedArgs)))
			end)

			if success then
				return true, table.unpack(results)
			end

			return false, err
		end

		plugin.Unloading:Connect(function()
			bindable.OnInvoke = function() end
		end)
	end
end

local function createBindings(plugin: Plugin)
	local interface = script.Interface:Clone()
	interface.Name = "Bindings"

	local descendants = interface:GetDescendants()
	table.insert(descendants, interface)

	for _, descendant in descendants do
		trove:Add(descendant)
	end

	connectBindings(plugin, interface)

	local camera = Instance.new("Camera")
	camera.Name = "Photobooth"
	camera.Archivable = false
	trove:Add(camera)

	interface.Parent = camera
	return camera
end

local isEnabled = false
local function setEnabled(plugin: Plugin, enabled: boolean)
	if isEnabled ~= enabled then
		isEnabled = enabled
		trove:Clean()

		if isEnabled then
			local interface = createBindings(plugin)
			interface.Parent = ServerStorage
		end
	end
end

-- Public

function Bindings.setup(plugin: Plugin)
	setEnabled(plugin, State.read().saved.bindingsEnabled)
	local disconnect = State.onChanged(function()
		setEnabled(plugin, State.read().saved.bindingsEnabled)
	end)

	local camera = Instance.new("Camera")
	camera.Name = "Photobooth"
	camera.Archivable = false
	camera.Parent = CaptureService

	local connectBinding = Instance.new("BindableFunction")
	connectBinding.Archivable = false
	connectBinding.Name = "ConnectBinding"
	connectBinding.OnInvoke = function(instance: Instance)
		if instance:HasTag("Photobooth_Bindings") then
			connectBindings(plugin, instance)
		end
	end
	connectBinding.Parent = camera

	plugin.Unloading:Connect(function()
		connectBinding:Destroy()
		camera:Destroy()
		disconnect()
		setEnabled(plugin, false)
	end)
end

--

return Bindings


--!strict

local CoreGui = game:GetService("CoreGui") :: CoreGui

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Types = require(pluginRoot.Main.Bindings.Interface.Types)
local Trove = require(pluginRoot.Packages.Trove)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)
local SceneControl = require(photoRoot.SceneControl)
local UIBounds = require(photoRoot.Tools.UIBounds)
local UIHider = require(photoRoot.Tools.UIHider)

local capturesRoot = script:FindFirstAncestor("Captures")
local Composers = require(capturesRoot.Composers)

local function operation(options: Types.CaptureUIOptions, trove: Trove.Trove)
	local uiBounds = UIBounds.new(CoreGui, options.subject, (options.scale or Vector2.new(1, 1)))
	local undoExclude = UIHider.addExclusion(uiBounds.surfaceGui)
	local uiHider = UIHider.new({ "LayerCollector" })

	trove:Add(function()
		uiBounds:Destroy()
		uiHider:Destroy()
		undoExclude()
	end)

	local targetRect = uiBounds:GetRect()

	uiBounds:SetColor(Color3.new(1, 1, 1))
	SceneControl.yieldUntilNextFrame()
	local onWhite = Screenshot.rect(targetRect)

	uiBounds:SetColor(Color3.new(0, 0, 0))
	SceneControl.yieldUntilNextFrame()
	local onBlack = Screenshot.rect(targetRect)

	local colorCorrection = Instance.new("ColorCorrectionEffect")
	colorCorrection.Brightness = 1
	trove:Add(colorCorrection)

	local capture = Composers.reverseBlend(onWhite, onBlack, nil, colorCorrection)
	if options.alphaBleed then
		capture = Composers.alphaBleed(capture)
	end

	return capture
end

return operation


--!strict

local CoreGui = game:GetService("CoreGui") :: CoreGui

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Types = require(pluginRoot.Main.Bindings.Interface.Types)
local Trove = require(pluginRoot.Packages.Trove)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)
local SceneControl = require(photoRoot.SceneControl)
local UIBounds = require(photoRoot.Tools.UIBounds)
local UIHider = require(photoRoot.Tools.UIHider)

local capturesRoot = script:FindFirstAncestor("Captures")
local Composers = require(capturesRoot.Composers)

local function operation(options: Types.CaptureUIOptions, trove: Trove.Trove)
	local uiBounds = UIBounds.new(CoreGui, options.subject, options.scale or Vector2.new(1, 1))
	local undoExclude = UIHider.addExclusion(uiBounds.surfaceGui)
	local uiHider = UIHider.new({ "LayerCollector" })

	trove:Add(function()
		uiBounds:Destroy()
		uiHider:Destroy()
		undoExclude()
	end)

	local targetRect = uiBounds:GetRect()

	uiBounds:SetColor(Color3.new(1, 1, 1))
	SceneControl.yieldUntilNextFrame()
	local onWhite = Screenshot.rect(targetRect)

	uiBounds:SetColor(Color3.new(0, 0, 0))
	SceneControl.yieldUntilNextFrame()
	local onBlack = Screenshot.rect(targetRect)

	local colorCorrection = Instance.new("ColorCorrectionEffect")
	trove:Add(colorCorrection)

	local capture = Composers.reverseBlend(onWhite, onBlack, nil, colorCorrection)
	if options.alphaBleed then
		capture = Composers.alphaBleed(capture)
	end

	return capture
end

return operation


--!strict

local Lighting = game:GetService("Lighting") :: Lighting

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Image = require(pluginRoot.Utilities.Image)
local Trove = require(pluginRoot.Packages.Trove)

local Types = require(pluginRoot.Main.Bindings.Interface.Types)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)
local Background = require(photoRoot.Tools.Background)
local SceneControl = require(photoRoot.SceneControl)

local capturesRoot = script:FindFirstAncestor("Captures")
local Composers = require(capturesRoot.Composers)

local function getAppliedAtmosphere(): Atmosphere?
	local children = Lighting:GetChildren()
	for i = #children, 1, -1 do
		local child = children[i]
		if child:IsA("Atmosphere") then
			return child
		end
	end
	return nil
end

local function operation(options: Types.CaptureViewportOptions, trove: Trove.Trove): ...Image.Image
	trove:Add(SceneControl.hideScreenUI())
	local colorCorrection = trove:Add(SceneControl.getAppliedColorCorrectionCopy())

	-- capture on graded black
	local preRetroYieldCount = 1
	local gradedOnBlack: Image.Image?
	if SceneControl.isGradedLightingRequired() or Lighting.ExposureCompensation ~= 0 then
		local undoGradedLighting = trove:Add(SceneControl.applyGradedLighting())

		local atmosphere = getAppliedAtmosphere()
		if atmosphere then
			preRetroYieldCount = 100

			trove:Add(SceneControl.setProperties(atmosphere, {
				Haze = 10,
				Glare = 0,
				Color = Color3.new(0, 0, 0),
				Decay = Color3.new(0, 0, 0),
			}))
		end

		-- SceneControl.yieldUntilNextFrame()
		gradedOnBlack = Screenshot.rect(options.rect)

		undoGradedLighting()
	end

	-- prepare for retro scene capturing
	trove:Add(SceneControl.applyRetroLighting())

	local fogBackground = Background.new(workspace)
	fogBackground:SetColor(Color3.new(0, 0, 0))
	fogBackground:SetEnabled(true)

	trove:Add(SceneControl.once(function()
		fogBackground:Destroy()
	end))

	-- capture on retro black
	for _i = 1, preRetroYieldCount do
		SceneControl.yieldUntilNextFrame()
	end
	local onBlackRetro = Screenshot.rect(options.rect)

	-- capture on retro white
	fogBackground:SetColor(Color3.new(1, 1, 1))
	SceneControl.yieldUntilNextFrame()
	local onWhiteRetro = Screenshot.rect(options.rect)

	-- remove background & alpha bleed
	local capture = Composers.reverseBlend(onWhiteRetro, onBlackRetro, gradedOnBlack, colorCorrection)
	if options.alphaBleed then
		capture = Composers.alphaBleed(capture)
	end

	return capture
end

return operation


--!strict

local Lighting = game:GetService("Lighting") :: Lighting

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Image = require(pluginRoot.Utilities.Image)
local Trove = require(pluginRoot.Packages.Trove)

local Types = require(pluginRoot.Main.Bindings.Interface.Types)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)
local Background = require(photoRoot.Tools.Background)
local SceneControl = require(photoRoot.SceneControl)

local capturesRoot = script:FindFirstAncestor("Captures")
local Composers = require(capturesRoot.Composers)

local function operation(options: Types.CaptureViewportOptions, trove: Trove.Trove): ...Image.Image
	if Lighting:FindFirstChildWhichIsA("Atmosphere") then
		error("WARN: Skybox cannot be removed when Atmosphere is present.", 0)
	end

	trove:Add(SceneControl.hideScreenUI())
	local colorCorrection = trove:Add(SceneControl.getAppliedColorCorrectionCopy())

	local fogBackground = Background.new(workspace)
	fogBackground:SetColor(Color3.new(0, 0, 0))
	fogBackground:SetEnabled(true)

	trove:Add(SceneControl.once(function()
		fogBackground:Destroy()
	end))

	local gradedOnBlack: Image.Image?
	if SceneControl.isGradedLightingRequired() or Lighting.ExposureCompensation ~= 0 then
		local undoGradedLighting = trove:Add(SceneControl.applyGradedLighting())

		SceneControl.yieldUntilNextFrame()
		gradedOnBlack = Screenshot.rect(options.rect)

		undoGradedLighting()
	end

	-- prepare for retro scene capturing
	trove:Add(SceneControl.applyRetroLighting())

	-- capture on retro black
	SceneControl.yieldUntilNextFrame()
	local onBlackRetro = Screenshot.rect(options.rect)

	-- capture on retro white
	fogBackground:SetColor(Color3.new(1, 1, 1))
	SceneControl.yieldUntilNextFrame()
	local onWhiteRetro = Screenshot.rect(options.rect)

	local capture = Composers.reverseBlend(onWhiteRetro, onBlackRetro, gradedOnBlack, colorCorrection)
	if options.alphaBleed then
		capture = Composers.alphaBleed(capture)
	end

	return capture
end

return operation


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Trove = require(pluginRoot.Packages.Trove)

local Types = require(pluginRoot.Main.Bindings.Interface.Types)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)
local SceneControl = require(photoRoot.SceneControl)

local capturesRoot = script:FindFirstAncestor("Captures")
local Composers = require(capturesRoot.Composers)

local function operation(options: Types.CaptureViewportOptions, trove: Trove.Trove)
	trove:Add(SceneControl.hideScreenUI())

	SceneControl.yieldUntilNextFrame()
	local capture = Screenshot.rect(options.rect)
	if options.alphaBleed then
		capture = Composers.alphaBleed(capture)
	end

	return capture
end

return operation


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local function workerAction(imgBuffer: buffer, imgSize: Vector2, visited: buffer, pending: { number })
	local img = Image.fromBuffer(imgBuffer, imgSize)

	while #pending > 0 do
		local pendingNext = {}

		for i = 1, #pending do
			local index = pending[i]

			local rgb = { 0, 0, 0 }
			local contributing = 0

			for _, neighborIndex in img:IterateNeighbors(index) do
				if bit32.band(buffer.readu8(visited, neighborIndex - 1), 0b10) > 0 then
					local r, g, b = img:GetRGBA255(neighborIndex)

					rgb[1] = rgb[1] + r
					rgb[2] = rgb[2] + g
					rgb[3] = rgb[3] + b

					contributing = contributing + 1
				elseif not (bit32.band(buffer.readu8(visited, neighborIndex - 1), 0b01) > 0) then
					buffer.writeu8(visited, neighborIndex - 1, 0b01)
					table.insert(pendingNext, neighborIndex)
				end

				Timeout.tryYield()
			end

			if contributing > 0 then
				img:SetRGBA255(index, rgb[1] / contributing, rgb[2] / contributing, rgb[3] / contributing, 0)
			end

			Timeout.tryYield()
		end

		for i = 1, #pending do
			buffer.writeu8(visited, pending[i] - 1, 0b11)
			Timeout.tryYield()
		end

		pending = pendingNext
	end

	return img.buffer
end

return workerAction


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Parallel = require(pluginRoot.Utilities.Parallel)
local Image = require(pluginRoot.Utilities.Image)

type WorkerAction = typeof(require(script.Worker))

local scheduler = Parallel.new(1, {
	script = script.Worker,
	typecast = (nil :: any) :: WorkerAction,
})

local function parallelize(img: Image.Image, visited: buffer, pending: { number })
	-- not really taking advantage of parallel luau here
	-- however, by using a worker we stop studio from getting hung up

	scheduler:WaitForReady()
	scheduler:Schedule(img.buffer, img.size, visited, pending)
	local packedResults = scheduler:Work()

	local result = Image.fromBuffer(packedResults[1](), img.size)
	result.templateUri = img.templateUri
	return result
end

return parallelize


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local IntegralImage = require(pluginRoot.Utilities.IntegralImage)
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local function workerAction(chunkBuffer: buffer, chunkSize: Vector2, crop: Rect, minIndex: number)
	local chunkImg = Image.fromBuffer(chunkBuffer, chunkSize)
	local alphaSignIntegralImg = IntegralImage.fromImage(chunkImg, function(u32)
		return math.sign(bit32.extract(u32, 24, 8))
	end)

	local pending = {}
	local visited = buffer.create(crop.Width * crop.Height)

	local fromIndex = chunkImg:GetPixelIndex(crop.Min.X + 1, crop.Min.Y + 1)
	local toIndex = chunkImg:GetPixelIndex(crop.Max.X, crop.Max.Y)

	for index = fromIndex, toIndex do
		Timeout.tryYield()

		local croppedOffset = index - fromIndex
		local _, _, _, alpha = chunkImg:GetRGBA255(index)

		if alpha ~= 0 then
			buffer.writeu8(visited, croppedOffset, 0b11)
			continue
		end

		local x, y = chunkImg:GetXYFromPixelIndex(index)
		if alphaSignIntegralImg:readRadius(x, y, 1) > 0 then
			table.insert(pending, minIndex + fromIndex - 1 + croppedOffset)
			buffer.writeu8(visited, croppedOffset, 0b01)
		end
	end

	return visited, pending
end

return workerAction


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Parallel = require(pluginRoot.Utilities.Parallel)
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

type WorkerAction = typeof(require(script.Worker))

local scheduler = Parallel.new(32, {
	script = script.Worker,
	typecast = (nil :: any) :: WorkerAction,
})

local function parallelize(img: Image.Image)
	scheduler:WaitForReady()

	for _, chunk in img:Split(scheduler:GetActorCount(), 1) do
		scheduler:Schedule(chunk.buffer, chunk.size, chunk.crop, chunk.minIndex)
		Timeout.tryYield()
	end

	local packedResults = scheduler:Work()
	Timeout.tryYield()

	local pending = {}
	local copyIndex = 0
	local visited = buffer.create(img:GetNumberOfPixels())
	for _, unpackResult in packedResults do
		local visitedChunk, pendingChunk = unpackResult()
		buffer.copy(visited, copyIndex, visitedChunk)
		copyIndex = copyIndex + buffer.len(visitedChunk)
		table.move(pendingChunk, 1, #pendingChunk, #pending + 1, pending)
		Timeout.tryYield()
	end

	return visited, pending
end

return parallelize


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Image = require(pluginRoot.Utilities.Image)

local FirstPass = require(script.FirstPass)
local Bleed = require(script.Bleed)

local function alphaBleedParallel(img: Image.Image)
	local visited, pending = FirstPass(img)
	local copy = Bleed(img, visited, pending)
	return copy
end

return alphaBleedParallel


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local COMPARE_MARGIN = 0

local Wb = { 255, 255, 255, 255 }
local Bb = { 0, 0, 0, 255 }

local function compareByMargin(a: { number }, b: { number }, margin: number): boolean
	-- stylua: ignore
	return (
			math.abs(a[1] - b[1]) <= margin
		and math.abs(a[2] - b[2]) <= margin
		and math.abs(a[3] - b[3]) <= margin
	)
end

local function workerAction(
	size: Vector2,
	colorCorrection: ColorCorrectionEffect,
	whiteRetroBuffer: buffer,
	blackRetroBuffer: buffer,
	blackBuffer: buffer?
)
	local brightness = colorCorrection.Brightness
	local contrast = colorCorrection.Contrast + 1
	local saturation = colorCorrection.Saturation + 1

	local tintColor = colorCorrection.TintColor
	local tint = { tintColor.R, tintColor.G, tintColor.B }

	local onWhiteRetro = Image.fromBuffer(whiteRetroBuffer, size)
	local onBlackRetro = Image.fromBuffer(blackRetroBuffer, size)
	local onBlack = if blackBuffer then Image.fromBuffer(blackBuffer, size) else onBlackRetro:Clone()

	for h = 1, onBlack.size.Y do
		for w = 1, onBlack.size.X do
			Timeout.tryYield()

			local pixelIndex = onBlack:GetPixelIndex(w, h)

			local Wo = { onWhiteRetro:GetRGBA255(pixelIndex) }
			local Bo = { onBlackRetro:GetRGBA255(pixelIndex) }

			if compareByMargin(Wo, Wb, 0) and compareByMargin(Bo, Bb, 0) then
				-- in both samples the pixels match their backgrounds perfectly
				-- this means that this pixel is guaranteed to be a background pixel
				onBlack:SetRGBA255(pixelIndex, 0, 0, 0, 0)
				continue
			end

			if not compareByMargin(Wo, Bo, COMPARE_MARGIN) then
				-- the pixels differ between the two versions of the image
				-- this means their must be some alpha we have to calculate
				local uBo = { onBlack:GetRGBA255(pixelIndex) }

				local maxAlpha = 0
				for i = 1, 3 do
					local alpha = (Wo[i] - Wb[i] - Bo[i] + Bb[i]) / (Bb[i] - Wb[i])
					maxAlpha = math.max(maxAlpha, math.clamp(alpha, 0, 1))
				end

				local rgb = { 0, 0, 0 }
				for i = 1, 3 do
					rgb[i] = math.clamp(Bb[i] + (uBo[i] - Bb[i]) / maxAlpha, 0, 255)
				end

				onBlack:SetRGBA255(pixelIndex, rgb[1], rgb[2], rgb[3], maxAlpha * 255)
			end

			-- apply color correction
			local rgba = { onBlack:GetRGBA(pixelIndex) }

			local color = Color3.new(rgba[1], rgba[2], rgba[3])
			local hue, _saturation, value = color:ToHSV()
			local grayColor = Color3.fromHSV(hue, 0, value)
			local gray = math.max(grayColor.R, grayColor.G, grayColor.B)

			for i = 1, 3 do
				local component = rgba[i]
				component = component * saturation + (1 - saturation) * gray -- saturation
				component = ((component - 0.5) * math.max(0, contrast)) + 0.5 -- contrast
				component = component + brightness -- brightness
				component = component * tint[i] -- tint
				rgba[i] = math.clamp(component, 0, 1)
			end

			onBlack:SetRGBA(pixelIndex, rgba[1], rgba[2], rgba[3], rgba[4])
		end
	end

	return onBlack.buffer
end

return workerAction


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Parallel = require(pluginRoot.Utilities.Parallel)
local Image = require(pluginRoot.Utilities.Image)

type WorkerAction = typeof(require(script.Worker))

local scheduler = Parallel.new(32, {
	script = script.Worker,
	typecast = (nil :: any) :: WorkerAction,
})

local function parallelize(
	onWhiteRetro: Image.Image,
	onBlackRetro: Image.Image,
	onBlack: Image.Image?,
	colorCorrection: ColorCorrectionEffect
)
	scheduler:WaitForReady()

	local splitOnWhiteRetro = onWhiteRetro:Split(scheduler:GetActorCount())
	local splitOnBlackRetro = onBlackRetro:Split(scheduler:GetActorCount())
	local splitOnBlack = if onBlack then onBlack:Split(scheduler:GetActorCount()) else {}

	for i = 1, #splitOnWhiteRetro do
		local onWhiteRetroChunk = splitOnWhiteRetro[i]
		local onBlackRetroChunk = splitOnBlackRetro[i]
		local onBlackChunk = splitOnBlack[i]

		scheduler:Schedule(
			onWhiteRetroChunk.size,
			colorCorrection,
			onWhiteRetroChunk.buffer,
			onBlackRetroChunk.buffer,
			if onBlackChunk then onBlackChunk.buffer else nil
		)
	end

	local packedResults = scheduler:Work()

	local copyIndex = 0
	local resultBuffer = buffer.create(onWhiteRetro:GetNumberOfPixels() * 4)
	for _, unpackResult in packedResults do
		local commitBuffer = unpackResult()
		buffer.copy(resultBuffer, copyIndex, commitBuffer)
		copyIndex = copyIndex + buffer.len(commitBuffer)
	end

	local result = Image.fromBuffer(resultBuffer, onWhiteRetro.size)
	result.templateUri = onWhiteRetro.templateUri
	return result
end

return parallelize


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local IntegralImage = require(pluginRoot.Utilities.IntegralImage)
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local function alphaBleedSerial(img: Image.Image)
	local alphaSignIntegralImg = IntegralImage.fromImage(img, function(u32)
		return math.sign(bit32.extract(u32, 24, 8))
	end)

	local copy = img:Clone()
	local width, height = copy.size.X, copy.size.Y

	local pending = {}
	local visited = buffer.create(width * height)

	for y = 1, height do
		for x = 1, width do
			Timeout.tryYield()

			local index = copy:GetPixelIndex(x, y)
			local _, _, _, alpha = copy:GetRGBA255(index)

			if alpha ~= 0 then
				buffer.writeu8(visited, index - 1, 0b11)
				continue
			end

			if alphaSignIntegralImg:readRadius(x, y, 1) > 0 then
				table.insert(pending, index)
				buffer.writeu8(visited, index - 1, 0b01)
			end
		end
	end

	while #pending > 0 do
		local pendingNext = {}

		for i = 1, #pending do
			local index = pending[i]

			local rgb = { 0, 0, 0 }
			local contributing = 0

			for _, neighborIndex in copy:IterateNeighbors(index) do
				if bit32.band(buffer.readu8(visited, neighborIndex - 1), 0b10) > 0 then
					local r, g, b = copy:GetRGBA255(neighborIndex)

					rgb[1] = rgb[1] + r
					rgb[2] = rgb[2] + g
					rgb[3] = rgb[3] + b

					contributing = contributing + 1
				elseif not (bit32.band(buffer.readu8(visited, neighborIndex - 1), 0b01) > 0) then
					buffer.writeu8(visited, neighborIndex - 1, 0b01)
					table.insert(pendingNext, neighborIndex)
				end

				Timeout.tryYield()
			end

			if contributing > 0 then
				copy:SetRGBA255(index, rgb[1] / contributing, rgb[2] / contributing, rgb[3] / contributing, 0)
			end

			Timeout.tryYield()
		end

		for i = 1, #pending do
			buffer.writeu8(visited, pending[i] - 1, 0b11)
			Timeout.tryYield()
		end

		pending = pendingNext
	end

	return copy
end

return alphaBleedSerial


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local function fullyOpaque(img: Image.Image)
	local copy = img:Clone()
	local width, height = copy.size.X, copy.size.Y

	for y = 1, height do
		for x = 1, width do
			Timeout.tryYield()

			local index = copy:GetPixelIndex(x, y)
			local r, g, b = copy:GetRGBA255(index)

			copy:SetRGBA255(index, r, g, b, 255)
		end
	end

	return copy
end

return fullyOpaque


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local COMPARE_MARGIN = 0

local Wb = { 255, 255, 255, 255 }
local Bb = { 0, 0, 0, 255 }

local function compareByMargin(a: { number }, b: { number }, margin: number): boolean
	-- stylua: ignore
	return (
			math.abs(a[1] - b[1]) <= margin
		and math.abs(a[2] - b[2]) <= margin
		and math.abs(a[3] - b[3]) <= margin
	)
end

local function reverseBlendSerial(
	onWhiteRetro: Image.Image,
	onBlackRetro: Image.Image,
	onBlackBase: Image.Image?,
	colorCorrection: ColorCorrectionEffect
)
	local brightness = colorCorrection.Brightness
	local contrast = colorCorrection.Contrast + 1
	local saturation = colorCorrection.Saturation + 1

	local tintColor = colorCorrection.TintColor
	local tint = { tintColor.R, tintColor.G, tintColor.B }

	local onBlack = onBlackBase or onBlackRetro
	for h = 1, onBlack.size.Y do
		for w = 1, onBlack.size.X do
			Timeout.tryYield()

			local pixelIndex = onBlack:GetPixelIndex(w, h)

			local Wo = { onWhiteRetro:GetRGBA255(pixelIndex) }
			local Bo = { onBlackRetro:GetRGBA255(pixelIndex) }

			if compareByMargin(Wo, Wb, 0) and compareByMargin(Bo, Bb, 0) then
				-- in both samples the pixels match their backgrounds perfectly
				-- this means that this pixel is guaranteed to be a background pixel
				onBlack:SetRGBA255(pixelIndex, 0, 0, 0, 0)
				continue
			end

			if not compareByMargin(Wo, Bo, COMPARE_MARGIN) then
				-- the pixels differ between the two versions of the image
				-- this means their must be some alpha we have to calculate
				local uBo = { onBlack:GetRGBA255(pixelIndex) }

				local maxAlpha = 0
				for i = 1, 3 do
					local alpha = (Wo[i] - Wb[i] - Bo[i] + Bb[i]) / (Bb[i] - Wb[i])
					maxAlpha = math.max(maxAlpha, math.clamp(alpha, 0, 1))
				end

				local rgb = { 0, 0, 0 }
				for i = 1, 3 do
					rgb[i] = math.clamp(Bb[i] + (uBo[i] - Bb[i]) / maxAlpha, 0, 255)
				end

				onBlack:SetRGBA255(pixelIndex, rgb[1], rgb[2], rgb[3], maxAlpha * 255)
			end

			-- apply color correction
			local rgba = { onBlack:GetRGBA(pixelIndex) }

			local color = Color3.new(rgba[1], rgba[2], rgba[3])
			local hue, _saturation, value = color:ToHSV()
			local grayColor = Color3.fromHSV(hue, 0, value)
			local gray = math.max(grayColor.R, grayColor.G, grayColor.B)

			for i = 1, 3 do
				local component = rgba[i]
				component = component * saturation + (1 - saturation) * gray -- saturation
				component = ((component - 0.5) * math.max(0, contrast)) + 0.5 -- contrast
				component = component + brightness -- brightness
				component = component * tint[i] -- tint
				rgba[i] = math.clamp(component, 0, 1)
			end

			onBlack:SetRGBA(pixelIndex, rgba[1], rgba[2], rgba[3], rgba[4])
		end
	end

	return onBlack
end

return reverseBlendSerial


--!strict

local serialFolder = script.Serial
local parallelFolder = script.Parallel

return {
	alphaBleed = require(parallelFolder.AlphaBleed),
	reverseBlend = require(parallelFolder.ReverseBlend),
	fullyOpaque = require(serialFolder.FullyOpaque),
}


--!strict

local AssetService = game:GetService("AssetService") :: AssetService
local CaptureService = game:GetService("CaptureService") :: CaptureService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Future = require(pluginRoot.Packages.Future)
local Image = require(pluginRoot.Utilities.Image)
local Constants = require(pluginRoot.Constants)
local State = require(pluginRoot.Main.State)

local Screenshot = {}

-- Private

local function captureEditableImage()
	local contentUriFuture = Future.new() :: Future.Future<string>
	CaptureService:CaptureScreenshot(function(contentUri: string)
		-- this is a significant bottleneck for speed
		-- it can take roughly 0.3 seconds to complete
		contentUriFuture:complete(contentUri)
	end)

	local contentUri = contentUriFuture:expect()
	local editImage = AssetService:CreateEditableImageAsync(Content.fromUri(contentUri)) :: EditableImage

	return editImage, contentUri
end

local function screenshot(rect: Rect?)
	local editImage, contentUri = captureEditableImage()

	local viewportSize = workspace.CurrentCamera.ViewportSize :: Vector2
	local scale = editImage.Size / viewportSize

	if scale ~= State.read().temporary.scaleOS then
		error("WARN: OS Scale mismatch. You may need to go into photobooth settings and refresh the OS scale value.", 0)
	end

	local img = Image.fromEditableImage(editImage, rect)

	if rect then
		editImage:Destroy()
	else
		img:SetTemplateUri(contentUri)
	end

	return img
end

-- Public

function Screenshot.calculateScaleOS()
	local editImage = captureEditableImage()
	local viewportSize = workspace.CurrentCamera.ViewportSize :: Vector2
	local scale = editImage.Size / viewportSize
	editImage:Destroy()
	return scale
end

function Screenshot.isUnitScaleOS(scaleOS: Vector2)
	return scaleOS.X == 1 and scaleOS.Y == 1
end

function Screenshot.isFullscreenRect(rect: Rect)
	if not Constants.AllowFullscreen then
		return false
	end

	local scaleOS = State.read().temporary.scaleOS
	local viewportSize = workspace.CurrentCamera.ViewportSize :: Vector2

	-- stylua: ignore
	local scaledViewportSize = Vector2.new(
		math.round(viewportSize.X * scaleOS.X),
		math.round(viewportSize.Y * scaleOS.Y)
	)

	return rect.Min == Vector2.zero and rect.Max == scaledViewportSize
end

function Screenshot.rect(rect: Rect)
	if Screenshot.isFullscreenRect(rect) then
		return screenshot()
	else
		assert(
			not (rect.Width > Constants.MaxCaptureRectSize.X or rect.Height > Constants.MaxCaptureRectSize.Y),
			`Capture target is larger than {Constants.MaxCaptureRectSize.X} x {Constants.MaxCaptureRectSize.Y}.`
		)

		return screenshot(rect)
	end
end

--

return Screenshot


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Image = require(pluginRoot.Utilities.Image)
local Sift = require(pluginRoot.Packages.Sift)

local State = require(pluginRoot.Main.State)
local Types = require(pluginRoot.Main.Bindings.Interface.Types)

-- Private

local ticket = 0
local ticketLookup = {}

local function wrap<T...>(callback: (T...) -> ...Image.Image)
	ticket = ticket + 1
	local myTicket = ticket

	local function wrapped(...: T...)
		return { callback(...) }
	end

	ticketLookup[wrapped] = myTicket
	return wrapped
end

local function getOrder<T...>(exported: { [string]: (T...) -> { Image.Image } })
	local order = Sift.Dictionary.keys(exported)
	table.sort(order, function(a, b)
		return ticketLookup[exported[a]] < ticketLookup[exported[b]]
	end)
	return order
end

-- Public

export type UICaptureTypes = Types.UICaptureTypes
export type ViewportCaptureTypes = Types.ViewportCaptureTypes

local captureTypes = {
	UserInterface = {
		NoBackground = wrap(require(script.CaptureTypes.UserInterface.NoBackground)),
		AlphaMask = wrap(require(script.CaptureTypes.UserInterface.AlphaMask)),
	},
	Viewport = {
		NoSkybox = wrap(require(script.CaptureTypes.Viewport.NoSkybox)),
		Unmodified = wrap(require(script.CaptureTypes.Viewport.Unmodified)),
	},
}

if State.read().temporary.isExperimentalMode then
	captureTypes.Viewport.Experimental_NSB_A1 = wrap(require(script.CaptureTypes.Viewport.Experimental_NSB_A1))
end

return {
	UserInterface = {
		Order = getOrder(captureTypes.UserInterface),
		Processors = captureTypes.UserInterface,
	},
	Viewport = {
		Order = getOrder(captureTypes.Viewport),
		Processors = captureTypes.Viewport,
	},
}


--!strict

local BillboardBackgroundClass = {}
BillboardBackgroundClass.__index = BillboardBackgroundClass
BillboardBackgroundClass.ClassName = "BillboardBackground"

export type BillboardBackground = typeof(setmetatable(
	{} :: {
		part: BasePart,
		billboard: BillboardGui,
		frame: Frame,
	},
	BillboardBackgroundClass
))

-- Constructors

function BillboardBackgroundClass.new(parent: Instance): BillboardBackground
	local self = setmetatable({}, BillboardBackgroundClass) :: BillboardBackground

	self.part, self.billboard, self.frame = self:_createInstances()
	self.part.Parent = parent

	self:SetEnabled(false)
	self:Update()

	return self
end

-- Private

local function rayPlaneIntersection(ro: Vector3, rd: Vector3, po: Vector3, pn: Vector3)
	return -(ro - po):Dot(pn) / rd:Dot(pn)
end

local function getCorners(cframe: CFrame, size2: Vector3)
	local corners = {}
	for i = 0, 7 do
		-- stylua: ignore
		corners[i + 1] = cframe * (size2 * Vector3.new(
			2 * (math.floor(i / 4) % 2) - 1,
			2 * (math.floor(i / 2) % 2) - 1,
			2 * (i % 2) - 1
		))
	end
	return corners
end

local function getBoundsIntersection(cameraCF: CFrame)
	local sceneCF: CFrame, sceneSize: Vector3 = workspace:GetBoundingBox()

	local terrainRegion16 = workspace.Terrain.MaxExtents
	local terrainRegion = Region3.new(
		Vector3.new(terrainRegion16.Min.X, terrainRegion16.Min.Y, terrainRegion16.Min.Z),
		Vector3.new(terrainRegion16.Max.X, terrainRegion16.Max.Y, terrainRegion16.Max.Z)
	)

	-- stylua: ignore
	local iterable = {
		{ CFrame = sceneCF, Size = sceneSize },
		{ CFrame = terrainRegion.CFrame, Size = terrainRegion.Size }
	}

	local maxCorner: Vector3
	local cameraPos = cameraCF.Position
	local lookVector = -cameraCF.ZVector

	local maxDist = -math.huge
	for _, target in iterable do
		for _, corner in getCorners(target.CFrame, target.Size / 2) do
			local dist = (corner - cameraPos):Dot(lookVector)
			if dist > maxDist then
				maxCorner = corner
				maxDist = dist
			end
		end
	end

	local t = rayPlaneIntersection(cameraPos, lookVector, maxCorner, -lookVector)
	return cameraCF.Position + lookVector * math.max(0, t + 1)
end

function BillboardBackgroundClass._createInstances(self: BillboardBackground)
	local part = Instance.new("Part")
	part.CFrame = CFrame.identity
	part.Size = Vector3.new(1, 1, 1)
	part.Transparency = 1
	part.CanCollide = false
	part.Anchored = true
	part.CanTouch = false
	part.CanQuery = false

	local billboard = Instance.new("BillboardGui")
	billboard.Enabled = false
	billboard.Size = UDim2.fromOffset(100_000, 100_000) -- assuming nobody has a monitor bigger than this...
	billboard.LightInfluence = 0
	billboard.Adornee = part
	billboard.Parent = part

	local frame = Instance.new("Frame")
	frame.Size = UDim2.fromScale(1, 1)
	frame.BorderSizePixel = 0
	frame.Parent = billboard

	return part, billboard, frame
end

-- Public

function BillboardBackgroundClass.Update(self: BillboardBackground)
	self.part.CFrame = CFrame.new(getBoundsIntersection(workspace.CurrentCamera.CFrame))
end

function BillboardBackgroundClass.SetEnabled(self: BillboardBackground, enabled: boolean)
	self.billboard.Enabled = enabled
end

function BillboardBackgroundClass.SetColor(self: BillboardBackground, color: Color3)
	self.frame.BackgroundColor3 = color
end

function BillboardBackgroundClass.Destroy(self: BillboardBackground)
	self.part:Destroy()
end

--

return BillboardBackgroundClass


--!strict

local Lighting = game:GetService("Lighting") :: Lighting

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Trove = require(pluginRoot.Packages.Trove)

local MARGIN_OF_ERROR_DISTANCE = 1000

local MeshPartFogBackgroundClass = {}
MeshPartFogBackgroundClass.__index = MeshPartFogBackgroundClass
MeshPartFogBackgroundClass.ClassName = "MeshPartBackground"

export type MeshPartFogBackground = typeof(setmetatable(
	{} :: {
		part: BasePart,
		mesh: SpecialMesh,
		fogDistance: number,
		fogTrove: Trove.Trove,
	},
	MeshPartFogBackgroundClass
))

-- Constructors

function MeshPartFogBackgroundClass.new(parent: Instance): MeshPartFogBackground
	local self = setmetatable({}, MeshPartFogBackgroundClass) :: MeshPartFogBackground

	self.part, self.mesh = self:_createInstances()
	self.part.Parent = parent

	self.fogDistance = -1
	self.fogTrove = Trove.new()

	self:SetEnabled(false)
	self:_update()

	return self
end

function MeshPartFogBackgroundClass.neon(parent: Instance): MeshPartFogBackground
	local self = MeshPartFogBackgroundClass.new(parent)

	self.fogDistance = math.huge
	self.part.Material = Enum.Material.Neon

	return self
end

-- Private

local function getBoundsIntersection(cameraCF: CFrame)
	-- the logic here used to be much more complex
	-- originally, we'd try and calculate this based on the existing geometry relative to the camera
	-- however, it's much more consistent to pick a fixed distance that renders on all quality levels
	-- technically this means you can't capture things more than 90k studs away, but is that really an issue?
	return cameraCF * Vector3.new(0, 0, -90_000)
end

function MeshPartFogBackgroundClass._createInstances(_self: MeshPartFogBackground)
	local part = Instance.new("Part")
	part.Material = Enum.Material.SmoothPlastic
	part.CFrame = CFrame.identity
	part.Size = Vector3.new(1, 1, 1)
	part.Color = Color3.new(1, 1, 1)
	part.Transparency = 0
	part.CastShadow = false
	part.CanCollide = false
	part.Anchored = true
	part.CanTouch = false
	part.CanQuery = false
	part.Locked = true

	local mesh = Instance.new("SpecialMesh")
	mesh.Scale = Vector3.new(1_000_000_000, 1_000_000_000) -- we could calc, but why bother
	mesh.MeshType = Enum.MeshType.Brick
	mesh.Parent = part

	return part, mesh
end

function MeshPartFogBackgroundClass._update(self: MeshPartFogBackground)
	local cameraCF = workspace.CurrentCamera.CFrame
	local intersection = getBoundsIntersection(cameraCF)
	self.part.CFrame = CFrame.new(intersection) * cameraCF.Rotation
	self.fogDistance = -cameraCF:PointToObjectSpace(intersection).Z - MARGIN_OF_ERROR_DISTANCE
end

-- Public

function MeshPartFogBackgroundClass.SetEnabled(self: MeshPartFogBackground, enabled: boolean)
	self.part.Transparency = if enabled then 0 else 1
	if enabled and self.fogDistance > 0 then
		local color = Lighting.FogColor
		local fogStart = Lighting.FogStart
		local fogEnd = Lighting.FogEnd
		self.fogTrove:Add(function()
			Lighting.FogColor = color
			Lighting.FogStart = fogStart
			Lighting.FogEnd = fogEnd
		end)

		Lighting.FogColor = self.part.Color
		Lighting.FogStart = self.fogDistance
		Lighting.FogEnd = self.fogDistance
	else
		self.fogTrove:Clean()
	end
end

function MeshPartFogBackgroundClass.SetColor(self: MeshPartFogBackground, color: Color3)
	self.part.Color = color
	if self.part.Transparency == 0 then
		Lighting.FogColor = color
	end
end

function MeshPartFogBackgroundClass.Destroy(self: MeshPartFogBackground)
	self:SetEnabled(false)
	self.fogTrove:Destroy()
	self.part:Destroy()
end

--

return MeshPartFogBackgroundClass


--!strict

local _BillboardBackground = require(script.BillboardBackground)
local MeshPartFogBackground = require(script.MeshPartFogBackground)

export type Background = MeshPartFogBackground.MeshPartFogBackground

return MeshPartFogBackground


--!strict

local Lighting = game:GetService("Lighting") :: Lighting

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Icons = require(pluginRoot.Main.UserInterface.Icons)

local SkyboxClass = {}
SkyboxClass.__index = SkyboxClass
SkyboxClass.ClassName = "Skybox"

SkyboxClass.Textures = {
	White = Icons.swatches.white,
	Black = Icons.swatches.black,
	Red = Icons.swatches.red,
	Green = Icons.swatches.green,
	Blue = Icons.swatches.blue,
}

export type Skybox = typeof(setmetatable(
	{} :: {
		cameraCF: CFrame,
		offsetCF: CFrame,

		sky: Sky,
	},
	SkyboxClass
))

function SkyboxClass.new()
	local self = setmetatable({}, SkyboxClass) :: Skybox

	self.sky = Instance.new("Sky")
	self.sky.CelestialBodiesShown = false

	return self
end

function SkyboxClass.SetEnabled(self: Skybox, enabled: boolean)
	self.sky.Parent = if enabled then Lighting else nil
end

function SkyboxClass.SetTexture(self: Skybox, texture: string)
	self.sky.SkyboxBk = texture
	self.sky.SkyboxDn = texture
	self.sky.SkyboxFt = texture
	self.sky.SkyboxLf = texture
	self.sky.SkyboxRt = texture
	self.sky.SkyboxUp = texture
end

function SkyboxClass.Destroy(self: Skybox)
	if self.sky then
		self.sky:Destroy()
	end
end

return SkyboxClass


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local RenderRect = require(pluginRoot.Main.Bindings.Interface.Helpers.RenderRect)
local State = require(pluginRoot.Main.State)

local ALWAYS_CLIPPED = true
local ALWAYS_CLIP_TEXT = true

-- Class

local UIBoundsClass = {}
UIBoundsClass.__index = UIBoundsClass
UIBoundsClass.ClassName = "UIBounds"

export type UIBounds = typeof(setmetatable(
	{} :: {
		scale: Vector2,
		subject: GuiObject,

		surfaceGui: SurfaceGui,
		surfacePart: BasePart,

		backgroundFrame: Frame,
		aabbFrame: Frame,
	},
	UIBoundsClass
))

-- Constructors

function UIBoundsClass.new(parent: Instance, subject: GuiObject, scale: Vector2): UIBounds
	local self = setmetatable({}, UIBoundsClass) :: UIBounds

	self.scale = scale
	self.subject = subject

	self.surfaceGui, self.surfacePart = self:_createSurface(parent)
	self.backgroundFrame, self.aabbFrame = self:_createBackgroundFrame(subject)
	self.backgroundFrame.Parent = self.surfaceGui

	self:SetColor(Color3.new(1, 1, 1))

	return self
end

-- Private

function UIBoundsClass._findNearestStyleLink(self: UIBounds)
	local styleLink: StyleLink?
	local pointer = self.subject.Parent :: Instance?

	while pointer and not styleLink do
		styleLink = pointer:FindFirstChildWhichIsA("StyleLink")
		pointer = pointer.Parent
	end

	return styleLink
end

function UIBoundsClass._createSurface(self: UIBounds, parent: Instance)
	local camera = workspace.CurrentCamera
	local viewportSize = camera.ViewportSize
	local aspect = viewportSize.X / viewportSize.Y
	local distance = 0.5 / math.tan(math.rad(camera.FieldOfView / 2))

	local surfacePart = Instance.new("Part")
	surfacePart.Anchored = true
	surfacePart.CFrame = camera.CFrame * CFrame.new(0, 0, -(distance + 0.5))
	surfacePart.Size = Vector3.new(aspect, 1, 1)
	surfacePart.Parent = parent

	local layerCollector = self.subject:FindFirstAncestorWhichIsA("LayerCollector")
	local zIndexBehavior = if layerCollector then layerCollector.ZIndexBehavior else Enum.ZIndexBehavior.Sibling

	local canvasScale = State.read().temporary.scaleOS / self.scale
	local canvasSize = viewportSize * canvasScale

	local surfaceGui = Instance.new("SurfaceGui")
	surfaceGui.AlwaysOnTop = true
	surfaceGui.Face = Enum.NormalId.Back
	surfaceGui.Name = "UIBounds"
	surfaceGui.ZIndexBehavior = zIndexBehavior
	surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.FixedSize
	surfaceGui.CanvasSize = canvasSize
	surfaceGui.Adornee = surfacePart

	surfaceGui.Parent = surfacePart
	surfacePart.Parent = parent

	return surfaceGui, surfacePart
end

local function isClipped(frame: GuiObject, root: GuiObject): boolean
	local parent: Instance = frame
	while parent ~= root do
		parent = parent.Parent :: Instance

		if parent:IsA("GuiObject") and (ALWAYS_CLIPPED or parent.ClipsDescendants) then
			-- if the ancestor is a canvas group it's for sure clipped
			-- if not, then the ancestor must have not have any rotation for clipping to work
			if parent:IsA("CanvasGroup") or parent.AbsoluteRotation == 0 then
				return true
			end
		end
	end

	return false
end

local function getQualified(root: GuiObject): { GuiObject }
	local qualified = {}
	local stack: { Instance } = { root }

	while #stack > 0 do
		local popped = table.remove(stack) :: Instance

		if popped:IsA("GuiObject") then
			if popped.Visible and not isClipped(popped, root) then
				table.insert(qualified, popped)
			else
				continue
			end
		elseif popped:IsA("ScreenGui") then
			if not popped.Enabled then
				continue
			end
		elseif not popped:IsA("Folder") then
			continue
		end

		for _, child in popped:GetChildren() do
			table.insert(stack, child)
		end
	end

	return qualified
end

local function isTextFrame(frame: GuiObject): boolean
	return frame:IsA("TextLabel") or frame:IsA("TextButton") or frame:IsA("TextBox")
end

local function getCorners(frame: GuiObject): { Vector2 }
	local absSize = frame.AbsoluteSize
	local absPosition = frame.AbsolutePosition
	local absRotation = frame.AbsoluteRotation

	local center = absPosition + absSize / 2
	local rotator = CFrame.fromEulerAnglesXYZ(0, 0, math.rad(absRotation))

	local corners = {}
	for w = 0, 1 do
		for h = 0, 1 do
			local delta = (absPosition + absSize * Vector2.new(w, h)) - center
			local rDelta = rotator * Vector3.new(delta.X, delta.Y, 0)
			local newCorner = center + Vector2.new(rDelta.X, rDelta.Y)
			table.insert(corners, newCorner)
		end
	end

	return corners
end

local function getTextBoundsCorners(textFrame: TextLabel | TextButton | TextBox)
	local anchorX = 1
	if textFrame.TextXAlignment == Enum.TextXAlignment.Left then
		anchorX = 0
	elseif textFrame.TextXAlignment == Enum.TextXAlignment.Center then
		anchorX = 0.5
	end

	local anchorY = 1
	if textFrame.TextYAlignment == Enum.TextYAlignment.Top then
		anchorY = 0
	elseif textFrame.TextYAlignment == Enum.TextYAlignment.Center then
		anchorY = 0.5
	end

	local absSize = textFrame.AbsoluteSize
	local absPosition = textFrame.AbsolutePosition
	local absRotation = textFrame.AbsoluteRotation

	local center = absPosition + absSize / 2
	local rotator = CFrame.fromEulerAnglesXYZ(0, 0, math.rad(absRotation))

	local textBounds = textFrame.TextBounds
	local anchor = Vector2.new(anchorX, anchorY)
	local tlc = (absSize - textBounds) * anchor

	local corners = {}
	for w = 0, 1 do
		for h = 0, 1 do
			local delta = (absPosition + tlc + textBounds * Vector2.new(w, h)) - center
			local rDelta = rotator * Vector3.new(delta.X, delta.Y, 0)
			local newCorner = center + Vector2.new(rDelta.X, rDelta.Y)
			table.insert(corners, newCorner)
		end
	end

	return corners
end

local function getBoundingRect(qualified: { GuiObject }): Rect
	local corners = {}
	for _, frame in qualified do
		for _, corner in getCorners(frame) do
			table.insert(corners, corner)
		end

		if not ALWAYS_CLIP_TEXT and not frame.ClipsDescendants and isTextFrame(frame) then
			for _, corner in getTextBoundsCorners(frame :: TextLabel) do
				table.insert(corners, corner)
			end
		end
	end

	local max = corners[1]
	local min = corners[1]
	for i = 2, #corners do
		max = max:Max(corners[i])
		min = min:Min(corners[i])
	end

	return RenderRect.fromAbs(min, max - min)
end

local function getMinZIndex(subject: GuiObject): number
	local minZIndex = subject.ZIndex
	for _, descendant in subject:QueryDescendants("GuiObject") do
		if descendant:IsA("GuiObject") then
			minZIndex = math.min(minZIndex, descendant.ZIndex)
		end
	end
	return minZIndex
end

function UIBoundsClass._createBackgroundFrame(self: UIBounds, subject: GuiObject)
	local qualified = getQualified(subject)
	local aabbRect = getBoundingRect(qualified)
	local minSubjectZIndex = getMinZIndex(subject)

	local backgroundFrame = Instance.new("Frame")
	backgroundFrame.ZIndex = minSubjectZIndex - 1
	backgroundFrame.Size = UDim2.fromOffset(aabbRect.Width + 1, aabbRect.Height + 1)
	backgroundFrame.BorderSizePixel = 0

	local boundingFrame = Instance.new("Frame")
	boundingFrame.ZIndex = minSubjectZIndex - 1
	boundingFrame.Size = UDim2.fromOffset(aabbRect.Width, aabbRect.Height)
	boundingFrame.BorderSizePixel = 0
	boundingFrame.BackgroundTransparency = 1
	boundingFrame.Parent = backgroundFrame

	local subjectAbsSize = subject.AbsoluteSize
	local subjectAbsPosition = subject.AbsolutePosition
	local subjectRect = RenderRect.fromAbs(subjectAbsPosition, subjectAbsSize)

	local folder = Instance.new("Folder")
	folder.Name = "SubjectFolder"
	folder.Parent = boundingFrame

	local styleLink = self:_findNearestStyleLink()
	if styleLink then
		styleLink:Clone().Parent = folder
	end

	local subjectCopy = subject:Clone()
	subjectCopy.AnchorPoint = Vector2.zero
	subjectCopy.Size = UDim2.fromOffset(subjectRect.Width, subjectRect.Height)
	subjectCopy.Rotation = subject.AbsoluteRotation
	subjectCopy.Position = UDim2.fromOffset(subjectRect.Min.X - aabbRect.Min.X, subjectRect.Min.Y - aabbRect.Min.Y)
	subjectCopy.Parent = folder

	return backgroundFrame, boundingFrame
end

-- Public

function UIBoundsClass.GetRect(self: UIBounds)
	return RenderRect.fromAbs(self.aabbFrame.AbsolutePosition, self.aabbFrame.AbsoluteSize * self.scale)
end

function UIBoundsClass.SetColor(self: UIBounds, color: Color3)
	self.backgroundFrame.BackgroundColor3 = color
end

function UIBoundsClass.Destroy(self: UIBounds)
	self.surfacePart:Destroy()
end

--

return UIBoundsClass


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Signal = require(pluginRoot.Packages.Signal)
local Trove = require(pluginRoot.Packages.Trove)
local State = require(pluginRoot.Main.State)

local photoRoot = script:FindFirstAncestor("Photo")
local Screenshot = require(photoRoot.Captures.Screenshot)

local Players = game:GetService("Players")
local CoreGui = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")

local PLAYER_GUI: PlayerGui?
pcall(function()
	-- team create can cause there to be a player with no player gui
	PLAYER_GUI = Players.LocalPlayer and Players.LocalPlayer.PlayerGui
end)

local UIHiderStatic = {}

local UIHiderClass = {}
UIHiderClass.__index = UIHiderClass
UIHiderClass.ClassName = "UIHider"

export type UIHider = typeof(setmetatable(
	{} :: {
		trove: Trove.Trove,
		watchTroves: { [LayerCollector]: Trove.Trove },
		hiddenClasses: { string },
	},
	UIHiderClass
))

local globalExclusionsSet = {}
local globalExclusionsChanged = Signal.new()

-- Constructor

function UIHiderStatic.new(hiddenClasses: { string })
	local self = setmetatable({}, UIHiderClass) :: UIHider

	self.trove = Trove.new()
	self.watchTroves = {}
	self.hiddenClasses = table.clone(hiddenClasses)

	for _, uiSource in { CoreGui, StarterGui, PLAYER_GUI } do
		self.trove:Add(uiSource.DescendantAdded:Connect(function(child)
			self:_attemptWatch(child)
		end))

		self.trove:Add(uiSource.DescendantRemoving:Connect(function(child)
			self:_attemptStopWatch(child)
		end))

		for _, child in uiSource:QueryDescendants("LayerCollector") do
			self:_attemptWatch(child)
		end
	end

	self.trove:Add(workspace.DescendantAdded:Connect(function(child)
		self:_attemptWatch(child)
	end))

	self.trove:Add(workspace.DescendantRemoving:Connect(function(child)
		self:_attemptStopWatch(child)
	end))

	for _, child in workspace:QueryDescendants("LayerCollector") do
		self:_attemptWatch(child)
	end

	return self
end

-- Static

function UIHiderStatic.isViewportAppExcluded(rect: Rect)
	local scaleOS = State.read().temporary.scaleOS
	return not Screenshot.isFullscreenRect(rect) and Screenshot.isUnitScaleOS(scaleOS)
end

function UIHiderStatic.addExclusion(exclusion: LayerCollector)
	globalExclusionsSet[exclusion] = true
	globalExclusionsChanged:Fire()

	local hasCalled = false
	return function()
		if not hasCalled then
			hasCalled = true
			globalExclusionsSet[exclusion] = nil
			globalExclusionsChanged:Fire()
		end
	end
end

-- Private

local function isExcluded(layerCollector: LayerCollector): boolean
	return not not globalExclusionsSet[layerCollector]
end

function UIHiderClass._isValidLayerCollector(self: UIHider, layerCollector: LayerCollector)
	for _, className in self.hiddenClasses do
		if layerCollector:IsA(className) then
			return true
		end
	end
	return false
end

function UIHiderClass._watch(self: UIHider, layerCollector: LayerCollector)
	local trove = Trove.new()
	local wasEnabled = layerCollector.Enabled

	-- signal behavior deferred...
	local function refreshEnabled()
		if isExcluded(layerCollector) then
			layerCollector.Enabled = wasEnabled
		else
			layerCollector.Enabled = false
		end
	end

	local enabledChanged = layerCollector:GetPropertyChangedSignal("Enabled"):Connect(function()
		refreshEnabled()
	end)

	local exclusionsChanged = globalExclusionsChanged:Connect(function()
		refreshEnabled()
	end)

	trove:Add(function()
		enabledChanged:Disconnect()
		exclusionsChanged:Disconnect()
		layerCollector.Enabled = wasEnabled
	end)

	self.watchTroves[layerCollector] = trove
	refreshEnabled()
end

function UIHiderClass._stopWatching(self: UIHider, layerCollector: LayerCollector)
	local trove = self.watchTroves[layerCollector]
	if trove then
		self.watchTroves[layerCollector] = nil
		trove:Destroy()
	end
end

function UIHiderClass._attemptWatch(self: UIHider, child: Instance)
	if child:IsA("LayerCollector") and self:_isValidLayerCollector(child) and not self.watchTroves[child] then
		self:_watch(child)
	end
end

function UIHiderClass._attemptStopWatch(self: UIHider, child: Instance)
	if child:IsA("LayerCollector") and self.watchTroves[child] then
		self:_stopWatching(child)
	end
end

-- Public

function UIHiderClass.Destroy(self: UIHider)
	self.trove:Destroy()
	for layerCollector, _ in self.watchTroves do
		self:_stopWatching(layerCollector)
	end
end

--

return UIHiderStatic


--!strict

local StudioService = game:GetService("StudioService") :: StudioService
local AssetService = game:GetService("AssetService") :: AssetService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Future = require(pluginRoot.Packages.Future)
local Trove = require(pluginRoot.Packages.Trove)
local Sift = require(pluginRoot.Packages.Sift)

local Timeout = require(pluginRoot.Utilities.Timeout)
local Image = require(pluginRoot.Utilities.Image)

local Types = require(pluginRoot.Main.Bindings.Interface.Types)
local State = require(pluginRoot.Main.State)

local SceneControl = require(script.SceneControl)
local Captures = require(script.Captures)

local Photo = {}

-- Private

local function createIsCapturingTrove()
	assert(not State.read().temporary.isCapturing, "A capture is already in progress.")

	State.mutate(function(state)
		state.temporary.isCapturing = true
	end)

	local trove = Trove.new()
	trove:Add(function()
		State.mutate(function(state)
			state.temporary.isCapturing = false
		end)
	end)

	Timeout.tryYield(true)
	return trove
end

local quadFuture: Future.Future<MeshPart>
local function getQuadMeshPart()
	if not quadFuture then
		local editMesh = AssetService:CreateEditableMesh({ FixedSize = false })

		local v0 = editMesh:AddVertex(Vector3.new(-0.5, 0.5, 0))
		local v1 = editMesh:AddVertex(Vector3.new(0.5, 0.5, 0))
		local v2 = editMesh:AddVertex(Vector3.new(0.5, -0.5, 0))
		local v3 = editMesh:AddVertex(Vector3.new(-0.5, -0.5, 0))

		local n0 = editMesh:AddNormal(Vector3.new(0, 0, -1))
		local n1 = editMesh:AddNormal(Vector3.new(0, 0, -1))
		local n2 = editMesh:AddNormal(Vector3.new(0, 0, -1))
		local n3 = editMesh:AddNormal(Vector3.new(0, 0, -1))

		local uv0 = editMesh:AddUV(Vector2.new(0, 0))
		local uv1 = editMesh:AddUV(Vector2.new(1, 0))
		local uv2 = editMesh:AddUV(Vector2.new(1, 1))
		local uv3 = editMesh:AddUV(Vector2.new(0, 1))

		local f0 = editMesh:AddTriangle(v0, v1, v2)
		local f1 = editMesh:AddTriangle(v0, v2, v3)

		editMesh:SetFaceNormals(f0, { n0, n1, n2 })
		editMesh:SetFaceUVs(f0, { uv0, uv1, uv2 })
		editMesh:SetFaceNormals(f1, { n0, n2, n3 })
		editMesh:SetFaceUVs(f1, { uv0, uv2, uv3 })

		local fixedMesh = AssetService:CreateEditableMeshAsync(Content.fromObject(editMesh), { FixedSize = true })
		editMesh:Destroy()

		local meshPart = AssetService:CreateMeshPartAsync(Content.fromObject(fixedMesh))
		quadFuture = Future.completed(meshPart)
	end

	return quadFuture:expect()
end

-- Public

function Photo.captureUI(options: Types.CaptureUIOptions)
	options = Sift.Dictionary.merge({
		delay = 0,
		alphaBleed = State.read().saved.alphaBleedingEnabled,
	}, options)

	local processor = Captures.UserInterface.Processors[options.type]

	local results: { Image.Image }
	local trove = createIsCapturingTrove()
	local success, err = SceneControl.freeze(false, function()
		if options.delay and options.delay > 0 then
			task.wait(options.delay)
		end
		results = processor(options, trove)
	end)

	trove:Destroy()

	if not success then
		if err then
			if err:sub(1, 4) == "WARN" then
				err = err:sub(7)
				warn(err)
			else
				task.spawn(error, err)
			end
		end
		results = {}
	end

	return results, err
end

function Photo.captureViewport(options: Types.CaptureViewportOptions)
	options = Sift.Dictionary.merge({
		delay = 0,
		alphaBleed = State.read().saved.alphaBleedingEnabled,
	}, options)

	local processor = Captures.Viewport.Processors[options.type]

	if options.delay and options.delay > 0 then
		task.wait(options.delay)
	end

	local results: { Image.Image }
	local trove = createIsCapturingTrove()
	local success, err = SceneControl.freeze(true, function()
		results = processor(options, trove)
	end)

	trove:Destroy()

	if not success then
		if err then
			if err:sub(1, 4) == "WARN" then
				err = err:sub(7)
				warn(err)
			else
				task.spawn(error, err)
			end
		end
		results = {}
	end

	return results, err
end

function Photo.export(imgs: { Image.Image }, parent: Instance?)
	local results = {}

	for i, img in imgs do
		local name = `{DateTime.now():ToIsoDate()}_{i}`
		local meshPart = getQuadMeshPart():Clone()
		meshPart.Archivable = false
		meshPart.Anchored = true
		meshPart.CanCollide = false
		meshPart.CanTouch = false
		meshPart.Transparency = 0 -- cannot be transparent
		meshPart.Name = name
		meshPart.Size = Vector3.new(1, 1, 0)
		meshPart.TextureContent = Content.fromObject(img:Save())
		meshPart.Parent = parent

		table.insert(results, meshPart)
	end
	return results
end

function Photo.importFiles(parent: Instance?)
	local results = {}

	local files = (StudioService:PromptImportFilesAsync({ "png" }) :: any) :: { File }
	for _, file in files do
		local content = Content.fromUri(file:GetTemporaryId())
		local editImage = AssetService:CreateEditableImageAsync(content)

		local meshPart = getQuadMeshPart():Clone()
		meshPart.Archivable = false
		meshPart.Anchored = true
		meshPart.CanCollide = false
		meshPart.CanTouch = false
		meshPart.Transparency = 0 -- cannot be transparent
		meshPart.Name = file.Name:match("(.+)%.[^.]*$") or file.Name
		meshPart.Size = Vector3.new(1, 1, 1)
		meshPart.TextureContent = Content.fromObject(editImage)
		meshPart.Parent = parent

		table.insert(results, meshPart)
	end

	return results
end

--

return Photo


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Sift = require(pluginRoot.Packages.Sift)

local SaveFormatV1 = {}

export type Serialized = {
	alphaBleedingEnabled: boolean,
	bindingsEnabled: boolean,
	galleryListExpanded: boolean,
	experimentalModeEnabled: boolean,
}

export type Deserialized = {
	alphaBleedingEnabled: boolean,
	bindingsEnabled: boolean,
	galleryListExpanded: boolean,
	experimentalModeEnabled: boolean,
}

local function toboolean(bool: boolean)
	return if typeof(bool) == "boolean" then bool else nil
end

SaveFormatV1.version = 1

function SaveFormatV1.default(): Deserialized
	return {
		alphaBleedingEnabled = true,
		bindingsEnabled = false,
		galleryListExpanded = true,
		experimentalModeEnabled = false,
	}
end

function SaveFormatV1.migrate(_previousSerialized: any)
	error(`[SaveFormatV{SaveFormatV1.version}]: Cannot migrate from V1`)
end

function SaveFormatV1.serialize(deserialized: Deserialized): Serialized
	return {
		alphaBleedingEnabled = not not deserialized.alphaBleedingEnabled,
		bindingsEnabled = not not deserialized.bindingsEnabled,
		galleryListExpanded = not not deserialized.galleryListExpanded,
		experimentalModeEnabled = not not deserialized.experimentalModeEnabled,
	}
end

function SaveFormatV1.deserialize(serialized: Serialized): Deserialized
	return Sift.Dictionary.mergeDeep(SaveFormatV1.default(), {
		alphaBleedingEnabled = toboolean(serialized.alphaBleedingEnabled),
		bindingsEnabled = toboolean(serialized.bindingsEnabled),
		galleryListExpanded = toboolean(serialized.galleryListExpanded),
		experimentalModeEnabled = toboolean(serialized.experimentalModeEnabled),
	})
end

return SaveFormatV1


--!strict

local CurrentVersion = require(script.V1)

local SaveVersions = {}

export type Serialized = CurrentVersion.Serialized
export type Deserialized = CurrentVersion.Deserialized

export type VersionFormat = {
	migrate: (any) -> any,
	serialize: (any) -> any,
	deserialize: (any) -> any,
	version: number,
	index: number,
}

export type Saved = {
	version: number,
	serialized: any,
}

SaveVersions.byOrder = {}
SaveVersions.byVersion = {}
SaveVersions.latest = (CurrentVersion :: any) :: VersionFormat

local function init()
	for _, child in script:GetChildren() do
		local versionModule = require(child) :: VersionFormat
		SaveVersions.byVersion[versionModule.version] = versionModule
		table.insert(SaveVersions.byOrder, versionModule)
	end

	table.sort(SaveVersions.byOrder, function(a, b)
		return a.version < b.version
	end)

	for index, versionModule in SaveVersions.byOrder do
		versionModule.index = index
	end

	assert(
		SaveVersions.byOrder[#SaveVersions.byOrder].version == CurrentVersion.version,
		"Not using the latest save format version."
	)
end

-- Public

function SaveVersions.save(deserialized: Deserialized): Saved
	return {
		version = CurrentVersion.version,
		serialized = CurrentVersion.serialize(deserialized),
	}
end

function SaveVersions.load(saved: Saved): Deserialized
	local matchedVersion = SaveVersions.byVersion[saved.version]
	assert(matchedVersion, "Unable to find a matching format version.")

	local serialized = saved.serialized
	local migrationIndex = matchedVersion.index
	for i = migrationIndex + 1, #SaveVersions.byOrder do
		local versionFormat = SaveVersions.byOrder[i]
		serialized = versionFormat.migrate(serialized)
	end

	return SaveVersions.latest.deserialize(serialized)
end

--

init()
return SaveVersions


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Signal = require(pluginRoot.Packages.Signal)

local SaveVersions = require(script.SaveVersions)

local PLUGIN_SAVED_KEY = "egomoose_photobooth_save"

local State = {}

local currentState: State
local currentStateReadOnly: State
local stateChanged = Signal.new()

-- Private

local function defaultTemporary()
	return {
		scaleOS = Vector2.one,

		isCapturing = false,
		isFullscreen = false,
		isExperimentalMode = false,

		uiCaptureTypeIndex = 1,
		viewportCaptureTypeIndex = 1,
	}
end

local function readOnly<T>(tbl: T)
	return (setmetatable({}, {
		__index = tbl,
		__newindex = false,
	}) :: any) :: T
end

local function initState(saved: SaveVersions.Saved?)
	if not (saved and saved.version and saved.serialized) then
		saved = {
			version = SaveVersions.latest.version,
			serialized = {},
		}
	end

	currentState = {
		saved = SaveVersions.load(saved :: SaveVersions.Saved),
		temporary = defaultTemporary(),
	}

	currentStateReadOnly = {
		saved = readOnly(currentState.saved),
		temporary = readOnly(currentState.temporary),
	}
end

--

export type State = {
	saved: SaveVersions.Deserialized,
	temporary: typeof(defaultTemporary()),
}

function State.read()
	assert(currentState, "State has not been initialized.")
	return currentStateReadOnly
end

function State.mutate(callback: (state: State) -> ())
	assert(currentState, "State has not been initialized.")

	local tmpMutatedKeys = {}
	local tmpClone = table.clone(currentState.temporary)
	local tmpProxy = setmetatable({}, {
		__index = currentState.temporary,
		__newindex = function(_, key, value)
			if tmpClone[key] ~= value then
				tmpMutatedKeys[key] = true
			else
				tmpMutatedKeys[key] = nil
			end
			currentState.temporary[key] = value
		end,
	})

	local savedMutatedKeys = {}
	local savedClone = table.clone(currentState.saved)
	local savedProxy = setmetatable({}, {
		__index = currentState.saved,
		__newindex = function(_, key, value)
			if savedClone[key] ~= value then
				savedMutatedKeys[key] = true
			else
				savedMutatedKeys[key] = nil
			end
			currentState.saved[key] = value
		end,
	})

	callback({
		saved = savedProxy,
		temporary = tmpProxy,
	} :: any)

	for key, _ in savedMutatedKeys do
		stateChanged:Fire({ "saved", key }, currentState.saved[key], savedClone[key])
	end

	for key, _ in tmpMutatedKeys do
		stateChanged:Fire({ "temporary", key }, currentState.temporary[key], tmpClone[key])
	end
end

function State.onChanged(callback: (path: { string }, new: any, prev: any) -> ())
	local connection = stateChanged:Connect(callback)
	return function()
		connection:Disconnect()
	end
end

function State.setup(plugin: Plugin)
	local success, _err = pcall(function()
		local saved = plugin:GetSetting(PLUGIN_SAVED_KEY)
		initState(saved)
	end)

	if not success then
		initState(nil)
	end

	plugin.Unloading:Connect(function()
		local saved = SaveVersions.save(currentState.saved)
		plugin:SetSetting(PLUGIN_SAVED_KEY, saved)
	end)
end

function State.storybook()
	if not currentState then
		initState(nil)
	end
end

return State


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local BaseButton = require(uiRoot.Components.Shared.BaseButton)
local Hooks = require(uiRoot.Hooks)

type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	ViewportSize: Vector2,
	ViewportPosition: Vector2,

	DisplayedAbsSize: Vector2,
	OnDimensionInput: (Vector2) -> (),

	OnMaximize: () -> ()?,
	OnCapture: () -> (boolean, string?),

	Disabled: boolean,
}

type DimensionProps = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	DisplayedAbsSize: Vector2,
	OnDimensionInput: (Vector2) -> (),

	Disabled: boolean,
}

local function Dimensions(props: DimensionProps)
	local theme = StudioComponents.useTheme()

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder),
		BorderMode = Enum.BorderMode.Inset,
	}, {
		Layout = React.createElement("UIListLayout", {
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Horizontal,
			VerticalAlignment = Enum.VerticalAlignment.Center,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			Padding = UDim.new(0, 0),
		}),

		CrossContainer = React.createElement("Frame", {
			Size = UDim2.new(0, 14, 1, 0),
			BackgroundTransparency = 1,
			LayoutOrder = 2,
		}, {
			Cross = React.createElement("TextLabel", {
				Size = UDim2.fromScale(1, 1),
				Position = UDim2.new(0.5, 0, 0.5, 0),
				AnchorPoint = Vector2.new(0.5, 0.5),
				BackgroundTransparency = 1,

				FontFace = Font.fromName("SourceSansPro"),
				TextColor3 = Color3.fromRGB(102, 102, 102),
				TextXAlignment = Enum.TextXAlignment.Center,
				TextYAlignment = Enum.TextYAlignment.Center,
				TextSize = 14,
				Text = "x",
			}),
		}),

		Width = React.createElement("TextBox", {
			Size = UDim2.new(0.5, -7, 1, 0),
			BackgroundTransparency = 1,
			LayoutOrder = 1,

			FontFace = Font.fromName("SourceSansPro"),
			TextColor3 = Color3.fromRGB(102, 102, 102),
			TextXAlignment = Enum.TextXAlignment.Right,
			TextYAlignment = Enum.TextYAlignment.Center,
			TextSize = 14,
			Text = tostring(props.DisplayedAbsSize.X),

			[React.Event.FocusLost] = function(rbx, _enterPressed)
				local dimension = tonumber(rbx.Text)
				if dimension then
					dimension = math.floor(dimension)
					props.OnDimensionInput(Vector2.new(dimension, props.DisplayedAbsSize.Y))
				end
				rbx.Text = tostring(props.DisplayedAbsSize.X)
			end,
		}),

		Height = React.createElement("TextBox", {
			Size = UDim2.new(0.5, -7, 1, 0),
			BackgroundTransparency = 1,
			LayoutOrder = 3,

			FontFace = Font.fromName("SourceSansPro"),
			TextColor3 = Color3.fromRGB(102, 102, 102),
			TextXAlignment = Enum.TextXAlignment.Left,
			TextYAlignment = Enum.TextYAlignment.Center,
			TextSize = 14,
			Text = tostring(props.DisplayedAbsSize.Y),

			[React.Event.FocusLost] = function(rbx, _enterPressed)
				local dimension = tonumber(rbx.Text)
				if dimension then
					dimension = math.floor(dimension)
					props.OnDimensionInput(Vector2.new(props.DisplayedAbsSize.X, dimension))
				end
				rbx.Text = tostring(props.DisplayedAbsSize.Y)
			end,
		}),
	})
end

local function ActionBar(props: Props)
	local theme = StudioComponents.useTheme()
	local cameraIcon = assert(Lucide.GetAsset("camera", 16))
	local maximizeIcon = assert(Lucide.GetAsset("maximize", 16))

	local underFrameRef = React.useRef(nil)
	local underFrameAbsPosition = Hooks.useRefProperty(underFrameRef, "AbsolutePosition", Vector2.zero)
	local underFrameAbsSize = Hooks.useRefProperty(underFrameRef, "AbsoluteSize", Vector2.zero)
	local underFrameBottomY = underFrameAbsPosition.Y + underFrameAbsSize.Y
	local useUnderFrame = underFrameBottomY <= (props.ViewportPosition.Y + props.ViewportSize.Y)

	local shakeOffset, makeShake = Hooks.useShake()
	local errorOffset, makeError = Hooks.useShake()

	local overlayColor = React.useMemo(function()
		return theme
			:GetColor(Enum.StudioStyleGuideColor.MainBackground)
			:Lerp(theme:GetColor(Enum.StudioStyleGuideColor.ErrorText), 0.2)
	end, { theme })

	local dimensionsWidthOffset = 0
	if props.OnMaximize then
		dimensionsWidthOffset = -24 - 4
	end

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
	}, {
		ShakeFrame = React.createElement("Frame", {
			Size = UDim2.fromScale(1, 1),
			Position = UDim2.fromOffset(shakeOffset.X, shakeOffset.Y),
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
		}, {
			Overlay = shakeOffset.Z < 1 and React.createElement("Frame", {
				Active = false,
				BackgroundTransparency = math.lerp(0.6, 1, shakeOffset.Z ^ 2),
				BackgroundColor3 = overlayColor,
				Size = UDim2.fromScale(1, 1),
				ZIndex = 2,
			}),
			Content = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.fromScale(1, 1),
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = UDim.new(0, 4),
					PaddingRight = UDim.new(0, 4),
					PaddingTop = UDim.new(0, 4),
					PaddingBottom = UDim.new(0, 4),
				}),

				Dimensions = React.createElement(Dimensions, {
					AnchorPoint = Vector2.new(0, 0),
					Position = UDim2.fromScale(0, 0),
					Size = UDim2.new(1, -24 - 4 + dimensionsWidthOffset, 1, 0),

					DisplayedAbsSize = props.DisplayedAbsSize,
					OnDimensionInput = props.OnDimensionInput,

					Disabled = props.Disabled,
				}),

				Maximize = props.OnMaximize and React.createElement(BaseButton, {
					AnchorPoint = Vector2.new(1, 0),
					Position = UDim2.new(1, -24 - 4, 0, 0),
					Size = UDim2.new(0, 24, 1, 0),

					BackgroundColorStyle = Enum.StudioStyleGuideColor.Button,
					BorderColorStyle = Enum.StudioStyleGuideColor.Border,
					TextColorStyle = Enum.StudioStyleGuideColor.ButtonText,

					Icon = {
						Image = maximizeIcon.Url,
						RectSize = maximizeIcon.ImageRectSize,
						RectOffset = maximizeIcon.ImageRectOffset,

						Size = maximizeIcon.ImageRectSize,
						Transparency = if props.Disabled then 0.7 else 0,
						UseThemeColor = true,
					},

					OnActivated = props.OnMaximize,
					Disabled = props.Disabled,
				}),

				Capture = React.createElement(StudioComponents.MainButton, {
					AnchorPoint = Vector2.new(1, 0),
					Position = UDim2.fromScale(1, 0),
					Size = UDim2.new(0, 24, 1, 0),

					Icon = {
						Image = cameraIcon.Url,
						RectSize = cameraIcon.ImageRectSize,
						RectOffset = cameraIcon.ImageRectOffset,

						Size = cameraIcon.ImageRectSize,
						Transparency = if props.Disabled then 0.7 else 0,
						Color = Color3.new(1, 1, 1),
					},

					OnActivated = function()
						local success, _err = props.OnCapture()
						if not success then
							makeShake(1, Vector2.new(7, 4), Vector2.new(4, 2))
							makeError(1.5, Vector2.new(7, 4), Vector2.new(0, 0))
						end
					end,
					Disabled = props.Disabled,
				}),
			}),
		}),

		ErrorLabel = errorOffset.Z < 1 and React.createElement("TextLabel", {
			BackgroundTransparency = 1,
			Position = if useUnderFrame then UDim2.new(0, 0, 1, 2) else UDim2.new(1, 8, 0, 0),
			Size = UDim2.fromScale(1, 1),
			FontFace = Font.fromName("SourceSansPro", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
			Text = "Error: Check the studio output (Window > Output)",
			TextSize = 14,
			TextTransparency = errorOffset.Z ^ 20,
			TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ErrorText),
			TextWrapped = true,
			TextXAlignment = Enum.TextXAlignment.Center, --if useUnderFrame then Enum.TextXAlignment.Center else Enum.TextXAlignment.Left,
			TextYAlignment = if useUnderFrame then Enum.TextYAlignment.Top else Enum.TextYAlignment.Center,
		}, {
			UIStroke = React.createElement("UIStroke", {
				Thickness = 1,
				Transparency = errorOffset.Z ^ 20,
				Color = Color3.new(0, 0, 0),
			}),
		}),
		UnderFrame = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
			Position = UDim2.new(0, 0, 1, 2),
			ref = underFrameRef,
		}),
	})
end

return ActionBar


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	MarginBump: number,
	OverlapBump: number,

	Transparency: number,
	Color: Color3,

	ZIndex: number,
}

local function Blinder(props: Props)
	local anchorPoint = props.AnchorPoint
	local position = props.Position
	local size = props.Size

	local centerXScale = position.X.Scale + size.X.Scale * (0.5 - anchorPoint.X)
	local centerYScale = position.Y.Scale + size.Y.Scale * (0.5 - anchorPoint.Y)
	local centerXOffset = position.X.Offset + size.X.Offset * (0.5 - anchorPoint.X)
	local centerYOffset = position.Y.Offset + size.Y.Offset * (0.5 - anchorPoint.Y)

	-- using a canvas group avoid overlap lines that may occur
	-- when half-pixels are involved
	return React.createElement("CanvasGroup", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ClipsDescendants = true,
		GroupTransparency = props.Transparency,
		GroupColor3 = props.Color,
		ZIndex = props.ZIndex,
	}, {
		Top = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0.5, 1),
			Position = UDim2.new(
				centerXScale,
				centerXOffset,
				centerYScale - size.Y.Scale / 2,
				centerYOffset - size.Y.Offset / 2 + props.MarginBump
			),
			Size = UDim2.new(2, 0, 2, 0),
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 0,
			BorderSizePixel = 0,
		}),
		Bottom = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0.5, 0),
			Position = UDim2.new(
				centerXScale,
				centerXOffset,
				centerYScale + size.Y.Scale / 2,
				centerYOffset + size.Y.Offset / 2 - props.MarginBump
			),
			Size = UDim2.new(2, 0, 2, 0),
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 0,
			BorderSizePixel = 0,
		}),
		Left = React.createElement("Frame", {
			AnchorPoint = Vector2.new(1, 0.5),
			Position = UDim2.new(
				centerXScale - size.X.Scale / 2,
				centerXOffset - size.X.Offset / 2 + props.MarginBump,
				centerYScale,
				centerYOffset
			),
			Size = UDim2.new(2, 0, size.Y.Scale, size.Y.Offset + (props.OverlapBump - props.MarginBump) * 2),
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 0,
			BorderSizePixel = 0,
		}),
		Right = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0, 0.5),
			Position = UDim2.new(
				centerXScale + size.X.Scale / 2,
				centerXOffset + size.X.Offset / 2 - props.MarginBump,
				centerYScale,
				centerYOffset
			),
			Size = UDim2.new(2, 0, size.Y.Scale, size.Y.Offset + (props.OverlapBump - props.MarginBump) * 2),
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 0,
			BorderSizePixel = 0,
		}),
	})
end

return Blinder


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Hooks = require(uiRoot.Hooks)

-- stylua: ignore
type OnDragCallback = (
	rbx: GuiObject,
	input: InputObject,
	direction: Vector2
) -> ()

type Props = {
	Transparency: number,
	Thickness: UDim,
	Padding: UDim,
	Length: UDim,

	CornersEnabled: boolean,
	MiddlesEnabled: boolean,

	OnDrag: OnDragCallback,
	OnDragStart: OnDragCallback,
	OnDragFinish: () -> (),

	Disabled: boolean,
}

type GenericGrabberProps = {
	RootProps: Props,
	AnchorPoint: Vector2,
}

type GrabberProps = GenericGrabberProps & {
	Color: Color3,
	Direction: Vector2,
	OnInputBegan: (GuiObject, InputObject) -> (),
	OnInputChanged: (GuiObject, InputObject) -> (),
	OnInputEnded: (GuiObject, InputObject) -> (),
}

local function GrabCorner(props: GrabberProps)
	local signX = props.Direction.X
	local signY = props.Direction.Y

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = UDim2.fromScale(props.AnchorPoint.X, props.AnchorPoint.Y),
		Size = UDim2.new(
			props.RootProps.Length.Scale,
			props.RootProps.Length.Offset,
			props.RootProps.Length.Scale,
			props.RootProps.Length.Offset
		),
		BackgroundTransparency = 1,
	}, {
		Corner = React.createElement("ImageButton", {
			AutoButtonColor = false,
			AnchorPoint = props.AnchorPoint,
			Position = UDim2.fromScale(props.AnchorPoint.X, props.AnchorPoint.Y),
			Size = UDim2.new(
				props.RootProps.Thickness.Scale,
				props.RootProps.Thickness.Offset,
				props.RootProps.Thickness.Scale,
				props.RootProps.Thickness.Offset
			),
			BackgroundTransparency = props.RootProps.Transparency,
			BackgroundColor3 = props.Color,
			BorderSizePixel = 0,

			[React.Event.InputBegan] = if props.RootProps then props.OnInputBegan else nil,
			[React.Event.InputChanged] = if props.RootProps then props.OnInputChanged else nil,
			[React.Event.InputEnded] = if props.RootProps then props.OnInputEnded else nil,
		}),
		Vertical = React.createElement("ImageButton", {
			AutoButtonColor = false,
			AnchorPoint = props.AnchorPoint,
			Position = UDim2.new(
				props.AnchorPoint.X,
				0,
				props.AnchorPoint.Y + props.RootProps.Thickness.Scale * -signY,
				props.RootProps.Thickness.Offset * -signY
			),
			Size = UDim2.new(
				props.RootProps.Thickness.Scale,
				props.RootProps.Thickness.Offset,
				1 - props.RootProps.Thickness.Scale,
				-props.RootProps.Thickness.Offset
			),
			BackgroundTransparency = props.RootProps.Transparency,
			BackgroundColor3 = props.Color,
			BorderSizePixel = 0,

			[React.Event.InputBegan] = if props.RootProps then props.OnInputBegan else nil,
			[React.Event.InputChanged] = if props.RootProps then props.OnInputChanged else nil,
			[React.Event.InputEnded] = if props.RootProps then props.OnInputEnded else nil,
		}),
		Horizontal = React.createElement("ImageButton", {
			AutoButtonColor = false,
			AnchorPoint = props.AnchorPoint,
			Position = UDim2.new(
				props.AnchorPoint.X + props.RootProps.Thickness.Scale * -signX,
				props.RootProps.Thickness.Offset * -signX,
				props.AnchorPoint.Y,
				0
			),
			Size = UDim2.new(
				1 - props.RootProps.Thickness.Scale,
				-props.RootProps.Thickness.Offset,
				props.RootProps.Thickness.Scale,
				props.RootProps.Thickness.Offset
			),
			BackgroundTransparency = props.RootProps.Transparency,
			BackgroundColor3 = props.Color,
			BorderSizePixel = 0,

			[React.Event.InputBegan] = if props.RootProps then props.OnInputBegan else nil,
			[React.Event.InputChanged] = if props.RootProps then props.OnInputChanged else nil,
			[React.Event.InputEnded] = if props.RootProps then props.OnInputEnded else nil,
		}),
	})
end

local function GrabCardinal(props: GrabberProps)
	local isHorizontal = (math.abs(props.Direction.X) == 1)
	local width = if isHorizontal then props.RootProps.Thickness else props.RootProps.Length
	local height = if isHorizontal then props.RootProps.Length else props.RootProps.Thickness

	return React.createElement("ImageButton", {
		AutoButtonColor = false,
		AnchorPoint = props.AnchorPoint,
		Position = UDim2.fromScale(props.AnchorPoint.X, props.AnchorPoint.Y),
		Size = UDim2.new(width.Scale, width.Offset, height.Scale, height.Offset),
		BackgroundTransparency = props.RootProps.Transparency,
		BackgroundColor3 = props.Color,
		BorderSizePixel = 0,

		[React.Event.InputBegan] = if props.RootProps then props.OnInputBegan else nil,
		[React.Event.InputChanged] = if props.RootProps then props.OnInputChanged else nil,
		[React.Event.InputEnded] = if props.RootProps then props.OnInputEnded else nil,
	})
end

local function GrabDragger(props: GenericGrabberProps)
	local isCorner = not (props.AnchorPoint.X == 0.5 or props.AnchorPoint.Y == 0.5)
	-- stylua: ignore
	local direction = Vector2.new(
		(2 * props.AnchorPoint.X) - 1,
		(2 * props.AnchorPoint.Y) - 1
	)

	local dragger = Hooks.useMouseDrag({
		onDragCallback = function(rbx, input)
			props.RootProps.OnDrag(rbx, input, direction)
		end,
		onBeganCallback = function(rbx, input)
			props.RootProps.OnDragStart(rbx, input, direction)
		end,
		onEndedCallback = props.RootProps.OnDragFinish,
	})

	if props.RootProps.Disabled and dragger.isActive() then
		dragger.cancel()
	end

	local color = Color3.fromRGB(255, 255, 255)
	if props.RootProps.Disabled or dragger.isActive() then
		color = Color3.fromRGB(60, 60, 60)
	end

	return React.createElement(if isCorner then GrabCorner else GrabCardinal, {
		RootProps = props.RootProps,
		AnchorPoint = props.AnchorPoint,
		Color = color,
		Direction = direction,
		OnInputBegan = dragger.onInputBegan,
		OnInputChanged = dragger.onInputChanged,
		OnInputEnded = dragger.onInputEnded,
	})
end

local function Grabbers(props: Props)
	local gap = props.Thickness + props.Padding

	return React.createElement("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.new(1 + gap.Scale * 2, gap.Offset * 2, 1 + gap.Scale * 2, gap.Offset * 2),
		BackgroundTransparency = 1,
	}, {
		TopLeft = props.CornersEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(0, 0),
			RootProps = props,
		}),
		TopMiddle = props.MiddlesEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(0.5, 0),
			RootProps = props,
		}),
		TopRight = props.CornersEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(1, 0),
			RootProps = props,
		}),
		RightMiddle = props.MiddlesEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(1, 0.5),
			RootProps = props,
		}),
		BottomRight = props.CornersEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(1, 1),
			RootProps = props,
		}),
		BottomMiddle = props.MiddlesEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(0.5, 1),
			RootProps = props,
		}),
		BottomLeft = props.CornersEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(0, 1),
			RootProps = props,
		}),
		LeftMiddle = props.MiddlesEnabled and React.createElement(GrabDragger, {
			AnchorPoint = Vector2.new(0, 0.5),
			RootProps = props,
		}),
	})
end

return Grabbers


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local React = require(pluginRoot.Packages.React)
local Sift = require(pluginRoot.Packages.Sift)

local RenderRect = require(pluginRoot.Main.Bindings.Interface.Helpers.RenderRect)
local Constants = require(pluginRoot.Constants)

local ActionBar = require(script.ActionBar)
local Grabbers = require(script.Grabbers)
local Blinder = require(script.Blinder)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Hooks = require(uiRoot.Hooks)

local MAX_CAPTURE_RECT_SIZE = Vector2.new(Constants.MaxCaptureRectSize.X, Constants.MaxCaptureRectSize.Y)

local GRABBERS = {
	CornersEnabled = true,
	MiddlesEnabled = true,

	MaintainAspectKeys = Sift.Array.toSet({
		Enum.KeyCode.LeftControl,
		Enum.KeyCode.RightControl,
		Enum.KeyCode.LeftMeta,
		Enum.KeyCode.RightMeta,
	}),

	Length = 30,
	Padding = 8,
	Thickness = 4,
}

local ACTION_BAR = {
	Height = 28,
	Padding = 8,
}

local grabberMargin = GRABBERS.Padding + GRABBERS.Thickness
local actionBarPadding = if GRABBERS.MiddlesEnabled
	then ACTION_BAR.Padding + grabberMargin
	else ACTION_BAR.Padding + grabberMargin
local actionBarMargin = ACTION_BAR.Height + actionBarPadding

export type Props = {
	InitialSize: UDim2,
	FocusLenseSizeRef: { current: Vector2? },
	OnCapture: (Rect) -> (boolean, string?),
	OnMaximize: () -> ()?,
	Disabled: boolean,
}

local function useLenseSize(size: Vector2, viewportSize: Vector2, scale: Vector2, grabberMin: number)
	local width = viewportSize.X - grabberMargin * 2
	local height = viewportSize.Y - actionBarMargin * 2

	local minX = math.max(0, math.min(width, grabberMin * scale.X))
	local minY = math.max(0, math.min(height, grabberMin * scale.Y))

	local maxX = math.min(MAX_CAPTURE_RECT_SIZE.X * scale.X, width)
	local maxY = math.min(MAX_CAPTURE_RECT_SIZE.Y * scale.Y, height)

	local clampedX = math.clamp(size.X * scale.X, minX, math.max(minX, maxX))
	local clampedY = math.clamp(size.Y * scale.X, minY, math.max(minY, maxY))

	return Vector2.new(clampedX, clampedY)
end

local function CroppedViewport(props: Props & { ViewportSize: Vector2, ViewportPosition: Vector2 })
	local theme = StudioComponents.useTheme()
	local scaleOS = Hooks.useStatePath(function(state)
		return state.temporary.scaleOS
	end)

	local viewportSize = props.ViewportSize
	local scaledViewport = Vector2.new(math.round(viewportSize.X * scaleOS.X), math.round(viewportSize.Y * scaleOS.Y))

	local aspectKeysDown, setAspectKeysDown = React.useState({})

	local size, setSize = React.useState(MAX_CAPTURE_RECT_SIZE / 2)
	local sizeInitiated = React.useRef(false)
	local prevSize = React.useRef(MAX_CAPTURE_RECT_SIZE / 2)
	local startDrag = React.useRef(nil :: Vector3?)

	local actionBarWidth = if props.OnMaximize then 148 else 120
	local grabberMin = GRABBERS.Length * 3

	React.useEffect(function()
		local minX = grabberMin + grabberMargin * 2
		local minY = grabberMin + actionBarMargin * 2

		if viewportSize.X >= minX and viewportSize.Y >= minY then
			local initialSize = Vector2.new(
				props.InitialSize.X.Offset + props.InitialSize.X.Scale * scaledViewport.X,
				props.InitialSize.Y.Offset + props.InitialSize.Y.Scale * scaledViewport.Y
			)

			local targetSize = if sizeInitiated.current then size else initialSize
			local clamped = useLenseSize(targetSize, scaledViewport, Vector2.new(1, 1), grabberMin)
			setSize(clamped)
			prevSize.current = clamped
			sizeInitiated.current = true
		end
	end, { viewportSize })

	-- this is used for displaying dimensions + capture rect
	local scaledLenseAbsSize = useLenseSize(size, scaledViewport, Vector2.new(1, 1), grabberMin)
	local scaledLenseAbsPosition = (scaledViewport - scaledLenseAbsSize) / 2
	local scaledLenseRect = RenderRect.fromAbs(scaledLenseAbsPosition, scaledLenseAbsSize)
	local scaledLenseRectSize = Vector2.new(scaledLenseRect.Width, scaledLenseRect.Height)

	-- this is what renders
	local lenseAbsSize = (scaledLenseRectSize / scaleOS):Ceil()
	local lenseAbsPosition = (scaledLenseRect.Min / scaleOS):Floor()
	local lenseRect = Rect.new(lenseAbsPosition, lenseAbsPosition + lenseAbsSize)
	local lenseUDim2Size = UDim2.fromOffset(lenseRect.Width, lenseRect.Height)
	local lenseUDim2Position = UDim2.fromOffset(lenseRect.Min.X, lenseRect.Min.Y)

	React.useEffect(function()
		props.FocusLenseSizeRef.current = scaledLenseRectSize
	end, { scaledLenseRectSize })

	return React.createElement("Frame", {
		Position = UDim2.fromScale(0, 0),
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,

		[React.Event.InputBegan] = function(_, input)
			if GRABBERS.MaintainAspectKeys[input.KeyCode] then
				aspectKeysDown[input.KeyCode] = true
				setAspectKeysDown(aspectKeysDown)

				if startDrag.current then
					prevSize.current = size
					startDrag.current = input.Position
				end
			end
		end,
		[React.Event.InputEnded] = function(_, input)
			if GRABBERS.MaintainAspectKeys[input.KeyCode] then
				aspectKeysDown[input.KeyCode] = nil
				setAspectKeysDown(aspectKeysDown)
			end
		end,
	}, {
		Focus = React.createElement("Frame", {
			AnchorPoint = Vector2.zero,
			Position = lenseUDim2Position,
			Size = lenseUDim2Size,
			BackgroundTransparency = 1,
			ZIndex = 2,
		}, {
			Outline = React.createElement("Frame", {
				Size = UDim2.fromScale(1, 1),
				BackgroundTransparency = 1,
			}, {
				UIStroke = React.createElement("UIStroke", {
					Thickness = 2,
					Transparency = 0,
					Color = theme:GetColor(
						Enum.StudioStyleGuideColor.InputFieldBorder,
						Enum.StudioStyleGuideModifier.Selected
					),
				}),
			}),
			Grabbers = React.createElement(Grabbers, {
				Transparency = 0.4,
				Thickness = UDim.new(0, GRABBERS.Thickness),
				Padding = UDim.new(0, GRABBERS.Padding),
				Length = UDim.new(0, GRABBERS.Length),

				CornersEnabled = GRABBERS.CornersEnabled,
				MiddlesEnabled = GRABBERS.MiddlesEnabled,

				OnDragStart = function(_rbx, input)
					prevSize.current = size
					startDrag.current = input.Position
				end,

				OnDragFinish = function()
					startDrag.current = nil
				end,

				OnDrag = function(_rbx, input, direction)
					if startDrag.current then
						local delta3 = input.Position - startDrag.current
						local delta2 = Vector2.new(delta3.X, delta3.Y)

						if next(aspectKeysDown) then
							local directionAbsSum = math.abs(direction.X) + math.abs(direction.Y)
							local isDiagonal = directionAbsSum >= 2

							if isDiagonal then
								local projectionAxis = (prevSize.current * direction).Unit
								delta2 = delta2:Dot(projectionAxis) * projectionAxis
							end
						end

						local offset = 2 * direction * scaleOS * delta2
						local newSize = prevSize.current + offset

						setSize(useLenseSize(newSize, scaledViewport, Vector2.new(1, 1), grabberMin))
					end
				end,

				Disabled = props.Disabled,
			}),
			ActionBar = React.createElement(ActionBar, {
				AnchorPoint = Vector2.new(0.5, 0),
				Position = UDim2.new(0.5, 0, 1, actionBarPadding),
				Size = UDim2.fromOffset(actionBarWidth, ACTION_BAR.Height),

				ViewportSize = props.ViewportSize,
				ViewportPosition = props.ViewportPosition,

				DisplayedAbsSize = scaledLenseRectSize,
				OnDimensionInput = function(dimensions)
					setSize(useLenseSize(dimensions, scaledViewport, Vector2.new(1, 1), grabberMin))
				end,

				OnMaximize = props.OnMaximize,
				OnCapture = function()
					return props.OnCapture(scaledLenseRect)
				end,

				Disabled = props.Disabled,
			}),
		}),

		Blinder = React.createElement(Blinder, {
			AnchorPoint = Vector2.zero,
			Position = lenseUDim2Position,
			Size = lenseUDim2Size,

			-- this should be hidden by the UI stroke
			-- it acts as a margin of error against half-pixel scaling
			MarginBump = -1,
			OverlapBump = 10,

			Color = Color3.new(0, 0, 0),
			Transparency = 0.7,
			ZIndex = 1,

			Disabled = props.Disabled,
		}),
	})
end

return function(props: Props)
	local containerRef = React.useRef(nil :: GuiObject?)
	local containerSize = Hooks.useRefProperty(containerRef, "AbsoluteSize", Vector2.zero)
	local containerPosition = Hooks.useRefProperty(containerRef, "AbsolutePosition", Vector2.zero)

	return React.createElement("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ref = containerRef,
	}, {
		CroppedViewport = React.createElement(CroppedViewport, {
			ViewportSize = containerSize,
			ViewportPosition = containerPosition,
			InitialSize = props.InitialSize,
			FocusLenseSizeRef = props.FocusLenseSizeRef,
			OnMaximize = props.OnMaximize,
			OnCapture = props.OnCapture,
			Disabled = props.Disabled,
		}),
	})
end


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Hooks = require(uiRoot.Hooks)

export type Props = {
	SetIsDragging: (boolean) -> (),

	InitialPosition: UDim2,
	AnchorPoint: Vector2,
	Size: UDim2,

	Scale: number,

	Disabled: boolean,
	children: React.ReactNode,
}

local function convertToAbsSize(udim2: UDim2, containerSize: Vector2)
	return Vector2.new(
		udim2.X.Scale * containerSize.X + udim2.X.Offset,
		udim2.Y.Scale * containerSize.Y + udim2.Y.Offset
	)
end

local function FreeDrag(props: Props & { ContainerSize: Vector2 })
	local position, setPosition = React.useState(props.InitialPosition)

	local containerSize = props.ContainerSize
	local absSize = convertToAbsSize(props.Size, containerSize) * props.Scale
	local absAnchorPosition = convertToAbsSize(position, containerSize) * props.Scale
	local absAnchorSize = props.AnchorPoint * absSize
	local absPosition = absAnchorPosition - absAnchorSize

	local clampedPosition = UDim2.fromOffset(
		math.clamp(absPosition.X, 0, math.max(0, containerSize.X - absSize.X)),
		math.clamp(absPosition.Y, 0, math.max(0, containerSize.Y - absSize.Y))
	)

	local dragStart = React.useRef(nil :: { input: Vector3, position: UDim2 }?)
	local dragger = Hooks.useMouseDrag({
		onDragCallback = function(_rbx, input)
			if dragStart.current then
				local delta3 = (input.Position - dragStart.current.input) / props.Scale
				setPosition(dragStart.current.position + UDim2.fromOffset(delta3.X, delta3.Y))
			end
		end,
		onBeganCallback = function(_rbx, input)
			dragStart.current = {
				input = input.Position,
				position = UDim2.fromOffset(
					(clampedPosition.X.Offset + absAnchorSize.X) / props.Scale,
					(clampedPosition.Y.Offset + absAnchorSize.Y) / props.Scale
				),
			}
		end,
	})

	local isDragging = dragger.isActive()
	React.useEffect(function()
		if props.Disabled then
			dragger.cancel()
			props.SetIsDragging(false)
		else
			props.SetIsDragging(isDragging)
		end
	end, { isDragging })

	return React.createElement("ImageButton", {
		Size = props.Size,
		Position = clampedPosition,
		BackgroundTransparency = 1,

		Image = "",
		AutoButtonColor = false,

		[React.Event.InputBegan] = dragger.onInputBegan,
		[React.Event.InputChanged] = dragger.onInputChanged,
		[React.Event.InputEnded] = dragger.onInputEnded,
	}, {
		UIScale = props.Scale ~= 1 and React.createElement("UIScale", {
			Scale = props.Scale,
		}),
	}, props.children)
end

return function(props: Props)
	local containerRef = React.useRef(nil :: GuiObject?)
	local containerSize = Hooks.useRefProperty(containerRef, "AbsoluteSize", Vector2.zero)

	return React.createElement("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ref = containerRef,
	}, {
		FreeDrag = React.createElement(FreeDrag, {
			SetIsDragging = props.SetIsDragging,
			ContainerSize = containerSize,

			InitialPosition = props.InitialPosition,
			AnchorPoint = props.AnchorPoint,
			Size = props.Size,

			Scale = props.Scale,

			Disabled = props.Disabled,
		}, props.children),
	})
end