Files
2026-07-08 12:17:21 +10:00

223 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Newbury Exhibit — placement plan</title>
<style>
:root { --ink:#12131a; --dim:#666; --line:#cfd3da;
--red:#f65151; --yellow:#e0b400; --blue:#529eff; --anchor:#00a88f; --bldg:#c98a4b; }
* { box-sizing:border-box; }
html,body { margin:0; background:#f4f5f7; color:var(--ink);
font-family:'Inter',system-ui,sans-serif; }
#bar { position:sticky; top:0; z-index:10; background:#fff; border-bottom:1px solid var(--line);
padding:10px 16px; display:flex; align-items:center; gap:14px; flex-wrap:wrap;
box-shadow:0 1px 4px rgba(0,0,0,0.06); }
#bar h1 { font-size:15px; margin:0; font-weight:700; }
#bar h1 span { color:var(--blue); }
#bar .spacer { flex:1; }
#bar label { font-size:12px; color:var(--dim); display:flex; align-items:center; gap:5px; }
#bar button { border:0; cursor:pointer; font-weight:600; font-size:13px; padding:8px 16px;
border-radius:8px; background:#111827; color:#fff; }
#bar button.ghost { background:#eef0f4; color:#111827; }
#wrap { padding:18px; max-width:1000px; margin:0 auto; }
.planbox { background:#fff; border:1px solid #e3e5ea; border-radius:12px; padding:16px; margin-bottom:16px; }
svg { display:block; width:100%; height:auto; }
table { width:100%; border-collapse:collapse; font-size:12.5px; }
th,td { text-align:left; padding:7px 10px; border-bottom:1px solid #eceef2; }
th { font-size:11px; text-transform:uppercase; letter-spacing:0.04em; color:var(--dim); }
td.mono, th.mono { font-family:ui-monospace,monospace; }
.chip { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:6px; vertical-align:middle; }
.legend { display:flex; gap:18px; flex-wrap:wrap; font-size:12px; color:var(--dim); margin-top:10px; }
.legend span { display:flex; align-items:center; gap:6px; }
h2 { font-size:13px; margin:0 0 10px; }
.note { font-size:11px; color:var(--dim); margin-top:8px; line-height:1.5; }
@media print {
#bar { display:none; }
body { background:#fff; }
.planbox { border:none; box-shadow:none; page-break-inside:avoid; }
}
</style>
</head>
<body>
<div id="bar">
<h1>Newbury <span>placement plan</span></h1>
<div class="spacer"></div>
<label><input id="grid" type="checkbox" checked /> 100mm grid</label>
<label><input id="labels" type="checkbox" checked /> labels</label>
<button class="ghost" id="reloadBtn">Reload</button>
<button id="printBtn">Print</button>
</div>
<div id="wrap">
<div class="planbox">
<h2>Top-down plan (looking down at the table)</h2>
<div id="plan">loading…</div>
<div class="legend">
<span><i class="chip" style="background:var(--anchor)"></i> anchor (crest) — arrow = facing/yaw</span>
<span><i class="chip" style="background:var(--bldg)"></i> building footprint</span>
<span><i class="chip" style="background:var(--blue)"></i> ghost spawn / path</span>
</div>
<div class="note">Origin (0,0) is the front-left corner. X runs right (length), Z runs away from you (depth). Measure from that corner when placing pieces. Print at 100% for a to-scale reference, or just use the coordinate tables below.</div>
</div>
<div class="planbox">
<h2>Anchors — where to place each crest</h2>
<div id="anchorTable"></div>
</div>
<div class="planbox">
<h2>Buildings — footprints</h2>
<div id="bldgTable"></div>
</div>
<div class="planbox">
<h2>Ghost spawns</h2>
<div id="ghostTable"></div>
</div>
</div>
<script>
(function () {
'use strict';
const A = '#00a88f', B = '#c98a4b';
const LURE = { Red:'#f65151', Yellow:'#e0b400', Blue:'#529eff' };
let scene = null;
async function load() {
try {
const r = await fetch('/api/scene');
scene = await r.json();
} catch (e) {
document.getElementById('plan').textContent = 'Could not load /api/scene: ' + e.message;
return;
}
render();
}
function render() {
const showGrid = document.getElementById('grid').checked;
const showLabels = document.getElementById('labels').checked;
const W = scene.world.buildSize.x, D = scene.world.buildSize.z;
// SVG scaled so the longer side fits ~920px; keep mm aspect.
const pad = 46;
const scale = (920 - pad * 2) / Math.max(W, D);
const sw = W * scale + pad * 2, sh = D * scale + pad * 2;
const X = (x) => pad + x * scale;
const Z = (z) => pad + z * scale; // top-down: z downward on the page
let s = `<svg viewBox="0 0 ${sw} ${sh}" xmlns="http://www.w3.org/2000/svg" font-family="Inter,system-ui,sans-serif">`;
s += `<rect x="${pad}" y="${pad}" width="${W*scale}" height="${D*scale}" fill="#fafbfc" stroke="#b9bec8"/>`;
// grid
if (showGrid) {
for (let x = 0; x <= W; x += 100) {
s += `<line x1="${X(x)}" y1="${pad}" x2="${X(x)}" y2="${pad+D*scale}" stroke="#eceef2"/>`;
if (x % 500 === 0) s += `<text x="${X(x)}" y="${pad-6}" font-size="10" fill="#999" text-anchor="middle">${x}</text>`;
}
for (let z = 0; z <= D; z += 100) {
s += `<line x1="${pad}" y1="${Z(z)}" x2="${pad+W*scale}" y2="${Z(z)}" stroke="#eceef2"/>`;
if (z % 500 === 0) s += `<text x="${pad-8}" y="${Z(z)+3}" font-size="10" fill="#999" text-anchor="end">${z}</text>`;
}
}
// origin marker + axes
s += `<circle cx="${X(0)}" cy="${Z(0)}" r="4" fill="#111"/>`;
s += `<line x1="${X(0)}" y1="${Z(0)}" x2="${X(0)+40}" y2="${Z(0)}" stroke="#f65151" stroke-width="2"/><text x="${X(0)+46}" y="${Z(0)+4}" font-size="11" fill="#f65151">X</text>`;
s += `<line x1="${X(0)}" y1="${Z(0)}" x2="${X(0)}" y2="${Z(0)+40}" stroke="#529eff" stroke-width="2"/><text x="${X(0)+4}" y="${Z(0)+52}" font-size="11" fill="#529eff">Z</text>`;
// buildings (footprints)
for (const bl of (scene.blockers || [])) {
const x = X(bl.position.x), y = Z(bl.position.z);
const w = bl.size.x * scale, h = bl.size.z * scale;
s += `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${B}22" stroke="${B}" stroke-width="1.5"/>`;
if (showLabels) s += `<text x="${x + w/2}" y="${y + h/2 + 3}" font-size="10" fill="${B}" text-anchor="middle">${bl.label || 'bldg'}</text>`;
}
// ghost paths + spawns
for (const sp of (scene.spawns || [])) {
const col = LURE[sp.color] || LURE.Blue;
if (sp.motion === 'patrol' && sp.path && sp.path.length > 1) {
const pts = sp.path.map(p => `${X(p.x)},${Z(p.z)}`).join(' ');
s += `<polygon points="${pts}" fill="none" stroke="${col}" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>`;
for (const p of sp.path) s += `<circle cx="${X(p.x)}" cy="${Z(p.z)}" r="3" fill="${col}"/>`;
const h = sp.path[0];
if (showLabels) s += `<text x="${X(h.x)+6}" y="${Z(h.z)-6}" font-size="10" fill="${col}">${sp.label||sp.ghostId||'ghost'}</text>`;
} else if (sp.position) {
const x = X(sp.position.x), y = Z(sp.position.z);
s += `<circle cx="${x}" cy="${y}" r="6" fill="${col}" opacity="0.85"/>`;
if (sp.hoverRadiusMm) s += `<circle cx="${x}" cy="${y}" r="${sp.hoverRadiusMm*scale}" fill="none" stroke="${col}" stroke-width="1" stroke-dasharray="2 2" opacity="0.6"/>`;
if (showLabels) s += `<text x="${x+9}" y="${y+3}" font-size="10" fill="${col}">${sp.label||sp.ghostId||'ghost'}</text>`;
}
}
// anchors with orientation arrow (yaw)
for (const a of (scene.anchors || [])) {
const x = X(a.position.x), y = Z(a.position.z);
const sz = (a.fiducialSizeMm || 96) * scale;
s += `<rect x="${x - sz/2}" y="${y - sz/2}" width="${sz}" height="${sz}" fill="${A}22" stroke="${A}" stroke-width="1.5"/>`;
// yaw arrow: 0deg points +Z (down the page). rotate clockwise for +yaw.
const yaw = (a.rotationDeg && a.rotationDeg.y || 0) * Math.PI / 180;
const ax = x + Math.sin(yaw) * (sz/2 + 12), ay = y + Math.cos(yaw) * (sz/2 + 12);
s += `<line x1="${x}" y1="${y}" x2="${ax}" y2="${ay}" stroke="${A}" stroke-width="2"/>`;
s += `<circle cx="${ax}" cy="${ay}" r="3" fill="${A}"/>`;
if (showLabels) s += `<text x="${x}" y="${y - sz/2 - 6}" font-size="10" fill="${A}" text-anchor="middle">${a.label || ('id '+a.fiducialId)}</text>`;
}
s += '</svg>';
document.getElementById('plan').innerHTML = s;
renderTables();
}
function renderTables() {
// anchors
let t = `<table><thead><tr><th>Crest</th><th class="mono">id</th><th class="mono">X (mm)</th><th class="mono">Z (mm)</th><th class="mono">Y height</th><th class="mono">yaw°</th><th class="mono">size</th><th>mount</th></tr></thead><tbody>`;
for (const a of (scene.anchors || [])) {
t += `<tr><td><span class="chip" style="background:${A}"></span>${a.label||a.id}</td>`
+ `<td class="mono">${a.fiducialId}</td>`
+ `<td class="mono">${Math.round(a.position.x)}</td><td class="mono">${Math.round(a.position.z)}</td>`
+ `<td class="mono">${Math.round(a.position.y||0)}</td>`
+ `<td class="mono">${Math.round((a.rotationDeg&&a.rotationDeg.y)||0)}</td>`
+ `<td class="mono">${a.fiducialSizeMm||96}mm</td>`
+ `<td>${a.mount||'flat'}</td></tr>`;
}
t += `</tbody></table>`;
document.getElementById('anchorTable').innerHTML = (scene.anchors||[]).length ? t : '<div class="note">No anchors yet.</div>';
// buildings
let b = `<table><thead><tr><th>Building</th><th class="mono">corner X</th><th class="mono">corner Z</th><th class="mono">W (x)</th><th class="mono">H (y)</th><th class="mono">D (z)</th></tr></thead><tbody>`;
for (const bl of (scene.blockers || [])) {
b += `<tr><td><span class="chip" style="background:${B}"></span>${bl.label||bl.id}</td>`
+ `<td class="mono">${Math.round(bl.position.x)}</td><td class="mono">${Math.round(bl.position.z)}</td>`
+ `<td class="mono">${bl.size.x}</td><td class="mono">${bl.size.y}</td><td class="mono">${bl.size.z}</td></tr>`;
}
b += `</tbody></table>`;
document.getElementById('bldgTable').innerHTML = (scene.blockers||[]).length ? b : '<div class="note">No buildings yet.</div>';
// ghosts
let g = `<table><thead><tr><th>Ghost</th><th>lure</th><th>motion</th><th class="mono">X</th><th class="mono">Y</th><th class="mono">Z</th><th>detail</th></tr></thead><tbody>`;
for (const sp of (scene.spawns || [])) {
const col = LURE[sp.color] || LURE.Blue;
const home = sp.motion === 'patrol' ? (sp.path && sp.path[0]) : sp.position;
const detail = sp.motion === 'patrol' ? `${(sp.path||[]).length} pts · ${sp.patrolPeriodS||16}s loop`
: `r${sp.hoverRadiusMm||0} · bob${sp.bobAmplitudeMm||0}`;
g += `<tr><td><span class="chip" style="background:${col}"></span>${sp.label||sp.ghostId||sp.id}</td>`
+ `<td>${sp.color||'?'}</td><td>${sp.motion}</td>`
+ `<td class="mono">${home?Math.round(home.x):'-'}</td><td class="mono">${home?Math.round(home.y):'-'}</td><td class="mono">${home?Math.round(home.z):'-'}</td>`
+ `<td class="mono">${detail}</td></tr>`;
}
g += `</tbody></table>`;
document.getElementById('ghostTable').innerHTML = (scene.spawns||[]).length ? g : '<div class="note">No ghosts yet.</div>';
}
document.getElementById('grid').addEventListener('change', render);
document.getElementById('labels').addEventListener('change', render);
document.getElementById('reloadBtn').addEventListener('click', load);
document.getElementById('printBtn').addEventListener('click', () => window.print());
load();
})();
</script>
</body>
</html>