[object Object]

← back to Quadrille Showroom

Photoreal room (SDXL warm-oak floor + limewash plaster walls), brighter white-box light, CONTINUOUS open-board design (L=U0-0.5 / R=U0.5-1, no mirror), HUD copy

e7268c687c4903d22ea96edacc113ffe4f35d33b · 2026-06-27 10:48:23 -0700 · Steve

Files touched

Diff

commit e7268c687c4903d22ea96edacc113ffe4f35d33b
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 27 10:48:23 2026 -0700

    Photoreal room (SDXL warm-oak floor + limewash plaster walls), brighter white-box light, CONTINUOUS open-board design (L=U0-0.5 / R=U0.5-1, no mirror), HUD copy
---
 REVIEW.md                         |  66 ++++++++++++++++++++
 public/js/showroom.js             | 123 ++++++++++++++++++-------------------
 public/showroom.html              |   8 +--
 public/textures/floor-oak.png     | Bin 0 -> 991121 bytes
 public/textures/floor-stone.png   | Bin 0 -> 1350580 bytes
 public/textures/wall-limewash.png | Bin 0 -> 1104346 bytes
 scripts/gen-room-textures.js      | 126 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 255 insertions(+), 68 deletions(-)

diff --git a/REVIEW.md b/REVIEW.md
new file mode 100644
index 0000000..9131a99
--- /dev/null
+++ b/REVIEW.md
@@ -0,0 +1,66 @@
+# REVIEW — Quadrille Showroom vs a real Phillip Jeffries showroom photo
+
+Reviewer: VP Research & Content (officer-yolo). Date: 2026-06-27.
+Reference standard: a real PJ showroom photo — a bright white-box gallery, crisp,
+evenly-lit, large wallcovering panels you can read at a glance, calm and expensive.
+
+## Verdict (harsh, honest)
+
+**It does not yet look like a PJ showroom photo. It looks like a competent
+real-time Three.js demo of a wallpaper rack.** The gap is not "almost there" — it's
+a category gap: PJ photos read as *photographs of a real room*; this reads as *a 3D
+scene*. The single biggest tell is that **every surface is procedurally drawn** — the
+oak floor, the plaster walls, the ceiling — so they have that flat, vector-ish, "drawn
+in canvas" quality the eye instantly clocks as not-real. No real material grain, no
+believable light falloff, no photographic micro-detail.
+
+Scoring it against a PJ photo, blunt:
+
+| Dimension | PJ photo | This build | Grade |
+|---|---|---|---|
+| Material realism (floor/walls) | photoreal oak + plaster | procedural canvas — reads as cartoon | **D** |
+| Even, bright gallery light | bright, shadow-soft, white-box | dim, warm, museum-murky — too dark | **C-** |
+| Panel legibility (read the design) | large, flat, crisp, head-on | OK when open, but boards are tall+narrow slivers | **C+** |
+| Framing / staging | clean, uncluttered | mostly clean — olive tree + chairs are fine | **B-** |
+| Placards / brand typography | crisp engraved brass | legible but small; "CHINA SEAS" wall text is flat/low-craft | **C** |
+| Overall "is this a photo?" | yes | no — clearly a game engine | **D+** |
+
+## Specifics from the actual screenshots
+
+**01-overview.png** — The room is too DARK and too WARM for a PJ gallery. PJ
+showrooms are *bright white boxes*; this is a dim amber-lit space. The boards are tall
+narrow slabs with chunky dark-brown frames — they read like locker doors, not gallery
+panels. The floor is an obvious repeating procedural plank texture (you can see the
+tiling). The "CHINA SEAS" wall wordmark is small, flat gold, and low-resolution — it
+does not read as a crisp high-res brand mark. The whole frame is busy at the bottom
+(two HUD panels + a control bar + a thumbnail) which fights the "calm gallery" goal.
+
+**04-spec-card.png** — This is the BEST frame and it's genuinely close to usable: one
+board open, design large and readable, spec card on the right is clean and legible.
+BUT: the open board is a single tall panel that doesn't fill the frame the way a PJ
+hero panel does; the floor under it is still obviously procedural; and the lighting is
+flat/greenish on the pattern rather than the even white light a PJ photo would have.
+The wings flare out at the top (perspective) which slightly distorts the design.
+
+**02-bookmatch.png** — Confirms the STEP-2 problem: the open board currently shows a
+**mirrored book-match** (left leaf is a flip of the right), so you are NOT seeing the
+real continuous design — you're seeing a Rorschach of it. A buyer wants to see the
+*actual repeat*, not a mirror.
+
+## The three highest-impact fixes (what I will execute)
+
+1. **Photoreal materials** — replace the procedural canvas floor + plaster with real
+   photographic textures (warm-oak floor, honed limewash plaster), 2K, tileable,
+   max-anisotropy. This is the #1 lever on "is this a photo."
+2. **Brighter, more even gallery light** — lift exposure/ambient toward a white-box PJ
+   gallery; kill the amber murk; soften shadows further.
+3. **Continuous design on the open board** — left leaf = U 0..0.5, right leaf =
+   U 0.5..1 (computeRepeat for vertical tiling) so the open board shows the WHOLE real
+   design, not a mirror. Crisp, max-aniso, true colour.
+
+## Single biggest remaining gap (call it now)
+
+**Photographic material fidelity of the room shell.** Until the floor and walls stop
+reading as procedurally-drawn canvas and become real photo textures with believable
+light, the scene will always say "3D demo," never "PJ showroom photo." Everything else
+(framing, placards, brightness) is a B-grade polish problem; the materials are the D.
diff --git a/public/js/showroom.js b/public/js/showroom.js
index 952847f..40420ba 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -180,13 +180,33 @@ function buildEnvironment() {
 // Tag a texture as color data so the renderer gamma-corrects it (legacy three API)
 function srgb(tex) { if (tex && THREE.sRGBEncoding) tex.encoding = THREE.sRGBEncoding; return tex; }
 
+// Photoreal room textures (Replicate SDXL → public/textures/*.png). Loaded as
+// RepeatWrapping maps with max anisotropy so the warm-oak floor + limewash plaster
+// read as real PHOTOGRAPHS, not procedural canvas. repeat=(x,y) tiles the swatch.
+function loadTex(name, rx, ry) {
+  const t = new THREE.TextureLoader().load('/textures/' + name + '.png');
+  t.wrapS = t.wrapT = THREE.RepeatWrapping;
+  t.repeat.set(rx || 1, ry || 1);
+  t.minFilter = THREE.LinearMipmapLinearFilter;
+  t.magFilter = THREE.LinearFilter;
+  t.generateMipmaps = true;
+  if (renderer && renderer.capabilities && renderer.capabilities.getMaxAnisotropy) {
+    t.anisotropy = renderer.capabilities.getMaxAnisotropy();
+  }
+  return srgb(t);
+}
+
 // ============================================================
 // MATERIALS — all shared
 // ============================================================
 function initMaterials() {
   MAT.floor = new THREE.MeshStandardMaterial({ color: 0x9a7a52, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }); // warm satin oak
-  MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xe7e1d6, roughness: 0.96, metalness: 0.0 }); // warm plaster
-  MAT.wall = new THREE.MeshStandardMaterial({ color: 0xe2dccf, roughness: 0.95, metalness: 0.0, envMapIntensity: 0.35 }); // matte limewash plaster
+  MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xeae4da, roughness: 0.96, metalness: 0.0 }); // bright warm plaster
+  // PHOTOREAL limewash plaster (Replicate SDXL, public/textures/wall-limewash.png) —
+  // bright warm off-white with a faint hand-troweled mottle. Replaces the flat
+  // procedural wall so the room reads as a real PJ white-box gallery. color tint
+  // keeps it bright; low repeat = large-scale mottle.
+  MAT.wall = new THREE.MeshStandardMaterial({ map: loadTex('wall-limewash', 2, 1.4), color: 0xfbf7ef, roughness: 0.95, metalness: 0.0, envMapIntensity: 0.3 });
   MAT.chrome = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.25, metalness: 0.9, envMapIntensity: 1.0 }); // brushed brass
   MAT.wood = new THREE.MeshStandardMaterial({ color: 0x6b4a32, roughness: 0.5, metalness: 0.0, envMapIntensity: 0.5 }); // satin walnut
   MAT.chair = new THREE.MeshStandardMaterial({ color: 0x4a3528, roughness: 0.65, metalness: 0.05, envMapIntensity: 0.4 }); // cognac leather
