← 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
A REVIEW.mdM public/js/showroom.jsM public/showroom.htmlA public/textures/floor-oak.pngA public/textures/floor-stone.pngA public/textures/wall-limewash.pngA scripts/gen-room-textures.js
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 & right within 4 ft (B)">📖</button>
+ <button id="btn-bookmatch" class="icon-btn active" title="Open board — wings open within 4 ft to present the full continuous design (B)">📖</button>
<button id="btn-walls" class="icon-btn active" title="Wallpaper the room walls on focus">▦</button>
<button id="btn-music" class="icon-btn" title="Listen to Music">♫</button>
<button id="btn-lighting" class="icon-btn" title="Lighting Mode">☼</button>
@@ -138,7 +138,7 @@
</div>
<div id="bottom-bar">
- <span id="info-text">WASD to walk up to a wing · within 4 ft it opens book-matched · Click to inspect · P to Peruse</span>
+ <span id="info-text">WASD to walk up to a wing · within 4 ft it opens to the full continuous design · Click to inspect · 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 — book-matched left & right</div>
- <div class="tip-row"><kbd>Click</kbd> Inspect a wing · <kbd>B</kbd> book-match</div>
+ <div class="tip-row"><kbd>Walk up</kbd> Within 4 ft a wing opens — full continuous design</div>
+ <div class="tip-row"><kbd>Click</kbd> Inspect a wing · <kbd>B</kbd> open board</div>
<div class="tip-row"><kbd>Space</kbd> Stop & inspect · <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 →