[object Object]

← back to Wallco Ai

generator: add Texture component picker (15 real DW natural-material grounds) — wired UI->payload->prompt so designs paint on a real grasscloth/cork/raffia/silk/etc. ground per the hard texture-anchor rule

06a0e7c797fa0417d5c1a04cadcb46ac5e8d0a08 · 2026-06-01 16:40:45 -0700 · Steve Abrams

Files touched

Diff

commit 06a0e7c797fa0417d5c1a04cadcb46ac5e8d0a08
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 16:40:45 2026 -0700

    generator: add Texture component picker (15 real DW natural-material grounds) — wired UI->payload->prompt so designs paint on a real grasscloth/cork/raffia/silk/etc. ground per the hard texture-anchor rule
---
 server.js | 146 ++++++++++++++++++++++++++++++++++++++++----------------------
 1 file changed, 95 insertions(+), 51 deletions(-)

diff --git a/server.js b/server.js
index fbf6338..3b32b6e 100644
--- a/server.js
+++ b/server.js
@@ -21643,7 +21643,7 @@ app.post('/api/generator/batch', (req, res) => {
   // its siblings (palette/cooldowns/digest/recipes) all carry — so an anonymous
   // POST could burn the houses' GPU at will. Gate it consistently.
   if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
-  const { colors = '', elements = [], styles = [], patterns = [], extra_text = '' } = req.body || {};
+  const { colors = '', elements = [], styles = [], texture = [], patterns = [], extra_text = '' } = req.body || {};
   // Clamp the requested count to a sane 1–50 so a fat-fingered number can't
   // spawn a runaway render job.
   let n = Math.max(1, Math.min(50, parseInt(req.body && req.body.n, 10) || 1));
@@ -21654,6 +21654,9 @@ app.post('/api/generator/batch', (req, res) => {
   const patternTitles = patterns.map(p => p.title).filter(Boolean).slice(0, 3);
   const patternHex  = patterns.map(p => p.dominant_hex).filter(Boolean);
   const allColors = [colors.trim(), ...patternHex.slice(0, 4)].filter(Boolean).join(', ');
+  // Texture component (hard project rule): the motif must paint ON a real
+  // natural-material ground, never a flat paper. Normalize + cap to the first 2.
+  const texList = (Array.isArray(texture) ? texture : [texture]).filter(Boolean).map(String).slice(0, 2);
   const promptParts = [
     'A seamless wallpaper pattern',
     artistLine.length ? `in the visual lineage of ${artistLine.join(' and ')}` : '',
@@ -21661,6 +21664,7 @@ app.post('/api/generator/batch', (req, res) => {
     elements.length  ? `featuring ${elements.slice(0,8).join(', ')}` : '',
     patternTitles.length ? `referencing the look of ${patternTitles.join(' / ')}` : '',
     allColors        ? `palette ${allColors}` : '',
+    texList.length   ? `painted on a real ${texList.join(' and ')} natural wallcovering texture, the woven ${texList[0]} weave and slubs showing through` : '',
     extra_text || '',
     'high detail, museum-quality, 24x24 inch tileable, no text, no signature.'
   ].filter(Boolean);
@@ -21948,7 +21952,7 @@ app.get('/generator', (_req, res) => {
 ${htmlHeader('')}
 <main style="padding:32px 40px;max-width:1300px;margin:0 auto">
   <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:32px;margin:0 0 4px">Pattern Generator</h1>
-  <p style="color:#666;margin:0 0 28px">Every 30 min, the cron picks a recipe → fetches a palette from recent DW Shopify orders → renders a new design.</p>
+  <p style="color:#666;margin:0 0 28px">Every 30 min, the cron picks a recipe → seeds a palette from the style-guide houses → renders a new design.</p>
 
   <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:28px">
     <div style="padding:18px;border:1px solid #ddd;border-radius:8px;background:#fafafa">
@@ -21961,9 +21965,9 @@ ${htmlHeader('')}
       </div>
     </div>
     <div style="padding:18px;border:1px solid #ddd;border-radius:8px;background:#fafafa">
-      <h2 style="margin:0 0 8px;font-size:16px">DW orders palette</h2>
-      <p id="palette-status" style="margin:0 0 10px;font-size:13px;color:#555">Loading…</p>
-      <button class="btn-outline" id="btn-refresh-orders">Refresh from Shopify (last 50 orders)</button>
+      <h2 style="margin:0 0 8px;font-size:16px">Style guide palettes</h2>
+      <p id="sg-status" style="margin:0 0 10px;font-size:13px;color:#555">Loading…</p>
+      <a class="btn-outline" href="/admin/styleguides">Manage style guides</a>
     </div>
   </div>
 
@@ -21988,6 +21992,9 @@ ${htmlHeader('')}
       <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Styles</label>
       <div id="batch-styles-picker" style="display:flex;flex-wrap:wrap;gap:5px;max-height:80px;overflow-y:auto;padding:4px"></div>
 
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Texture <span style="text-transform:none;color:#666">(natural ground — the design paints on top)</span></label>
+      <div id="batch-texture-picker" style="display:flex;flex-wrap:wrap;gap:5px;max-height:80px;overflow-y:auto;padding:4px"></div>
+
       <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Patterns</label>
       <div>
         <input id="batch-patterns" type="text" placeholder="Search base patterns (peacock, baroque, Morris…)" style="width:100%;padding:6px 10px;background:#0f0e0c;color:#fff;border:1px solid #444;border-radius:4px;font-family:inherit;font-size:13px">
@@ -22021,27 +22028,16 @@ ${htmlHeader('')}
   <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:24px 0 10px">Recent runs</h2>
   <div id="runs"></div>
 
-  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 6px">DW Orders palette (last 40)</h2>
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 6px">Style guide palettes</h2>
+  <p style="color:#666;margin:0 0 12px;font-size:13px">Click a swatch to add its hex to the batch <b>Colors</b> field above — or click a house name to load its whole palette.</p>
   <div style="display:flex;gap:14px;align-items:center;margin:6px 0 12px;font:11px var(--sans,system-ui)">
-    <span id="palette-status-2" style="color:#555">Loading…</span>
+    <span id="sg-status-2" style="color:#555">Loading…</span>
     <label style="display:inline-flex;align-items:center;gap:8px;letter-spacing:.08em;text-transform:uppercase;color:#888;margin-left:auto">Tile size
-      <input type="range" id="palette-density" min="80" max="240" step="20" value="160" style="flex:0 0 120px">
+      <input type="range" id="sg-density" min="80" max="240" step="20" value="160" style="flex:0 0 120px">
     </label>
-    <span id="palette-density-readout" style="font-variant-numeric:tabular-nums;color:#555">160px</span>
+    <span id="sg-density-readout" style="font-variant-numeric:tabular-nums;color:#555">160px</span>
   </div>
-  <div id="palette" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px"></div>
-  <script>
-  (function(){
-    var sl = document.getElementById('palette-density');
-    var grid = document.getElementById('palette');
-    var ro = document.getElementById('palette-density-readout');
-    var saved = parseInt(localStorage.getItem('wallco-palette-density')||'',10);
-    if (saved >= 80 && saved <= 240) sl.value = saved;
-    function paint(v){ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax('+v+'px, 1fr))'; ro.textContent = v+'px'; }
-    paint(sl.value);
-    sl.addEventListener('input', function(){ paint(this.value); localStorage.setItem('wallco-palette-density', this.value); });
-  })();
-  </script>
+  <div id="styleguides"></div>
 </main>
 ${FOOTER}
 
@@ -22057,23 +22053,47 @@ async function loadCron() {
     ? '<span style="color:#155724;font-weight:500">✓ LOADED</span> — <code style="font-size:11px">'+j.status+'</code>'
     : '<span style="color:#856404;font-weight:500">○ STOPPED</span> — click Start cron to enable 30-min auto-generation.';
 }
-async function loadPaletteStatus() {
-  var p = await get('/api/generator/palette');
-  var msg = p.length + ' palette swatches from recent DW orders';
-  var s1 = document.getElementById('palette-status'); if (s1) s1.textContent = msg;
-  var s2 = document.getElementById('palette-status-2'); if (s2) s2.textContent = msg;
-  document.getElementById('palette').innerHTML = p.map(function(x){
-    var title = (x.product_title||x.sku||'').replace(/"/g,'&quot;');
-    var tagSummary = ((x.style_tags||[]).slice(0,3).join(' · '));
-    return '<button type="button" data-hex="'+x.dominant_hex+'" title="'+title+(tagSummary?' — '+tagSummary:'')+'"' +
-      ' onclick="(function(h){var el=document.getElementById(\\'batch-colors\\');if(!el)return;var cur=el.value.trim();el.value=cur?(cur+\\', \\'+h):h;el.focus();})(this.dataset.hex)"' +
-      ' style="text-align:left;background:transparent;border:0;padding:0;cursor:pointer">' +
-      '<div style="aspect-ratio:1;background:'+x.dominant_hex+';border:1px solid rgba(0,0,0,.08);border-radius:6px;transition:transform .15s"></div>' +
-      '<div style="font-size:10px;color:#666;margin-top:4px;font-family:ui-monospace,monospace">'+x.dominant_hex+'</div>' +
-      '<div style="font-size:10px;color:#333;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+title+'</div>' +
-      (x.vendor ? '<div style="font-size:9px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+x.vendor+'</div>' : '') +
-      '</button>';
-  }).join('') || '<p style="color:#999;grid-column:1/-1">No palette data yet. Click "Refresh from Shopify" above.</p>';
+function sgEscAttr(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;'); }
+function applySgDensity(){
+  var sl = document.getElementById('sg-density'); if (!sl) return;
+  var v = sl.value;
+  var ro = document.getElementById('sg-density-readout'); if (ro) ro.textContent = v+'px';
+  document.querySelectorAll('#styleguides .sg-grid').forEach(function(g){
+    g.style.gridTemplateColumns = 'repeat(auto-fill, minmax('+v+'px, 1fr))';
+  });
+}
+// Replaces the old DW-orders palette: the clickable color source is now the
+// style-guide houses (/api/styleguides/brands — fashion + wallcovering + custom).
+async function loadStyleguidePalettes() {
+  var j; try { j = await get('/api/styleguides/brands'); } catch (e) { j = null; }
+  var groups = (j && j.ok) ? [].concat(j.wallcovering||[], j.fashion||[], j.custom||[]) : [];
+  var count = groups.reduce(function(a,g){ return a + ((g.hex||[]).length); }, 0);
+  var msg = groups.length + ' style guides · ' + count + ' swatches';
+  var s1 = document.getElementById('sg-status'); if (s1) s1.textContent = msg;
+  var s2 = document.getElementById('sg-status-2'); if (s2) s2.textContent = msg;
+  var wrap = document.getElementById('styleguides');
+  if (!groups.length) { wrap.innerHTML = '<p style="color:#999">No style guides available — add one on the Style Guides page.</p>'; return; }
+  wrap.innerHTML = groups.map(function(g){
+    var hexes = (g.hex||[]).filter(function(h){ return /^#[0-9a-fA-F]{6}$/.test(h); });
+    var meta = [g.cat, g.country, g.est].filter(Boolean).join(' · ');
+    var swatches = hexes.map(function(h){
+      return '<button type="button" class="sg-swatch" data-hex="'+h+'" title="'+sgEscAttr(g.label+' '+h)+'"' +
+        ' style="text-align:left;background:transparent;border:0;padding:0;cursor:pointer">' +
+        '<div style="aspect-ratio:1;background:'+h+';border:1px solid rgba(0,0,0,.08);border-radius:6px;transition:transform .15s"></div>' +
+        '<div style="font-size:10px;color:#666;margin-top:4px;font-family:ui-monospace,monospace">'+h+'</div>' +
+        '</button>';
+    }).join('');
+    return '<div style="margin:0 0 18px">' +
+      '<div style="display:flex;align-items:baseline;gap:10px;margin:0 0 8px">' +
+        '<button type="button" class="sg-house" data-hexes="'+sgEscAttr(hexes.join(','))+'" title="Load all '+hexes.length+' colors into Colors"' +
+          ' style="background:none;border:0;padding:0;cursor:pointer;font-size:18px;color:#1a1a1a">'+sgEscAttr(g.label)+'</button>' +
+        (meta ? '<span style="font-size:11px;color:#999;text-transform:uppercase;letter-spacing:.06em">'+sgEscAttr(meta)+'</span>' : '') +
+        (g.signature ? '<span style="font-size:11px;color:#aaa;font-style:italic;margin-left:auto;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+sgEscAttr(g.signature)+'</span>' : '') +
+      '</div>' +
+      '<div class="sg-grid" style="display:grid;gap:10px">'+swatches+'</div>' +
+    '</div>';
+  }).join('');
+  applySgDensity();
 }
 async function loadRecipes() {
   var r = await get('/api/generator/recipes');
@@ -22139,15 +22159,20 @@ document.getElementById('btn-run-now').addEventListener('click', async function(
   alert((j.ok?'OK':'FAIL')+'\\n\\n'+(j.output||'').slice(-800));
   loadRuns();
 });
-document.getElementById('btn-refresh-orders').addEventListener('click', async function(){
-  this.disabled = true;
-  this.textContent = 'Fetching…';
-  var j = await post('/api/generator/refresh-orders', { limit: 50 });
-  this.disabled = false;
-  this.textContent = 'Refresh from Shopify (last 50 orders)';
-  alert((j.ok?'OK':'FAIL')+'\\n\\n'+(j.output||'').slice(-800));
-  loadPaletteStatus();
+// Style-guide swatch clicks feed the batch Colors field (event-delegated so it
+// survives re-render). Swatch → append one hex; house name → load whole palette.
+document.getElementById('styleguides').addEventListener('click', function(e){
+  var el = document.getElementById('batch-colors'); if (!el) return;
+  var sw = e.target.closest('.sg-swatch');
+  if (sw) { var h = sw.dataset.hex; var cur = el.value.trim(); el.value = cur ? (cur+', '+h) : h; el.focus(); return; }
+  var hs = e.target.closest('.sg-house');
+  if (hs) { el.value = (hs.dataset.hexes||'').split(',').filter(Boolean).join(', '); el.focus(); }
+});
+document.getElementById('sg-density').addEventListener('input', function(){
+  applySgDensity(); localStorage.setItem('wallco-sg-density', this.value);
 });
+(function(){ var s = parseInt(localStorage.getItem('wallco-sg-density')||'',10);
+  if (s>=80 && s<=240) document.getElementById('sg-density').value = s; })();
 
 document.getElementById('recipe-form').addEventListener('submit', async function(e){
   e.preventDefault();
@@ -22166,9 +22191,17 @@ document.getElementById('recipe-form').addEventListener('submit', async function
 });
 
 // ── ONE-STEP BATCH GENERATE ────────────────────────────────────────────
-var BATCH = { elements: new Set(), styles: new Set(), patterns: [] };
+var BATCH = { elements: new Set(), styles: new Set(), texture: new Set(), patterns: [] };
+
+// Canonical DW natural-material grounds — every design paints on a REAL textile
+// (hard project rule), never a flat hallucinated paper. The chosen texture is
+// injected into the prompt as the ground so the weave/slubs show through.
+var TEXTURE_OPTIONS = ['grasscloth','cork','raffia','mica','linen','silk','sisal','abaca','seagrass','jute','hemp','arrowroot','paperweave','bamboo','reed'];
 
 async function loadBatchPickers(){
+  document.getElementById('batch-texture-picker').innerHTML = TEXTURE_OPTIONS.map(function(t){
+    return '<button onclick="toggleBatchTexture(\''+t+'\')" data-tex="'+t+'" style="padding:3px 9px;font-size:11px;border-radius:11px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer;text-transform:capitalize">'+t+'</button>';
+  }).join('');
   var [styles, elements] = await Promise.all([
     fetch('/api/studio/styles').then(r=>r.json()),
     fetch('/api/studio/elements').then(r=>r.json())
@@ -22213,6 +22246,16 @@ window.toggleBatchStyle = function(name){
     btn.style.borderColor = on ? '#d2b15c' : '#555';
   }
 };
+window.toggleBatchTexture = function(name){
+  if (BATCH.texture.has(name)) BATCH.texture.delete(name); else BATCH.texture.add(name);
+  var btn = document.querySelector('button[data-tex="'+name+'"]');
+  if (btn) {
+    var on = BATCH.texture.has(name);
+    btn.style.background = on ? '#d2b15c' : '#0f0e0c';
+    btn.style.color = on ? '#1a1a1a' : '#aaa';
+    btn.style.borderColor = on ? '#d2b15c' : '#555';
+  }
+};
 
 document.getElementById('batch-patterns').addEventListener('input', async function(e){
   var q = e.target.value.trim();
@@ -22244,8 +22287,9 @@ document.getElementById('btn-batch-go').addEventListener('click', async function
   var colors = document.getElementById('batch-colors').value.trim();
   var elements = Array.from(BATCH.elements);
   var styles = Array.from(BATCH.styles);
+  var texture = Array.from(BATCH.texture);
   var patterns = BATCH.patterns;
-  if (!colors && !elements.length && !styles.length && !patterns.length) {
+  if (!colors && !elements.length && !styles.length && !texture.length && !patterns.length) {
     if (!confirm('No selections — use the default Morris/Merian recipe for all ' + n + ' designs?')) return;
   }
   this.disabled = true;
@@ -22257,7 +22301,7 @@ document.getElementById('btn-batch-go').addEventListener('click', async function
   try {
     var j = await fetch('/api/generator/batch', {
       method:'POST', headers:{'Content-Type':'application/json'},
-      body: JSON.stringify({ n: n, colors: colors, elements: elements, styles: styles,
+      body: JSON.stringify({ n: n, colors: colors, elements: elements, styles: styles, texture: texture,
         patterns: patterns.map(function(p){return {source:p.source, id:p.id, title:p.title, artist:p.artist, tags:p.tags, dominant_hex:p.dominant_hex};})
       })
     }).then(r=>r.json());
@@ -22285,7 +22329,7 @@ document.getElementById('btn-batch-go').addEventListener('click', async function
 });
 
 loadBatchPickers();
-loadCron(); loadPaletteStatus(); loadRecipes(); loadRuns();
+loadCron(); loadStyleguidePalettes(); loadRecipes(); loadRuns();
 setInterval(loadRuns, 30000);
 </script>
 ${HAMBURGER_JS}

← bd05114 PDP v11-best (live theme): Wallpaper/Wall Mural toggle on ev  ·  back to Wallco Ai  ·  add Path B surgical color-name backfill (re-derive title col 46878b0 →