1/** 2 * Snap-while-dragging: when the move tool drags a layer near another 3 * layer's edge or the canvas centre/edges, gently lock the proposed 4 * (nx, ny) to the nearest target within SNAP_PX. 5 * 6 * The implementation is pure â it takes the layer being moved + the 7 * trial offset + a context describing zoom + the other layers, and 8 * returns the snapped position plus any guides to draw. 9 * 10 * The legacy gallery editor's `_computeSnap` is a one-line wrapper 11 * that builds the context from module state. 12 * 13 * @param {{canvas: HTMLCanvasElement, id: string}} layer 14 * The layer currently being moved. 15 * @param {number} nx / ny 16 * Trial offset (top-left) in canvas pixels, before snapping. 17 * @param {{ 18 * zoom: number, 19 * canvasW: number, 20 * canvasH: number, 21 * otherLayers: Array<{visible: boolean, id: string, canvas: HTMLCanvasElement, offset: {x:number, y:number}}>, 22 * }} ctx 23 * @returns {{x: number, y: number, guides: Array}} 24 */ 25export function computeSnap(layer, nx, ny, ctx) { 26 if (!layer || !layer.canvas || !ctx) return { x: nx, y: ny, guides: [] }; 27 const zoom = Number.isFinite(Number(ctx.zoom)) ? Number(ctx.zoom) : 1; 28 const SNAP_PX = 6 / Math.max(zoom, 0.0001); 29 const cw = Number(ctx.canvasW) || 0, ch = Number(ctx.canvasH) || 0; 30 const w = layer.canvas.width, h = layer.canvas.height; 31 32 const vTargets = [ 33 { x: 0, label: 'canvas-l' }, 34 { x: cw, label: 'canvas-r' }, 35 { x: cw / 2, label: 'canvas-cx' }, 36 ]; 37 const hTargets = [ 38 { y: 0, label: 'canvas-t' }, 39 { y: ch, label: 'canvas-b' }, 40 { y: ch / 2, label: 'canvas-cy' }, 41 ]; 42 const otherLayers = Array.isArray(ctx.otherLayers) ? ctx.otherLayers : []; 43 for (const other of otherLayers) { 44 if (!other.visible || other.id === layer.id) continue; 45 const o = other.offset || { x: 0, y: 0 }; 46 const ow = other.canvas.width, oh = other.canvas.height; 47 vTargets.push({ x: o.x, label: 'layer-l' }); 48 vTargets.push({ x: o.x + ow, label: 'layer-r' }); 49 vTargets.push({ x: o.x + ow / 2, label: 'layer-cx' }); 50 hTargets.push({ y: o.y, label: 'layer-t' }); 51 hTargets.push({ y: o.y + oh, label: 'layer-b' }); 52 hTargets.push({ y: o.y + oh / 2, label: 'layer-cy' }); 53 } 54 55 const myEdgesX = { l: nx, cx: nx + w / 2, r: nx + w }; 56 const myEdgesY = { t: ny, cy: ny + h / 2, b: ny + h }; 57 let bestX = null, bestDx = Infinity; 58 let bestY = null, bestDy = Infinity; 59 for (const [src, val] of Object.entries(myEdgesX)) { 60 for (const t of vTargets) { 61 const d = Math.abs(t.x - val); 62 if (d < SNAP_PX && d < bestDx) { 63 bestDx = d; 64 bestX = { snapTo: t.x, src, target: t }; 65 } 66 } 67 } 68 for (const [src, val] of Object.entries(myEdgesY)) { 69 for (const t of hTargets) { 70 const d = Math.abs(t.y - val); 71 if (d < SNAP_PX && d < bestDy) { 72 bestDy = d; 73 bestY = { snapTo: t.y, src, target: t }; 74 } 75 } 76 } 77 78 const guides = []; 79 let snappedX = nx, snappedY = ny; 80 if (bestX) { 81 if (bestX.src === 'l') snappedX = bestX.snapTo; 82 else if (bestX.src === 'cx') snappedX = bestX.snapTo - w / 2; 83 else snappedX = bestX.snapTo - w; 84 guides.push({ vertical: true, x: bestX.snapTo }); 85 } 86 if (bestY) { 87 if (bestY.src === 't') snappedY = bestY.snapTo; 88 else if (bestY.src === 'cy') snappedY = bestY.snapTo - h / 2; 89 else snappedY = bestY.snapTo - h; 90 guides.push({ vertical: false, y: bestY.snapTo }); 91 } 92 return { x: snappedX, y: snappedY, guides }; 93} 94 95 96/** 97 * CSS cursor name for each transform-tool handle. 98 * 99 * @param {'tl'|'tr'|'bl'|'br'|'rot'|string} id 100 * @returns {string} 101 */ 102export function cursorForHandle(id) { 103 switch (id) { 104 case 'tl': case 'br': return 'nwse-resize'; 105 case 'tr': case 'bl': return 'nesw-resize'; 106 case 'rot': return 'grab'; 107 default: return 'default'; 108 } 109}
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.