using System.Numerics; using AcDream.Core.Selection; using AcDream.Plugin.Abstractions; namespace AcDream.App.UI.Layout; /// /// Retained-view projection of VTank's ShowCollisionDebug shapes. /// The collision query remains in the canonical physics world; this owner /// only projects its detached per-quantum samples into the already-open UI /// phase, avoiding a nested Vulkan backbuffer pass. /// internal sealed class ProjectileDebugOverlayController { private static readonly Vector4 ClearColor = new(0f, 1f, 0f, 0.95f); private static readonly Vector4 BlockedColor = new(1f, 0f, 0f, 0.95f); private readonly UiPanel _root; private readonly Func> _samples; private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> _camera; private readonly List _markers = []; private ProjectileDebugOverlayController( UiPanel root, Func> samples, Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> camera) { _root = root; _samples = samples; _camera = camera; } internal static ProjectileDebugOverlayController Mount( UiRoot host, Func> samples, Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> camera) { ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(samples); ArgumentNullException.ThrowIfNull(camera); var root = new UiPanel { Name = "PluginProjectileDebugOverlay", BackgroundColor = Vector4.Zero, BorderColor = Vector4.Zero, ClickThrough = true, Visible = false, ZOrder = -9_999, Anchors = AnchorEdges.None, }; host.AddChild(root); return new ProjectileDebugOverlayController(root, samples, camera); } internal void Tick() { IReadOnlyList samples = _samples(); var camera = _camera(); if (samples.Count == 0 || camera.Viewport.X <= 0f || camera.Viewport.Y <= 0f) { HideAll(); return; } EnsureMarkerCount(samples.Count); _root.Left = 0f; _root.Top = 0f; _root.Width = camera.Viewport.X; _root.Height = camera.Viewport.Y; int visible = 0; for (int index = 0; index < samples.Count; index++) { PluginProjectileDebugSample sample = samples[index]; if (!ScreenProjection.TryProjectSphereToScreenRect( sample.WorldPosition, sample.Radius, camera.View, camera.Projection, camera.Viewport, out Vector2 minimum, out Vector2 maximum, out _, minSidePixels: 4f) || maximum.X < 0f || maximum.Y < 0f || minimum.X > camera.Viewport.X || minimum.Y > camera.Viewport.Y) { continue; } UiPanel marker = _markers[visible++]; marker.Left = MathF.Max(0f, minimum.X); marker.Top = MathF.Max(0f, minimum.Y); marker.Width = MathF.Max( 1f, MathF.Min(camera.Viewport.X, maximum.X) - marker.Left); marker.Height = MathF.Max( 1f, MathF.Min(camera.Viewport.Y, maximum.Y) - marker.Top); marker.BorderColor = sample.IsClear ? ClearColor : BlockedColor; marker.Visible = true; } for (int index = visible; index < _markers.Count; index++) _markers[index].Visible = false; _root.Visible = visible > 0; } private void EnsureMarkerCount(int count) { while (_markers.Count < count) { var marker = new UiPanel { Name = $"PluginProjectileDebugMarker{_markers.Count}", BackgroundColor = Vector4.Zero, BorderColor = ClearColor, BorderThickness = 1.5f, ClickThrough = true, Visible = false, Anchors = AnchorEdges.None, }; _markers.Add(marker); _root.AddChild(marker); } } private void HideAll() { _root.Visible = false; for (int index = 0; index < _markers.Count; index++) _markers[index].Visible = false; } }