ui/phase-9: PixiJS map renderer with torus and no-wrap modes
Stand up the vector map renderer in ui/frontend/src/map/ on top of PixiJS v8 + pixi-viewport@^6. Torus mode renders nine container copies for seamless wrap; no-wrap mode pins the camera at world bounds and centres on an axis when the viewport exceeds the world along that axis. Hit-test is a brute-force pass with deterministic [-priority, distSq, kindOrder, id] ordering and torus-shortest distance, validated by hand-built unit cases. The development playground at /__debug/map exposes a window debug surface for the Playwright spec, which forces WebGPU on chromium-desktop, WebGL on webkit-desktop, and accepts the auto-picked backend on mobile projects. Algorithm spec lives in ui/docs/renderer.md, which also pins the new deprecation status of galaxy/client (the entire Fyne client module, including client/world). client/world/README.md and the Phase 9 stub in ui/PLAN.md gain matching deprecation banners. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// Geometry primitives used by the map renderer.
|
||||
//
|
||||
// All distances are in world units (TS numbers, float64). Functions
|
||||
// in this file are pure and side-effect-free; tests exercise them
|
||||
// directly.
|
||||
|
||||
import type { Camera, Viewport } from "./world";
|
||||
|
||||
// clamp returns v constrained to [lo, hi]. If lo > hi the function
|
||||
// returns lo (callers are expected to keep the bounds well-formed).
|
||||
export function clamp(v: number, lo: number, hi: number): number {
|
||||
if (v < lo) return lo;
|
||||
if (v > hi) return hi;
|
||||
return v;
|
||||
}
|
||||
|
||||
// torusShortestDelta returns the signed delta from a to b on a circle
|
||||
// of circumference `size`, picking the direction with the smaller
|
||||
// absolute distance. Result lies in (-size/2, size/2].
|
||||
//
|
||||
// At exactly size/2 the function returns +size/2 (positive direction);
|
||||
// the lower bound is exclusive so a delta of -size/2 wraps to +size/2.
|
||||
// This deterministic tie-break keeps the function self-consistent
|
||||
// regardless of input order. The `+0` at the end normalises -0 (which
|
||||
// JavaScript produces for some modulo cases) to +0.
|
||||
export function torusShortestDelta(a: number, b: number, size: number): number {
|
||||
if (!(size > 0)) {
|
||||
throw new Error(`torusShortestDelta: size must be positive, got ${size}`);
|
||||
}
|
||||
let d = (b - a) % size;
|
||||
if (d > size / 2) d -= size;
|
||||
else if (d <= -size / 2) d += size;
|
||||
return d + 0;
|
||||
}
|
||||
|
||||
// distSqPointToSegment returns the squared distance from point (px,py)
|
||||
// to the segment (ax,ay)–(bx,by). For zero-length segments it falls
|
||||
// back to point-to-point distance.
|
||||
export function distSqPointToSegment(
|
||||
px: number,
|
||||
py: number,
|
||||
ax: number,
|
||||
ay: number,
|
||||
bx: number,
|
||||
by: number,
|
||||
): number {
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const lenSq = dx * dx + dy * dy;
|
||||
if (lenSq === 0) {
|
||||
const ex = px - ax;
|
||||
const ey = py - ay;
|
||||
return ex * ex + ey * ey;
|
||||
}
|
||||
let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
||||
if (t < 0) t = 0;
|
||||
else if (t > 1) t = 1;
|
||||
const fx = ax + t * dx;
|
||||
const fy = ay + t * dy;
|
||||
const ex = px - fx;
|
||||
const ey = py - fy;
|
||||
return ex * ex + ey * ey;
|
||||
}
|
||||
|
||||
// screenToWorld converts cursor pixel coordinates (relative to the
|
||||
// viewport top-left) to world coordinates under the given camera.
|
||||
export function screenToWorld(
|
||||
cursorPx: { x: number; y: number },
|
||||
camera: Camera,
|
||||
viewport: Viewport,
|
||||
): { x: number; y: number } {
|
||||
const offX = cursorPx.x - viewport.widthPx / 2;
|
||||
const offY = cursorPx.y - viewport.heightPx / 2;
|
||||
return {
|
||||
x: camera.centerX + offX / camera.scale,
|
||||
y: camera.centerY + offY / camera.scale,
|
||||
};
|
||||
}
|
||||
|
||||
// worldToScreen converts a world-space point to viewport pixel
|
||||
// coordinates under the given camera.
|
||||
export function worldToScreen(
|
||||
world: { x: number; y: number },
|
||||
camera: Camera,
|
||||
viewport: Viewport,
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: viewport.widthPx / 2 + (world.x - camera.centerX) * camera.scale,
|
||||
y: viewport.heightPx / 2 + (world.y - camera.centerY) * camera.scale,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user