[object Object]

← back to Dw Design Bridge

feat: SVG pattern preview generator (botanical/deco/damask/pop/metallic/woven/patina/geometric)

c2c19fadb7c7c5a9f5501dbe90b7e3ee27e7ffcf · 2026-05-07 23:30:39 -0700 · Steve

Files touched

Diff

commit c2c19fadb7c7c5a9f5501dbe90b7e3ee27e7ffcf
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu May 7 23:30:39 2026 -0700

    feat: SVG pattern preview generator (botanical/deco/damask/pop/metallic/woven/patina/geometric)
---
 jobs/2026-05-08-06-04-13-5b665c.json |  51 ++++++++++++++
 jobs/2026-05-08-06-04-13-ad14fb.json |  30 ++++++++
 jobs/2026-05-08-06-04-13-fd95e4.json |  51 ++++++++++++++
 public/index.html                    |   6 +-
 server.js                            | 131 ++++++++++++++++++++++++++++++++++-
 5 files changed, 266 insertions(+), 3 deletions(-)

diff --git a/jobs/2026-05-08-06-04-13-5b665c.json b/jobs/2026-05-08-06-04-13-5b665c.json
new file mode 100644
index 0000000..b3df7d1
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-5b665c.json
@@ -0,0 +1,51 @@
+{
+  "id": "2026-05-08-06-04-13-5b665c",
+  "brief": "Modern apartment bedroom · pastel · peel-and-stick · botanical small-scale · serene",
+  "count": 3,
+  "status": "done",
+  "created_at": "2026-05-08T06:04:13.202Z",
+  "progress": "3/3",
+  "designs": [
+    {
+      "idx": 0,
+      "ok": true,
+      "design": {
+        "name": "Whispering Petals",
+        "pattern_concept": "Subtle botanical motifs in soft pastels, creating a calming, organic ambiance",
+        "palette": [
+          "#f5e6ea",
+          "#e0f2f1",
+          "#f0e6fa",
+          "#f5f5dc"
+        ],
+        "scale": "small",
+        "repeat": "drop match 21in vertical",
+        "material_pairing": "vinyl",
+        "style_tags": [
+          "modern",
+          "serene",
+          "botanical"
+        ],
+        "use_case": "modern apartment bedroom accent wall",
+        "marketing_copy": "Soft pastel botanicals bring tranquil elegance to modern bedrooms",
+        "trade_grade": "residential",
+        "price_band": "$$"
+      },
+      "generated_at": "2026-05-08T06:05:33.989Z"
+    },
+    {
+      "idx": 1,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:06:50.161Z"
+    },
+    {
+      "idx": 2,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:08:10.788Z"
+    }
+  ],
+  "started_at": "2026-05-08T06:04:13.202Z",
+  "finished_at": "2026-05-08T06:08:10.794Z"
+}
\ No newline at end of file
diff --git a/jobs/2026-05-08-06-04-13-ad14fb.json b/jobs/2026-05-08-06-04-13-ad14fb.json
new file mode 100644
index 0000000..ddfb07b
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-ad14fb.json
@@ -0,0 +1,30 @@
+{
+  "id": "2026-05-08-06-04-13-ad14fb",
+  "brief": "Restaurant private dining room · moody · art-deco geometric · gilded accents · silk-luxe",
+  "count": 3,
+  "status": "done",
+  "created_at": "2026-05-08T06:04:13.185Z",
+  "progress": "3/3",
+  "designs": [
+    {
+      "idx": 0,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:04:52.719Z"
+    },
+    {
+      "idx": 1,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:05:57.531Z"
+    },
+    {
+      "idx": 2,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:07:16.724Z"
+    }
+  ],
+  "started_at": "2026-05-08T06:04:13.185Z",
+  "finished_at": "2026-05-08T06:07:16.729Z"
+}
\ No newline at end of file
diff --git a/jobs/2026-05-08-06-04-13-fd95e4.json b/jobs/2026-05-08-06-04-13-fd95e4.json
new file mode 100644
index 0000000..6143634
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-fd95e4.json
@@ -0,0 +1,51 @@
+{
+  "id": "2026-05-08-06-04-13-fd95e4",
+  "brief": "Hotel lobby feature wall · trade-grade · jewel-toned · botanical large-scale · evening dramatic · class A vinyl",
+  "count": 3,
+  "status": "done",
+  "created_at": "2026-05-08T06:04:13.143Z",
+  "progress": "3/3",
+  "designs": [
+    {
+      "idx": 0,
+      "ok": true,
+      "design": {
+        "name": "Nocturnal Bloom",
+        "pattern_concept": "Dramatic, large-scale botanical motif with deep, jewel-toned foliage and flowers under a moody, twilight sky",
+        "palette": [
+          "#2E1B52",
+          "#4A235A",
+          "#5E3A8C",
+          "#1B1E3D"
+        ],
+        "scale": "large",
+        "repeat": "drop match 24in vertical",
+        "material_pairing": "vinyl",
+        "style_tags": [
+          "botanical",
+          "luxury",
+          "dramatic"
+        ],
+        "use_case": "hotel lobby feature wall",
+        "marketing_copy": "Dramatic botanical elegance in rich jewel tones for a statement hotel lobby wall",
+        "trade_grade": "class A",
+        "price_band": "$$$"
+      },
+      "generated_at": "2026-05-08T06:05:15.923Z"
+    },
+    {
+      "idx": 1,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:06:23.336Z"
+    },
+    {
+      "idx": 2,
+      "ok": false,
+      "error": "no JSON found in response",
+      "generated_at": "2026-05-08T06:07:43.227Z"
+    }
+  ],
+  "started_at": "2026-05-08T06:04:13.144Z",
+  "finished_at": "2026-05-08T06:07:43.234Z"
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 8079ef3..445ab42 100644
--- a/public/index.html
+++ b/public/index.html
@@ -82,15 +82,17 @@ async function refresh() {
       <div class="brief">${escapeHtml(j.brief)}</div>
       <div class="progress">${j.progress || ''} ${j.error ? '· ' + escapeHtml(j.error) : ''}</div>
       <div class="designs">
-        ${(j.designs || []).filter(d => d.ok).map(d => renderDesign(d.design)).join('')}
+        ${(j.designs || []).filter(d => d.ok).map(d => renderDesign(d.design, j.id, d.idx)).join('')}
       </div>
     </div>
   `).join('') || '<div class="job"><div class="brief" style="color:var(--muted)">No jobs yet — queue one above.</div></div>';
 }
 
-function renderDesign(d) {
+function renderDesign(d, jobId, idx) {
   const palette = (d.palette || []).map(c => `<span style="background:${escapeHtml(c)}"></span>`).join('');
+  const previewSrc = jobId != null && idx != null ? `/jobs/${jobId}/${idx}.svg` : '';
   return `<div class="design">
+    ${previewSrc ? `<img src="${previewSrc}" alt="${escapeHtml(d.name || '')}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px;margin-bottom:8px;display:block">` : ''}
     <div class="name">${escapeHtml(d.name || 'Untitled')}</div>
     <div class="palette">${palette}</div>
     <div class="meta">
diff --git a/server.js b/server.js
index cc05dea..b1d2d22 100644
--- a/server.js
+++ b/server.js
@@ -81,6 +81,125 @@ Output STRICTLY one JSON object on a single line, no prose, no markdown fence:
   }
 }
 
