164f23fbed
`MovedEvent.type` in pixi-viewport@6 is a closed union of built-in plugin names; the prior `"manual"` value tripped svelte-check. `"animate"` is the closest semantic match for a programmatic move and the renderer's listeners read only `viewport`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
217 lines
5.6 KiB
Svelte
217 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import { onDestroy, onMount } from "svelte";
|
|
import { page } from "$app/state";
|
|
import {
|
|
createRenderer,
|
|
minScaleNoWrap,
|
|
sampleWorld,
|
|
type RendererHandle,
|
|
type RendererPreference,
|
|
type WrapMode,
|
|
} from "../../../map/index";
|
|
|
|
interface DebugMapSurface {
|
|
ready: true;
|
|
getMode(): WrapMode;
|
|
setMode(mode: WrapMode): void;
|
|
getCamera(): { centerX: number; centerY: number; scale: number };
|
|
setCameraCenter(centerX: number, centerY: number): void;
|
|
getViewport(): { widthPx: number; heightPx: number };
|
|
getBackend(): string;
|
|
getWorldSize(): { width: number; height: number };
|
|
hitAt(x: number, y: number): number | null;
|
|
}
|
|
|
|
type DebugMapWindow = typeof globalThis & { __galaxyMap?: DebugMapSurface };
|
|
|
|
let canvasEl: HTMLCanvasElement | null = $state(null);
|
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
let mode: WrapMode = $state("torus");
|
|
let backend = $state("");
|
|
let initError: string | null = $state(null);
|
|
|
|
let handle: RendererHandle | null = null;
|
|
let onResize: (() => void) | null = null;
|
|
|
|
function readPreference(): RendererPreference | RendererPreference[] {
|
|
const v = page.url.searchParams.get("renderer");
|
|
if (v === "webgl") return "webgl";
|
|
if (v === "webgpu") return "webgpu";
|
|
return ["webgpu", "webgl"];
|
|
}
|
|
|
|
function describe(err: unknown): string {
|
|
if (err instanceof Error) return `${err.name}: ${err.message}`;
|
|
return String(err);
|
|
}
|
|
|
|
onMount(() => {
|
|
(async () => {
|
|
if (canvasEl === null || containerEl === null) return;
|
|
const world = sampleWorld();
|
|
try {
|
|
handle = await createRenderer({
|
|
canvas: canvasEl,
|
|
world,
|
|
mode,
|
|
preference: readPreference(),
|
|
});
|
|
} catch (err) {
|
|
initError = describe(err);
|
|
return;
|
|
}
|
|
backend = handle.getBackend();
|
|
|
|
// Initial camera: world centre, zoomed slightly past the
|
|
// fits-the-viewport floor so neighbouring torus copies are
|
|
// visible too.
|
|
handle.viewport.moveCenter(world.width / 2, world.height / 2);
|
|
const minScale = minScaleNoWrap(
|
|
{ widthPx: containerEl.clientWidth, heightPx: containerEl.clientHeight },
|
|
world,
|
|
);
|
|
handle.viewport.setZoom(minScale * 1.2, true);
|
|
if (mode === "no-wrap") handle.setMode("no-wrap"); // re-clamp post zoom
|
|
|
|
const surface: DebugMapSurface = {
|
|
ready: true,
|
|
getMode: () => handle?.getMode() ?? "torus",
|
|
setMode: (m) => {
|
|
if (handle === null) return;
|
|
handle.setMode(m);
|
|
mode = m;
|
|
},
|
|
getCamera: () => handle?.getCamera() ?? { centerX: 0, centerY: 0, scale: 1 },
|
|
setCameraCenter: (cx, cy) => {
|
|
if (handle === null) return;
|
|
// `pixi-viewport`'s built-in plugins (drag, wheel,
|
|
// decelerate, …) emit `'moved'` themselves, but
|
|
// programmatic `moveCenter` does not. Emit it
|
|
// manually so the renderer's torus-wrap listener
|
|
// (and any future per-move callback) sees the
|
|
// change — matches the semantics of a user drag.
|
|
handle.viewport.moveCenter(cx, cy);
|
|
// `MovedEvent.type` is a closed literal union over the
|
|
// built-in plugin names; `"animate"` is the closest
|
|
// match for a programmatic move and the renderer's
|
|
// listeners read only `viewport`.
|
|
handle.viewport.emit("moved", {
|
|
viewport: handle.viewport,
|
|
type: "animate",
|
|
});
|
|
},
|
|
getViewport: () =>
|
|
handle?.getViewport() ?? { widthPx: 0, heightPx: 0 },
|
|
getBackend: () => handle?.getBackend() ?? "",
|
|
getWorldSize: () => ({ width: world.width, height: world.height }),
|
|
hitAt: (x, y) => {
|
|
if (handle === null) return null;
|
|
const hit = handle.hitAt({ x, y });
|
|
return hit?.primitive.id ?? null;
|
|
},
|
|
};
|
|
(window as DebugMapWindow).__galaxyMap = surface;
|
|
|
|
onResize = (): void => {
|
|
if (handle === null || containerEl === null) return;
|
|
handle.resize(containerEl.clientWidth, containerEl.clientHeight);
|
|
};
|
|
window.addEventListener("resize", onResize);
|
|
})();
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (onResize !== null) {
|
|
window.removeEventListener("resize", onResize);
|
|
onResize = null;
|
|
}
|
|
if (handle !== null) {
|
|
handle.dispose();
|
|
handle = null;
|
|
}
|
|
const w = window as DebugMapWindow;
|
|
if (w.__galaxyMap !== undefined) delete w.__galaxyMap;
|
|
});
|
|
|
|
function toggleMode(): void {
|
|
if (handle === null) return;
|
|
const next: WrapMode = mode === "torus" ? "no-wrap" : "torus";
|
|
handle.setMode(next);
|
|
mode = next;
|
|
}
|
|
</script>
|
|
|
|
<main>
|
|
<header>
|
|
<h1>map debug</h1>
|
|
<div class="controls">
|
|
<button type="button" data-testid="mode-toggle" onclick={toggleMode}>
|
|
mode: {mode}
|
|
</button>
|
|
<span data-testid="backend" data-backend={backend}>backend: {backend || "…"}</span>
|
|
</div>
|
|
</header>
|
|
{#if initError !== null}
|
|
<p class="error" data-testid="init-error">{initError}</p>
|
|
{/if}
|
|
<div class="canvas-wrap" bind:this={containerEl}>
|
|
<canvas bind:this={canvasEl}></canvas>
|
|
</div>
|
|
</main>
|
|
|
|
<style>
|
|
main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
margin: 0;
|
|
font-family: system-ui, sans-serif;
|
|
color: #e8eaf6;
|
|
background: #0a0e1a;
|
|
}
|
|
header {
|
|
padding: 0.5rem 1rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
border-bottom: 1px solid #20253a;
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
}
|
|
.controls {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
}
|
|
button {
|
|
padding: 0.25rem 0.75rem;
|
|
background: #1c2238;
|
|
color: #e8eaf6;
|
|
border: 1px solid #2a3150;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
}
|
|
button:hover {
|
|
background: #232b48;
|
|
}
|
|
.canvas-wrap {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.error {
|
|
padding: 0.5rem 1rem;
|
|
background: #4a1820;
|
|
color: #ffb4b4;
|
|
}
|
|
</style>
|