/* Studio 76 Atelier — Apparel Bench. Upload a garment photo (or a sketch) → AI spec, technical flat / tech-pack, front·back·detail plate, coloured illustration, and "see it worn" on a model (Instagram-ready), plus variations, reforms and guided edits. Talks only to apparel.php (keys server-side). */ const WEAR = [ { k: 'full', label: 'Full length' }, { k: 'runway', label: 'Runway' }, { k: 'half', label: 'Waist up' }, { k: 'back', label: 'Back view' }, { k: 'seated', label: 'Seated' }, { k: 'detail', label: 'Fabric detail' }, { k: 'street', label: 'Street style' }, ]; const MODELS = [ { k: 'woman', label: 'On her' }, { k: 'man', label: 'On him' }, ]; const LOOKS = [ { k: 'editorial', label: 'Editorial' }, { k: 'indian', label: 'Indian' }, { k: 'millennial', label: 'Millennial' }, { k: 'glam', label: 'Glam' }, { k: 'studio', label: 'Studio' }, { k: 'goldenhour', label: 'Golden hour' }, ]; const FORMS = [ { k: 'dress', label: 'As a dress' }, { k: 'top', label: 'As a top' }, { k: 'skirt', label: 'As a skirt' }, { k: 'trousers', label: 'As trousers' }, { k: 'jacket', label: 'As a jacket' }, { k: 'jumpsuit', label: 'As a jumpsuit' }, ]; /* Guided-edit vocab — discrete controls, each = one precise change on the zoomed render. */ const NECKLINES = ['Crew', 'V-neck', 'Square', 'Sweetheart', 'Halter', 'Boat', 'Off-shoulder', 'Cowl', 'Collared']; const SLEEVES = ['Sleeveless', 'Cap', 'Short', 'Three-quarter', 'Long', 'Puff', 'Bishop', 'Bell']; const FABRICS = ['Silk', 'Satin', 'Cotton', 'Linen', 'Velvet', 'Chiffon', 'Crepe', 'Denim', 'Wool', 'Tulle']; const FITS = ['Slim', 'Tailored', 'Relaxed', 'Oversized']; const COLORS = [ { name: 'Ivory', hex: '#F3EEE3' }, { name: 'Black', hex: '#1B130D' }, { name: 'Emerald', hex: '#0F6B4F' }, { name: 'Ruby red', hex: '#8E1B2E' }, { name: 'Blush', hex: '#E7C3C0' }, { name: 'Royal blue', hex: '#27408B' }, { name: 'Sapphire navy', hex: '#1C2B4A' }, { name: 'Champagne', hex: '#E4D2A8' }, { name: 'Sage', hex: '#A7B79A' }, { name: 'Terracotta', hex: '#C2552B' }, ]; const DETAILS = [ { k: 'belt', op: 'add', params: { element: 'a slim matching waist belt' }, label: '+ Belt' }, { k: 'pockets', op: 'add', params: { element: 'discreet side-seam pockets' }, label: '+ Pockets' }, { k: 'ruffle', op: 'add', params: { element: 'a soft ruffle along the hem' }, label: '+ Ruffle hem' }, { k: 'sleeveless', op: 'remove', params: { element: 'the sleeves' }, label: '– Sleeves' }, ]; const SEL0 = { length: null, neckline: null, sleeve: null, fabric: null, color: null, fit: null, details: [], note: '' }; function cap1(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; } /* aspect of a generated key, for the refine (perfect-it) pass */ function aspectFor(key) { if (key === 'views') return '3:2'; if (key.slice(0, 4) === 'mag_') return '3:4'; if (key.slice(0, 2) === 'w_') return '4:5'; return '3:4'; } async function benchApi(action, payload) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 95000); let r; try { r = await fetch('apparel.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action }, payload)), signal: ctrl.signal }); } catch (e) { clearTimeout(timer); throw new Error(e && e.name === 'AbortError' ? 'That took too long — the studio is busy. Please try again.' : 'Network hiccup — please try again.'); } clearTimeout(timer); let j = null; try { j = await r.json(); } catch (e) {} if (!r.ok || !j || j.error) throw new Error((j && j.error) || ('The studio is busy right now (' + r.status + ') — please try again.')); return j; } function downscale(file, max) { return new Promise((res, rej) => { const fr = new FileReader(); fr.onerror = () => rej(new Error('read')); fr.onload = () => { const img = new Image(); img.onload = () => { const s = Math.min(1, max / Math.max(img.width, img.height)); const w = Math.round(img.width * s), h = Math.round(img.height * s); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(img, 0, 0, w, h); res(c.toDataURL('image/jpeg', 0.85)); }; img.onerror = () => rej(new Error('img')); img.src = fr.result; }; fr.readAsDataURL(file); }); } function isUrl(x) { return typeof x === 'string' && x.slice(0, 5) === 'data:'; } function Bench() { const [stage, setStage] = React.useState('idle'); const [entry, setEntry] = React.useState('piece'); const [src, setSrc] = React.useState(null); const [id, setId] = React.useState(null); const [spec, setSpec] = React.useState(null); const [out, setOut] = React.useState({}); const [look, setLook] = React.useState('editorial'); const [model, setModel] = React.useState('woman'); const [shots, setShots] = React.useState([]); const [morphs, setMorphs] = React.useState([]); const [fixes, setFixes] = React.useState([]); const [fixN, setFixN] = React.useState(0); const [varN, setVarN] = React.useState(0); const [varText, setVarText] = React.useState(''); const [saving, setSaving] = React.useState(false); const [savedN, setSavedN] = React.useState(''); const [savedUrl, setSavedUrl] = React.useState(''); const [drag, setDrag] = React.useState(false); const [err, setErr] = React.useState(''); const [urlText, setUrlText] = React.useState(''); const [modalSrc, setModalSrc] = React.useState(''); const [modalKey, setModalKey] = React.useState(''); const [sel, setSel] = React.useState(SEL0); const [cap, setCap] = React.useState(null); const [copied, setCopied] = React.useState(false); const fileRef = React.useRef(null); const savedSetRef = React.useRef(null); const savingRef = React.useRef(false); const aspectsRef = React.useRef({}); async function ingestRealize(srcPayload) { const a1 = await benchApi('analyze', srcPayload); if (!a1.id) throw new Error('Could not read that sketch — please try again.'); const r = await benchApi('realize', { id: a1.id }); setSrc(r.dataUrl); const a2 = await benchApi('analyze', { image: r.dataUrl }); if (!a2.id) throw new Error('Could not finish reading — please try again.'); setId(a2.id); setSpec(a2.spec); } function resetForIngest(sketch) { setErr(''); setEntry(sketch ? 'sketch' : 'piece'); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setShots([]); setMorphs([]); setFixes([]); setVarN(0); setSavedUrl(''); savedSetRef.current = null; } async function handleFile(file, sketch) { if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; } resetForIngest(sketch); try { const dataUrl = await downscale(file, 1280); setSrc(dataUrl); if (sketch) { await ingestRealize({ image: dataUrl }); } else { const j = await benchApi('analyze', { image: dataUrl }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); } setStage('ready'); } catch (e) { setErr(e.message || 'Could not read that image.'); setStage('idle'); } } async function handleUrl(url, sketch) { url = (url || '').trim(); if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; } resetForIngest(sketch); setSrc(url); try { if (sketch) { await ingestRealize({ imageUrl: url }); } else { const j = await benchApi('analyze', { imageUrl: url }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); } setStage('ready'); } catch (e) { setErr(e.message || 'Could not bring that image in.'); setStage('idle'); } } React.useEffect(() => { const u = new URLSearchParams(window.location.search).get('img'); if (u) { setUrlText(u); handleUrl(u); } }, []); async function gen(key, action, payload) { if (!id) return; setOut(o => Object.assign({}, o, { [key]: 'loading' })); try { const j = await benchApi(action, Object.assign({ id }, payload)); setOut(o => Object.assign({}, o, { [key]: j.dataUrl })); } catch (e) { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Generation failed.'); } } function genWear(place) { const w = WEAR.find(x => x.k === place), l = LOOKS.find(x => x.k === look); const key = 'w_' + place + '_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: w.label, lookLabel }].concat(prev)); gen(key, 'wear', { place, look, model }); } function genMagazine() { const l = LOOKS.find(x => x.k === look); const key = 'mag_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: 'Magazine cover', lookLabel }].concat(prev)); gen(key, 'magazine', { look, model }); } function genEditSet(ops, label) { const srcKey = modalKey, base = out[srcKey]; if (!id || !srcKey || !isUrl(base) || !ops.length) return; const aspect = aspectsRef.current[srcKey] || aspectFor(srcKey); const n = fixN + 1; setFixN(n); const key = 'fix_' + n; aspectsRef.current[key] = aspect; setFixes(prev => [{ key, label }].concat(prev)); closeModal(); setOut(o => Object.assign({}, o, { [key]: 'loading' })); benchApi('edit', { id, image: base, ops, aspect }) .then(j => setOut(o => Object.assign({}, o, { [key]: j.dataUrl }))) .catch(e => { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Edit failed.'); }); } function applyEdits() { const ops = [], parts = []; if (sel.length) { ops.push({ op: 'length', params: { dir: sel.length } }); parts.push(sel.length === 'longer' ? 'Longer' : 'Shorter'); } if (sel.neckline) { ops.push({ op: 'neckline', params: { to: sel.neckline } }); parts.push(cap1(sel.neckline) + ' neck'); } if (sel.sleeve) { ops.push({ op: 'sleeve', params: { to: sel.sleeve } }); parts.push(cap1(sel.sleeve) + ' sleeve'); } if (sel.fabric) { ops.push({ op: 'fabric', params: { to: sel.fabric } }); parts.push(cap1(sel.fabric)); } if (sel.color) { ops.push({ op: 'color', params: { to: sel.color } }); parts.push(cap1(sel.color)); } if (sel.fit) { ops.push({ op: 'fit', params: { to: sel.fit } }); parts.push(cap1(sel.fit) + ' fit'); } sel.details.forEach(k => { const d = DETAILS.find(x => x.k === k); if (d) { ops.push({ op: d.op, params: d.params }); parts.push(d.label); } }); const note = sel.note.trim(); if (note) { ops.push({ op: 'note', params: { text: note } }); parts.push('“' + (note.length > 24 ? note.slice(0, 24) + '…' : note) + '”'); } if (!ops.length) return; let label = parts.slice(0, 3).join(' · '); if (parts.length > 3) label += ' +' + (parts.length - 3); genEditSet(ops, label); } function closeModal() { setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); } function capFull(c) { return (c.caption || '') + '\n\n' + (c.hashtags || []).map(h => '#' + String(h).replace(/^#/, '').replace(/\s+/g, '')).join(' '); } function loadCaption() { if (!id) return; setCap('loading'); setCopied(false); benchApi('caption', { id }).then(j => setCap(j)).catch(e => { setCap(null); setErr(e.message || 'Could not write a caption.'); }); } function copyCap(c) { const t = capFull(c); const done = () => { setCopied(true); setTimeout(() => setCopied(false), 1800); }; try { navigator.clipboard.writeText(t).then(done, done); } catch (e) { done(); } } function genVar() { if (!id) return; const n = varN + 1; setVarN(n); const text = varText.trim(); const key = 'var_' + n; const label = text ? ('“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”') : ('Variation ' + n); setMorphs(prev => [{ key, label }].concat(prev)); gen(key, 'variation', { n, instructions: text }); setVarText(''); } function genForm(form) { const f = FORMS.find(x => x.k === form), key = 'form_' + form; if (!morphs.find(m => m.key === key)) setMorphs(prev => [{ key, label: f.label }].concat(prev)); gen(key, 'reform', { form }); } async function save() { if (!id || saving || savingRef.current) return; savingRef.current = true; setSaving(true); setSavedUrl(''); setSavedN(''); setErr(''); try { const start = await benchApi('save_start', { id, spec, original: isUrl(src) ? src : '', setid: savedSetRef.current || undefined }); savedSetRef.current = start.setid; const items = []; if (isUrl(out.blueprint)) items.push({ key: 'blueprint', label: 'Technical flat', url: out.blueprint }); if (isUrl(out.views)) items.push({ key: 'views', label: 'Front · Back · Detail', url: out.views }); if (isUrl(out.realize)) items.push({ key: 'realize', label: 'Made real', url: out.realize }); if (isUrl(out.rendering)) items.push({ key: 'rendering', label: 'Fashion illustration', url: out.rendering }); shots.forEach(s => { if (isUrl(out[s.key])) items.push({ key: s.key, label: s.placeLabel + ' · ' + s.lookLabel, url: out[s.key] }); }); morphs.forEach(m => { if (isUrl(out[m.key])) items.push({ key: m.key, label: m.label, url: out[m.key] }); }); fixes.forEach(f => { if (isUrl(out[f.key])) items.push({ key: f.key, label: 'Perfected · ' + f.label, url: out[f.key] }); }); let n = 0; for (const it of items) { await benchApi('save_item', { setid: start.setid, key: it.key, label: it.label, dataUrl: it.url }); n++; setSavedN(n + '/' + items.length); } try { const k = 'studio76_apparel_sets', arr = JSON.parse(localStorage.getItem(k) || '[]').filter(s => s.id !== start.setid); arr.unshift({ id: start.setid, url: start.url, title: (spec && spec.garment_type) || 'Saved look', n: items.length, created: Date.now() }); localStorage.setItem(k, JSON.stringify(arr.slice(0, 60))); } catch (e) {} setSavedUrl(start.url); } catch (e) { setErr(e.message || 'Save failed.'); } savingRef.current = false; setSaving(false); } function reset() { setStage('idle'); setSrc(null); setId(null); setSpec(null); setOut({}); setShots([]); setMorphs([]); setFixes([]); setFixN(0); setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); aspectsRef.current = {}; setVarN(0); setSaving(false); setSavedN(''); setSavedUrl(''); savedSetRef.current = null; savingRef.current = false; setErr(''); } const meas = spec && spec.estimated_measurements_cm ? [spec.estimated_measurements_cm.bust && ('bust ' + spec.estimated_measurements_cm.bust), spec.estimated_measurements_cm.waist && ('waist ' + spec.estimated_measurements_cm.waist), spec.estimated_measurements_cm.length && ('length ' + spec.estimated_measurements_cm.length)].filter(Boolean).join(' · ') : ''; const selCount = (sel.length ? 1 : 0) + (sel.neckline ? 1 : 0) + (sel.sleeve ? 1 : 0) + (sel.fabric ? 1 : 0) + (sel.color ? 1 : 0) + (sel.fit ? 1 : 0) + sel.details.length + (sel.note.trim() ? 1 : 0); const Row = ({ l, v }) => v ?
{l}{v}
: null; const Tile = ({ keyName, title, sub }) => { const v = out[keyName]; return (
{v === 'loading' ?
{sub || 'Working…'}
: (typeof v === 'string') ? {title} { setModalSrc(v); setModalKey(keyName); setCap(null); setCopied(false); setSel(SEL0); }} /> :
{sub}
}
{title} {(typeof v === 'string' && v !== 'loading') && Download}
); }; return (
Studio 76 AtelierHandbagsSaved looksJewellery✦ Apparel Bench

From a photo to a pattern.

Upload any garment — a photo, a screenshot, a runway look you saved. Our atelier reads it, drafts a technical flat, breaks down the spec, and shows it worn on a model, made to your measurements.

{stage === 'idle' && (
)} {stage === 'idle' && (
fileRef.current && fileRef.current.click()} onDragOver={e => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0], entry === 'sketch'); }}>

{entry === 'sketch' ? 'Drop your sketch here, or ' : 'Drop a photo here, or '}browse

{entry === 'sketch' ? 'A drawing, croquis or concept — we’ll render it as a finished piece.' : 'JPG / PNG · a clear, well-lit shot works best'}

handleFile(e.target.files[0], entry === 'sketch')} />
)} {stage === 'idle' && (
setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText, entry === 'sketch'); }} />
)} {stage === 'idle' && (

✦ Clip a look from Pinterest in one tap ↗

)} {err &&

{err}

} {stage !== 'idle' && (
{savedUrl ? View / share your look ↗ : Stores the spec, flat & every render at a shareable link.}
✦ See it worn — Instagram-ready

Pick who wears it and a look, then tap a shot. Mix & match to build a campaign.

{MODELS.map(m => ( ))} {LOOKS.map(l => ( ))}
{WEAR.map(w => { const k = 'w_' + w.k + '_' + look + '_' + model; return ( ); })} {(() => { const k = 'mag_' + look + '_' + model; return ( ); })()}
{shots.length > 0 && (
{shots.map(s => )}
)}
✦ Make it a collection — variations & forms

Tell us what to change, or just spin off a fresh take — then reimagine it as a matching dress, top, skirt…

setVarText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') genVar(); }} />
{FORMS.map(f => { const k = 'form_' + f.k; return ; })}
{morphs.length > 0 && (
{morphs.map(m => )}
)}
{fixes.length > 0 && (
✦ Perfected takes

Refined renders — zoom any image and tell the atelier what to change to make more.

{fixes.map(f => )}
)}
)} {modalSrc && (
×
e.stopPropagation()}>
{modalKey && isUrl(out[modalKey]) && (
✦ Refine this design

Stack as many changes as you like, then Apply — they’re built in a single pass, the rest of the garment held steady. The new take lands in “Perfected takes”.

Length
Neckline
{NECKLINES.map(c => { const v = c.toLowerCase(); return ; })}
Sleeves
{SLEEVES.map(st => { const v = st.toLowerCase(); return ; })}
Fabric
{FABRICS.map(m => { const v = m.toLowerCase(); return ; })}
Colour
{COLORS.map(c => { const v = c.name.toLowerCase(); return ; })}
Fit
{FITS.map(m => { const v = m.toLowerCase(); return ; })}
Detailing
{DETAILS.map(d => )}
Anything else (optional)
setSel(s => ({ ...s, note: e.target.value }))} onKeyDown={e => { if (e.key === 'Enter') applyEdits(); }} />
{selCount > 0 && }
)} {spec && (
✦ Post to Instagram {!cap &&

We’ll write the caption & hashtags — download the image and post it in seconds.

} {!cap && } {cap === 'loading' &&

Writing a caption…

} {cap && cap !== 'loading' && (