+// SVG pattern generator — uses the design's palette + style_tags to render a tileable preview.
+// Deterministic per (id, idx) so previews are stable. Runs free + instantly (no LLM call).
+function patternSvg(design, seed) {
+  const pal = (design.palette && design.palette.length) ? design.palette : ['#1a1a1a', '#c9a14b', '#e8e8e8', '#5a3a1a'];
+  const tags = (design.style_tags || []).map(t => String(t).toLowerCase());
+  const scale = design.scale || 'medium';
+  const tileSize = { small: 80, medium: 140, large: 220, oversize: 320 }[scale] || 140;
+
+  // Seeded PRNG so previews are stable
+  let s = 0;
+  for (let i = 0; i < seed.length; i++) s = (s * 31 + seed.charCodeAt(i)) | 0;
+  const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
+
+  // Pick motif family from style_tags
+  let motif = 'geometric';
+  if (tags.some(t => /botan|floral|leaf|garden|jungle|tropical/.test(t))) motif = 'botanical';
+  else if (tags.some(t => /deco|art.deco|geometric|sunburst|fan/.test(t))) motif = 'deco';
+  else if (tags.some(t => /damask|victorian|brocade|chinoiserie|toile/.test(t))) motif = 'damask';
+  else if (tags.some(t => /op.art|memphis|pop|psychedelic|mod/.test(t))) motif = 'pop';
+  else if (tags.some(t => /metallic|foil|chrome|mylar|silver|gold|gild/.test(t))) motif = 'metallic';
+  else if (tags.some(t => /natural|grasscloth|cork|jute|raffia|woven|texture/.test(t))) motif = 'woven';
+  else if (tags.some(t => /aged|distressed|patina|crackle/.test(t))) motif = 'patina';
+
+  const fg = pal[1] || pal[0];
+  const fg2 = pal[2] || fg;
+  const accent = pal[3] || fg2;
+
+  let pattern = '';
+  if (motif === 'botanical') {
+    // Stylized leaf cluster
+    const cx = tileSize / 2, cy = tileSize / 2;
+    pattern = `
+      <ellipse cx="${cx}" cy="${cy - tileSize*0.18}" rx="${tileSize*0.14}" ry="${tileSize*0.32}" fill="${fg}" opacity="0.85" transform="rotate(-15 ${cx} ${cy})"/>
+      <ellipse cx="${cx}" cy="${cy - tileSize*0.18}" rx="${tileSize*0.14}" ry="${tileSize*0.32}" fill="${fg2}" opacity="0.7" transform="rotate(20 ${cx} ${cy})"/>
+      <circle cx="${cx}" cy="${cy + tileSize*0.18}" r="${tileSize*0.06}" fill="${accent}"/>
+      <path d="M ${cx} ${cy + tileSize*0.05} Q ${cx - tileSize*0.18} ${cy - tileSize*0.08} ${cx - tileSize*0.32} ${cy + tileSize*0.18}" stroke="${fg}" stroke-width="2" fill="none"/>
+      <path d="M ${cx} ${cy + tileSize*0.05} Q ${cx + tileSize*0.18} ${cy - tileSize*0.08} ${cx + tileSize*0.32} ${cy + tileSize*0.18}" stroke="${fg}" stroke-width="2" fill="none"/>
+    `;
+  } else if (motif === 'deco') {
+    // Sunburst / fan
+    const cx = tileSize / 2, cy = tileSize;
+    let rays = '';
+    for (let r = 0; r < 9; r++) {
+      const a = (r / 8) * Math.PI - Math.PI;
+      const x = cx + Math.cos(a) * tileSize * 0.7;
+      const y = cy + Math.sin(a) * tileSize * 0.7;
+      rays += `<line x1="${cx}" y1="${cy}" x2="${x.toFixed(1)}" y2="${y.toFixed(1)}" stroke="${r % 2 ? fg : accent}" stroke-width="2"/>`;
+    }
+    pattern = rays + `<circle cx="${cx}" cy="${cy}" r="${tileSize*0.12}" fill="${fg2}"/>`;
+  } else if (motif === 'damask') {
+    // Center floret + corner mirror
+    const cx = tileSize / 2, cy = tileSize / 2;
+    pattern = `
+      <path d="M ${cx} ${cy - tileSize*0.32} Q ${cx + tileSize*0.18} ${cy - tileSize*0.10} ${cx} ${cy} Q ${cx - tileSize*0.18} ${cy - tileSize*0.10} ${cx} ${cy - tileSize*0.32} Z" fill="${fg}"/>
+      <path d="M ${cx} ${cy + tileSize*0.32} Q ${cx + tileSize*0.18} ${cy + tileSize*0.10} ${cx} ${cy} Q ${cx - tileSize*0.18} ${cy + tileSize*0.10} ${cx} ${cy + tileSize*0.32} Z" fill="${fg2}"/>
+      <circle cx="${cx}" cy="${cy}" r="${tileSize*0.05}" fill="${accent}"/>
+    `;
+  } else if (motif === 'pop') {
+    // Circles + dots
+    let dots = '';
+    for (let i = 0; i < 5; i++) {
+      const cx = rand() * tileSize, cy = rand() * tileSize;
+      const r = (tileSize * 0.06) + rand() * tileSize * 0.10;
+      const c = [pal[0], fg, fg2, accent][Math.floor(rand() * 4)];
+      dots += `<circle cx="${cx.toFixed(0)}" cy="${cy.toFixed(0)}" r="${r.toFixed(1)}" fill="${c}"/>`;
+    }
+    pattern = dots;
+  } else if (motif === 'metallic') {
+    // Diagonal lines + highlight
+    let lines = '';
+    for (let i = -2; i < 8; i++) {
+      const x = i * tileSize / 6;
+      lines += `<line x1="${x}" y1="0" x2="${x + tileSize}" y2="${tileSize}" stroke="${i % 2 ? fg : fg2}" stroke-width="${1.5 + (i % 3)}" opacity="0.7"/>`;
+    }
+    pattern = lines;
+  } else if (motif === 'woven') {
+    // Crosshatch weave
+    let weave = '';
+    const step = tileSize / 8;
+    for (let i = 0; i < 8; i++) {
+      const x = i * step;
+      weave += `<rect x="${x}" y="0" width="${step*0.5}" height="${tileSize}" fill="${i % 2 ? fg : fg2}" opacity="0.8"/>`;
+      weave += `<rect x="0" y="${x}" width="${tileSize}" height="${step*0.5}" fill="${i % 2 ? accent : fg2}" opacity="0.4"/>`;
+    }
+    pattern = weave;
+  } else if (motif === 'patina') {
+    // Distressed splotches
+    let splotches = '';
+    for (let i = 0; i < 12; i++) {
+      const cx = rand() * tileSize, cy = rand() * tileSize;
+      const rx = (tileSize * 0.04) + rand() * tileSize * 0.08;
+      const ry = rx * (0.5 + rand());
+      const c = [fg, fg2, accent][Math.floor(rand() * 3)];
+      splotches += `<ellipse cx="${cx.toFixed(0)}" cy="${cy.toFixed(0)}" rx="${rx.toFixed(1)}" ry="${ry.toFixed(1)}" fill="${c}" opacity="${(0.3 + rand() * 0.5).toFixed(2)}"/>`;
+    }
+    pattern = splotches;
+  } else {
+    // geometric default — diamond grid
+    const half = tileSize / 2;
+    pattern = `
+      <polygon points="${half},0 ${tileSize},${half} ${half},${tileSize} 0,${half}" fill="${fg}" opacity="0.85"/>
+      <polygon points="${half},${tileSize*0.18} ${tileSize*0.82},${half} ${half},${tileSize*0.82} ${tileSize*0.18},${half}" fill="${fg2}"/>
+      <circle cx="${half}" cy="${half}" r="${tileSize*0.06}" fill="${accent}"/>
+    `;
+  }
+
+  // 2x2 tiled preview to show repeat
+  const W = tileSize * 2, H = tileSize * 2;
+  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="100%" height="auto" preserveAspectRatio="xMidYMid slice">
+    <defs>
+      <pattern id="p${seed}" x="0" y="0" width="${tileSize}" height="${tileSize}" patternUnits="userSpaceOnUse">
+        <rect width="${tileSize}" height="${tileSize}" fill="${pal[0]}"/>
+        ${pattern}
+      </pattern>
+    </defs>
+    <rect width="${W}" height="${H}" fill="url(#p${seed})"/>
+  </svg>`;
+}
+
 async function runJob(id) {
   const job = loadJob(id);
   if (!job) return;
@@ -88,11 +207,18 @@ async function runJob(id) {
   job.started_at = new Date().toISOString();
   saveJob(id, job);
 
+  const jobDir = path.join(JOBS_DIR, id);
+  if (!fs.existsSync(jobDir)) fs.mkdirSync(jobDir, { recursive: true });
+
   const designs = [];
   for (let i = 0; i < job.count; i++) {
     try {
       const d = await generateDesign(job.brief);
-      designs.push({ idx: i, ok: true, design: d, generated_at: new Date().toISOString() });
+      const seed = `${id}-${i}`;
+      const svg = patternSvg(d, seed);
+      const svgPath = path.join(jobDir, `${i}.svg`);
+      fs.writeFileSync(svgPath, svg);
+      designs.push({ idx: i, ok: true, design: d, preview_url: `/jobs/${id}/${i}.svg`, generated_at: new Date().toISOString() });
     } catch (e) {
       designs.push({ idx: i, ok: false, error: e.message, generated_at: new Date().toISOString() });
     }
@@ -105,6 +231,9 @@ async function runJob(id) {
   saveJob(id, job);
 }
 
+// Serve generated SVG previews
+app.use('/jobs', express.static(JOBS_DIR));
+
 app.post('/api/jobs', async (req, res) => {
   const brief = String(req.body.brief || '').trim().slice(0, 1000);
   const count = Math.max(1, Math.min(20, parseInt(req.body.count) || 5));

← 312628c initial: dw-design-bridge — AI factory × DW design generator  ·  back to Dw Design Bridge  ·  fix: progressive-truncation JSON parser handles qwen3 traili f660c5c →