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