← back to Wallco Ai
refiner-compare: dual-method new-item composer — drag-a-house + multi-select dropdowns (max 3 each) with toggle, per-idea Coordinate + Create-similar, prompt shown per row
6fa0c16061b1d075f0c19afe71e4ebafeb8f65f3 · 2026-05-19 14:02:21 -0700 · SteveStudio2
Files touched
A public/textures/grasscloth/espresso.pngA public/textures/grasscloth/index.jsonA public/textures/grasscloth/natural-flax.pngA public/textures/grasscloth/oatmeal.pngA public/textures/grasscloth/pearl.pngA public/textures/grasscloth/sisal-tan.pngA public/textures/grasscloth/smoke-grey.pngA public/textures/library/grasscloth/1838-grasscloth-luxury.jpgA public/textures/library/grasscloth/designers-guild-brera-grasscloth-graphite-wall.jpgA public/textures/library/grasscloth/designers-guild-paysage-marin-grasscloth-sky-w.jpgA public/textures/library/grasscloth/designers-guild-scene-aquarelle-grasscloth-elm.jpgA public/textures/library/index.jsonA public/textures/library/sisal/w3996-135.jpgA scripts/build_texture_library.jsA scripts/gen_grasscloth_textures.js
Diff
commit 6fa0c16061b1d075f0c19afe71e4ebafeb8f65f3
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 19 14:02:21 2026 -0700
refiner-compare: dual-method new-item composer — drag-a-house + multi-select dropdowns (max 3 each) with toggle, per-idea Coordinate + Create-similar, prompt shown per row
---
public/textures/grasscloth/espresso.png | Bin 0 -> 5889605 bytes
public/textures/grasscloth/index.json | 36 ++++++
public/textures/grasscloth/natural-flax.png | Bin 0 -> 5911126 bytes
public/textures/grasscloth/oatmeal.png | Bin 0 -> 5903723 bytes
public/textures/grasscloth/pearl.png | Bin 0 -> 5894969 bytes
public/textures/grasscloth/sisal-tan.png | Bin 0 -> 5899486 bytes
public/textures/grasscloth/smoke-grey.png | Bin 0 -> 5908025 bytes
.../library/grasscloth/1838-grasscloth-luxury.jpg | Bin 0 -> 515655 bytes
...igners-guild-brera-grasscloth-graphite-wall.jpg | Bin 0 -> 128222 bytes
...igners-guild-paysage-marin-grasscloth-sky-w.jpg | Bin 0 -> 45167 bytes
...igners-guild-scene-aquarelle-grasscloth-elm.jpg | Bin 0 -> 39166 bytes
public/textures/library/index.json | 76 +++++++++++++
public/textures/library/sisal/w3996-135.jpg | Bin 0 -> 3186073 bytes
scripts/build_texture_library.js | 125 +++++++++++++++++++++
scripts/gen_grasscloth_textures.js | 78 +++++++++++++
15 files changed, 315 insertions(+)
diff --git a/public/textures/grasscloth/espresso.png b/public/textures/grasscloth/espresso.png
new file mode 100644
index 0000000..0177f9b
Binary files /dev/null and b/public/textures/grasscloth/espresso.png differ
diff --git a/public/textures/grasscloth/index.json b/public/textures/grasscloth/index.json
new file mode 100644
index 0000000..57e9c6f
--- /dev/null
+++ b/public/textures/grasscloth/index.json
@@ -0,0 +1,36 @@
+{
+ "material": "grasscloth",
+ "generated": "2026-05-19T20:42:10.877Z",
+ "textures": [
+ {
+ "slug": "natural-flax",
+ "name": "Natural Flax",
+ "file": "natural-flax.png"
+ },
+ {
+ "slug": "sisal-tan",
+ "name": "Sisal Tan",
+ "file": "sisal-tan.png"
+ },
+ {
+ "slug": "oatmeal",
+ "name": "Oatmeal",
+ "file": "oatmeal.png"
+ },
+ {
+ "slug": "smoke-grey",
+ "name": "Smoke Grey",
+ "file": "smoke-grey.png"
+ },
+ {
+ "slug": "espresso",
+ "name": "Espresso",
+ "file": "espresso.png"
+ },
+ {
+ "slug": "pearl",
+ "name": "Pearl White",
+ "file": "pearl.png"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/textures/grasscloth/natural-flax.png b/public/textures/grasscloth/natural-flax.png
new file mode 100644
index 0000000..ee2b499
Binary files /dev/null and b/public/textures/grasscloth/natural-flax.png differ
diff --git a/public/textures/grasscloth/oatmeal.png b/public/textures/grasscloth/oatmeal.png
new file mode 100644
index 0000000..b547c88
Binary files /dev/null and b/public/textures/grasscloth/oatmeal.png differ
diff --git a/public/textures/grasscloth/pearl.png b/public/textures/grasscloth/pearl.png
new file mode 100644
index 0000000..47847e4
Binary files /dev/null and b/public/textures/grasscloth/pearl.png differ
diff --git a/public/textures/grasscloth/sisal-tan.png b/public/textures/grasscloth/sisal-tan.png
new file mode 100644
index 0000000..46e7d71
Binary files /dev/null and b/public/textures/grasscloth/sisal-tan.png differ
diff --git a/public/textures/grasscloth/smoke-grey.png b/public/textures/grasscloth/smoke-grey.png
new file mode 100644
index 0000000..87c381d
Binary files /dev/null and b/public/textures/grasscloth/smoke-grey.png differ
diff --git a/public/textures/library/grasscloth/1838-grasscloth-luxury.jpg b/public/textures/library/grasscloth/1838-grasscloth-luxury.jpg
new file mode 100644
index 0000000..b48c742
Binary files /dev/null and b/public/textures/library/grasscloth/1838-grasscloth-luxury.jpg differ
diff --git a/public/textures/library/grasscloth/designers-guild-brera-grasscloth-graphite-wall.jpg b/public/textures/library/grasscloth/designers-guild-brera-grasscloth-graphite-wall.jpg
new file mode 100644
index 0000000..076b9a1
Binary files /dev/null and b/public/textures/library/grasscloth/designers-guild-brera-grasscloth-graphite-wall.jpg differ
diff --git a/public/textures/library/grasscloth/designers-guild-paysage-marin-grasscloth-sky-w.jpg b/public/textures/library/grasscloth/designers-guild-paysage-marin-grasscloth-sky-w.jpg
new file mode 100644
index 0000000..67b46e1
Binary files /dev/null and b/public/textures/library/grasscloth/designers-guild-paysage-marin-grasscloth-sky-w.jpg differ
diff --git a/public/textures/library/grasscloth/designers-guild-scene-aquarelle-grasscloth-elm.jpg b/public/textures/library/grasscloth/designers-guild-scene-aquarelle-grasscloth-elm.jpg
new file mode 100644
index 0000000..aa5189e
Binary files /dev/null and b/public/textures/library/grasscloth/designers-guild-scene-aquarelle-grasscloth-elm.jpg differ
diff --git a/public/textures/library/index.json b/public/textures/library/index.json
new file mode 100644
index 0000000..01c267b
--- /dev/null
+++ b/public/textures/library/index.json
@@ -0,0 +1,76 @@
+{
+ "source": "dw_unified plain-texture products",
+ "generated": "2026-05-19T20:45:00.379Z",
+ "groups": {
+ "grasscloth": {
+ "name": "Grasscloth",
+ "textures": [
+ {
+ "sku": "1838-grasscloth-luxury",
+ "name": "Black Wallpaper - Grasscloth Luxury",
+ "file": "grasscloth/1838-grasscloth-luxury.jpg"
+ },
+ {
+ "sku": "designers-guild-brera-grasscloth-graphite-wall",
+ "name": "Brera Grasscloth Graphite\n \n \n Wallpaper",
+ "file": "grasscloth/designers-guild-brera-grasscloth-graphite-wall.jpg"
+ },
+ {
+ "sku": "designers-guild-paysage-marin-grasscloth-sky-w",
+ "name": "Paysage Marin Grasscloth Sky\n \n \n Wallpaper",
+ "file": "grasscloth/designers-guild-paysage-marin-grasscloth-sky-w.jpg"
+ },
+ {
+ "sku": "designers-guild-scene-aquarelle-grasscloth-elm",
+ "name": "Scene Aquarelle Grasscloth Elm\n \n \n Wallpaper",
+ "file": "grasscloth/designers-guild-scene-aquarelle-grasscloth-elm.jpg"
+ }
+ ]
+ },
+ "sisal": {
+ "name": "Sisal",
+ "textures": [
+ {
+ "sku": "W3996-135",
+ "name": "Kravet Design",
+ "file": "sisal/w3996-135.jpg"
+ }
+ ]
+ },
+ "generated": {
+ "name": "Generated Grasscloth Weave",
+ "textures": [
+ {
+ "sku": "gen-espresso",
+ "name": "Espresso",
+ "file": "../grasscloth/espresso.png"
+ },
+ {
+ "sku": "gen-natural-flax",
+ "name": "Natural Flax",
+ "file": "../grasscloth/natural-flax.png"
+ },
+ {
+ "sku": "gen-oatmeal",
+ "name": "Oatmeal",
+ "file": "../grasscloth/oatmeal.png"
+ },
+ {
+ "sku": "gen-pearl",
+ "name": "Pearl White",
+ "file": "../grasscloth/pearl.png"
+ },
+ {
+ "sku": "gen-sisal-tan",
+ "name": "Sisal Tan",
+ "file": "../grasscloth/sisal-tan.png"
+ },
+ {
+ "sku": "gen-smoke-grey",
+ "name": "Smoke Grey",
+ "file": "../grasscloth/smoke-grey.png"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/textures/library/sisal/w3996-135.jpg b/public/textures/library/sisal/w3996-135.jpg
new file mode 100644
index 0000000..37f00f2
Binary files /dev/null and b/public/textures/library/sisal/w3996-135.jpg differ
diff --git a/scripts/build_texture_library.js b/scripts/build_texture_library.js
new file mode 100644
index 0000000..4a63083
--- /dev/null
+++ b/scripts/build_texture_library.js
@@ -0,0 +1,125 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * build_texture_library — wallco.ai's texture-background library, sourced from
+ * REAL plain natural-fibre wallcovering products in the dw_unified database.
+ *
+ * Steve's rule: plain flat backgrounds look low-end. Compositing a generated
+ * pattern over a genuine grasscloth / sisal / cork / raffia product photo gives
+ * the render an honest, premium textile substrate.
+ *
+ * The intended filter was "36 inch wide" — the standard width for natural-fibre
+ * goods — but dw_unified.products.width_in is unpopulated for ~99% of rows, so
+ * width can't be filtered on. Instead we select by plain-texture MATERIAL and
+ * exclude anything whose name reads as a print/pattern. ~416 products qualify.
+ *
+ * Output: public/textures/library/<bucket>/<slug>.jpg
+ * public/textures/library/index.json (manifest, grouped by material)
+ *
+ * node scripts/build_texture_library.js
+ */
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const OUT = path.join(__dirname, '..', 'public', 'textures', 'library');
+fs.mkdirSync(OUT, { recursive: true });
+
+const TEX_RE = '(grasscloth|grass cloth|sisal|raffia|abaca|jute|hemp|cork|natural weave|paper weave|arrowroot|sea ?grass)';
+const PRINT_RE = '(floral|damask|stripe|scenic|toile|chinoiserie|print|motif|trellis|paisley|ikat|medallion|leaf|leaves|bird|botanical|animal|geometric)';
+
+// material bucket — first match wins
+function bucket(blob) {
+ if (/grass ?cloth/.test(blob)) return { slug: 'grasscloth', name: 'Grasscloth' };
+ if (/sisal/.test(blob)) return { slug: 'sisal', name: 'Sisal' };
+ if (/raffia/.test(blob)) return { slug: 'raffia', name: 'Raffia' };
+ if (/abaca/.test(blob)) return { slug: 'abaca', name: 'Abaca' };
+ if (/sea ?grass|arrowroot/.test(blob)) return { slug: 'seagrass', name: 'Seagrass & Arrowroot' };
+ if (/\bcork\b/.test(blob)) return { slug: 'cork', name: 'Cork' };
+ if (/jute|hemp/.test(blob)) return { slug: 'jute', name: 'Jute & Hemp' };
+ if (/paper weave|natural weave/.test(blob)) return { slug: 'weave', name: 'Paper & Natural Weave' };
+ return { slug: 'natural', name: 'Other Natural Fibre' };
+}
+
+function slugify(s) {
+ return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'tex';
+}
+
+// ---- pull rows from dw_unified -------------------------------------------
+console.log('querying dw_unified for plain-texture products…');
+const sql = `
+ select coalesce(json_agg(row_to_json(t)), '[]') from (
+ select sku, name, material, pattern_name,
+ (case when array_length(image_urls,1)>0 then image_urls[1] else thumbnail_url end) as img
+ from products
+ where lower(coalesce(material,'')||' '||coalesce(name,'')||' '||coalesce(pattern_name,'')) ~ '${TEX_RE}'
+ and lower(coalesce(material,'')||' '||coalesce(name,'')||' '||coalesce(pattern_name,'')) !~ '${PRINT_RE}'
+ and (array_length(image_urls,1)>0 or thumbnail_url is not null)
+ ) t`;
+const raw = execSync(`psql -d dw_unified -tA -c ${JSON.stringify(sql.replace(/\s+/g, ' '))}`, { encoding: 'utf8', maxBuffer: 64 << 20 });
+let rows = JSON.parse(raw.trim());
+rows = rows.filter(r => r.img && /^https?:\/\//.test(r.img));
+console.log(` ${rows.length} products with a usable image URL`);
+
+// dedupe by image URL — many SKUs share one flatshot
+const seen = new Map();
+for (const r of rows) if (!seen.has(r.img)) seen.set(r.img, r);
+const uniq = [...seen.values()];
+console.log(` ${uniq.length} unique images after dedupe`);
+
+// ---- download with concurrency -------------------------------------------
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
+const manifest = {};
+let ok = 0, fail = 0, done = 0;
+
+async function fetchOne(r) {
+ const blob = `${r.material || ''} ${r.name || ''} ${r.pattern_name || ''}`.toLowerCase();
+ const b = bucket(blob);
+ const dir = path.join(OUT, b.slug);
+ fs.mkdirSync(dir, { recursive: true });
+ const ext = (r.img.match(/\.(jpe?g|png|webp)(\?|$)/i) || [, 'jpg'])[1].toLowerCase().replace('jpeg', 'jpg');
+ const file = `${slugify(r.sku)}.${ext}`;
+ const dest = path.join(dir, file);
+ try {
+ if (!fs.existsSync(dest) || fs.statSync(dest).size < 1000) {
+ const ctl = new AbortController();
+ const timer = setTimeout(() => ctl.abort(), 20000);
+ const res = await fetch(r.img, { headers: { 'User-Agent': UA }, signal: ctl.signal });
+ clearTimeout(timer);
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const buf = Buffer.from(await res.arrayBuffer());
+ if (buf.length < 1000) throw new Error('tiny (' + buf.length + 'B)');
+ fs.writeFileSync(dest, buf);
+ }
+ (manifest[b.slug] ||= { name: b.name, textures: [] }).textures.push({
+ sku: r.sku, name: (r.name || r.sku).trim(), file: `${b.slug}/${file}`,
+ });
+ ok++;
+ } catch (e) {
+ fail++;
+ }
+ if (++done % 40 === 0) console.log(` …${done}/${uniq.length} (ok ${ok} / fail ${fail})`);
+}
+
+(async () => {
+ const CONC = 10;
+ for (let i = 0; i < uniq.length; i += CONC) {
+ await Promise.all(uniq.slice(i, i + CONC).map(fetchOne));
+ }
+ // fold in the procedurally-generated grasscloth weaves as their own group
+ const genDir = path.join(__dirname, '..', 'public', 'textures', 'grasscloth');
+ if (fs.existsSync(path.join(genDir, 'index.json'))) {
+ const gen = JSON.parse(fs.readFileSync(path.join(genDir, 'index.json'), 'utf8'));
+ manifest['generated'] = {
+ name: 'Generated Grasscloth Weave',
+ textures: gen.textures.map(t => ({ sku: 'gen-' + t.slug, name: t.name, file: `../grasscloth/${t.file}` })),
+ };
+ }
+ for (const k of Object.keys(manifest)) manifest[k].textures.sort((a, b) => a.name.localeCompare(b.name));
+ fs.writeFileSync(path.join(OUT, 'index.json'),
+ JSON.stringify({ source: 'dw_unified plain-texture products', generated: new Date().toISOString(), groups: manifest }, null, 1));
+ const total = Object.values(manifest).reduce((n, g) => n + g.textures.length, 0);
+ console.log(`\ndone — ${total} textures across ${Object.keys(manifest).length} material groups`);
+ for (const [k, g] of Object.entries(manifest)) console.log(` ${g.name}: ${g.textures.length}`);
+ console.log(`(${fail} downloads failed and were skipped)`);
+})();
diff --git a/scripts/gen_grasscloth_textures.js b/scripts/gen_grasscloth_textures.js
new file mode 100644
index 0000000..309618a
--- /dev/null
+++ b/scripts/gen_grasscloth_textures.js
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * gen_grasscloth_textures — wallco.ai's own seamless grasscloth texture library.
+ *
+ * Generates 6 seamless, tileable bare-grasscloth weave substrates procedurally
+ * via ImageMagick. No external assets, no licensing — these are ours.
+ *
+ * Grasscloth = woven natural fibre: dominant horizontal weft strands laid
+ * course-by-course, faint vertical warp threads, irregular slubs, neutral
+ * palette. We build a grayscale weave once, then duotone-map it into 6
+ * natural colorways.
+ *
+ * Output: public/textures/grasscloth/<slug>.png (1024x1024, seamless)
+ * public/textures/grasscloth/index.json (manifest)
+ *
+ * node scripts/gen_grasscloth_textures.js
+ */
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const OUT = path.join(__dirname, '..', 'public', 'textures', 'grasscloth');
+fs.mkdirSync(OUT, { recursive: true });
+const SIZE = 1024;
+
+// 6 natural grasscloth colorways — duotone endpoints [shadow, highlight].
+const COLORWAYS = [
+ { slug: 'natural-flax', name: 'Natural Flax', dark: '#473a29', light: '#e7d9bd' },
+ { slug: 'sisal-tan', name: 'Sisal Tan', dark: '#54402d', light: '#d8c099' },
+ { slug: 'oatmeal', name: 'Oatmeal', dark: '#655c4d', light: '#ede5d4' },
+ { slug: 'smoke-grey', name: 'Smoke Grey', dark: '#393733', light: '#ccc7be' },
+ { slug: 'espresso', name: 'Espresso', dark: '#221913', light: '#9c7f58' },
+ { slug: 'pearl', name: 'Pearl White', dark: '#787161', light: '#f3eee1' },
+];
+
+const GRAY = path.join(OUT, '_weave-gray.png');
+
+function sh(cmd) { execSync(cmd, { stdio: ['ignore', 'ignore', 'inherit'] }); }
+
+// 1. Grayscale weave base.
+// Grasscloth = long horizontal weft strands. We need HIGH vertical
+// frequency (many thin stacked strands) + LOW horizontal frequency
+// (each strand varies slowly along its length = slubs).
+// - noise generated at low vertical res (256) then stretched -> ~4px strands
+// - heavy horizontal motion-blur -> long fibrous streaks / slubs
+// - hard sigmoidal contrast -> the weave actually reads, not a flat field
+// - 4-way mirror fold -> every edge matches its opposite = seamless tile
+console.log('building grayscale weave base…');
+sh(`magick -size ${SIZE}x256 xc: +noise Random -colorspace Gray `
+ + `-resize ${SIZE}x${SIZE}\\! `
+ + `-virtual-pixel tile -motion-blur 0x44+0 -motion-blur 0x12+0 `
+ + `-auto-level -sigmoidal-contrast 9x46% `
+ + `\\( -size ${SIZE}x40 xc: +noise Random -colorspace Gray -resize ${SIZE}x${SIZE}\\! `
+ + `-virtual-pixel tile -motion-blur 0x70+90 -auto-level \\) `
+ + `-compose softlight -composite `
+ + `-blur 0x0.4 -auto-level `
+ + `\\( +clone -flip \\) -append \\( +clone -flop \\) +append `
+ + `-resize ${SIZE}x${SIZE}\\! `
+ + `"${GRAY}"`);
+
+// 2. Duotone-map the weave into each colorway + a touch of fibre grain.
+const manifest = [];
+for (const c of COLORWAYS) {
+ const out = path.join(OUT, `${c.slug}.png`);
+ sh(`magick "${GRAY}" `
+ + `+level-colors "${c.dark}","${c.light}" `
+ + `-attenuate 0.5 +noise Gaussian `
+ + `-quality 95 "${out}"`);
+ const kb = (fs.statSync(out).size / 1024).toFixed(0);
+ console.log(` ${c.slug}.png ${kb} KB`);
+ manifest.push({ slug: c.slug, name: c.name, file: `${c.slug}.png` });
+}
+
+fs.unlinkSync(GRAY);
+fs.writeFileSync(path.join(OUT, 'index.json'),
+ JSON.stringify({ material: 'grasscloth', generated: new Date().toISOString(), textures: manifest }, null, 2));
+console.log(`\ndone — ${manifest.length} grasscloth textures + index.json in public/textures/grasscloth/`);
← 4a89b0e Inspiration pool: Replicate backend + settlement gate + plac
·
back to Wallco Ai
·
wallco /designs: hide failed-generation blank designs from a bed0442 →