From 58016e86b254a1db7f5bc660558e78a63ecb6df1 Mon Sep 17 00:00:00 2001 From: Io Date: Mon, 28 Sep 2026 21:54:43 +0000 Subject: [PATCH] AI: Add 2D translation gizmo for axis-constrained node moves --- .../ZeroSpace.MapEditor/Views/MapDrawable.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/ZeroSpace/ZeroSpace.MapEditor/Views/MapDrawable.cs b/ZeroSpace/ZeroSpace.MapEditor/Views/MapDrawable.cs index d37c07a..32dde07 100644 --- a/ZeroSpace/ZeroSpace.MapEditor/Views/MapDrawable.cs +++ b/ZeroSpace/ZeroSpace.MapEditor/Views/MapDrawable.cs @@ -4,6 +4,15 @@ using ZeroSpace.MapEditor.Services; namespace ZeroSpace.MapEditor.Views; +/// Which part of the translation gizmo was hit or is active. +public enum GizmoPart +{ + None, + Center, + XAxis, + YAxis, +} + /// /// Draws the star map: connection lines between connected systems and /// one circle per system. Names are intentionally not drawn on the @@ -13,6 +22,9 @@ public sealed class MapDrawable : IDrawable { public const double NodeRadius = 14; + /// Screen-space length of each gizmo axis arm, in pixels. + public const double GizmoArmLength = 64; + public IList Nodes { get; set; } = Array.Empty(); public Func FindNode { get; set; } = _ => null; @@ -32,6 +44,12 @@ public sealed class MapDrawable : IDrawable /// Whether the galaxy map background is shown. public bool ShowBackground { get; set; } = true; + /// Node the translation gizmo is drawn for (the active selection). + public MapNode? GizmoNode { get; set; } + + /// Gizmo part currently hovered or being dragged, for highlight. + public GizmoPart ActiveGizmoPart { get; set; } = GizmoPart.None; + public void Draw(ICanvas canvas, RectF dirtyRect) { canvas.FillColor = Color.FromArgb("#1e2430"); @@ -92,6 +110,82 @@ public sealed class MapDrawable : IDrawable canvas.StrokeSize = 1.5f; canvas.DrawRectangle(box); } + + if (GizmoNode is not null) + { + DrawGizmo(canvas, GizmoNode); + } + } + + /// + /// Draws the 2D translation gizmo at the node's screen position: + /// a red +X arm, a green +Y arm, and a center square for free XY moves. + /// Screen-space, so it stays the same size at any zoom level. + /// + private void DrawGizmo(ICanvas canvas, MapNode node) + { + var cx = (float)(node.X * Scale + OffsetX); + var cy = (float)(node.Y * Scale + OffsetY); + const float arm = (float)GizmoArmLength; + + DrawGizmoArm(canvas, cx, cy, cx + arm, cy, isX: true, + active: ActiveGizmoPart == GizmoPart.XAxis); + DrawGizmoArm(canvas, cx, cy, cx, cy + arm, isX: false, + active: ActiveGizmoPart == GizmoPart.YAxis); + + // Center handle: free XY move. + const float size = 12f; + var centerColor = ActiveGizmoPart == GizmoPart.Center + ? Color.FromArgb("#fde047") + : Color.FromArgb("#eab308"); + canvas.FillColor = Color.FromArgb("#111827"); + canvas.FillRectangle(cx - size / 2 - 2, cy - size / 2 - 2, size + 4, size + 4); + canvas.FillColor = centerColor; + canvas.FillRectangle(cx - size / 2, cy - size / 2, size, size); + } + + private static void DrawGizmoArm(ICanvas canvas, float x1, float y1, float x2, float y2, + bool isX, bool active) + { + var color = isX + ? (active ? Color.FromArgb("#f87171") : Color.FromArgb("#ef4444")) + : (active ? Color.FromArgb("#4ade80") : Color.FromArgb("#22c55e")); + + // Dark underlay so the arm reads against the bright galaxy map. + canvas.StrokeColor = Color.FromArgb("#111827"); + canvas.StrokeSize = 5; + canvas.DrawLine(x1, y1, x2, y2); + canvas.StrokeColor = color; + canvas.StrokeSize = 3; + canvas.DrawLine(x1, y1, x2, y2); + + // Arrowhead pointing along the axis. + const float headLen = 12f; + const float headHalf = 6f; + PointF tip, a, b; + if (isX) + { + tip = new PointF(x2, y2); + a = new PointF(x2 - headLen, y2 - headHalf); + b = new PointF(x2 - headLen, y2 + headHalf); + } + else + { + tip = new PointF(x2, y2); + a = new PointF(x2 - headHalf, y2 - headLen); + b = new PointF(x2 + headHalf, y2 - headLen); + } + + var head = new PathF(); + head.MoveTo(tip); + head.LineTo(a); + head.LineTo(b); + head.Close(); + canvas.FillColor = color; + canvas.FillPath(head); + canvas.StrokeColor = Color.FromArgb("#111827"); + canvas.StrokeSize = 1.5f; + canvas.DrawPath(head); } ///