feat(ui): F8-12 — owner feedback round 2 (#55)
* Bug fix: theme flip no longer leaves planets oversized. The camera-preserving remount now calls a new `RendererHandle.refreshCameraDerivedDraws` explicitly after the manual moveCenter/setZoom pair so the post-mount geometry tracks `viewport.scaled` even if pixi-viewport's `'zoomed'` listener races the next Ticker tick. * Doc #3: clicks on a planet label route through the same hit-test path as a click on the disc. The label `Container` now has a pointer hit area sized to the text + frame padding; pointertap simulates a click at the planet centre, so selection and pick-mode resolution behave identically. * Doc #4: battle X-crosses + cargo arrowhead wings grow sub-linearly with zoom (PLANET_SIZE_ZOOM_ALPHA). New `Style.softLengthAnchor` ('center' / 'start') makes the renderer treat the recorded endpoints as the geometry "at the reference scale" and rescale around the midpoint (X-cross) or the start endpoint (arrow wings). Arrowhead base length is halved from 6 to 3 world units to match the owner's "in half" request. * Doc #5: picker overlay loses the anchor ring at the source, the cursor line drops to a cargo-route-thin 0.6 px stroke, and the hover ring around the destination is replaced by a planet-style outline (visible disc + 1 px padding) in the `pickHighlight` accent — so candidate destinations read like selection in warm yellow. * Doc #6: regression test pins the in-disc hit zone. * Perf #1: camera-driven redraws are throttled onto the next Ticker tick. A rapid wheel / pinch burst now coalesces into at most one `clear() + redraw` pass per painted frame, which keeps the 500-planet map responsive on zoom and toggle flips. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -137,17 +137,31 @@ describe("buildCargoRouteLines", () => {
|
||||
);
|
||||
const lines = buildCargoRouteLines(report);
|
||||
expect(lines.length).toBe(12);
|
||||
const styleByPriority = new Map<number, typeof lines[number]["style"]>();
|
||||
// F8-12 / #4 follow-up: shafts and wings now use different
|
||||
// Style objects so the arrowhead wings can carry
|
||||
// `softLengthAnchor: "start"`. Colour / priority remain shared
|
||||
// across both, which is what the de-dupe loop here verifies.
|
||||
const colourByPriority = new Map<number, number | undefined>();
|
||||
const softLengthByLineId = new Map<number, string | undefined>();
|
||||
for (const line of lines) {
|
||||
const existing = styleByPriority.get(line.priority);
|
||||
if (existing === undefined) styleByPriority.set(line.priority, line.style);
|
||||
else expect(existing).toBe(line.style);
|
||||
const existing = colourByPriority.get(line.priority);
|
||||
if (existing === undefined) {
|
||||
colourByPriority.set(line.priority, line.style.strokeColor);
|
||||
} else {
|
||||
expect(line.style.strokeColor).toBe(existing);
|
||||
}
|
||||
softLengthByLineId.set(line.id & 0xf, line.style.softLengthAnchor);
|
||||
}
|
||||
// Shaft (offset 0) stays linear; wings (offsets 1/2) get the
|
||||
// new softening anchor so the arrowhead grows sub-linearly.
|
||||
expect(softLengthByLineId.get(0)).toBeUndefined();
|
||||
expect(softLengthByLineId.get(1)).toBe("start");
|
||||
expect(softLengthByLineId.get(2)).toBe("start");
|
||||
// Default (dark) palette colours, one per load type.
|
||||
expect(styleByPriority.get(8)?.strokeColor).toBe(DARK_THEME.routeCol);
|
||||
expect(styleByPriority.get(7)?.strokeColor).toBe(DARK_THEME.routeCap);
|
||||
expect(styleByPriority.get(6)?.strokeColor).toBe(DARK_THEME.routeMat);
|
||||
expect(styleByPriority.get(5)?.strokeColor).toBe(DARK_THEME.routeEmp);
|
||||
expect(colourByPriority.get(8)).toBe(DARK_THEME.routeCol);
|
||||
expect(colourByPriority.get(7)).toBe(DARK_THEME.routeCap);
|
||||
expect(colourByPriority.get(6)).toBe(DARK_THEME.routeMat);
|
||||
expect(colourByPriority.get(5)).toBe(DARK_THEME.routeEmp);
|
||||
});
|
||||
|
||||
test("uses the supplied palette's stroke colours", () => {
|
||||
|
||||
@@ -280,6 +280,21 @@ describe("hitTest — empty results and scale", () => {
|
||||
expect(ids(w, "torus", cam05, cursorOver(516, 500, cam05))).toBe(null);
|
||||
});
|
||||
|
||||
test("F8-12 / #6 — clicks inside the disc hit, not just on its edge", () => {
|
||||
// At scale=1 with pointRadiusBasePx=10 and scaleRef=1, the
|
||||
// visible world radius is 10. Any cursor inside that disc must
|
||||
// resolve to the planet — the bug owner spotted in the picker
|
||||
// was the click being ignored once the cursor moved off the
|
||||
// circumference toward the centre.
|
||||
const camAtRef = camAt(500, 500, 1);
|
||||
const w = new World(1000, 1000, [
|
||||
point(1, 500, 500, { style: { pointRadiusBasePx: 10 } }),
|
||||
]);
|
||||
for (const dx of [0, 2, 5, 8, 9.5]) {
|
||||
expect(ids(w, "torus", camAtRef, cursorOver(500 + dx, 500, camAtRef))).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("pointRadiusBasePx scales softly with zoom (F8-12 / #31)", () => {
|
||||
// world 1000×1000, viewport 200×200 → scaleRef = 0.2. At
|
||||
// scale=0.5 the on-screen pixel size is
|
||||
|
||||
@@ -8,7 +8,7 @@ import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
ANCHOR_PADDING_WORLD,
|
||||
HOVER_PADDING_WORLD,
|
||||
HOVER_PADDING_PX,
|
||||
computePickOverlay,
|
||||
type PickModeOptions,
|
||||
} from "../src/map/pick-mode";
|
||||
@@ -206,7 +206,7 @@ describe("computePickOverlay", () => {
|
||||
expect(spec.hoverOutline).toEqual({
|
||||
x: 200,
|
||||
y: 100,
|
||||
radius: 5 + HOVER_PADDING_WORLD,
|
||||
radius: 5 + HOVER_PADDING_PX,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ describe("computePickOverlay", () => {
|
||||
expect(spec.hoverOutline).toBeNull();
|
||||
});
|
||||
|
||||
test("hoverOutline reflects the reachable target with HOVER_PADDING_WORLD", () => {
|
||||
test("hoverOutline reflects the reachable target with HOVER_PADDING_PX", () => {
|
||||
const spec = computePickOverlay(
|
||||
makeOptions(),
|
||||
{ x: 1, y: 1 },
|
||||
@@ -254,7 +254,7 @@ describe("computePickOverlay", () => {
|
||||
expect(spec.hoverOutline).toEqual({
|
||||
x: 200,
|
||||
y: 100,
|
||||
radius: 5 + HOVER_PADDING_WORLD,
|
||||
radius: 5 + HOVER_PADDING_PX,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,7 +267,7 @@ describe("computePickOverlay", () => {
|
||||
allIds,
|
||||
);
|
||||
expect(spec.hoverOutline?.radius).toBe(
|
||||
DEFAULT_POINT_RADIUS_PX + HOVER_PADDING_WORLD,
|
||||
DEFAULT_POINT_RADIUS_PX + HOVER_PADDING_PX,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user