Files

214 lines
9.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<!-- js-aruco chain (order matters): cv -> svd -> posit -> aruco -> dictionary.
The dictionary file references AR from aruco.js; loading it alone throws. -->
<script src="vendor/cv.js"></script>
<script src="vendor/svd.js"></script>
<script src="vendor/posit1.js"></script>
<script src="vendor/aruco.js"></script>
<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) {
// Full js-aruco chain is loaded, so AR.Dictionary is available.
const d = new AR.Dictionary('ARUCO_4X4_1000');
const code = d.codeList[id]; // 16-char bit string, e.g. "1011..."
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>