Update exhibit
This commit is contained in:
@@ -125,6 +125,7 @@
|
||||
<div class="spacer"></div>
|
||||
<span id="dirty" title="unsaved changes"></span>
|
||||
<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="saveBtn" class="primary" disabled>Save to server</button>
|
||||
</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');
|
||||
});
|
||||
|
||||
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 = ''; } });
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user