feat(selection): port retail polygon picking and vivid marker

Replace the projected Setup-sphere rectangle and independent physics-wall ray with retail's render-coupled picker: only visible server-object parts participate, each exact drawing sphere broad-phases the camera-eye ray, and first-in-DAT-order visual polygon hits globally outrank sphere fallbacks.

Replace the devtools-only procedural triangles with the retained gameplay VividTargetIndicator using retail client-enum surfaces 1..4, radar-blip colorization, Setup selection-sphere framing, and the exact eight-pixel viewport clamp.

Release build succeeds with zero warnings and all 5,886 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 21:32:51 +02:00
parent 0f82a08f0a
commit 146a963aeb
26 changed files with 1302 additions and 1340 deletions

View file

@ -0,0 +1,113 @@
using System.Numerics;
using AcDream.Core.Selection;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Selection;
/// <summary>
/// Render-thread owner of the last complete set of visible selectable parts.
/// The renderer builds one frame while input queries the previously completed
/// frame, avoiding partial visibility state during multi-slice portal drawing.
/// </summary>
internal sealed class RetailSelectionScene : IRetailSelectionRenderSink
{
private readonly RetailSelectionGeometryCache _geometry;
private List<RetailSelectionPart> _building = new();
private List<RetailSelectionPart> _published = new();
private readonly HashSet<PartKey> _buildingKeys = new();
private HashSet<uint> _buildingGuids = new();
private HashSet<uint> _publishedGuids = new();
private FrustumPlanes? _viewFrustum;
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
public RetailSelectionScene(RetailSelectionGeometryCache geometry)
=> _geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
public void BeginFrame()
{
_building.Clear();
_buildingKeys.Clear();
_buildingGuids.Clear();
_viewFrustum = null;
}
/// <summary>
/// Supplies retail DrawMesh's current view-cone gate. Animated entities
/// deliberately bypass acdream's coarse entity-AABB CPU cull, so this
/// per-part drawing sphere check is the load-bearing retail equivalent.
/// </summary>
public void SetViewFrustum(FrustumPlanes viewFrustum)
=> _viewFrustum = viewFrustum;
public void AddVisiblePart(
WorldEntity entity,
int partIndex,
uint gfxObjId,
Matrix4x4 partWorld)
{
if (entity.ServerGuid == 0u)
return;
if (!_buildingKeys.Add(new PartKey(entity.Id, partIndex, gfxObjId)))
return;
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
if (mesh is null)
return;
if (_viewFrustum is not { } frustum
|| !DrawingSphereIntersectsFrustum(mesh, partWorld, frustum))
return;
_building.Add(new RetailSelectionPart(
entity.ServerGuid,
partIndex,
partWorld,
mesh));
_buildingGuids.Add(entity.ServerGuid);
}
public void CompleteFrame()
{
(_published, _building) = (_building, _published);
(_publishedGuids, _buildingGuids) = (_buildingGuids, _publishedGuids);
}
public uint? Pick(
float mouseX,
float mouseY,
Vector2 viewport,
Matrix4x4 view,
Matrix4x4 projection,
uint skipServerGuid)
{
if (viewport.X <= 0f || viewport.Y <= 0f)
return null;
var ray = WorldPicker.BuildRay(
mouseX, mouseY, viewport.X, viewport.Y, view, projection);
return RetailWorldPicker.Pick(
ray.Origin, ray.Direction, _published, skipServerGuid)?.ServerGuid;
}
public bool WasVisible(uint serverGuid) => _publishedGuids.Contains(serverGuid);
internal static bool DrawingSphereIntersectsFrustum(
RetailSelectionMesh mesh,
Matrix4x4 localToWorld,
FrustumPlanes frustum)
{
Vector3 center = Vector3.Transform(mesh.SphereCenter, localToWorld);
float scaleX = new Vector3(localToWorld.M11, localToWorld.M12, localToWorld.M13).Length();
float scaleY = new Vector3(localToWorld.M21, localToWorld.M22, localToWorld.M23).Length();
float scaleZ = new Vector3(localToWorld.M31, localToWorld.M32, localToWorld.M33).Length();
float radius = mesh.SphereRadius * MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
return TestPlane(frustum.Left, center, radius)
&& TestPlane(frustum.Right, center, radius)
&& TestPlane(frustum.Bottom, center, radius)
&& TestPlane(frustum.Top, center, radius)
&& TestPlane(frustum.Near, center, radius)
&& TestPlane(frustum.Far, center, radius);
}
private static bool TestPlane(Vector4 plane, Vector3 center, float radius)
=> plane.X * center.X + plane.Y * center.Y + plane.Z * center.Z + plane.W >= -radius;
}