using System.Numerics;
namespace AcDream.Core.Selection;
///
/// Shared screen-space projection math for the target indicator and the
/// world picker. Both call into
/// so the click hit-area is guaranteed to match the visible indicator
/// rect — "what you see is what you click".
///
///
/// Retail equivalent: SmartBox::GetObjectBoundingBox at
/// 0x00452e20, which uses
/// Render::GetViewerBBox(selection_sphere, &corner1, &corner2)
/// to compute a camera-aligned bbox of the sphere and projects the two
/// corner points. We use the mathematical equivalent (project center,
/// compute screen radius analytically) — both produce identical pixel
/// rects for a standard right-handed perspective.
///
///
public static class ScreenProjection
{
///
/// Project a world-space sphere to a screen-space axis-aligned square
/// rectangle.
///
/// Sphere center in world space.
/// Sphere radius in world space.
/// View matrix (System.Numerics row-vector convention).
/// Projection matrix. M22 = cot(fovY/2)
/// for a standard right-handed perspective.
/// Viewport size in pixels (X = width, Y = height).
/// Out: top-left corner of the rect in viewport pixels.
/// Out: bottom-right corner of the rect in viewport pixels.
/// Out: camera-space depth (clip.W) of the sphere
/// center — use this for nearest-first sorting when multiple rects overlap.
/// Minimum side length of the rect. Distant
/// entities clamp to this so they remain pickable / visible. 12 px
/// matches the indicator's clamp floor.
///
/// true if the sphere is in front of the camera and the rect was
/// produced; false if the center is behind the camera
/// (clip.W <= 0) or the rect is more than a screen offset
/// from the viewport (obviously off-screen).
///
public static bool TryProjectSphereToScreenRect(
Vector3 worldCenter, float worldRadius,
Matrix4x4 view, Matrix4x4 projection, Vector2 viewport,
out Vector2 rectMin, out Vector2 rectMax, out float depth,
float minSidePixels = 12f)
{
rectMin = default;
rectMax = default;
depth = 0f;
var viewProj = view * projection;
var clip = Vector4.Transform(new Vector4(worldCenter, 1f), viewProj);
if (clip.W <= 0.001f) return false;
depth = clip.W;
float ndcX = clip.X / clip.W;
float ndcY = clip.Y / clip.W;
float screenX = (ndcX * 0.5f + 0.5f) * viewport.X;
float screenY = (1f - (ndcY * 0.5f + 0.5f)) * viewport.Y;
// Screen-space radius. projection.M22 = cot(fovY/2). clip.W is
// the camera-space distance.
float scaleY = projection.M22;
if (scaleY <= 0f) return false;
float screenRadius = worldRadius * scaleY * viewport.Y / (2f * clip.W);
// Cull obviously-off-screen entities (more than a screen away).
if (screenX + screenRadius < -viewport.X || screenX - screenRadius > 2f * viewport.X) return false;
if (screenY + screenRadius < -viewport.Y || screenY - screenRadius > 2f * viewport.Y) return false;
// Floor at minSidePixels so distant entities still get a visible /
// clickable rect. The picker must apply the same floor as the
// indicator or distant clicks won't match the visible bracket.
if (screenRadius < minSidePixels * 0.5f) screenRadius = minSidePixels * 0.5f;
rectMin = new Vector2(screenX - screenRadius, screenY - screenRadius);
rectMax = new Vector2(screenX + screenRadius, screenY + screenRadius);
return true;
}
}