From 4cf6e39471649289ce8ef48d7c4f975f7a14859f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 20:51:09 +0000 Subject: [PATCH 1/5] Preview: a grid overlay and per-block density values A Grid switch draws block lines when zoomed in, chunk lines (16-block sections up a side view) further out, and region lines further still. Each step shows once its lines are at least 6 CSS pixels apart. The grid is SVG over the image, so lines stay thin however coarse the render is, and it follows the view each image was rendered at, not the camera that moved since. A Per block switch, in density views, samples each pixel at the block it falls in (x, y and z floored), as the game does, so zoomed in a block is one flat cell and the hover readout shows the game's value there, interpolated() cells included. Pixels in the same block reuse one sample. Both switches are remembered in the browser. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AX3TszbXS3f1tAdyDwbKFH --- packages/preview/src/preview/Preview.svelte | 134 +++++++++++++++++- .../preview/src/preview/preview.worker.ts | 39 ++++- packages/preview/src/preview/protocol.ts | 7 + web/tests/smoke.spec.ts | 49 +++++++ 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/packages/preview/src/preview/Preview.svelte b/packages/preview/src/preview/Preview.svelte index d56e7e7..7ade6aa 100644 --- a/packages/preview/src/preview/Preview.svelte +++ b/packages/preview/src/preview/Preview.svelte @@ -66,6 +66,12 @@ const defaultView = (kind: PreviewKind, mode: PreviewMode) => ({ ...defaults[mode === 'biomes' ? 'biomes' : kind] }); let canvas: HTMLCanvasElement; + /** + * The grid, as SVG paths over the image (in CSS pixels, so its lines + * stay thin however coarse the render is), finest step first. + */ + let gridPaths = $state.raw<{ d: string; faint: boolean }[]>([]); + let gridSize = $state.raw({ w: 0, h: 0 }); let box: HTMLDivElement; let worker: Worker | null = null; let view = $state({ ...defaults.density_function }); @@ -97,6 +103,10 @@ type Point = { x: number; y: number; z: number }; let lock = $state(null); let seq = 0; + /** The 2D view each render in flight was asked for, so the grid lines up with the image shown, not the camera that moved since. */ + const sentViews = new Map(); + /** The view the image on screen was rendered at. */ + let shownView: PreviewView | null = null; let lastTarget: string | null = null; let mode = $state('density'); let biome = $state('minecraft:plains'); @@ -111,6 +121,38 @@ /** The side view's y while top-down terrain looks down from the top of the world. */ let sideY: number | null = null; + /** A switch remembered in this browser. */ + function storedFlag(key: string, fallback: boolean): boolean { + try { + const v = localStorage.getItem(key); + if (v === '1' || v === '0') return v === '1'; + } catch { + // Storage can be unavailable (private windows, some webviews). + } + return fallback; + } + function storeFlag(key: string, on: boolean) { + try { + localStorage.setItem(key, on ? '1' : '0'); + } catch { + // Not remembered; still applies now. + } + } + /** Grid lines over the 2D views: blocks when zoomed in, chunks further out, regions further still. */ + let grid = $state(storedFlag('mms.preview.grid', false)); + /** Density sampled at whole block positions, the values the game computes there (see PreviewView.blocks). */ + let blockValues = $state(storedFlag('mms.preview.blocks', false)); + function setGrid(on: boolean) { + grid = on; + storeFlag('mms.preview.grid', on); + drawGrid(); + } + function setBlockValues(on: boolean) { + blockValues = on; + storeFlag('mms.preview.blocks', on); + request(); + } + // Density colouring: a palette per kind, remembered in this browser, and // the range it spans (the values in view, -1 to 1, or the user's own). // A biome map's climate layers and its distance-to-win layer have their own. @@ -375,6 +417,8 @@ } error = ''; last = r; + shownView = sentViews.get(r.seq) ?? null; + for (const k of sentViews.keys()) if (k < r.seq) sentViews.delete(k); // The worker picks the default size and keeps zooming out within what fills the canvas. if (r.scale && (!isoView.scale || r.scale > isoView.scale)) isoView.scale = r.scale; draw(); @@ -524,6 +568,8 @@ const height = Math.max(16, Math.floor(box.clientHeight / resolution)); setBusy(true); if (mode === 'iso' || topBlocks) startHelpers(); + const sent: PreviewView = { ...$state.snapshot(view), down: lookDown && canDown, blocks: mode === 'density' && blockValues }; + sentViews.set(seq + 1, sent); worker.postMessage({ type: 'render', seq: ++seq, @@ -532,7 +578,7 @@ id: info.id, width, height, - view: mode === 'iso' ? $state.snapshot(isoView) : { ...$state.snapshot(view), down: lookDown && canDown }, + view: mode === 'iso' ? $state.snapshot(isoView) : sent, seed, options: { biome, terrain } } satisfies PreviewRequest); @@ -586,6 +632,62 @@ ctx.arc(px, py, 4, 0, Math.PI * 2); ctx.stroke(); } + drawGrid(); + } + + /** + * Grid steps, finest first: blocks, chunks (and 16-block sections up a + * side view), and regions. Each step is drawn once its lines are at + * least minGap CSS pixels apart; the finest drawn is faint, the coarser + * ones stronger over it. + */ + const gridSteps = [ + { size: 1, name: 'blocks' }, + { size: 16, name: 'chunks' }, + { size: 512, name: 'regions' } + ]; + const minGap = 6; + /** The grid steps drawn at the current zoom (none in the 3D view or when off). */ + const gridShown = $derived.by(() => { + if (!grid || mode === 'iso' || !info) return []; + // CSS pixels per block: a render pixel is `resolution` CSS pixels and view.scale blocks. + const perBlock = resolution / view.scale; + return gridSteps.filter((s) => s.size * perBlock >= minGap); + }); + $effect(() => { + void gridShown; + untrack(drawGrid); + }); + function drawGrid() { + const v = shownView; + if (!box || !last || !v || gridShown.length === 0 || last.bitmap) { + if (gridPaths.length) gridPaths = []; + return; + } + const W = box.clientWidth; + const H = box.clientHeight; + const { width: w, height: h } = last; + // The image is stretched over the whole box. + const sx = W / w; + const sy = H / h; + const a = screenAxes(v.facing); + // The world plane at the image's centre: u to the right, and v down + // the screen (top-down) or up it (a side view, where v is y). + const cu = v.x * a.rx + v.z * a.rz; + const cv = flat ? v.x * a.dx + v.z * a.dz : v.y; + const xOf = (u: number) => ((u - cu) / v.scale + w / 2) * sx; + const yOf = (t: number) => ((flat ? t - cv : cv - t) / v.scale + h / 2) * sy; + const halfU = (w / 2) * v.scale; + const halfV = (h / 2) * v.scale; + const r = (n: number) => Math.round(n * 2) / 2; + gridSize = { w: W, h: H }; + gridPaths = gridShown.map((step, k) => { + const d: string[] = []; + for (let u = Math.ceil((cu - halfU) / step.size) * step.size; u <= cu + halfU; u += step.size) d.push(`M${r(xOf(u))} 0V${H}`); + // Regions are columns: a side view has no region lines up y. + if (flat || step.name !== 'regions') for (let t = Math.ceil((cv - halfV) / step.size) * step.size; t <= cv + halfV; t += step.size) d.push(`M0 ${r(yOf(t))}H${W}`); + return { d: d.join(''), faint: k === 0 && gridShown.length > 1 }; + }); } /** @@ -1015,6 +1117,26 @@ {/if} {/if} + {#if mode !== 'iso'} +
+ + {#if mode === 'density'} + + {/if} +
+ {/if}