← 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 };