@@ -199,27 +219,10 @@ function initMaterials() {
   MAT.books = [0x1a5276, 0x7d3c98, 0x1e8449, 0xb9770e, 0x922b21, 0x2c3e50, 0xd4ac0d, 0xa04000]
     .map(c => new THREE.MeshStandardMaterial({ color: c, roughness: 0.8, metalness: 0.0 }));
 
-  // Limewash plaster — large-scale soft mottle (NOT brick). Matches the other walls
-  // but with a faint hand-troweled cloudiness so the left wall reads as real plaster.
-  const bc = document.createElement('canvas');
-  bc.width = 256; bc.height = 256;
-  const bx = bc.getContext('2d');
-  bx.fillStyle = '#e2dccf'; bx.fillRect(0, 0, 256, 256);
-  // Soft overlapping translucent blobs = limewash cloudiness (warm + cool drift)
-  for (let i = 0; i < 40; i++) {
-    const cx = Math.random() * 256, cy = Math.random() * 256, r = 30 + Math.random() * 60;
-    const warm = Math.random() > 0.5;
-    const g = bx.createRadialGradient(cx, cy, 0, cx, cy, r);
-    const tint = warm ? '210,200,184' : '196,191,180';
-    g.addColorStop(0, `rgba(${tint},${0.05 + Math.random() * 0.06})`);
-    g.addColorStop(1, 'rgba(231,225,214,0)');
-    bx.fillStyle = g;
-    bx.beginPath(); bx.arc(cx, cy, r, 0, Math.PI * 2); bx.fill();
-  }
-  const bt = srgb(new THREE.CanvasTexture(bc));
-  bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(2, 2);
-  if (renderer.capabilities) bt.anisotropy = renderer.capabilities.getMaxAnisotropy();
-  MAT.brick = new THREE.MeshStandardMaterial({ map: bt, color: 0xe2dccf, roughness: 0.96, metalness: 0.0, envMapIntensity: 0.35 });
+  // Left wall — same PHOTOREAL limewash plaster (NOT brick), offset repeat so it
+  // doesn't read as a clone of the back wall. (MAT.brick name kept for the existing
+  // roomWalls reference; it is plaster now.)
+  MAT.brick = new THREE.MeshStandardMaterial({ map: loadTex('wall-limewash', 1.6, 1.6), color: 0xfbf7ef, roughness: 0.95, metalness: 0.0, envMapIntensity: 0.3 });
 }
 
 // ============================================================
