← back to Dw Pairs Well
lib/classify.js
118 lines
const COLORS = new Set([
'beige','black','blue','brown','copper','cream','gold','gray','grey','green',
'ivory','navy','orange','peach','pearl','pink','purple','red','rose','rust',
'sage','salmon','silver','tan','taupe','teal','turquoise','white','yellow',
'mauve','plum','aqua','charcoal','khaki','lavender','olive','platinum','bronze',
'mustard','indigo','burgundy','coral','mint','seafoam','sand','stone',
'light gray','light grey','light blue','light green','dark gray','dark grey',
'dark blue','dark green','dusty blue','dusty pink','off white'
]);
const MOTIFS = new Set([
'floral','damask','stripe','striped','geometric','solid','botanical','paisley',
'toile','trellis','animal','abstract','texture','textured','medallion','plaid',
'check','checkered','dot','polka dot','herringbone','chevron','ikat','suzani',
'moroccan','arabesque','foliage','leaves','leaf','grass','grasscloth','brick',
'wood','marble','tile','scenic','mural','figurative','typographic','script',
'metallic','glitter','crystal','beaded','flocked','linen','silk','suede',
'cork','foil','tropical','jungle','garden','landscape','urban','industrial'
]);
const ERAS = new Set([
'modern','contemporary','traditional','mid-century','mid century','art deco',
'victorian','rococo','transitional','vintage','retro','classic','antique',
'colonial','federal','regency','baroque','neoclassical','gothic','arts and crafts',
'art nouveau','bohemian','boho','minimalist','maximalist','farmhouse','rustic',
'industrial','coastal','hamptons','scandinavian','japandi','mediterranean'
]);
const MATERIALS = new Set([
'paper','vinyl','grasscloth','cork','foil','silk','linen','suede','glass beaded',
'flocked','metallic','natural fiber','sisal','jute','raffia','bamboo','mica',
'mylar','non-woven','non woven','peel and stick','peel-and-stick'
]);
function parseTags(tagsField) {
if (!tagsField) return [];
return String(tagsField)
.split(/[,;|]/)
.map(t => t.trim())
.filter(Boolean);
}
function classify(tags) {
const out = { colors: new Set(), motifs: new Set(), eras: new Set(), materials: new Set(), vendor: null, raw: tags };
for (const t of tags) {
const k = t.toLowerCase();
if (COLORS.has(k)) out.colors.add(k);
else if (MOTIFS.has(k)) out.motifs.add(k);
else if (ERAS.has(k)) out.eras.add(k);
else if (MATERIALS.has(k)) out.materials.add(k);
}
return out;
}
function intersect(a, b) {
const out = [];
for (const x of a) if (b.has(x)) out.push(x);
return out;
}
function score(source, candidate, candidateVendor, sourceVendor, candidatePattern, sourcePattern) {
const sharedColors = intersect(source.colors, candidate.colors);
const sharedEras = intersect(source.eras, candidate.eras);
const sharedMotifs = intersect(source.motifs, candidate.motifs);
const sharedMaterials = intersect(source.materials, candidate.materials);
let s = 0;
s += sharedColors.length * 3;
s += sharedEras.length * 2;
s += sharedMaterials.length * 1;
s -= sharedMotifs.length * 2;
if (sourceVendor && candidateVendor && sourceVendor === candidateVendor) s += 1;
if (sourcePattern && candidatePattern && sourcePattern.toLowerCase() === candidatePattern.toLowerCase()) s -= 5;
const why = [];
if (sharedColors.length) why.push(`Shared color: ${sharedColors.join(', ')}`);
if (sharedEras.length) why.push(`Shared era: ${sharedEras.join(', ')}`);
if (sourceVendor && candidateVendor && sourceVendor === candidateVendor) why.push(`Same designer (${sourceVendor})`);
const contrastMotifs = [...candidate.motifs].filter(m => !source.motifs.has(m));
if (contrastMotifs.length && source.motifs.size) {
why.push(`Motif contrast: ${[...source.motifs][0]} → ${contrastMotifs[0]}`);
}
return { score: s, why };
}
// Inverted sibling of score() for the "Similar Designs" feature.
// Where score() REWARDS contrast (coordinating a hero pattern), this REWARDS sameness
// (finding visual alternatives that look like the source): same motif, finish, era, palette.
// Same pattern_name is a colorway of the SAME design, not a similar one — hard-killed.
function scoreSimilar(source, candidate, candidateVendor, sourceVendor, candidatePattern, sourcePattern) {
const sharedColors = intersect(source.colors, candidate.colors);
const sharedEras = intersect(source.eras, candidate.eras);
const sharedMotifs = intersect(source.motifs, candidate.motifs);
const sharedMaterials = intersect(source.materials, candidate.materials);
const samePattern = !!(sourcePattern && candidatePattern
&& sourcePattern.toLowerCase() === candidatePattern.toLowerCase());
let s = 0;
s += sharedMotifs.length * 5; // same motif is the strongest "looks like it" signal
s += sharedMaterials.length * 3; // same finish/texture (grasscloth↔grasscloth)
s += sharedEras.length * 3; // same design era/style
s += sharedColors.length * 3; // shared palette
if (sourceVendor && candidateVendor && sourceVendor === candidateVendor) s += 1;
if (samePattern) s -= 100; // exclude this design's own colorways
const why = [];
if (sharedMotifs.length) why.push(`Same motif: ${sharedMotifs.slice(0, 2).join(', ')}`);
if (sharedMaterials.length) why.push(`Same material: ${sharedMaterials.slice(0, 2).join(', ')}`);
if (sharedEras.length) why.push(`Shared style: ${sharedEras.slice(0, 2).join(', ')}`);
if (sharedColors.length) why.push(`Shared color: ${sharedColors.slice(0, 2).join(', ')}`);
return { score: s, why, samePattern };
}
module.exports = { parseTags, classify, score, scoreSimilar, COLORS, MOTIFS, ERAS, MATERIALS };