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