@@ -316,33 +319,12 @@ function generatePatternCanvas(patternType, baseHex) {
 function buildRoom() {
   const W = CONFIG.room.width, D = CONFIG.room.depth, H = CONFIG.room.height;
 
-  // Floor — warm wide-plank oak (satin). Planks run the room depth with subtle grain
-  // streaks + hairline seams between boards.
-  const fc = document.createElement('canvas'); fc.width = 256; fc.height = 512;
-  const fx = fc.getContext('2d');
-  fx.fillStyle = '#9a7a52'; fx.fillRect(0, 0, 256, 512);
-  const planks = 4, pw = 256 / planks;
-  for (let p = 0; p < planks; p++) {
-    // Per-plank base tone variation
-    const base = 138 + (Math.random() * 26 - 13);
-    fx.fillStyle = `rgb(${base|0},${(base*0.79)|0},${(base*0.54)|0})`;
-    fx.fillRect(p * pw, 0, pw, 512);
-    // Lengthwise grain streaks
-    for (let s = 0; s < 36; s++) {
-      const gx = p * pw + Math.random() * pw;
-      fx.strokeStyle = `rgba(${(base*0.7)|0},${(base*0.55)|0},${(base*0.36)|0},${0.05 + Math.random() * 0.12})`;
-      fx.lineWidth = 0.5 + Math.random();
-      fx.beginPath(); fx.moveTo(gx, 0);
-      fx.bezierCurveTo(gx + (Math.random() - 0.5) * 6, 170, gx + (Math.random() - 0.5) * 6, 340, gx + (Math.random() - 0.5) * 4, 512);
-      fx.stroke();
-    }
-    // Hairline seam at plank edge
-    fx.strokeStyle = 'rgba(60,42,24,0.5)'; fx.lineWidth = 1;
-    fx.beginPath(); fx.moveTo(p * pw, 0); fx.lineTo(p * pw, 512); fx.stroke();
-  }
-  const ft = srgb(new THREE.CanvasTexture(fc)); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(3, 3);
-  if (renderer.capabilities) ft.anisotropy = renderer.capabilities.getMaxAnisotropy();
-  const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }));
+  // Floor — PHOTOREAL warm honey-oak wide-plank (Replicate SDXL, public/textures/
+  // floor-oak.png). Tiled ~2.2× across the room so the real plank grain reads at
+  // walking scale; max anisotropy keeps it crisp at grazing angle. (Was a
+  // procedural canvas that read as a cartoon — see REVIEW.md.)
+  const ft = loadTex('floor-oak', 2.4, 2.4);
+  const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.5, metalness: 0.0, envMapIntensity: 0.35 }));
   floorMesh.rotation.x = -Math.PI/2; floorMesh.receiveShadow = true; scene.add(floorMesh);
 
   // Ceiling
