Point at a crest, hold steady, type the real distance you measured with a tape, then Solve. It back-calculates the true marker size so on-table distances are accurate.
+
+
+
+
+
Default 96mm = 6 cells × 16mm (outer black square). Change it here to see distance update live, or Solve to compute it from a measurement.
+
+
+
+
Crest calibration
+
Measures the marker pose so ghost scale and placement are accurate. You'll need a tape measure.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/admin.js b/public/js/admin.js
index 9211042..6168139 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -464,6 +464,7 @@ function anchorEditor(a) {
const box = document.createElement('div'); box.style.cssText = 'padding:4px 2px 12px';
box.appendChild(textRow('Label', a.label, (v) => { a.label = v; markDirty(); renderAnchorsSoft(); }));
box.appendChild(selectRow('Marker', a.fiducialType || 'aruco', ['aruco', 'apriltag'], (v) => { a.fiducialType = v; markDirty(); }));
+ box.appendChild(selectRow('Mount', a.mount || 'flat', ['flat', 'wall'], (v) => { a.mount = v; markDirty(); }));
box.appendChild(numRow('Marker id', a.fiducialId, (v) => { a.fiducialId = v | 0; markDirty(); renderAnchorsSoft(); }, { step: 1, min: 0 }));
box.appendChild(numRow('Size (mm)', a.fiducialSizeMm, (v) => { a.fiducialSizeMm = v; rebuildAnchors(); markDirty(); }, { step: 1 }));
box.appendChild(xyzRow('Pos (mm)', a.position, ['x', 'y', 'z'], () => { rebuildAnchors(); markDirty(); }));
@@ -727,6 +728,15 @@ if (printAnchorsBtn) printAnchorsBtn.addEventListener('click', () => {
window.open('anchors-print.html', '_blank');
});
+const planBtn = document.getElementById('planBtn');
+if (planBtn) planBtn.addEventListener('click', () => {
+ if (dirty) toast('Tip: save first so the plan matches your latest layout', '');
+ window.open('placement-plan.html', '_blank');
+});
+
+const calibrateBtn = document.getElementById('calibrateBtn');
+if (calibrateBtn) calibrateBtn.addEventListener('click', () => window.open('calibrate.html', '_blank'));
+
window.addEventListener('beforeunload', (e) => { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
// ===========================================================================
diff --git a/public/js/exhibit.js b/public/js/exhibit.js
index 72e0f96..a4fc992 100644
--- a/public/js/exhibit.js
+++ b/public/js/exhibit.js
@@ -84,16 +84,31 @@ function markerToCameraMatrix(m) {
return { M, err: pose.bestError, dist: Math.hypot(t[0], t[1], t[2]) };
}
-// anchor world placement -> matrix (world coords of the marker's centre + yaw).
-// The marker lies flat-ish on the build; we treat its plane with the anchor yaw
-// about the vertical (Y) axis. Anchor.position is the marker centre in world mm.
+// anchor world placement -> matrix mapping the MARKER's local frame (as POS-IT
+// sees it: marker in its XY plane, +Z out of the printed face) into WORLD space.
+//
+// A crest lying flat on the table has its face pointing UP, so the marker's
+// local +Z must map to world +Y. That's a -90 deg rotation about X. On top of
+// that we apply the anchor's yaw (spin on the table) and its world position.
+//
+// mount 'flat' (default): marker lies on the table, face up -> rotX(-90) then yaw about Y
+// mount 'wall': marker stands vertical, face outward -> yaw about Y only
+//
+// The order matters: worldFromMarker = T(pos) * Ry(yaw) * planeTilt.
function anchorToWorldMatrix(anchor) {
const p = anchor.position || { x: 0, y: 0, z: 0 };
const yaw = THREE.MathUtils.degToRad(anchor.rotationDeg?.y || 0);
- const m = new THREE.Matrix4();
- m.makeRotationY(yaw);
- m.setPosition(p.x, p.y, p.z);
- return m;
+ const mount = anchor.mount || 'flat';
+
+ const planeTilt = new THREE.Matrix4();
+ if (mount === 'wall') planeTilt.identity(); // face already points sideways
+ else planeTilt.makeRotationX(-Math.PI / 2); // flat: tip face up to +Y
+
+ const spin = new THREE.Matrix4().makeRotationY(yaw);
+ const trans = new THREE.Matrix4().setPosition(p.x, p.y, p.z);
+
+ // trans * spin * planeTilt
+ return trans.multiply(spin).multiply(planeTilt);
}
// Given a detected marker with a known anchor, compute world->camera and apply
diff --git a/public/placement-plan.html b/public/placement-plan.html
new file mode 100644
index 0000000..1b862db
--- /dev/null
+++ b/public/placement-plan.html
@@ -0,0 +1,222 @@
+
+
+
+
+
+Newbury Exhibit — placement plan
+
+
+
+
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.