clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

Piece bench: per-piece orbit focus + navigation + CUA path

Dami committed 811a690
USER: the bench now frames ONE piece at a time (orbit cam re-framed
per selection, padded for one-cell pieces) instead of a far grid view.
Navigate with prev/next buttons, arrow keys, a jump-to-id text box
(substring match), or by clicking a plot. CUA path for harnesses:
window.benchGo(delta) / window.benchJump(id), advanceTime via
installCua, and render_game_to_text reports {mode:'bench', index,
total, piece, collision, simulating}. Status reads 'k/N piece-id'.
commit
811a690edc5f07ae088a8fa43477b8a3a0a8b01c
parent
195570d3abe2c86f8461b20b80e06fffbb308c27
author
Dami <dami.etoile@gmail.com>
1 file changed, +72 -5
Msrc/client/pieceBench.ts77++++++++++++++++++++++++++++--
M src/client/pieceBench.ts +72 -5
@@ -30,6 +30,7 @@ import { DIR_STEP, turnLeft, turnRight } from "../gen/geom.ts";
3030 import { channelProxies, type CollisionProxy } from "../gen/endless/proxies.ts";3131 import { scaleSeg, scaleVec, TRACK_SCALE } from "../gen/endless/scale.ts";3232 import { instantiatePlacements, type Spinner } from "./track.ts";33+import { installCua } from "./cua.ts";3334 import { gapSegmentsOf, type Rail, railForSegment } from "./physics/rail.ts";3435 import type { LoopPath } from "../shared/types.ts";3536 import {
@@ -288,6 +289,19 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
288289 ui.appendChild(b);289290 return b;290291 };292+ const prevBtn = mkBtn("◀", () => {});293+ const nextBtn = mkBtn("▶", () => {});294+ const jumpInput = document.createElement("input");295+ jumpInput.placeholder = "jump to id…";296+ Object.assign(jumpInput.style, {297+ background: "rgba(10,15,26,0.8)",298+ color: "#dce6f5",299+ border: "1px solid rgba(220,230,245,0.4)",300+ padding: "6px 8px",301+ width: "130px",302+ font: "inherit",303+ });304+ ui.appendChild(jumpInput);291305 const status = document.createElement("span");292306 Object.assign(status.style, {293307 color: "#dce6f5",
@@ -415,6 +429,7 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
415429 });416430 417431 // Pick a plot by nearest origin on click.432+ let onPick: (plot: Plot) => void = () => {};418433 const ray = new THREE.Raycaster();419434 const ndc = new THREE.Vector2();420435 ctx.renderer.domElement.addEventListener("pointerdown", (e: PointerEvent) => {
@@ -450,10 +465,7 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
450465 if (best > 8) return;451466 }452467 if (!plot) return;453- selected = plot;454- marker.visible = true;455- marker.position.set(plot.origin.x, 0.03, plot.origin.z);456- status.textContent = plot.label;468+ onPick(plot);457469 });458470 459471 // Sim stepping + marble mirroring.
@@ -482,8 +494,63 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
482494 fitShadowsTo(ctx.sun, box);483495 ctx.scene.fog = null;484496 const cams = createCameras(ctx, null, null);485- cams.frame(box);486497 ctx.onFrame.add((dt) => cams.update(dt));498+499+ // ---- single-piece focus (USER): orbit cam per piece + navigation ------500+ let current = -1;501+ const focus = (i: number) => {502+ if (plots.length === 0) return;503+ current = ((i % plots.length) + plots.length) % plots.length;504+ const plot = plots[current];505+ selected = plot;506+ marker.visible = true;507+ marker.position.set(plot.origin.x, 0.03, plot.origin.z);508+ const pbox = new THREE.Box3().setFromObject(plot.group);509+ // A one-cell piece frames too tight for orbit's min distance — pad.510+ pbox.expandByScalar(1.2);511+ cams.frame(pbox);512+ status.textContent = `${current + 1}/${plots.length} ${plot.label}`;513+ };514+ const jump = (q: string): boolean => {515+ const needle = q.trim().toLowerCase();516+ if (!needle) return false;517+ const at = plots.findIndex((p) => p.label.toLowerCase().includes(needle));518+ if (at < 0) {519+ status.textContent = `no piece matching "${q}"`;520+ return false;521+ }522+ focus(at);523+ return true;524+ };525+ prevBtn.addEventListener("click", () => focus(current - 1));526+ nextBtn.addEventListener("click", () => focus(current + 1));527+ jumpInput.addEventListener("keydown", (ev) => {528+ if (ev.key === "Enter") jump(jumpInput.value);529+ ev.stopPropagation();530+ });531+ addEventListener("keydown", (ev) => {532+ if (document.activeElement === jumpInput) return;533+ if (ev.key === "ArrowLeft") focus(current - 1);534+ if (ev.key === "ArrowRight") focus(current + 1);535+ });536+ // Clicking a plot focuses it too.537+ onPick = (plot) => focus(plots.indexOf(plot));538+539+ // CUA path: scripted navigation + bench state for harnesses.540+ (globalThis as unknown as Record<string, unknown>).benchGo = (d: number) =>541+ focus(current + (d | 0));542+ (globalThis as unknown as Record<string, unknown>).benchJump = (id: string) =>543+ jump(String(id));544+ installCua(ctx, () => ({545+ mode: "bench",546+ index: current,547+ total: plots.length,548+ piece: current >= 0 ? plots[current].label : null,549+ collision: showCollision,550+ simulating: simPlot ? simPlot.label : null,551+ }));552+553+ focus(0);487554 }488555 489556 function makeBenchLabel(text: string): THREE.Sprite | null {