using System.Numerics;
using AcDream.Core.Selection;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Selection;
///
/// 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.
///
internal sealed class RetailSelectionScene : IRetailSelectionRenderSink, IRetailSelectionLightingSource
{
private readonly RetailSelectionGeometryCache _geometry;
private readonly RetailSelectionLightingPulse _lightingPulse;
private List _building = new();
private List _published = new();
private readonly HashSet _buildingKeys = new();
private FrustumPlanes? _viewFrustum;
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
public RetailSelectionScene(
RetailSelectionGeometryCache geometry,
RetailSelectionLightingPulse? lightingPulse = null)
{
_geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
_lightingPulse = lightingPulse ?? new RetailSelectionLightingPulse();
}
///
/// Starts retail's SmartBox click pulse independently of persistent target
/// selection. Examine, use, and targeted-use clicks receive the same pulse.
///
public void BeginLightingPulse(uint serverGuid)
=> _lightingPulse.Start(serverGuid);
public void TickLighting()
=> _lightingPulse.Tick();
public bool TryGetLighting(uint serverGuid, out RetailSelectionLighting lighting)
=> _lightingPulse.TryGet(serverGuid, out lighting);
public void BeginFrame()
{
_building.Clear();
_buildingKeys.Clear();
_viewFrustum = null;
}
///
/// 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.
///
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));
}
public void CompleteFrame()
{
(_published, _building) = (_building, _published);
}
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;
}
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;
}