1import { state, updateCropOption } from './app.js'; 2import { getIdPhotoPreset, setIdPhotoGuideOverride, buildIdPhotoGuideFromPreset } from './id-photo-presets.js'; 3 4const DEFAULT_SOURCE_HEAD_WIDTH_RATIO = 0.48; 5const DEFAULT_OFFSET_Y = -0.01; 6 7let calibrateSnapshot = null; 8let draggingSilhouette = null; 9let dragStart = { x: 0, y: 0 }; 10let dragStartParams = null; 11let dragMeta = null; 12let activePointerId = null; 13let handlersAttached = false; 14 15function roundParam(value, digits = 4) { 16 const n = Number(value); 17 if (!Number.isFinite(n)) return 0; 18 const factor = 10 ** digits; 19 return Math.round(n * factor) / factor; 20} 21 22function ensureIdPhotoGuideBase() { 23 const current = state.cropOptions.idPhotoGuide; 24 if (current?.head && current?.face) return current; 25 const built = buildIdPhotoGuideFromPreset(getIdPhotoPreset(state.cropOptions.idPhotoPreset)); 26 if (!built) return current || { hideDefaultGrid: true, silhouette: {} }; 27 if (!current) return built; 28 return { 29 ...built, 30 ...current, 31 head: current.head || built.head, 32 face: current.face || built.face, 33 lines: Array.isArray(current.lines) && current.lines.length ? current.lines : built.lines, 34 silhouette: { 35 ...(built.silhouette || {}), 36 ...(current.silhouette && typeof current.silhouette === 'object' ? current.silhouette : {}) 37 } 38 }; 39} 40 41export function isSilhouetteCalibrateMode() { 42 return state.cropOptions.mode === 'idphoto' && state.cropOptions.idPhotoSilhouetteCalibrate === true; 43} 44 45export function getActiveSilhouetteParams() { 46 const guide = ensureIdPhotoGuideBase(); 47 const silhouette = guide?.silhouette && typeof guide.silhouette === 'object' ? guide.silhouette : {}; 48 const presetGuide = getIdPhotoPreset(state.cropOptions.idPhotoPreset)?.guide || {}; 49 50 return { 51 scale: Number.isFinite(silhouette.scale) 52 ? silhouette.scale 53 : (Number.isFinite(presetGuide.silhouetteScale) ? presetGuide.silhouetteScale : 1), 54 heightScale: Number.isFinite(silhouette.heightScale) 55 ? silhouette.heightScale 56 : (Number.isFinite(presetGuide.silhouetteHeightScale) ? presetGuide.silhouetteHeightScale : 1), 57 offsetXRatio: Number.isFinite(silhouette.offsetXRatio) 58 ? silhouette.offsetXRatio 59 : (Number.isFinite(presetGuide.silhouetteOffsetXRatio) ? presetGuide.silhouetteOffsetXRatio : 0), 60 offsetYRatio: Number.isFinite(silhouette.offsetYRatio) 61 ? silhouette.offsetYRatio 62 : (Number.isFinite(presetGuide.silhouetteOffsetYRatio) ? presetGuide.silhouetteOffsetYRatio : DEFAULT_OFFSET_Y), 63 sourceHeadWidthRatio: Number.isFinite(silhouette.sourceHeadWidthRatio) 64 ? silhouette.sourceHeadWidthRatio 65 : (Number.isFinite(presetGuide.silhouetteSourceHeadWidthRatio) 66 ? presetGuide.silhouetteSourceHeadWidthRatio 67 : DEFAULT_SOURCE_HEAD_WIDTH_RATIO) 68 }; 69} 70 71export function formatSilhouetteParams(params = getActiveSilhouetteParams()) { 72 return { 73 scale: roundParam(params.scale), 74 heightScale: roundParam(params.heightScale), 75 offsetXRatio: roundParam(params.offsetXRatio), 76 offsetYRatio: roundParam(params.offsetYRatio), 77 sourceHeadWidthRatio: roundParam(params.sourceHeadWidthRatio) 78 }; 79} 80 81function cloneGuide(guide) { 82 return guide ? JSON.parse(JSON.stringify(guide)) : null; 83} 84 85export function applySilhouetteParams(partial, { render = true } = {}) { 86 const currentGuide = ensureIdPhotoGuideBase(); 87 const current = getActiveSilhouetteParams(); 88 const nextParams = { 89 ...current, 90 ...partial, 91 scale: Math.max(0.2, Number(partial.scale ?? current.scale) || current.scale), 92 heightScale: Math.max(0.5, Number(partial.heightScale ?? current.heightScale) || current.heightScale), 93 offsetXRatio: Number(partial.offsetXRatio ?? current.offsetXRatio) || 0, 94 offsetYRatio: Number.isFinite(Number(partial.offsetYRatio ?? current.offsetYRatio)) 95 ? Number(partial.offsetYRatio ?? current.offsetYRatio) 96 : current.offsetYRatio, 97 sourceHeadWidthRatio: Math.max( 98 0.1, 99 Number(partial.sourceHeadWidthRatio ?? current.sourceHeadWidthRatio) || current.sourceHeadWidthRatio 100 ) 101 }; 102 103 const nextGuide = { 104 ...currentGuide, 105 hideDefaultGrid: true, 106 silhouette: { 107 ...(currentGuide.silhouette && typeof currentGuide.silhouette === 'object' ? currentGuide.silhouette : {}), 108 enabled: true, 109 scale: nextParams.scale, 110 heightScale: nextParams.heightScale, 111 offsetXRatio: nextParams.offsetXRatio, 112 offsetYRatio: nextParams.offsetYRatio, 113 sourceHeadWidthRatio: nextParams.sourceHeadWidthRatio 114 } 115 }; 116 117 updateCropOption('idPhotoGuide', nextGuide); 118 syncCalibrateReadout(); 119 if (render) {
120 import('./grid.js').then((grid) => grid.renderGrid()); 121 } 122 return formatSilhouetteParams(nextParams); 123} 124 125export function enableSilhouetteCalibrate() { 126 if (state.cropOptions.mode !== 'idphoto') return false; 127 const guide = ensureIdPhotoGuideBase(); 128 updateCropOption('idPhotoGuide', guide); 129 calibrateSnapshot = { 130 guide: cloneGuide(guide), 131 params: getActiveSilhouetteParams() 132 }; 133 updateCropOption('idPhotoSilhouetteCalibrate', true); 134 syncCalibratePanel(); 135 import('./grid.js').then((grid) => grid.renderGrid()); 136 return true; 137} 138 139export function cancelSilhouetteCalibrate() { 140 if (calibrateSnapshot?.guide) { 141 updateCropOption('idPhotoGuide', calibrateSnapshot.guide); 142 } 143 calibrateSnapshot = null; 144 updateCropOption('idPhotoSilhouetteCalibrate', false); 145 syncCalibratePanel(); 146 import('./grid.js').then((grid) => grid.renderGrid()); 147} 148 149export async function confirmSilhouetteCalibrate() { 150 const params = formatSilhouetteParams(); 151 const presetKey = state.cropOptions.idPhotoPreset; 152 const payload = { 153 presetKey, 154 silhouetteScale: params.scale, 155 silhouetteHeightScale: params.heightScale, 156 silhouetteOffsetXRatio: params.offsetXRatio, 157 silhouetteOffsetYRatio: params.offsetYRatio, 158 silhouetteSourceHeadWidthRatio: params.sourceHeadWidthRatio 159 }; 160 161 let writeResult = null; 162 let writeError = null; 163 try { 164 const response = await fetch('/api/save-id-photo-silhouette', { 165 method: 'POST', 166 headers: { 'Content-Type': 'application/json' }, 167 body: JSON.stringify(payload) 168 }); 169 writeResult = await response.json(); 170 if (!response.ok || !writeResult.ok) { 171 throw new Error(writeResult?.error || `Save failed (${response.status})`); 172 } 173 } catch (error) { 174 writeError = error; 175 } 176 177 const snippet = [ 178 `silhouetteScale: ${params.scale},`, 179 `silhouetteHeightScale: ${params.heightScale},`, 180 `silhouetteOffsetXRatio: ${params.offsetXRatio},`, 181 `silhouetteOffsetYRatio: ${params.offsetYRatio},`, 182 `silhouetteSourceHeadWidthRatio: ${params.sourceHeadWidthRatio},` 183 ].join('\n'); 184 185 try { 186 await navigator.clipboard.writeText(snippet); 187 } catch (_) { 188 // Clipboard may be blocked; UI still shows values. 189 } 190 191 calibrateSnapshot = null; 192 updateCropOption('idPhotoSilhouetteCalibrate', false); 193 setIdPhotoGuideOverride(presetKey, { 194 silhouetteScale: params.scale, 195 silhouetteHeightScale: params.heightScale, 196 silhouetteOffsetXRatio: params.offsetXRatio, 197 silhouetteOffsetYRatio: params.offsetYRatio, 198 silhouetteSourceHeadWidthRatio: params.sourceHeadWidthRatio 199 }); 200 applySilhouetteParams(params, { render: true }); 201 syncCalibratePanel({ 202 status: writeError 203 ? `å·²å¤å¶åæ°ï¼ä½åå ¥æä»¶å¤±è´¥ï¼${writeError.message}ãè¯·ç¡®è®¤ç¨ npm run dev å¯å¨ã` 204 : `å·²åå ¥ ${presetKey}ï¼å¹¶å¤å¶å°åªè´´æ¿ã`, 205 statusType: writeError ? 'error' : 'success' 206 }); 207 208 return { ok: !writeError, params, writeResult, writeError, snippet }; 209} 210 211export function syncCalibrateReadout() { 212 const params = formatSilhouetteParams(); 213 const scaleEl = document.getElementById('id-photo-silhouette-scale'); 214 const heightScaleEl = document.getElementById('id-photo-silhouette-height-scale'); 215 const offsetXEl = document.getElementById('id-photo-silhouette-offset-x'); 216 const offsetYEl = document.getElementById('id-photo-silhouette-offset-y'); 217 const scaleValueEl = document.getElementById('id-photo-silhouette-scale-value'); 218 const heightScaleValueEl = document.getElementById('id-photo-silhouette-height-scale-value'); 219 const offsetXValueEl = document.getElementById('id-photo-silhouette-offset-x-value'); 220 const offsetYValueEl = document.getElementById('id-photo-silhouette-offset-y-value'); 221 222 if (scaleEl) scaleEl.value = String(params.scale); 223 if (heightScaleEl) heightScaleEl.value = String(params.heightScale); 224 if (offsetXEl) offsetXEl.value = String(params.offsetXRatio); 225 if (offsetYEl) offsetYEl.value = String(params.offsetYRatio); 226 if (scaleValueEl) scaleValueEl.textContent = params.scale.toFixed(3); 227 if (heightScaleValueEl) heightScaleValueEl.textContent = params.heightScale.toFixed(3); 228 if (offsetXValueEl) offsetXValueEl.textContent = params.offsetXRatio.toFixed(3); 229 if (offsetYValueEl) offsetYValueEl.textContent = params.offsetYRatio.toFixed(3); 230} 231 232export function syncCalibratePanel(extra = {}) { 233 const panel = document.getElementById('id-photo-silhouette-calibrate-panel'); 234 const toggle = document.getElementById('id-photo-silhouette-calibrate-checkbox'); 235 const controls = document.getElementById('id-photo-silhouette-calibrate-control
235s'); 236 const statusEl = document.getElementById('id-photo-silhouette-calibrate-status'); 237 if (!panel) return; 238 239 const enabled = isSilhouetteCalibrateMode(); 240 if (toggle) toggle.checked = enabled; 241 if (controls) controls.hidden = !enabled; 242 panel.classList.toggle('is-active', enabled); 243 syncCalibrateReadout(); 244 245 if (statusEl && Object.prototype.hasOwnProperty.call(extra, 'status')) { 246 statusEl.textContent = extra.status || ''; 247 statusEl.dataset.type = extra.statusType || ''; 248 } 249} 250 251export function getSilhouetteCalibratePanelHTML() { 252 const enabled = isSilhouetteCalibrateMode(); 253 const params = formatSilhouetteParams(); 254 return ` 255 <div class="id-photo-calibrate ${enabled ? 'is-active' : ''}" id="id-photo-silhouette-calibrate-panel"> 256 <label class="id-photo-helper-toggle"> 257 <input type="checkbox" id="id-photo-silhouette-calibrate-checkbox" ${enabled ? 'checked' : ''}> 258 <span>æ ¡åé»è²äººåè½®å»</span> 259 </label> 260 <p class="id-photo-calibrate-hint">å¼å¯åä¼éå®è£åªæ¡ï¼å¯æå¨/缩æ¾è½®å»ï¼è°å¥½åç¹ãç¡®å®å¹¶åå ¥ãä¿åå°å½å presetã</p> 261 <div class="id-photo-calibrate-controls" id="id-photo-silhouette-calibrate-controls" ${enabled ? '' : 'hidden'}> 262 <label class="id-photo-calibrate-field"> 263 <span>Scale <strong id="id-photo-silhouette-scale-value">${params.scale.toFixed(3)}</strong></span> 264 <input type="range" id="id-photo-silhouette-scale" min="0.4" max="1.6" step="0.01" value="${params.scale}"> 265 </label> 266 <label class="id-photo-calibrate-field"> 267 <span>Height Scale <strong id="id-photo-silhouette-height-scale-value">${params.heightScale.toFixed(3)}</strong></span> 268 <input type="range" id="id-photo-silhouette-height-scale" min="0.7" max="1.6" step="0.01" value="${params.heightScale}"> 269 </label> 270 <label class="id-photo-calibrate-field"> 271 <span>Offset X <strong id="id-photo-silhouette-offset-x-value">${params.offsetXRatio.toFixed(3)}</strong></span> 272 <input type="range" id="id-photo-silhouette-offset-x" min="-0.25" max="0.25" step="0.005" value="${params.offsetXRatio}"> 273 </label> 274 <label class="id-photo-calibrate-field"> 275 <span>Offset Y <strong id="id-photo-silhouette-offset-y-value">${params.offsetYRatio.toFixed(3)}</strong></span> 276 <input type="range" id="id-photo-silhouette-offset-y" min="-0.25" max="0.25" step="0.005" value="${params.offsetYRatio}"> 277 </label> 278 <div class="id-photo-calibrate-actions"> 279 <button type="button" class="btn btn-secondary" id="id-photo-silhouette-calibrate-cancel">åæ¶</button> 280 <button type="button" class="btn btn-primary" id="id-photo-silhouette-calibrate-confirm">ç¡®å®å¹¶åå ¥</button> 281 </div> 282 <p class="id-photo-calibrate-status" id="id-photo-silhouette-calibrate-status" data-type=""></p> 283 </div> 284 </div> 285 `; 286} 287 288export function bindSilhouetteCalibratePanel(root = document) { 289 const toggle = root.querySelector('#id-photo-silhouette-calibrate-checkbox'); 290 if (toggle && !toggle.dataset.bound) { 291 toggle.dataset.bound = '1'; 292 toggle.addEventListener('change', () => { 293 if (toggle.checked) enableSilhouetteCalibrate(); 294 else cancelSilhouetteCalibrate(); 295 }); 296 } 297 298 const scaleEl = root.querySelector('#id-photo-silhouette-scale'); 299 const heightScaleEl = root.querySelector('#id-photo-silhouette-height-scale'); 300 const offsetXEl = root.querySelector('#id-photo-silhouette-offset-x'); 301 const offsetYEl = root.querySelector('#id-photo-silhouette-offset-y'); 302 const cancelBtn = root.querySelector('#id-photo-silhouette-calibrate-cancel'); 303 const confirmBtn = root.querySelector('#id-photo-silhouette-calibrate-confirm'); 304 305 const bindRange = (el, key) => { 306 if (!el || el.dataset.bound) return; 307 el.dataset.bound = '1'; 308 el.addEventListener('input', () => { 309 applySilhouetteParams({ [key]: Number(el.value) }); 310 }); 311 }; 312 313 bindRange(scaleEl, 'scale'); 314 bindRange(heightScaleEl, 'heightScale'); 315 bindRange(offsetXEl, 'offsetXRatio'); 316 bindRange(offsetYEl, 'offsetYRatio'); 317 318 if (cancelBtn && !cancelBtn.dataset.bound) { 319 cancelBtn.dataset.bound = '1'; 320 cancelBtn.addEventListener('click', () => cancelSilhouetteCalibrate()); 321 } 322 323 if (confirmBtn && !confirmBtn.dataset.bound) { 324 confirmBtn.dataset.bound = '1'; 325 confirmBtn.addEventListener('click', async () => { 326 confirmBtn.disabled = true;
327 try { 328 await confirmSilhouetteCalibrate(); 329 } finally { 330 confirmBtn.disabled = false; 331 } 332 }); 333 } 334 335 syncCalibratePanel(); 336} 337 338function parsePercent(value) { 339 const n = parseFloat(value); 340 return Number.isFinite(n) ? n / 100 : 0; 341} 342 343function paramsFromLayout(layout, meta) { 344 const width = Math.max(0.05, layout.width); 345 const scale = (width * meta.sourceHeadWidthRatio) / Math.max(meta.headWidth, 0.0001); 346 const heightScale = layout.height / Math.max(width * meta.cropAspect, 0.0001); 347 const offsetXRatio = layout.left - (meta.headCenterX - width / 2); 348 const offsetYRatio = layout.top - meta.headTop; 349 return { 350 scale: roundParam(scale), 351 heightScale: roundParam(Math.max(0.5, heightScale)), 352 offsetXRatio: roundParam(offsetXRatio), 353 offsetYRatio: roundParam(offsetYRatio), 354 sourceHeadWidthRatio: meta.sourceHeadWidthRatio 355 }; 356} 357 358function layoutFromParams(params, meta) { 359 const heightScale = Number.isFinite(params.heightScale) && params.heightScale > 0 ? params.heightScale : 1; 360 const width = Math.max(0.05, (meta.headWidth / meta.sourceHeadWidthRatio) * params.scale); 361 const height = width * meta.cropAspect * heightScale; 362 const left = meta.headCenterX - width / 2 + params.offsetXRatio; 363 const top = meta.headTop + params.offsetYRatio; 364 return { left, top, width, height }; 365} 366 367function applySilhouetteDom(silhouetteEl, layout) { 368 silhouetteEl.style.left = `${layout.left * 100}%`; 369 silhouetteEl.style.top = `${layout.top * 100}%`; 370 silhouetteEl.style.width = `${layout.width * 100}%`; 371 silhouetteEl.style.height = `${layout.height * 100}%`; 372} 373 374function readSilhouetteMeta(silhouetteEl) { 375 return { 376 headWidth: Number(silhouetteEl.dataset.headWidth) || 0.5, 377 headTop: Number(silhouetteEl.dataset.headTop) || 0, 378 headCenterX: Number(silhouetteEl.dataset.headCenterX) || 0.5, 379 cropAspect: Number(silhouetteEl.dataset.cropAspect) || 1, 380 sourceHeadWidthRatio: Number(silhouetteEl.dataset.sourceHeadWidthRatio) || DEFAULT_SOURCE_HEAD_WIDTH_RATIO, 381 heightScale: Number(silhouetteEl.dataset.heightScale) || 1 382 }; 383} 384 385function handleSilhouetteDrag(e) { 386 if (!draggingSilhouette || !dragMeta || !dragStartParams) return; 387 const card = document.querySelector(`[data-id="${draggingSilhouette.imageId}"]`); 388 if (!card) return; 389 const cropBox = card.querySelector('.crop-box'); 390 const silhouetteEl = card.querySelector('.idphoto-guide-silhouette'); 391 if (!cropBox || !silhouetteEl) return; 392 393 const rect = cropBox.getBoundingClientRect(); 394 if (rect.width <= 0 || rect.height <= 0) return; 395 396 const dx = (e.clientX - dragStart.x) / rect.width; 397 const dy = (e.clientY - dragStart.y) / rect.height; 398 let nextParams = { ...dragStartParams }; 399 const startLayout = layoutFromParams(dragStartParams, dragMeta); 400 const type = draggingSilhouette.type; 401 402 if (type === 'move') { 403 nextParams.offsetXRatio = dragStartParams.offsetXRatio + dx; 404 nextParams.offsetYRatio = dragStartParams.offsetYRatio + dy; 405 } else if (type === 'n' || type === 's') { 406 // Vertical-only stretch: keep width, change heightScale; anchor the opposite edge. 407 const startBottom = startLayout.top + startLayout.height; 408 let nextTop = startLayout.top; 409 let nextHeight = startLayout.height; 410 if (type === 's') { 411 nextHeight = Math.max(0.08, startLayout.height + dy); 412 } else { 413 nextTop = startLayout.top + dy; 414 nextHeight = Math.max(0.08, startBottom - nextTop); 415 } 416 nextParams.heightScale = nextHeight / Math.max(startLayout.width * dragMeta.cropAspect, 0.0001); 417 nextParams.offsetYRatio = nextTop - dragMeta.headTop; 418 } else { 419 // Corner resize: keep heightScale, change overall scale around center. 420 const heightScale = Number.isFinite(dragStartParams.heightScale) && dragStartParams.heightScale > 0 421 ? dragStartParams.heightScale 422 : 1; 423 const centerX = startLayout.left + startLayout.width / 2; 424 const centerY = startLayout.top + startLayout.height / 2; 425 const pointerX = ((e.clientX - rect.left) / rect.width); 426 const pointerY = ((e.clientY - rect.top) / rect.height); 427 const halfW = Math.max(Math.abs(pointerX - centerX), 0.03); 428 const halfH = Math.max(Math.abs(pointerY - centerY), 0.03); 429 const widthFromX = halfW * 2; 430 const widthFromY = (halfH * 2) / Math.max(dragMeta.cropAspect * heightScale, 0.0001); 431 const width = Math.max(0.08, (widthFromX + widthFromY) / 2); 432 nextParams.scale = (width * dragMeta.sourceHeadWidthRatio) / Math.max(dragMeta.headWidth, 0.0001); 433 nextParams.heightScale = heightScale; 434 } 435 436 const layout = layoutFromParams(nextParams, dragMeta); 437 applySilhouetteDom(silhouetteEl, layout);
438 applySilhouetteParams(nextParams, { render: false }); 439} 440 441function handleSilhouetteEnd() { 442 if (!draggingSilhouette) return; 443 draggingSilhouette = null; 444 dragStartParams = null; 445 dragMeta = null; 446 activePointerId = null; 447 import('./grid.js').then((grid) => grid.renderGrid()); 448} 449 450export function setupSilhouetteCalibrateHandlers() { 451 if (!isSilhouetteCalibrateMode()) return; 452 453 const grid = document.getElementById('image-grid'); 454 if (!grid) return; 455 456 grid.querySelectorAll('.idphoto-guide-silhouette.is-calibrating').forEach((silhouetteEl) => { 457 const imageId = silhouetteEl.dataset.imageId; 458 if (!imageId) return; 459 460 const startDrag = (type, clientX, clientY, pointerId = null, targetEl = silhouetteEl) => { 461 draggingSilhouette = { type, imageId }; 462 dragStart = { x: clientX, y: clientY }; 463 dragMeta = readSilhouetteMeta(silhouetteEl); 464 dragStartParams = paramsFromLayout({ 465 left: parsePercent(silhouetteEl.style.left), 466 top: parsePercent(silhouetteEl.style.top), 467 width: parsePercent(silhouetteEl.style.width), 468 height: parsePercent(silhouetteEl.style.height) 469 }, dragMeta); 470 activePointerId = pointerId; 471 try { 472 if (pointerId !== null) targetEl.setPointerCapture(pointerId); 473 } catch (_) {} 474 }; 475 476 silhouetteEl.addEventListener('pointerdown', (e) => { 477 if (e.target.classList.contains('silhouette-handle')) return; 478 if (activePointerId !== null) return; 479 startDrag('move', e.clientX, e.clientY, e.pointerId, silhouetteEl); 480 e.preventDefault(); 481 e.stopPropagation(); 482 }, { passive: false }); 483 484 silhouetteEl.querySelectorAll('.silhouette-handle').forEach((handle) => { 485 handle.addEventListener('pointerdown', (e) => { 486 if (activePointerId !== null) return; 487 const handleType = Array.from(handle.classList).find((c) => ['nw', 'ne', 'sw', 'se', 'n', 's'].includes(c)) || 'se'; 488 startDrag(handleType, e.clientX, e.clientY, e.pointerId, handle); 489 e.preventDefault(); 490 e.stopPropagation(); 491 }, { passive: false }); 492 }); 493 }); 494 495 if (!handlersAttached) { 496 handlersAttached = true; 497 document.addEventListener('pointermove', (e) => { 498 if (!draggingSilhouette) return; 499 if (activePointerId !== null && e.pointerId !== activePointerId) return; 500 handleSilhouetteDrag(e); 501 e.preventDefault(); 502 }, { passive: false }); 503 document.addEventListener('pointerup', (e) => { 504 if (!draggingSilhouette) return; 505 if (activePointerId !== null && e.pointerId !== activePointerId) return; 506 handleSilhouetteEnd(); 507 }); 508 document.addEventListener('pointercancel', () => { 509 if (!draggingSilhouette) return; 510 handleSilhouetteEnd(); 511 }); 512 } 513}
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.