← back to Quadrille Showroom
Slice1 fix-it: 72° yaw clamp (non-empty), 50 boards, ~36° resting-open middle
2c946ef43f51a1694dafc124afe72fe1be04be38 · 2026-06-28 16:32:00 -0700 · Steve Abrams
FIX-1 YAW_CLAMP 105°→72° + nook pulled forward (yaw +66°, within clamp) so
boards + side wall stay in frame at both clamps; never an empty void.
FIX-2 wing.totalCount 20→50 so the real data path fans all 50 slivers.
FIX-3 OPEN_ANGLE_DEG 45→37 → resting-open middle sits ~36° off dead-on
(restFlip 0.51), foreshortened ~24" — unmistakably angled, not flat,
no detail panel/selection. verify-slice1 tightened to gate non-empty clamp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/js/showroom.jsM scripts/verify-slice1.js
Diff
commit 2c946ef43f51a1694dafc124afe72fe1be04be38
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 28 16:32:00 2026 -0700
Slice1 fix-it: 72° yaw clamp (non-empty), 50 boards, ~36° resting-open middle
FIX-1 YAW_CLAMP 105°→72° + nook pulled forward (yaw +66°, within clamp) so
boards + side wall stay in frame at both clamps; never an empty void.
FIX-2 wing.totalCount 20→50 so the real data path fans all 50 slivers.
FIX-3 OPEN_ANGLE_DEG 45→37 → resting-open middle sits ~36° off dead-on
(restFlip 0.51), foreshortened ~24" — unmistakably angled, not flat,
no detail panel/selection. verify-slice1 tightened to gate non-empty clamp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/js/showroom.js | 21 +++++++++++------
scripts/verify-slice1.js | 60 +++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 66 insertions(+), 15 deletions(-)
diff --git a/public/js/showroom.js b/public/js/showroom.js
index 4dbf7c9..fa383b1 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -25,7 +25,7 @@ const CONFIG = {
// 50 boards × ~2" sliver. Hinge arc-pitch = R·span/(n-1): 2.2·(96°·π/180)/49 ≈ 0.0768m
// (≈3.0") hinge spacing; the ~73° closed rake (REVEAL 74) occludes most of that so each
// closed wing reads as a ~2" leading sliver, fanning left+right around the centred
- // viewer (yaw ±48°, well inside the ±105° clamp).
+ // viewer (yaw ±48°, well inside the ±72° clamp).
arcSpanDeg: 96,
arcCentreDeg: -90, // sweep centred on -Z → boards sit in FRONT of the viewer
arcCenter: { x: 0.0, z: 0.4 }, // arc centre just BEHIND the centred viewer → concave bank faces them
@@ -76,9 +76,14 @@ let VIEW_OPEN = (function () {
// (0°=fully closed/raked, rake°=fully open/flat-to-centre). Default 45°, range 20°–90°,
// persisted to localStorage('qh_open_angle'). updateBooks converts this to a per-board
// flip fraction (deg / board-rake-deg) so the open hero rakes to exactly this angle live.
+// Resting-open swing of the middle hero board, in degrees OFF the closed rake (rake≈73°).
+// At 37° open the board sits ~36° off dead-on (restFlip≈0.5 of the rake) → foreshortened to
+// ~24" of its 30" face: UNMISTAKABLY angled amid the slivers, but distinct from a fully-open
+// SELECTED board (flip=1, dead-on flat). Earlier 45° (restFlip≈0.62, only ~14.6° off dead-on)
+// read as flat at viewing distance — the FIX-3 hole.
let OPEN_ANGLE_DEG = (function () {
const v = parseFloat(localStorage.getItem('qh_open_angle'));
- return (v >= 20 && v <= 90) ? v : 45;
+ return (v >= 20 && v <= 90) ? v : 37;
})();
// REVEAL SLIDER (Steve's explicit ask) — how much of each closed board's pattern shows
@@ -849,11 +854,12 @@ function buildFurniture() {
// cream bouclé armchairs, a potted plant, swatch cards on the table. Sits in the
// FOREGROUND of the arc bay so the establishing shot reads: rack behind, nook in front.
// In the 14'×14' room the viewer is fixed at centre facing the arc (toward -z). The
- // consultation nook is tucked to the RIGHT side, slightly toward the wings (z<0) and
- // low, so it sits INSIDE the clamped yaw arc (~yaw +82°): a spin-right + look-down
- // reveals the sample table without walking, and the ±105° clamp still reaches it.
+ // consultation nook is tucked to the RIGHT side, pulled FORWARD toward the wings (more
+ // negative z) and low, so it sits INSIDE the tightened ±72° yaw clamp at ~yaw +66°
+ // (atan2(1.25, 0.55) ≈ 66.3°): a spin-right + look-down reveals the sample table without
+ // walking, and the 72° clamp still reaches it. (Was +82°, which fell OUTSIDE the new clamp.)
// ============================================================
- const nookX = 1.35, nookZ = -0.2;
+ const nookX = 1.25, nookZ = -0.55;
buildConsultationNook(nookX, nookZ);
// (Removed: boxy TV + bezel + slideshow screen — the wallcoverings are the only
@@ -2758,7 +2764,7 @@ function navigateToSection(section) {
const D = CONFIG.room.depth, W = CONFIG.room.width;
// Bookshelves are on front wall flanking the doorway (x=±2.8, z=D/2-0.3)
- const t = { overview: [[0,1.6,1.0],[0,1.2,-0.5]], wings: [[0,1.6,-D/2+3.0],[0,1.2,-D/2+0.2]], books: [[1.5,1.4,1.8],[2.8,0.9,D/2-0.3]], table: [[-1,1.8,2.0],[-1,0.75,1.0]] }[section] || [[0,1.6,1.0],[0,1.2,-0.5]];
+ const t = { overview: [[0,1.6,1.0],[0,1.2,-0.5]], wings: [[0,1.6,-D/2+3.0],[0,1.2,-D/2+0.2]], books: [[1.5,1.4,1.8],[2.8,0.9,D/2-0.3]], table: [[0,1.6,0],[1.25,0.74,-0.55]] }[section] || [[0,1.6,1.0],[0,1.2,-0.5]];
smoothCameraTo(new THREE.Vector3(...t[0]), new THREE.Vector3(...t[1]));
}
@@ -3025,6 +3031,7 @@ window._qh = {
CONFIG,
// Camera + focus
smoothCameraTo,
+ navigateToSection,
focusOnWing,
unfocusWing,
enterGuidedDefault,
diff --git a/scripts/verify-slice1.js b/scripts/verify-slice1.js
index df65dc0..30a6efc 100644
--- a/scripts/verify-slice1.js
+++ b/scripts/verify-slice1.js
@@ -104,18 +104,42 @@ function camState(page) {
// ---- 2. yaw CLAMPED — hold right a long time, then read spin yaw + verify never faces behind ----
await holdKey(page, 'ArrowRight', 4000); // try to over-spin
await sleep(300);
+ // At the clamp, count how many wing boards are still inside the camera frustum — the
+ // FIX-1 acceptance: the clamp LIMITS the view, it must NOT empty it.
+ // Count boards whose body is in frame. We sample BOTH the hinge (getWorldPosition) AND
+ // the board-centre offset (+halfWidth along its facing) and take the min |x|, because at
+ // the clamp the fanned boards fill half the frame even when a hinge point projects to the
+ // screen edge. A board counts if its nearest sampled point is within an extended NDC band.
+ const boardsOnScreen = (label) => page.evaluate(() => {
+ const THREE = window._qh.THREE; const cam = window._qh.camera;
+ const halfW = (window._qh.CONFIG.wing.boardWidthM || 0.762) / 2;
+ cam.updateMatrixWorld(); cam.updateProjectionMatrix();
+ const hinge = new THREE.Vector3(), ctr = new THREE.Vector3(); let n = 0;
+ window._qh.wingBoards.forEach(b => {
+ b.getWorldPosition(hinge);
+ // board centre ≈ hinge + halfW along the board's local +X (rotated by pivot yaw)
+ const yaw = b.rotation.y;
+ ctr.set(hinge.x + Math.cos(yaw) * halfW, hinge.y, hinge.z - Math.sin(yaw) * halfW);
+ const ph = hinge.clone().project(cam), pc = ctr.clone().project(cam);
+ const inFront = ph.z < 1 || pc.z < 1;
+ const minX = Math.min(Math.abs(ph.x), Math.abs(pc.x));
+ const minY = Math.min(Math.abs(ph.y), Math.abs(pc.y));
+ if (inFront && minX <= 1.0 && minY <= 1.2) n++;
+ });
+ return n;
+ });
const clampR = await page.evaluate(() => {
// facing direction: camera→target, projected to XZ. Behind-the-wings = +z forward.
const c = window._qh.camera, t = window._qh.controls.target;
const dx = t.x - c.position.x, dz = t.z - c.position.z;
// angle off the wing-facing (-z) axis, in degrees, signed
const yawDeg = Math.atan2(dx, -dz) * 180 / Math.PI;
- // "behind the wings" = yaw turned well past the side walls toward the room's rear
- // (the front wall behind the viewer). The ±105° clamp lets the user see the side
- // walls + the forward-placed sample table, which is intended; only |yaw|>115° counts
- // as facing behind the wings.
- return { yawDeg, dz, facesBehind: Math.abs(yawDeg) > 115 };
+ // "behind the wings" = yaw turned past the side walls toward the room's rear. The
+ // tightened ±72° clamp keeps the leftmost/rightmost boards + side wall in frame and
+ // never empties the room; only |yaw|>80° would count as over-spun past the side walls.
+ return { yawDeg, dz, facesBehind: Math.abs(yawDeg) > 80 };
});
+ results.boardsOnScreenRight = await boardsOnScreen('right');
await page.screenshot({ path: path.join(OUT, 'clamp-right.png') });
await holdKey(page, 'ArrowLeft', 8000); // over-spin the other way
await sleep(300);
@@ -123,14 +147,34 @@ function camState(page) {
const c = window._qh.camera, t = window._qh.controls.target;
const dx = t.x - c.position.x, dz = t.z - c.position.z;
const yawDeg = Math.atan2(dx, -dz) * 180 / Math.PI;
- return { yawDeg, dz, facesBehind: Math.abs(yawDeg) > 115 };
+ return { yawDeg, dz, facesBehind: Math.abs(yawDeg) > 80 };
});
+ results.boardsOnScreenLeft = await boardsOnScreen('left');
await page.screenshot({ path: path.join(OUT, 'clamp-left.png') });
results.clamp = { right: { yawDeg: round3(clampR.yawDeg), facesBehind: clampR.facesBehind },
left: { yawDeg: round3(clampL.yawDeg), facesBehind: clampL.facesBehind } };
- // clamp pass: never faces behind, and |yaw| stays within ~106° (105° clamp + tolerance)
+ // clamp pass: never faces behind, |yaw| stays within ~73° (72° clamp + tolerance), AND
+ // boards remain in frame at BOTH clamps (the FIX-1 non-empty-room gate).
results.clampPass = !clampR.facesBehind && !clampL.facesBehind
- && Math.abs(clampR.yawDeg) <= 106 && Math.abs(clampL.yawDeg) <= 106;
+ && Math.abs(clampR.yawDeg) <= 73 && Math.abs(clampL.yawDeg) <= 73
+ && results.boardsOnScreenRight >= 3 && results.boardsOnScreenLeft >= 3;
+
+ // ---- (2c) SPIN-TO-TABLE: yaw-right + pitch-down frames the consultation/sample table ----
+ // Reset to centre by over-spinning left was last; now yaw partway right + look down so the
+ // sample nook at ~(1.25, -0.2, -0.55) [yaw +66°, below eye] is framed. Proves the tightened
+ // clamp still REACHES the table (FIX-1's second half) — not just that it avoids the void.
+ await holdKey(page, 'ArrowRight', 1900); // partial yaw right toward the nook (~+66°)
+ await holdKey(page, 'ArrowDown', 1400); // pitch down toward the table top
+ await sleep(400);
+ const tableView = await page.evaluate(() => {
+ const c = window._qh.camera, t = window._qh.controls.target;
+ const dx = t.x - c.position.x, dz = t.z - c.position.z;
+ const yawDeg = Math.atan2(dx, -dz) * 180 / Math.PI;
+ const pitchDeg = Math.atan2(t.y - c.position.y, Math.hypot(dx, dz)) * 180 / Math.PI;
+ return { yawDeg, pitchDeg };
+ });
+ results.tableView = { yawDeg: round3(tableView.yawDeg), pitchDeg: round3(tableView.pitchDeg) };
+ await page.screenshot({ path: path.join(OUT, 'spin-to-table.png') });
// ---- 5. errorCount ----
results.errorCount = errors.length;
← 37affeb Slice 1: open-angle slider (qh_open_angle, 45° default, 20-9
·
back to Quadrille Showroom
·
/5x sweep-1 report: Slice-1 build verified clean (HTTP/rende 04ca167 →