Files
galaxy-game/ui/frontend/src/map/math.ts
T
Ilia Denisov 2bd1b54936
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Failing after 8m7s
feat(ui): Phase 29 map visibility toggles
Adds the gear-icon popover on the map view with per-game persistence
of every category toggle plus the wrap-mode radio. Hide-by-id and
visibility-fog facilities land on the renderer so every flip applies
within one frame without a Pixi remount; the wrap-mode toggle keeps
its existing remount + camera-preserve path. A new server-side turn
force-resets every flag to defaults so a hidden category never makes
the player miss the next turn's news.

Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go
(plus the single Go caller); the TS side keeps duplicating the formula
until a race-level WASM bridge lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:33:53 +02:00

113 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
// torusShortestDistance returns the wrap-aware Euclidean distance
// between (ax, ay) and (bx, by) on a torus of size width × height.
// Built on top of `torusShortestDelta` so the two axes share the
// "shortest signed delta" semantics. Used by the Phase 29 reach
// filter (hide planets beyond `FlightDistance` of every LOCAL
// planet); both modes (torus / no-wrap) consume the same metric — in
// no-wrap mode the wrapped distance is never shorter than the
// straight-line one because the player cannot fly across the seam.
export function torusShortestDistance(
ax: number,
ay: number,
bx: number,
by: number,
width: number,
height: number,
): number {
const dx = torusShortestDelta(ax, bx, width);
const dy = torusShortestDelta(ay, by, height);
return Math.hypot(dx, dy);
}
// 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,
};
}