@@ -452,8 +434,8 @@ function revertWalls() {
 function buildLighting() {
   // Bright, EVEN gallery light — Phillip-Jeffries white-box: high ambient + hemi so the
   // wallcovering swatches read crisp and true-colour, not greyed by moody low-key shadow.
-  scene.add(new THREE.AmbientLight(0xffffff, 0.62));
-  scene.add(new THREE.HemisphereLight(0xfffaf2, 0xded6c8, 0.5)); // sky/ground bounce — warm, even
+  scene.add(new THREE.AmbientLight(0xffffff, 0.72));
+  scene.add(new THREE.HemisphereLight(0xfffdf8, 0xe6ded2, 0.58)); // sky/ground bounce — warm, even, brighter white-box
 
   // KEY light — soft, gentle shadow (clean contact, not dramatic)
   const key = new THREE.DirectionalLight(0xfff6ea, 0.7);
@@ -1051,8 +1033,14 @@ function loadWingTexture(faceMesh, imageUrl, onDone) {
   img.src = src;
 }
 
-// Load a wing's pattern onto BOTH leaves — right leaf normal, left leaf mirrored
-// across the spine = a true book-match. Cached per-URL (shares one decoded image).
+// Load a wing's pattern across BOTH leaves as ONE CONTINUOUS design (NOT a
+// book-match mirror): the open 2-leaf board presents the WHOLE real wallcovering.
+// Each leaf is half the board width, so the left leaf shows U 0..0.5 of the full
+// board span and the right leaf shows U 0.5..1, with the same vertical tiling.
+// computeRepeat gives the full-board horizontal repeats (rx) and vertical (ry);
+// each leaf draws HALF the horizontal span. Cached per-URL (shares one image).
+//   left  leaf:  repeat.x = rx/2, offset.x = 0
+//   right leaf:  repeat.x = rx/2, offset.x = rx/2
 function loadWingBook(pivot, onDone) {
   const ud = pivot.userData;
   const imageUrl = ud.pendingImage;
@@ -1065,35 +1053,42 @@ function loadWingBook(pivot, onDone) {
   // Low env wash + matte paper sheen so the pattern reads crisp and true-colour, not greyed.
   const leafMat = (tex) => new THREE.MeshStandardMaterial({ map: tex, roughness: 0.7, metalness: 0.0, envMapIntensity: 0.12 });
   const apply = (cached) => {
-    ud.faceR.material = leafMat(cached.texture);
-    ud.faceL.material = leafMat(cached.mirror);
+    ud.faceL.material = leafMat(cached.left);   // left half U 0..0.5
+    ud.faceR.material = leafMat(cached.right);  // right half U 0.5..1 — continuous
     if (product) product.isFullDesign = cached.isFullDesign;
     setWingBanner(pivot, cached.isFullDesign === false);
     if (onDone) onDone();
   };
 
-  if (imageTexCache[imageUrl] && imageTexCache[imageUrl].mirror) { apply(imageTexCache[imageUrl]); return; }
+  if (imageTexCache[imageUrl] && imageTexCache[imageUrl].left && imageTexCache[imageUrl].right) { apply(imageTexCache[imageUrl]); return; }
 
   const img = new Image();
   img.onload = () => {
     const isFD = (product && product.isSeamlessTile) ? true : detectFullDesign(img).isFullDesign;
-    const make = (mirror) => {
+    // `side` = 'L' (left half) or 'R' (right half) of the continuous design.
+    const make = (side) => {
       const t = new THREE.Texture(img);
       t.minFilter = THREE.LinearMipmapLinearFilter; t.magFilter = THREE.LinearFilter; t.generateMipmaps = true;
       t.anisotropy = maxAniso; srgb(t);
       if (isFD) {
         const { rx, ry } = computeRepeat(wingW, product, img);
         t.wrapS = t.wrapT = THREE.RepeatWrapping;
-        t.repeat.set(mirror ? -rx : rx, ry);
-        if (mirror) t.offset.x = rx;       // U' = rx·(1−U): mirror about the spine
+        // Each leaf = half the board's horizontal span. Together they reconstruct
+        // the full design left→right with no mirror, no seam.
+        t.repeat.set(rx / 2, ry);
+        t.offset.x = (side === 'R') ? rx / 2 : 0;
       } else {
-        t.wrapT = THREE.ClampToEdgeWrapping;
-        if (mirror) { t.wrapS = THREE.RepeatWrapping; t.repeat.x = -1; t.offset.x = 1; }
-        else { t.wrapS = THREE.ClampToEdgeWrapping; }
+        // Non-seamless catalog photo: contain-fit the WHOLE image across both leaves
+        // (left half of the photo on the left leaf, right half on the right) so the
+        // open board still shows the real product, just not tiled.
+        t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
+        t.repeat.set(0.5, 1);
+        t.offset.x = (side === 'R') ? 0.5 : 0;
       }
       t.needsUpdate = true; return t;
     };
-    const cached = { texture: make(false), mirror: make(true), isFullDesign: isFD };
+    const cached = { left: make('L'), right: make('R'), isFullDesign: isFD };
+    cached.texture = cached.right; // alias so cladWalls() can read cached.texture.image
     imageTexCache[imageUrl] = cached;
     apply(cached);
   };
diff --git a/public/showroom.html b/public/showroom.html
index 2597a8b..a8d3628 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -69,7 +69,7 @@
         <button class="nav-btn active" data-section="overview" title="Full View">Overview</button>
       </div>
       <div class="top-right">
-        <button id="btn-bookmatch" class="icon-btn active" title="Book-match on — wings open left &amp; right within 4 ft (B)">&#128214;</button>
+        <button id="btn-bookmatch" class="icon-btn active" title="Open board — wings open within 4 ft to present the full continuous design (B)">&#128214;</button>
         <button id="btn-walls" class="icon-btn active" title="Wallpaper the room walls on focus">&#9638;</button>
         <button id="btn-music" class="icon-btn" title="Listen to Music">&#9835;</button>
         <button id="btn-lighting" class="icon-btn" title="Lighting Mode">&#9788;</button>
@@ -138,7 +138,7 @@
     </div>
 
     <div id="bottom-bar">
-      <span id="info-text">WASD to walk up to a wing &middot; within 4 ft it opens book-matched &middot; Click to inspect &middot; P to Peruse</span>
+      <span id="info-text">WASD to walk up to a wing &middot; within 4 ft it opens to the full continuous design &middot; Click to inspect &middot; P to Peruse</span>
       <span id="fps-counter">60 FPS</span>
     </div>
   </div>
@@ -148,8 +148,8 @@
     <div class="tip-title">SHOWROOM CONTROLS</div>
     <div class="tip-row"><kbd>W A S D</kbd> Walk around</div>
     <div class="tip-row"><kbd>P</kbd> Peruse the collection (auto-tour)</div>
-    <div class="tip-row"><kbd>Walk up</kbd> Within 4 ft a wing opens &mdash; book-matched left &amp; right</div>
-    <div class="tip-row"><kbd>Click</kbd> Inspect a wing &middot; <kbd>B</kbd> book-match</div>
+    <div class="tip-row"><kbd>Walk up</kbd> Within 4 ft a wing opens &mdash; full continuous design</div>
+    <div class="tip-row"><kbd>Click</kbd> Inspect a wing &middot; <kbd>B</kbd> open board</div>
     <div class="tip-row"><kbd>Space</kbd> Stop &amp; inspect &middot; <kbd>Esc</kbd> back</div>
   </div>
 
diff --git a/public/textures/floor-oak.png b/public/textures/floor-oak.png
new file mode 100644
index 0000000..4b351af
Binary files /dev/null and b/public/textures/floor-oak.png differ
diff --git a/public/textures/floor-stone.png b/public/textures/floor-stone.png
new file mode 100644
index 0000000..156c24c
Binary files /dev/null and b/public/textures/floor-stone.png differ
diff --git a/public/textures/wall-limewash.png b/public/textures/wall-limewash.png
new file mode 100644
index 0000000..63b45d7
Binary files /dev/null and b/public/textures/wall-limewash.png differ
diff --git a/scripts/gen-room-textures.js b/scripts/gen-room-textures.js
new file mode 100644
index 0000000..fbb770c
--- /dev/null
+++ b/scripts/gen-room-textures.js
@@ -0,0 +1,126 @@
+#!/usr/bin/env node
+/**
+ * gen-room-textures.js — photoreal, tileable PBR-ish room textures via Replicate SDXL.
+ *
+ * Replaces the procedural canvas floor + plaster walls in showroom.js with real
+ * photographic textures so the scene reads as a PJ-grade showroom PHOTO, not a
+ * canvas-drawn 3D demo. Outputs 1024² (SDXL native) seamless textures to
+ * public/textures/<name>.png — the showroom loads them as MeshStandardMaterial maps
+ * with RepeatWrapping + max anisotropy.
+ *
+ * HARD RULE (Steve): always show $ cost. SDXL on Replicate ≈ $0.011/image. Per-call +
+ * running total printed. --execute required to spend; --ceiling caps it.
+ *
+ *   node scripts/gen-room-textures.js                 # dry-run: prompts + cost preview ($0)
+ *   node scripts/gen-room-textures.js --execute --ceiling 0.20
+ */
+const fs = require('fs');
+const path = require('path');
+
+const OUT = path.join(__dirname, '..', 'public', 'textures');
+fs.mkdirSync(OUT, { recursive: true });
+
+const args = process.argv.slice(2);
+const has = (f) => args.includes(f);
+const val = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
+const EXECUTE = has('--execute');
+const CEILING = parseFloat(val('--ceiling', '0.20'));
+
+// SDXL on Replicate: ~ $0.011 per 1024² image (A40, ~6-8s).
+const COST_PER_IMG = 0.011;
+
+const TOKEN = (() => {
+  try {
+    const env = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
+    const m = env.match(/^REPLICATE_API_TOKEN=(.+)$/m);
+    return m ? m[1].trim() : process.env.REPLICATE_API_TOKEN;
+  } catch { return process.env.REPLICATE_API_TOKEN; }
+})();
+
+// SDXL model version (stability-ai/sdxl). Seamless via "tileable" prompt cues +
+// shared seed; SDXL has no native tiling toggle, so prompts emphasise a flat
+// top-down planar swatch that mirror-tiles acceptably for a floor/wall ground.
+const SDXL_VERSION = '7762fd07cf82c948538e41f63f77d685e02b063e37e496e96eefd46c929f9bdc';
+
+const TEXTURES = [
+  {
+    name: 'floor-oak',
+    prompt: 'top-down flat-lay photograph of a warm honey-oak wide-plank wood floor, ' +
+      'satin matte finish, fine natural grain, subtle plank seams running vertically, ' +
+      'evenly lit soft daylight, no shadows, no objects, no people, ' +
+      'seamless tileable texture, architectural material swatch, photoreal, 8k, sharp',
+    negative: 'shadow, vignette, dark corners, perspective, furniture, rug, reflection glare, ' +
+      'cartoon, illustration, low resolution, blurry, text, watermark, pattern, tile grout',
+  },
+  {
+    name: 'wall-limewash',
+    prompt: 'flat photograph of a smooth matte limewash plaster wall, soft warm off-white ' +
+      'cream colour, faint hand-troweled cloudy mottle, very subtle texture, ' +
+      'bright even gallery lighting, no shadows, no objects, ' +
+      'seamless tileable wall surface, architectural material swatch, photoreal, 8k, sharp',
+    negative: 'brick, stone, cracks, mold, stain, shadow, vignette, dark, perspective, ' +
+      'furniture, cartoon, illustration, low resolution, blurry, text, watermark, heavy texture',
+  },
+  {
+    name: 'floor-stone',
+    prompt: 'top-down flat-lay photograph of a honed pale travertine stone floor, ' +
+      'large format, soft warm beige, very subtle veining, matte non-reflective, ' +
+      'evenly lit soft daylight, no shadows, no objects, ' +
+      'seamless tileable texture, architectural material swatch, photoreal, 8k, sharp',
+    negative: 'shadow, vignette, dark corners, perspective, furniture, glossy reflection, ' +
+      'cartoon, illustration, low resolution, blurry, text, watermark, heavy veins, cracks',
+  },
+];
+
+async function sdxl(t, seed) {
+  const create = await fetch('https://api.replicate.com/v1/predictions', {
+    method: 'POST',
+    headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json', 'Prefer': 'wait' },
+    body: JSON.stringify({
+      version: SDXL_VERSION,
+      input: {
+        prompt: t.prompt, negative_prompt: t.negative,
+        width: 1024, height: 1024, num_inference_steps: 32,
+        guidance_scale: 7, scheduler: 'K_EULER', seed, num_outputs: 1, apply_watermark: false,
+      },
+    }),
+  });
+  let pred = await create.json();
+  // Poll if not resolved by Prefer:wait
+  const t0 = Date.now();
+  while (pred.status && !['succeeded', 'failed', 'canceled'].includes(pred.status) && Date.now() - t0 < 120000) {
+    await new Promise(r => setTimeout(r, 1500));
+    const g = await fetch(pred.urls.get, { headers: { 'Authorization': `Bearer ${TOKEN}` } });
+    pred = await g.json();
+  }
+  if (pred.status !== 'succeeded') throw new Error('SDXL ' + (pred.status || 'no-status') + ' ' + JSON.stringify(pred.error || pred.detail || ''));
+  const out = Array.isArray(pred.output) ? pred.output[0] : pred.output;
+  const img = await fetch(out);
+  return Buffer.from(await img.arrayBuffer());
+}
+
+(async () => {
+  console.log(`gen-room-textures — ${TEXTURES.length} SDXL textures @ ~$${COST_PER_IMG}/img`);
+  console.log(`  est total: $${(TEXTURES.length * COST_PER_IMG).toFixed(3)}  (ceiling $${CEILING.toFixed(2)})`);
+  if (!EXECUTE) {
+    console.log('\nDRY RUN ($0) — pass --execute to generate. Prompts:');
+    TEXTURES.forEach(t => console.log(`  • ${t.name}: ${t.prompt.slice(0, 80)}…`));
+    return;
+  }
+  if (!TOKEN) { console.error('No REPLICATE_API_TOKEN'); process.exit(1); }
+  let spent = 0;
+  for (const t of TEXTURES) {
+    if (spent + COST_PER_IMG > CEILING) { console.log(`  ⛔ ceiling $${CEILING} hit at $${spent.toFixed(3)} — stopping`); break; }
+    process.stdout.write(`  → ${t.name} … `);
+    try {
+      const buf = await sdxl(t, 42);
+      const dest = path.join(OUT, `${t.name}.png`);
+      fs.writeFileSync(dest, buf);
+      spent += COST_PER_IMG;
+      console.log(`saved ${(buf.length / 1024) | 0}KB · this $${COST_PER_IMG.toFixed(3)} · running $${spent.toFixed(3)}`);
+    } catch (e) {
+      console.log(`FAIL: ${e.message}`);
+    }
+  }
+  console.log(`\nTOTAL SPENT: $${spent.toFixed(3)}  (Replicate SDXL)`);
+})().catch(e => { console.error('FAILED:', e.message); process.exit(1); });

← 1793384 Add 2-min soak-test recorder (walk/open/Back-Next/Peruse; lo  ·  back to Quadrille Showroom  ·  PJ polish: true hi-res (4096px) engraved-brass wall wordmark a08b36a →