/** * Lasso + Magic Wand panel controls — sliders, mode toggles, and the * panel action buttons (Invert / Clear / Delete / Copy / To Mask / * Bg Remove). The actual selection algorithms live in their tool * modules (editor/tools/lasso.js, editor/tools/wand.js); this file * just wires the side-panel UI to them. * * Lasso section: * #ge-lasso-feather slider, updates label + preview, recomposites * #ge-lasso-grow slider, updates label + recomposites * #ge-lasso-invert → invertSelection * #ge-lasso-delete → lassoDeleteSelection * #ge-lasso-copy → lassoCopyToLayer * #ge-lasso-mask → lassoToMask * * Wand section: * #ge-wand-feather slider, updates label + recomposites * #ge-wand-grow slider, updates label + recomposites * #ge-wand-tolerance slider, updates future wand-click tolerance * #ge-wand-live opt-in rAF-coalesced live retune while dragging * .ge-wand-mode-btn segmented toggle (New / Add / Subtract) * #ge-wand-vis toggle the translucent red overlay * #ge-wand-clear / -invert / -delete / -copy / -mask / -rembg * * @param {{ * composite: () => void, * invertSelection: () => boolean, * lassoDeleteSelection: () => void, * lassoCopyToLayer: () => void, * lassoToMask: () => void, * runMagicWand: (x: number, y: number, mode: string, opts?: object) => void, * wandClear: () => void, * wandDeleteSelection: () => void, * wandCopyToNewLayer: () => void, * wandToMask: () => void, * buildSelectionHintMask: () => string | null, * applyImageTool: (endpoint, payload, name, btn, opts?) => Promise, * uiModule: object, * }} deps */ import { state } from './state.js'; const EYE_OPEN = ''; const EYE_OFF = ''; export function wireSelectionControls({ composite, invertSelection, lassoDeleteSelection, lassoCopyToLayer, lassoToMask, runMagicWand, wandClear, wandDeleteSelection, wandCopyToNewLayer, wandToMask, buildSelectionHintMask, applyImageTool, uiModule, toggleQuickMask, }) { document.querySelectorAll('.ge-quick-mask-toggle').forEach(button => { button.addEventListener('click', toggleQuickMask); }); // ── Rectangle / ellipse marquee ── document.querySelectorAll('.ge-marquee-shape-btn').forEach(btn => { btn.addEventListener('click', () => { state.marqueeShape = btn.dataset.marqueeShape === 'ellipse' ? 'ellipse' : 'rectangle'; document.querySelectorAll('.ge-marquee-shape-btn').forEach(candidate => { const active = candidate.dataset.marqueeShape === state.marqueeShape; candidate.classList.toggle('active', active); candidate.setAttribute('aria-pressed', active ? 'true' : 'false'); }); }); }); const constraint = document.getElementById('ge-marquee-constraint'); const dimensions = document.querySelector('.ge-marquee-dimensions'); const widthInput = document.getElementById('ge-marquee-width'); const heightInput = document.getElementById('ge-marquee-height'); const syncMarqueeConstraint = () => { state.marqueeConstraint = ['ratio', 'size'].includes(constraint?.value) ? constraint.value : 'free'; if (dimensions) dimensions.hidden = state.marqueeConstraint === 'free'; if (widthInput) widthInput.step = state.marqueeConstraint === 'size' ? '1' : '0.01'; if (heightInput) heightInput.step = state.marqueeConstraint === 'size' ? '1' : '0.01'; if (widthInput) widthInput.value = String(state.marqueeConstraint === 'size' ? state.marqueeFixedWidth : state.marqueeRatioWidth); if (heightInput) heightInput.value = String(state.marqueeConstraint === 'size' ? state.marqueeFixedHeight : state.marqueeRatioHeight); }; const commitMarqueeDimensions = () => { const w = Math.max(1, Number(widthInput?.value) || 1); const h = Math.max(1, Number(heightInput?.value) || 1); if (state.marqueeConstraint === 'size') { state.marqueeFixedWidth = Math.round(w); state.marqueeFixedHeight = Math.round(h); } else { state.marqueeRatioWidth = w; state.marqueeRatioHeight = h; } if (widthInput) widthInput.value = String(state.marqueeConstraint === 'size' ? state.marqueeFixedWidth : state.marqueeRatioWidth); if (heightInput) heightInput.value = String(state.marqueeConstraint === 'size' ? state.marqueeFixedHeight : state.marqueeRatioHeight); }; constraint?.addEventListener('change', syncMarqueeConstraint); widthInput?.addEventListener('change', commitMarqueeDimensions); heightInput?.addEventListener('change', commitMarqueeDimensions); document.getElementById('ge-marquee-swap')?.addEventListener('click', () => { const oldWidth = widthInput?.value; if (widthInput && heightInput) { widthInput.value = heightInput.value; heightInput.value = oldWidth; commitMarqueeDimensions(); } }); if (constraint) constraint.value = state.marqueeConstraint || 'free'; syncMarqueeConstraint(); document.getElementById('ge-marquee-clear')?.addEventListener('click', wandClear); document.getElementById('ge-marquee-invert')?.addEventListener('click', invertSelection); document.getElementById('ge-marquee-delete')?.addEventListener('click', wandDeleteSelection); document.getElementById('ge-marquee-copy')?.addEventListener('click', wandCopyToNewLayer); document.getElementById('ge-marquee-mask')?.addEventListener('click', wandToMask); // ── Lasso section ── const lassoFPrev = document.getElementById('ge-lasso-feather-preview'); function syncLassoFeather(v) { if (!lassoFPrev) return; const inner = Math.max(0, 50 - v * 1.0); lassoFPrev.style.background = `radial-gradient(circle, var(--fg) 0%, var(--fg) ${inner}%, transparent 75%)`; } syncLassoFeather(0); document.getElementById('ge-lasso-feather')?.addEventListener('input', (e) => { document.getElementById('ge-lasso-feather-label').textContent = e.target.value + 'px'; syncLassoFeather(parseInt(e.target.value, 10)); composite(); }); document.getElementById('ge-lasso-grow')?.addEventListener('input', (e) => { document.getElementById('ge-lasso-grow-label').textContent = e.target.value + 'px'; composite(); }); document.getElementById('ge-lasso-delete')?.addEventListener('click', () => { if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoDeleteSelection(); else if (state.wandMask) wandDeleteSelection(); }); document.getElementById('ge-lasso-copy')?.addEventListener('click', () => { if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoCopyToLayer(); else if (state.wandMask) wandCopyToNewLayer(); }); document.getElementById('ge-lasso-mask')?.addEventListener('click', () => { if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoToMask(); else if (state.wandMask) wandToMask(); }); document.getElementById('ge-lasso-invert')?.addEventListener('click', invertSelection); // ── Wand section ── document.getElementById('ge-wand-feather')?.addEventListener('input', (e) => { const v = parseInt(e.target.value, 10) || 0; document.getElementById('ge-wand-feather-label').textContent = v + 'px'; const prev = document.getElementById('ge-wand-feather-preview'); if (prev) prev.style.setProperty('--feather-blur', Math.min(v / 14, 8) + 'px'); composite(); }); document.getElementById('ge-wand-grow')?.addEventListener('input', (e) => { document.getElementById('ge-wand-grow-label').textContent = e.target.value + 'px'; composite(); }); // Tolerance slider fires `input` rapidly — coalesce to one wand run // per frame with rAF. Label updates synchronously so the number // tracks the cursor even when the flood-fill runs at ~60fps. let wandRetuneRaf = null; const retuneWand = () => { if (!state.wandLastSeed || !state.wandMask) return; if (wandRetuneRaf) return; wandRetuneRaf = requestAnimationFrame(() => { wandRetuneRaf = null; runMagicWand(state.wandLastSeed.x, state.wandLastSeed.y, 'replace', { retune: true }); }); }; const liveBtn = document.getElementById('ge-wand-live'); liveBtn?.addEventListener('click', () => { state.wandLiveRetune = !state.wandLiveRetune; liveBtn.classList.toggle('active', state.wandLiveRetune); liveBtn.setAttribute('aria-pressed', state.wandLiveRetune ? 'true' : 'false'); if (state.wandLiveRetune) retuneWand(); }); document.getElementById('ge-wand-tolerance')?.addEventListener('input', (e) => { state.wandTolerance = parseInt(e.target.value, 10); const lbl = document.getElementById('ge-wand-tol-label'); if (lbl) lbl.textContent = state.wandTolerance; const wp = document.getElementById('ge-wand-tol-preview'); if (wp) wp.style.opacity = (state.wandTolerance / 100).toFixed(2); if (state.wandLiveRetune) retuneWand(); }); // Wand mode segmented toggle (New / Add / Subtract). document.querySelectorAll('.ge-wand-mode-btn').forEach(btn => { btn.addEventListener('click', () => { const mode = btn.dataset.wandMode; if (!mode) return; state.wandMode = mode; document.querySelectorAll('.ge-wand-mode-btn').forEach(b => { b.classList.toggle('active', b.dataset.wandMode === mode); }); }); }); // Toggle the translucent red overlay for the wand selection. const toggleSelectionOverlay = () => { state.wandMaskVisible = !state.wandMaskVisible; ['ge-wand-vis', 'ge-marquee-vis', 'ge-sam-vis'].forEach(id => { const btn = document.getElementById(id); if (!btn) return; btn.innerHTML = state.wandMaskVisible ? EYE_OPEN : EYE_OFF; btn.title = state.wandMaskVisible ? 'Hide selection overlay' : 'Show selection overlay'; btn.classList.toggle('visible', state.wandMaskVisible); }); composite(); }; document.getElementById('ge-wand-vis')?.addEventListener('click', toggleSelectionOverlay); document.getElementById('ge-marquee-vis')?.addEventListener('click', toggleSelectionOverlay); document.getElementById('ge-wand-clear')?.addEventListener('click', wandClear); document.getElementById('ge-wand-invert')?.addEventListener('click', invertSelection); document.getElementById('ge-wand-delete')?.addEventListener('click', wandDeleteSelection); document.getElementById('ge-wand-copy')?.addEventListener('click', wandCopyToNewLayer); document.getElementById('ge-wand-mask')?.addEventListener('click', wandToMask); // Selection-constrained Bg Remove — reuses the same path the toolbar // Bg Remove button does. buildSelectionHintMask picks the active // wand/lasso selection, so this just kicks off the existing flow. document.getElementById('ge-wand-rembg')?.addEventListener('click', async () => { const btn = document.getElementById('ge-wand-rembg'); const hint = buildSelectionHintMask(); if (!hint) { if (uiModule) uiModule.showToast('Click to make a wand selection first'); return; } await applyImageTool('/api/image/remove-bg', { hint_mask: hint }, 'BG Removed', btn); wandClear(); }); // Live tolerance preview (just opacity-tracking like sharpen). const wandTolPrev = document.getElementById('ge-wand-tol-preview'); if (wandTolPrev) wandTolPrev.style.opacity = (state.wandTolerance / 100).toFixed(2); }