fix(ui): F8-07 cargo-route picker — torus-wrap overlay + thinner arrows
Tests · UI / test (push) Successful in 3m4s
Tests · UI / test (push) Successful in 3m4s
Pick overlay (anchor ring, cursor line, hover outline) was drawn into a single Pixi container — copies[ORIGIN_COPY_INDEX] — so any view of a wrap copy lost it: picker from A1/A2 to the right (across the seam) showed no hover highlight on A3's wrap copy, and the picker on A3 (x≈1.44, near the left edge) put its anchor far left of the viewport. Fix replicates the overlay across all nine torus copies (matching how primitives and fog already render) and switches the cursor-line endpoint to torus-shortest geometry via torusShortestDelta. Anchor and hover-outline coordinates stay canonical; the per-copy replication renders them under the user's view in whatever tile is on screen. Also reduces cargo-route arrow strokes: COL/CAP/MAT 2->0.6 wu and EMP 1->0.4 wu (~3 / ~2 screen px at typical zoom) per the owner's request. Tests cover the new torus path: source near the left edge with cursor on the wrap copy across the seam (x axis), source near the top edge with cursor across the y seam, and a guard that anchor / hover-outline coords stay canonical regardless of the world argument. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -596,10 +596,16 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
|
||||
// Pick-mode state. Owned by the renderer so all callers funnel
|
||||
// through `setPickMode`; tests for the pure overlay math live in
|
||||
// `pick-mode.ts`.
|
||||
// `pick-mode.ts`. The overlay graphic is replicated across all nine
|
||||
// torus copies — identical world-coord ops drawn into each copy's
|
||||
// container, which already carries the wrap offset — so the
|
||||
// anchor / cursor line / hover outline always render in whatever
|
||||
// copy the user is panned over. In no-wrap mode the eight wrap
|
||||
// copies are `visible = false`, so their overlay children render
|
||||
// nothing for free; no special-case path is needed.
|
||||
let pickModeActive = false;
|
||||
let pickOptions: PickModeOptions | null = null;
|
||||
let pickOverlay: Graphics | null = null;
|
||||
let pickOverlays: Graphics[] = [];
|
||||
const dimmedAlphaBackup = new Map<Graphics, number>();
|
||||
const dimmedTintBackup = new Map<Graphics, number>();
|
||||
const detachPickListeners: Array<() => void> = [];
|
||||
@@ -631,7 +637,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
};
|
||||
viewport.on("clicked", handleViewportClicked);
|
||||
const redrawPickOverlay = (): void => {
|
||||
if (pickOverlay === null || pickOptions === null) return;
|
||||
if (pickOverlays.length === 0 || pickOptions === null) return;
|
||||
const cursorWorld =
|
||||
lastCursorPx === null
|
||||
? null
|
||||
@@ -640,41 +646,48 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
handle.getCamera(),
|
||||
handle.getViewport(),
|
||||
);
|
||||
// Pass the world dims only in torus mode: the function
|
||||
// switches to torus-shortest line geometry when world is
|
||||
// non-null, so the line endpoint goes the wrap-short way.
|
||||
const spec = computePickOverlay(
|
||||
pickOptions,
|
||||
cursorWorld,
|
||||
lastHoveredId,
|
||||
pointPrimitivesById,
|
||||
allPrimitiveIds,
|
||||
mode === "torus"
|
||||
? { width: opts.world.width, height: opts.world.height }
|
||||
: null,
|
||||
);
|
||||
const g = pickOverlay;
|
||||
g.clear();
|
||||
g.circle(spec.anchor.x, spec.anchor.y, spec.anchor.radius);
|
||||
g.stroke({
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.anchor.alpha,
|
||||
width: PICK_OVERLAY_STYLE.anchor.width,
|
||||
});
|
||||
if (spec.line !== null) {
|
||||
g.moveTo(spec.line.x1, spec.line.y1);
|
||||
g.lineTo(spec.line.x2, spec.line.y2);
|
||||
for (const g of pickOverlays) {
|
||||
g.clear();
|
||||
g.circle(spec.anchor.x, spec.anchor.y, spec.anchor.radius);
|
||||
g.stroke({
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.line.alpha,
|
||||
width: PICK_OVERLAY_STYLE.line.width,
|
||||
});
|
||||
}
|
||||
if (spec.hoverOutline !== null) {
|
||||
g.circle(
|
||||
spec.hoverOutline.x,
|
||||
spec.hoverOutline.y,
|
||||
spec.hoverOutline.radius,
|
||||
);
|
||||
g.stroke({
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.hover.alpha,
|
||||
width: PICK_OVERLAY_STYLE.hover.width,
|
||||
alpha: PICK_OVERLAY_STYLE.anchor.alpha,
|
||||
width: PICK_OVERLAY_STYLE.anchor.width,
|
||||
});
|
||||
if (spec.line !== null) {
|
||||
g.moveTo(spec.line.x1, spec.line.y1);
|
||||
g.lineTo(spec.line.x2, spec.line.y2);
|
||||
g.stroke({
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.line.alpha,
|
||||
width: PICK_OVERLAY_STYLE.line.width,
|
||||
});
|
||||
}
|
||||
if (spec.hoverOutline !== null) {
|
||||
g.circle(
|
||||
spec.hoverOutline.x,
|
||||
spec.hoverOutline.y,
|
||||
spec.hoverOutline.radius,
|
||||
);
|
||||
g.stroke({
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.hover.alpha,
|
||||
width: PICK_OVERLAY_STYLE.hover.width,
|
||||
});
|
||||
}
|
||||
}
|
||||
requestRender();
|
||||
};
|
||||
@@ -687,10 +700,8 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
dimmedAlphaBackup.clear();
|
||||
for (const [g, tint] of dimmedTintBackup) g.tint = tint;
|
||||
dimmedTintBackup.clear();
|
||||
if (pickOverlay !== null) {
|
||||
pickOverlay.destroy();
|
||||
pickOverlay = null;
|
||||
}
|
||||
for (const g of pickOverlays) g.destroy();
|
||||
pickOverlays = [];
|
||||
pickOptions = null;
|
||||
// Un-dimming primitives and removing the overlay are scene
|
||||
// changes that do not move the camera.
|
||||
@@ -717,12 +728,18 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
g.tint = theme.pickDimTint;
|
||||
}
|
||||
}
|
||||
// Overlay graphic. Lives in the origin copy so the central
|
||||
// tile owns it; the camera always wraps back into this tile
|
||||
// (`wrapTorusCamera`), so the user sees the overlay
|
||||
// regardless of how far they have panned.
|
||||
pickOverlay = new Graphics();
|
||||
copies[ORIGIN_COPY_INDEX]!.addChild(pickOverlay);
|
||||
// Overlay graphics — one per torus copy. Every copy receives an
|
||||
// identical set of draw ops in world coords, and each copy's
|
||||
// container already applies its `(dx*W, dy*H)` transform, so
|
||||
// the anchor / cursor line / hover outline render in whatever
|
||||
// copy the user is currently panned over. In no-wrap mode the
|
||||
// non-origin copies are `visible = false`, so their overlay
|
||||
// children are invisible without extra wiring.
|
||||
pickOverlays = copies.map((c) => {
|
||||
const g = new Graphics();
|
||||
c.addChild(g);
|
||||
return g;
|
||||
});
|
||||
redrawPickOverlay();
|
||||
// Pointer-move drives the cursor line; hover changes drive
|
||||
// the outline. Both go through the renderer's existing
|
||||
|
||||
Reference in New Issue
Block a user