local function t(t,Z,u,B)debug.profilebegin("\113\117\101\114\121\080\111\105\110\116")local a=#blacklist u=u+nearPlaneZ local x=t+Z*u local A=inf local b=inf local g=t repeat local a,Q=workspace:FindPartOnRayWithIgnoreList(ray(g,x-g),blacklist,false,true)if a then if canOcclude(a)then local Z={a}local g=workspace:FindPartOnRayWithWhitelist(ray(x,Q-x),Z,true)local R=(Q-t).Magnitude if g then local t=false if B then t=workspace:FindPartOnRayWithWhitelist(ray(B,x-B),Z,true)or workspace:FindPartOnRayWithWhitelist(ray(x,B-x),Z,true)end if t then b=R elseif u<A then A=R end else b=R end end blacklist[#blacklist+1]=a g=Q-Z*.001 end until b<inf or not a eraseFromEnd(blacklist,a)debug.profileend()return A-nearPlaneZ,b-nearPlaneZ end local function Z(Z,u)debug.profilebegin("\113\117\101\114\121\086\105\101\119\112\111\114\116")local B=Z.p local a=Z.rightVector local x=Z.upVector local A=-Z.lookVector local b=camera.ViewportSize local g=inf local Q=inf for Z=0,1,1 do local R=a*((Z-.5)*projX)for a=0,1,1 do local c=x*((a-.5)*projY)local m=B+nearPlaneZ*(R+c)local I=(camera:ViewportPointToRay(b.x*Z,b.y*a)).Origin local s,z=t(m,A,u,I)if z<g then g=z end if s<Q then Q=s end end end debug.profileend()return Q,g end local function u(Z,u,B)debug.profilebegin("\116\101\115\116\080\114\111\109\111\116\105\111\110")local a=Z.p local x=Z.rightVector local A=Z.upVector local b=-Z.lookVector do debug.profilebegin("\101\120\116\114\097\112\111\108\097\116\101")local Z=.0625 local x=1.25 local A=(getCollisionPoint(a,B.posVelocity*x)-a).Magnitude local b=B.posVelocity.magnitude for Z=0,min(x,B.rotVelocity.magnitude+A/b),Z do local a=B.extrapolate(Z)if t(a.p,-a.lookVector,u)>=u then return false end end debug.profileend()end do debug.profilebegin("\116\101\115\116\079\102\102\115\101\116\115")for Z,B in ipairs(SCAN_SAMPLE_OFFSETS)do local g=B local Q=getCollisionPoint(a,x*g.x+A*g.y)if t(Q,((a+b*u)-Q).Unit,u)==inf then return false end end debug.profileend()end debug.profileend()return true end local function B(t,B,a)debug.profilebegin("\112\111\112\112\101\114")subjectRoot=subjectPart and subjectPart:GetRootPart()or subjectPart local x=B local A,b=Z(t,B)if b<x then x=b end if A<x and u(t,B,a)then x=A end subjectRoot=nil debug.profileend()return x end return B
--------------------------------------------------------------------------------

local function queryPoint(origin, unitDir, dist, lastPos)
	debug.profilebegin("queryPoint")

	local originalSize = #blacklist

	dist = dist + nearPlaneZ
	local target = origin + unitDir * dist

	local softLimit = inf
	local hardLimit = inf
	local movingOrigin = origin

	repeat
		local entryPart, entryPos =
			workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)

		if entryPart then
			if canOcclude(entryPart) then
				local wl = { entryPart }
				local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)

				local lim = (entryPos - origin).Magnitude

				if exitPart then
					local promote = false
					if lastPos then
						promote = workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true)
							or workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
					end

					if promote then
						-- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
						hardLimit = lim
					elseif dist < softLimit then
						-- Trivial soft limit
						softLimit = lim
					end
				else
					-- Trivial hard limit
					hardLimit = lim
				end
			end

			blacklist[#blacklist + 1] = entryPart
			movingOrigin = entryPos - unitDir * 1e-3
		end
	until hardLimit < inf or not entryPart

	eraseFromEnd(blacklist, originalSize)

	debug.profileend()
	return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
end

local function queryViewport(focus, dist)
	debug.profilebegin("queryViewport")

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	local viewport = camera.ViewportSize

	local hardBoxLimit = inf
	local softBoxLimit = inf

	-- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
	for viewX = 0, 1 do
		local worldX = fX * ((viewX - 0.5) * projX)

		for viewY = 0, 1 do
			local worldY = fY * ((viewY - 0.5) * projY)

			local origin = fP + nearPlaneZ * (worldX + worldY)
			local lastPos = camera:ViewportPointToRay(viewport.x * viewX, viewport.y * viewY).Origin

			local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)

			if hardPointLimit < hardBoxLimit then
				hardBoxLimit = hardPointLimit
			end
			if softPointLimit < softBoxLimit then
				softBoxLimit = softPointLimit
			end
		end
	end
	debug.profileend()

	return softBoxLimit, hardBoxLimit
end

local function testPromotion(focus, dist, focusExtrapolation)
	debug.profilebegin("testPromotion")

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	do
		-- Dead reckoning the camera rotation and focus
		debug.profilebegin("extrapolate")

		local SAMPLE_DT = 0.0625
		local SAMPLE_MAX_T = 1.25

		local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity * SAMPLE_MAX_T) - fP).Magnitude
		-- Metric that decides how many samples to take
		local combinedSpeed = focusExtrapolation.posVelocity.magnitude

		for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist / combinedSpeed), SAMPLE_DT do
			local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt

			if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
				return false
			end
		end

		debug.profileend()
	end

	do
		-- Test screen-space offsets from the focus for the presence of soft limits
		debug.profilebegin("testOffsets")

		for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
			local scaledOffset = offset
			local pos = getCollisionPoint(fP, fX * scaledOffset.x + fY * scaledOffset.y)
			if queryPoint(pos, (fP + fZ * dist - pos).Unit, dist) == inf then
				return false
			end
		end

		debug.profileend()
	end

	debug.profileend()
	return true
end

local function Popper(focus, targetDist, focusExtrapolation)
	debug.profilebegin("popper")

	subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart

	local dist = targetDist
	local soft, hard = queryViewport(focus, targetDist)
	if hard < dist then
		dist = hard
	end
	if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
		dist = soft
	end

	subjectRoot = nil

	debug.profileend()
	return dist
end

return Popper
