← back to Wallco Ai
replace-bg: accept any of the 5k library textures, not just the 10 curated bg_textures rows
774fe3053ed81d06431a409e43f7a6c273ee8369 · 2026-05-26 15:18:42 -0700 · Steve Abrams
Picker passes texture_image_url+name; endpoint falls back to the ad-hoc image path when slug isn't a curated bg_textures row.
Files touched
Diff
commit 774fe3053ed81d06431a409e43f7a6c273ee8369
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 26 15:18:42 2026 -0700
replace-bg: accept any of the 5k library textures, not just the 10 curated bg_textures rows
Picker passes texture_image_url+name; endpoint falls back to the ad-hoc image path when slug isn't a curated bg_textures row.
---
server.js | 46 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 35 insertions(+), 11 deletions(-)
diff --git a/server.js b/server.js
index d137de4..429c113 100644
--- a/server.js
+++ b/server.js
@@ -8908,17 +8908,24 @@ app.post('/api/design/:id/replace-bg', express.json({ limit: '8kb' }), async (re
const ad_hoc_name = String(req.body?.texture_name || 'custom').trim();
if (!slug && !ad_hoc_url) return res.status(400).json({ error: 'texture_slug or texture_image_url required' });
try {
- // Resolve texture: either DB slug OR ad-hoc URL (e.g., DW product image)
- let tex;
- if (ad_hoc_url) {
- tex = { slug: 'adhoc', name: ad_hoc_name, material: 'natural-wallcovering',
+ // Resolve texture. The picker (/api/bg-textures) unifies THREE sources —
+ // the curated bg_textures table PLUS wallco_textures + shopify_products —
+ // but only the ~10 bg_textures rows have a slug THIS endpoint can look up.
+ // So: try the curated slug first (best — it carries a hand-written
+ // description); otherwise fall back to the texture_image_url the picker now
+ // sends for every library item (ad-hoc path — works for any of the 5k+
+ // textures, not just the 10 curated ones).
+ let tex = null;
+ if (slug) {
+ const traw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT slug, name, material, hex, description, image_url FROM bg_textures WHERE slug=${"'" + slug.replace(/'/g,"''") + "'"} LIMIT 1) t;`);
+ if (traw && traw.length > 2) tex = JSON.parse(traw);
+ }
+ if (!tex && ad_hoc_url) {
+ tex = { slug: slug || 'adhoc', name: ad_hoc_name, material: 'natural-wallcovering',
hex: null, description: `Use the supplied texture image AS-IS as the background. Keep its natural fiber/weave look visible.`,
image_url: ad_hoc_url };
- } else {
- const traw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT slug, name, material, hex, description, image_url FROM bg_textures WHERE slug=${"'" + slug.replace(/'/g,"''") + "'"} LIMIT 1) t;`);
- if (!traw || traw.length < 3) return res.status(404).json({ error: 'texture not found: ' + slug });
- tex = JSON.parse(traw);
}
+ if (!tex) return res.status(404).json({ error: 'texture not found: ' + (slug || ad_hoc_url) });
// Lookup design
let d = null;
try {
@@ -13415,7 +13422,7 @@ ${(() => {
if (readout) readout.textContent = 'Applying ' + tex.name + '… (~10–20s)';
fetch('/api/design/' + DESIGN_ID + '/replace-bg', {
method:'POST', headers:{'Content-Type':'application/json'},
- body: JSON.stringify({ texture_slug: tex.slug })
+ body: JSON.stringify({ texture_slug: tex.slug, texture_image_url: tex.image_url, texture_name: tex.name })
}).then(function(r){
if (!r.ok) return r.text().then(function(t){ throw new Error(r.status+': '+t.slice(0,160)); });
return r.json();
@@ -19694,6 +19701,16 @@ const RECOLOR_HUES = {
charcoal:{ label: 'Charcoal', tail: 'pale grey ground; deep charcoal motif; tonal monochrome' },
};
+// Representative mid-tone hex per hue family. A recolor variant has no measured
+// dominant_hex at insert time, so hue-mode variants borrow this so titleFor()
+// (→ hueName) names the right colour instead of falling back to 'Bone'.
+const HUE_REP_HEX = {
+ red:'#B23A3A', orange:'#C2662E', yellow:'#C9A227', lime:'#8FAE3A',
+ green:'#4E7A4E', teal:'#2E7D7D', cyan:'#3AA6B0', blue:'#3A5FAE',
+ indigo:'#4B3FA0', violet:'#7A4FA0', magenta:'#A03A7A', pink:'#C97A8C',
+ neutral:'#9A8C78', charcoal:'#3A3A3A',
+};
+
app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
try {
@@ -19718,6 +19735,12 @@ app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
}
const targetKey = hex ? hex.slice(1).toLowerCase() : (brandKey || hueKey);
const targetLabel = hex ? hex : (brand ? brand.label : hue.label);
+ // Dominant hex for the new variant row — drives the title colour word via
+ // titleFor -> hueName. Without it the title falls back to Bone.
+ const domHex = hex
+ || (brand && Array.isArray(brand.hex) && brand.hex.length ? brand.hex[0] : null)
+ || HUE_REP_HEX[hueKey]
+ || null;
// Load source image bytes from local_path or by reading the file behind image_url
const d = DESIGNS.find(x => x.id === id);
@@ -19872,7 +19895,7 @@ app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
(kind, width_in, height_in, generator, prompt, seed, dominant_hex,
local_path, image_url, category, motifs, tags, is_published, request_text)
VALUES ('seamless_tile', 24, 24, 'gemini-2.5-flash-image-edit',
- '${promptDesc}', ${seed}, NULL,
+ '${promptDesc}', ${seed}, ${domHex ? "'" + domHex + "'" : 'NULL'},
'${outPath.replace(/'/g, "''")}', '/designs/img/${filename}',
${"'" + (d.category || 'recolor') + "'"},
${motifsSql}, ${tagSql}, TRUE,
@@ -19886,7 +19909,8 @@ app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
id: newId,
kind: 'seamless_tile',
category: d.category || 'recolor',
- title: newTitle,
+ dominant_hex: domHex || undefined,
+ title: domHex ? titleFor(d.category || 'recolor', domHex, newId) : newTitle,
handle: 'wallco-' + newId,
image_url: '/designs/img/' + filename,
filename,
← bb2d819 docs: correct CLAUDE.md — designs.json IS deployed, not excl
·
back to Wallco Ai
·
wallco recolor: set dominant_hex on variants so title color d2af6bb →