Update exhibit
This commit is contained in:
@@ -125,6 +125,7 @@
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<span id="dirty" title="unsaved changes"></span>
|
<span id="dirty" title="unsaved changes"></span>
|
||||||
<button id="reloadBtn" title="Discard changes and reload from server">Reload</button>
|
<button id="reloadBtn" title="Discard changes and reload from server">Reload</button>
|
||||||
|
<button id="printAnchorsBtn" title="Open printable anchor markers">Print anchors</button>
|
||||||
<button id="exportBtn" title="Download scene.json">Export</button>
|
<button id="exportBtn" title="Download scene.json">Export</button>
|
||||||
<button id="saveBtn" class="primary" disabled>Save to server</button>
|
<button id="saveBtn" class="primary" disabled>Save to server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -721,6 +721,12 @@ $('reloadBtn').addEventListener('click', async () => {
|
|||||||
toast('Reloaded from server');
|
toast('Reloaded from server');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const printAnchorsBtn = document.getElementById('printAnchorsBtn');
|
||||||
|
if (printAnchorsBtn) printAnchorsBtn.addEventListener('click', () => {
|
||||||
|
if (dirty) toast('Tip: save first so the print sheet matches your latest anchors', '');
|
||||||
|
window.open('anchors-print.html', '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', (e) => { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
window.addEventListener('beforeunload', (e) => { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user