PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/wire-selection-controls.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:26 UTC 8,411 bytes, 170 lines download raw bytes

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.