/** * The preset builder. * * This composes a Klarbild recipe and shows what it would run — it does not * run anything. Generating on a public page would mean putting an API key on * that page and paying for every curious visitor, and the picture it produced * would tell you about the model rather than about Klarbild. What Klarbild * decides is the part this does show: which tasks, which exact target size, * and the preset you save once and reuse. * * The target pixels come from `engine/format.ts`, copied verbatim from the * application, so the numbers here are the numbers it would produce. */ import { resolveDimensions } from '../engine/format'; export interface PresetExample { name: string; tasks: string[]; format: string; orientation: 'portrait' | 'landscape'; ext?: string; note: string; } interface Labels { resultKeep: string; contourNeedsCutout: string; dpiLine: string; // "{w} × {h} px at {dpi} dpi" } const ALL_TASKS = ['clean', 'cutout', 'format', 'contour']; export function mountRecipe(root: HTMLElement, presets: PresetExample[], labels: Labels) { const state = { tasks: new Set(presets[0]?.tasks ?? ['clean']), format: presets[0]?.format ?? 'keep', orientation: (presets[0]?.orientation ?? 'landscape') as 'portrait' | 'landscape', ext: presets[0]?.ext ?? '', name: presets[0]?.name ?? '', note: presets[0]?.note ?? '', }; const out = { result: root.querySelector('[data-r=result]')!, note: root.querySelector('[data-r=note]')!, recipe: root.querySelector('[data-r=recipe]')!, }; function dims() { if (state.format === 'keep') return null; const customCm: [number, number] | undefined = state.format === 'sticker' ? [5, 5] : undefined; try { return resolveDimensions({ format: state.format, orientation: state.orientation, dpi: 300, customCm }); } catch { return null; } } function render() { // Contour without Cut out is refused by the application, so the builder // refuses it too rather than composing a recipe that would be rejected. if (state.tasks.has('contour') && !state.tasks.has('cutout')) state.tasks.delete('contour'); for (const b of root.querySelectorAll('button[data-task]')) { const id = b.dataset.task!; b.setAttribute('aria-pressed', String(state.tasks.has(id))); b.disabled = id === 'contour' && !state.tasks.has('cutout'); } for (const b of root.querySelectorAll('button[data-orient]')) { b.setAttribute('aria-pressed', String(b.dataset.orient === state.orientation)); } for (const b of root.querySelectorAll('button[data-ext]')) { b.setAttribute('aria-pressed', String((b.dataset.ext || '') === state.ext)); } const sel = root.querySelector('#recipe-format'); if (sel && sel.value !== state.format) sel.value = state.format; const d = dims(); out.result.textContent = d ? labels.dpiLine.replace('{w}', String(d.w)).replace('{h}', String(d.h)).replace('{dpi}', '300') : labels.resultKeep; out.note.textContent = state.note; const recipe: Record = { name: state.name || '…', tasks: ALL_TASKS.filter((t) => state.tasks.has(t)), output_format: state.format, orientation: state.orientation, crop_mode: 'crop', dpi: 300, }; if (state.ext) recipe.output_ext = state.ext; if (state.tasks.has('contour')) recipe.contour_mm = 3; out.recipe.textContent = JSON.stringify(recipe, null, 2); } root.addEventListener('click', (e) => { const el = e.target as HTMLElement; const task = el.closest('button[data-task]'); if (task && !task.disabled) { const id = task.dataset.task!; state.tasks.has(id) ? state.tasks.delete(id) : state.tasks.add(id); state.name = ''; state.note = ''; return render(); } const orient = el.closest('button[data-orient]'); if (orient) { state.orientation = orient.dataset.orient as 'portrait' | 'landscape'; return render(); } const ext = el.closest('button[data-ext]'); if (ext) { state.ext = ext.dataset.ext || ''; return render(); } const preset = el.closest('button[data-preset]'); if (preset) { const p = presets[Number(preset.dataset.preset)]; state.tasks = new Set(p.tasks); state.format = p.format; state.orientation = p.orientation; state.ext = p.ext ?? ''; state.name = p.name; state.note = p.note; for (const sib of root.querySelectorAll('button[data-preset]')) { sib.setAttribute('aria-pressed', String(sib === preset)); } return render(); } }); root.addEventListener('change', (e) => { const sel = e.target as HTMLSelectElement; if (sel.id === 'recipe-format') { state.format = sel.value; state.name = ''; state.note = ''; render(); } }); const first = root.querySelector('button[data-preset="0"]'); first?.setAttribute('aria-pressed', 'true'); render(); }