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; } }