Update exhibit

This commit is contained in:
2026-07-07 17:36:23 +10:00
parent 14d7185a6e
commit f7de2c3b1f
3 changed files with 217 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Newbury Crests — print sheet</title>
<style>
:root { --ink:#11131c; --dim:#666; --line:#ccc;
--red:#f65151; --yellow:#fff35d; --blue:#529eff; }
* { box-sizing: border-box; }
html, body { margin: 0; background: #f4f5f7; color: var(--ink);
font-family: 'Inter', system-ui, sans-serif; }
/* screen-only control bar */
#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 input[type=number] { width: 62px; padding: 4px 6px; border: 1px solid var(--line);
border-radius: 6px; font-family: ui-monospace, monospace; font-size: 12px; }
#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; }
#bar .note { font-size: 11px; color: var(--dim); flex-basis: 100%; }
#sheet { padding: 16px; }
.card { background: #fff; border: 1px solid #e3e5ea; border-radius: 10px;
padding: 14px; margin: 0 auto 14px; max-width: 560px; }
.card .meta { display: flex; justify-content: space-between; align-items: baseline;
margin-bottom: 10px; }
.card .name { font-weight: 700; font-size: 14px; }
.card .sub { font-family: ui-monospace, monospace; font-size: 11px; color: var(--dim); }
.marker-wrap { display: flex; gap: 16px; align-items: center; }
.marker-wrap svg { display: block; }
.card .facts { font-size: 11px; color: var(--dim); line-height: 1.6; }
.card .facts b { color: var(--ink); }
.warn { color: #b45309; font-size: 11px; margin-top: 6px; }
#empty { text-align: center; color: var(--dim); padding: 60px 20px; }
@media print {
#bar { display: none; }
body, html { background: #fff; }
#sheet { padding: 0; }
.card { border: none; box-shadow: none; page-break-inside: avoid;
max-width: none; margin: 0 0 8mm; padding: 0; }
/* cut guides show in print */
}
</style>
</head>
<body>
<div id="bar">
<h1>Newbury <span>Crests</span> — print sheet</h1>
<div class="spacer"></div>
<label>cell size
<input id="cellMm" type="number" value="16" min="6" max="40" step="1" /> mm
</label>
<label><input id="showRing" type="checkbox" checked /> colour ring</label>
<label><input id="showGuides" type="checkbox" checked /> cut guides</label>
<button class="ghost" id="reloadBtn">Reload anchors</button>
<button id="printBtn">Print</button>
<div class="note">Print at <b>100% / actual size</b> (no "fit to page"). Measure a printed marker to confirm scale before building. Matte paper or matte UV — avoid gloss (glare breaks detection).</div>
</div>
<div id="sheet"><div id="empty">Loading anchors…</div></div>
<!-- vendored ArUco dictionary for the exact bit patterns -->
<script src="vendor/dictionaries/aruco_4x4_1000.js"></script>
<script>
(function () {
'use strict';
const LURE = { Red: '#f65151', Yellow: '#fff35d', Blue: '#529eff' };
const RING_CYCLE = ['Red', 'Yellow', 'Blue'];
const DPI = 96; // CSS px per inch
const MM_PER_IN = 25.4;
const pxPerMm = DPI / MM_PER_IN; // ~3.78 px/mm at 96dpi
// Build a 6x6 bit grid (0=black,1=white) for an ArUco 4x4 id.
// Outer ring is the quiet border (all black); inner 4x4 = the code bits.
function grid6(id) {
const dict = (window.AR && AR.DICTIONARIES && AR.DICTIONARIES.ARUCO_4X4_1000)
? AR.DICTIONARIES.ARUCO_4X4_1000 : null;
// js-aruco exposes the dictionary via AR.Dictionary; build one to read codeList
const d = new AR.Dictionary('ARUCO_4X4_1000');
const code = d.codeList[id];
if (!code) return null;
const g = [];
for (let y = -1; y <= 4; y++) {
const row = [];
for (let x = -1; x <= 4; x++) {
if (y < 0 || y > 3 || x < 0 || x > 3) row.push(0); // black border
else row.push(code[y * 4 + x] === '1' ? 1 : 0); // data bit
}
g.push(row);
}
return g;
}
function markerSVG(id, cellMm, showRing, showGuides) {
const core = grid6(id);
if (!core) return `<div class="warn">No pattern for id ${id} (out of range).</div>`;
const cell = cellMm * pxPerMm;
const ringCells = showRing ? 1 : 0;
const n = 6 + ringCells * 2; // total cells across
const dim = n * cell;
const guide = showGuides ? cell * 0.5 : 0; // margin for cut guides
const total = dim + guide * 2;
let s = `<svg width="${total}" height="${total}" viewBox="0 0 ${total} ${total}" xmlns="http://www.w3.org/2000/svg">`;
s += `<rect width="${total}" height="${total}" fill="#ffffff"/>`;
// colour-wheel ring (decorative, outside the detector core)
if (showRing) {
let ci = 0;
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {
const isRing = (x === 0 || y === 0 || x === n - 1 || y === n - 1);
if (!isRing) continue;
const col = LURE[RING_CYCLE[ci % RING_CYCLE.length]]; ci++;
s += `<rect x="${guide + x * cell}" y="${guide + y * cell}" width="${cell}" height="${cell}" fill="${col}"/>`;
}
}
// detector core 6x6 (inset by the ring)
for (let y = 0; y < 6; y++) for (let x = 0; x < 6; x++) {
const fill = core[y][x] ? '#ffffff' : '#000000';
const px = guide + (x + ringCells) * cell;
const py = guide + (y + ringCells) * cell;
s += `<rect x="${px}" y="${py}" width="${cell}" height="${cell}" fill="${fill}"/>`;
}
// cut guides (corner crop marks)
if (showGuides) {
const gl = guide * 0.8, gt = 1;
const marks = [
[0, 0, 1, 1], [total, 0, -1, 1], [0, total, 1, -1], [total, total, -1, -1],
];
for (const [cx, cy, dx, dy] of marks) {
s += `<line x1="${cx}" y1="${cy}" x2="${cx + dx * gl}" y2="${cy}" stroke="#999" stroke-width="${gt}"/>`;
s += `<line x1="${cx}" y1="${cy}" x2="${cx}" y2="${cy + dy * gl}" stroke="#999" stroke-width="${gt}"/>`;
}
}
s += '</svg>';
return s;
}
let anchors = [];
async function loadAnchors() {
const el = document.getElementById('sheet');
try {
const res = await fetch('/api/scene');
const scene = await res.json();
anchors = scene.anchors || [];
} catch (e) {
el.innerHTML = `<div id="empty">Couldn't load /api/scene: ${e.message}</div>`;
return;
}
render();
}
function render() {
const el = document.getElementById('sheet');
const cellMm = parseFloat(document.getElementById('cellMm').value) || 16;
const showRing = document.getElementById('showRing').checked;
const showGuides = document.getElementById('showGuides').checked;
if (!anchors.length) { el.innerHTML = `<div id="empty">No anchors in the scene yet. Add some in the workshop, then reload.</div>`; return; }
el.innerHTML = anchors.map((a) => {
const id = a.fiducialId ?? 0;
const coreMm = cellMm * 6;
const fullMm = showRing ? cellMm * 8 : coreMm;
const typeWarn = (a.fiducialType && a.fiducialType !== 'aruco')
? `<div class="warn">⚠ This anchor is set to "${a.fiducialType}", but detection uses ArUco 4×4. Set it to <b>aruco</b> in the workshop, or this printed marker won't match.</div>`
: '';
const scaleWarn = (a.fiducialSizeMm && Math.abs(a.fiducialSizeMm - coreMm) > 1)
? `<div class="warn">⚠ Anchor size is <b>${a.fiducialSizeMm}mm</b> but this prints the core at <b>${coreMm}mm</b> (cell ${cellMm}×6). Match them so pose scale is right: set cell to ${(a.fiducialSizeMm/6).toFixed(1)}mm or the anchor size to ${coreMm}mm.</div>`
: '';
return `
<div class="card">
<div class="meta">
<span class="name">${a.label || a.id}</span>
<span class="sub">ArUco 4×4 · id ${id}</span>
</div>
<div class="marker-wrap">
${markerSVG(id, cellMm, showRing, showGuides)}
<div class="facts">
core <b>${coreMm}mm</b> (6×${cellMm}mm)<br/>
${showRing ? `with ring <b>${fullMm}mm</b><br/>` : ''}
world pos <b>${Math.round(a.position?.x ?? 0)}, ${Math.round(a.position?.z ?? 0)}</b> mm<br/>
anchor id <span class="sub">${a.id}</span>
</div>
</div>
${typeWarn}${scaleWarn}
</div>`;
}).join('');
}
document.getElementById('cellMm').addEventListener('input', render);
document.getElementById('showRing').addEventListener('change', render);
document.getElementById('showGuides').addEventListener('change', render);
document.getElementById('reloadBtn').addEventListener('click', loadAnchors);
document.getElementById('printBtn').addEventListener('click', () => window.print());
loadAnchors();
})();
</script>
</body>
</html>