1/** 2 * Lasso + Magic Wand panel controls â sliders, mode toggles, and the 3 * panel action buttons (Invert / Clear / Delete / Copy / To Mask / 4 * Bg Remove). The actual selection algorithms live in their tool 5 * modules (editor/tools/lasso.js, editor/tools/wand.js); this file 6 * just wires the side-panel UI to them. 7 * 8 * Lasso section: 9 * #ge-lasso-feather slider, updates label + preview, recomposites 10 * #ge-lasso-grow slider, updates label + recomposites 11 * #ge-lasso-invert â invertSelection 12 * #ge-lasso-delete â lassoDeleteSelection 13 * #ge-lasso-copy â lassoCopyToLayer 14 * #ge-lasso-mask â lassoToMask 15 * 16 * Wand section: 17 * #ge-wand-feather slider, updates label + recomposites 18 * #ge-wand-grow slider, updates label + recomposites 19 * #ge-wand-tolerance slider, updates future wand-click tolerance 20 * #ge-wand-live opt-in rAF-coalesced live retune while dragging 21 * .ge-wand-mode-btn segmented toggle (New / Add / Subtract) 22 * #ge-wand-vis toggle the translucent red overlay 23 * #ge-wand-clear / -invert / -delete / -copy / -mask / -rembg 24 * 25 * @param {{ 26 * composite: () => void, 27 * invertSelection: () => boolean, 28 * lassoDeleteSelection: () => void, 29 * lassoCopyToLayer: () => void, 30 * lassoToMask: () => void, 31 * runMagicWand: (x: number, y: number, mode: string, opts?: object) => void, 32 * wandClear: () => void, 33 * wandDeleteSelection: () => void, 34 * wandCopyToNewLayer: () => void, 35 * wandToMask: () => void, 36 * buildSelectionHintMask: () => string | null, 37 * applyImageTool: (endpoint, payload, name, btn, opts?) => Promise<void>, 38 * uiModule: object, 39 * }} deps 40 */ 41import { state } from './state.js'; 42 43const EYE_OPEN = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'; 44const EYE_OFF = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>'; 45 46export function wireSelectionControls({ 47 composite, 48 invertSelection, 49 lassoDeleteSelection, lassoCopyToLayer, lassoToMask, 50 runMagicWand, 51 wandClear, wandDeleteSelection, wandCopyToNewLayer, wandToMask, 52 buildSelectionHintMask, applyImageTool, 53 uiModule, 54}) { 55 // ââ Lasso section ââ 56 const lassoFPrev = document.getElementById('ge-lasso-feather-preview'); 57 function syncLassoFeather(v) { 58 if (!lassoFPrev) return; 59 const inner = Math.max(0, 50 - v * 1.0); 60 lassoFPrev.style.background = `radial-gradient(circle, var(--fg) 0%, var(--fg) ${inner}%, transparent 75%)`; 61 } 62 syncLassoFeather(0); 63 document.getElementById('ge-lasso-feather')?.addEventListener('input', (e) => { 64 document.getElementById('ge-lasso-feather-label').textContent = e.target.value + 'px'; 65 syncLassoFeather(parseInt(e.target.value, 10)); 66 composite(); 67 }); 68 document.getElementById('ge-lasso-grow')?.addEventListener('input', (e) => { 69 document.getElementById('ge-lasso-grow-label').textContent = e.target.value + 'px'; 70 composite(); 71 }); 72 document.getElementById('ge-lasso-delete')?.addEventListener('click', () => { 73 if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoDeleteSelection(); 74 else if (state.wandMask) wandDeleteSelection(); 75 }); 76 document.getElementById('ge-lasso-copy')?.addEventListener('click', () => { 77 if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoCopyToLayer(); 78 else if (state.wandMask) wandCopyToNewLayer(); 79 }); 80 document.getElementById('ge-lasso-mask')?.addEventListener('click', () => { 81 if (state.lassoPoints.length >= 3 && !state.lassoActive) lassoToMask(); 82 else if (state.wandMask) wandToMask(); 83 }); 84 document.getElementById('ge-lasso-invert')?.addEventListener('click', invertSelection); 85 86 // ââ Wand section ââ 87 document.getElementById('ge-wand-feather')?.addEventListener('input', (e) => { 88 const v = parseInt(e.target.value, 10) || 0; 89 document.getElementById('ge-wand-feather-label').textContent = v + 'px'; 90 const prev = document.getElementById('ge-wand-feather-preview'); 91 if (prev) prev.style.setProperty('--feather-blur', Math.min(v / 14, 8) + 'px'); 92 composite(); 93 }); 94 document.getElementById('ge-wand-grow')?.addEventListener('input', (e) => { 95 document.getElementById('ge-wand-grow-label').textContent = e.target.value + 'px'; 96 composite(); 97 }); 98
99 // Tolerance slider fires `input` rapidly â coalesce to one wand run 100 // per frame with rAF. Label updates synchronously so the number 101 // tracks the cursor even when the flood-fill runs at ~60fps. 102 let wandRetuneRaf = null; 103 const retuneWand = () => { 104 if (!state.wandLastSeed || !state.wandMask) return; 105 if (wandRetuneRaf) return; 106 wandRetuneRaf = requestAnimationFrame(() => { 107 wandRetuneRaf = null; 108 runMagicWand(state.wandLastSeed.x, state.wandLastSeed.y, 'replace', { retune: true }); 109 }); 110 }; 111 const liveBtn = document.getElementById('ge-wand-live'); 112 liveBtn?.addEventListener('click', () => { 113 state.wandLiveRetune = !state.wandLiveRetune; 114 liveBtn.classList.toggle('active', state.wandLiveRetune); 115 liveBtn.setAttribute('aria-pressed', state.wandLiveRetune ? 'true' : 'false'); 116 if (state.wandLiveRetune) retuneWand(); 117 }); 118 document.getElementById('ge-wand-tolerance')?.addEventListener('input', (e) => { 119 state.wandTolerance = parseInt(e.target.value, 10); 120 const lbl = document.getElementById('ge-wand-tol-label'); 121 if (lbl) lbl.textContent = state.wandTolerance; 122 const wp = document.getElementById('ge-wand-tol-preview'); 123 if (wp) wp.style.opacity = (state.wandTolerance / 100).toFixed(2); 124 if (state.wandLiveRetune) retuneWand(); 125 }); 126 127 // Wand mode segmented toggle (New / Add / Subtract). 128 document.querySelectorAll('.ge-wand-mode-btn').forEach(btn => { 129 btn.addEventListener('click', () => { 130 const mode = btn.dataset.wandMode; 131 if (!mode) return; 132 state.wandMode = mode; 133 document.querySelectorAll('.ge-wand-mode-btn').forEach(b => { 134 b.classList.toggle('active', b.dataset.wandMode === mode); 135 }); 136 }); 137 }); 138 139 // Toggle the translucent red overlay for the wand selection. 140 document.getElementById('ge-wand-vis')?.addEventListener('click', () => { 141 state.wandMaskVisible = !state.wandMaskVisible; 142 const btn = document.getElementById('ge-wand-vis'); 143 if (btn) { 144 btn.innerHTML = state.wandMaskVisible ? EYE_OPEN : EYE_OFF; 145 btn.title = state.wandMaskVisible ? 'Hide selection overlay' : 'Show selection overlay'; 146 btn.classList.toggle('visible', state.wandMaskVisible); 147 } 148 composite(); 149 }); 150 151 document.getElementById('ge-wand-clear')?.addEventListener('click', wandClear); 152 document.getElementById('ge-wand-invert')?.addEventListener('click', invertSelection); 153 document.getElementById('ge-wand-delete')?.addEventListener('click', wandDeleteSelection); 154 document.getElementById('ge-wand-copy')?.addEventListener('click', wandCopyToNewLayer); 155 document.getElementById('ge-wand-mask')?.addEventListener('click', wandToMask); 156 // Selection-constrained Bg Remove â reuses the same path the toolbar 157 // Bg Remove button does. buildSelectionHintMask picks the active 158 // wand/lasso selection, so this just kicks off the existing flow. 159 document.getElementById('ge-wand-rembg')?.addEventListener('click', async () => { 160 const btn = document.getElementById('ge-wand-rembg'); 161 const hint = buildSelectionHintMask(); 162 if (!hint) { if (uiModule) uiModule.showToast('Click to make a wand selection first'); return; } 163 await applyImageTool('/api/image/remove-bg', { hint_mask: hint }, 'BG Removed', btn); 164 wandClear(); 165 }); 166 167 // Live tolerance preview (just opacity-tracking like sharpen). 168 const wandTolPrev = document.getElementById('ge-wand-tol-preview'); 169 if (wandTolPrev) wandTolPrev.style.opacity = (state.wandTolerance / 100).toFixed(2); 170}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.