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

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, isHit = 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
