diff --git a/ui/frontend/src/lib/active-view/map.svelte b/ui/frontend/src/lib/active-view/map.svelte index 8b60519..35f1255 100644 --- a/ui/frontend/src/lib/active-view/map.svelte +++ b/ui/frontend/src/lib/active-view/map.svelte @@ -117,6 +117,50 @@ preference the store already manages. let containerEl: HTMLDivElement | null = $state(null); let mountError: string | null = $state(null); + // F8-12 follow-up: an opt-in technical overlay activated by adding + // `?debug=1` to the URL. Shows the current camera scale and the + // world-units rectangle currently visible inside the viewport — so + // the owner can decide what to clamp `maxScale` to once it lands. + const debugOverlayEnabled = (() => { + if (typeof window === "undefined") return false; + return new URLSearchParams(window.location.search).get("debug") === "1"; + })(); + let debugInfo: { + scale: number; + scaleRef: number; + viewWorldWidth: number; + viewWorldHeight: number; + } | null = $state(null); + let debugFrame: number | null = null; + function startDebugLoop(): void { + if (!debugOverlayEnabled) return; + const tick = (): void => { + if (handle !== null) { + const camera = handle.getCamera(); + const vp = handle.getViewport(); + const safeScale = camera.scale > 0 ? camera.scale : 1; + const worldW = store?.report?.mapWidth ?? 1; + const worldH = store?.report?.mapHeight ?? 1; + debugInfo = { + scale: camera.scale, + scaleRef: Math.max(vp.widthPx / worldW, vp.heightPx / worldH), + viewWorldWidth: vp.widthPx / safeScale, + viewWorldHeight: vp.heightPx / safeScale, + }; + } else { + debugInfo = null; + } + debugFrame = requestAnimationFrame(tick); + }; + debugFrame = requestAnimationFrame(tick); + } + function stopDebugLoop(): void { + if (debugFrame !== null) { + cancelAnimationFrame(debugFrame); + debugFrame = null; + } + } + let handle: RendererHandle | null = null; let hitLookup = new Map(); // currentCategories / currentPlanetDependents are populated by @@ -797,6 +841,7 @@ preference the store already manages. onMount(() => { mounted = true; + startDebugLoop(); onResize = (): void => { if (handle === null || containerEl === null) return; handle.resize(containerEl.clientWidth, containerEl.clientHeight); @@ -814,6 +859,7 @@ preference the store already manages. onDestroy(() => { mounted = false; + stopDebugLoop(); if (onResize !== null) { window.removeEventListener("resize", onResize); onResize = null; @@ -868,6 +914,30 @@ preference the store already manages. {#if store !== undefined && store.status === "ready"} {/if} + {#if debugOverlayEnabled && debugInfo !== null} + + {/if} @@ -911,4 +981,32 @@ preference the store already manages. border-color: var(--color-danger); color: var(--color-danger); } + .debug-overlay { + position: absolute; + bottom: 0.5rem; + left: 0.5rem; + min-width: 11rem; + padding: 0.35rem 0.55rem; + background: rgba(0, 0, 0, 0.55); + color: #f3f5fb; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 0.72rem; + line-height: 1.25; + pointer-events: none; + user-select: none; + z-index: 5; + } + .debug-row { + display: flex; + justify-content: space-between; + gap: 0.5rem; + } + .debug-key { + color: rgba(243, 245, 251, 0.65); + } + .debug-val { + font-variant-numeric: tabular-nums; + } diff --git a/ui/frontend/src/map/render.ts b/ui/frontend/src/map/render.ts index 0d50cff..b94138d 100644 --- a/ui/frontend/src/map/render.ts +++ b/ui/frontend/src/map/render.ts @@ -772,7 +772,7 @@ export async function createRenderer(opts: RendererOptions): Promise 0) { const strokeAlpha = p.style.strokeAlpha ?? 1; const strokeWidth = displayStrokeWidthWorld(p.style, cameraScale); + // Retrace because Pixi's `fill()` closes the current path. + traceSmoothCircle(g, p.x, p.y, radius); g.stroke({ color: p.style.strokeColor, alpha: strokeAlpha, @@ -1549,9 +1581,14 @@ function drawCircle( theme: Theme, cameraScale: number, ): void { - g.circle(p.x, p.y, p.radius); - if (p.style.fillColor !== undefined) { - g.fill({ color: p.style.fillColor, alpha: p.style.fillAlpha ?? 1 }); + const hasFill = + p.style.fillColor !== undefined && (p.style.fillAlpha ?? 1) > 0; + traceSmoothCircle(g, p.x, p.y, p.radius); + if (hasFill) { + g.fill({ color: p.style.fillColor!, alpha: p.style.fillAlpha ?? 1 }); + // Pixi's `fill()` closes the current path — retrace before + // the stroke pass so the ring is actually painted on top. + traceSmoothCircle(g, p.x, p.y, p.radius); } const strokeColor = p.style.strokeColor ?? theme.circleStroke; const strokeAlpha = p.style.strokeAlpha ?? 1;