diff --git a/src/index.html b/src/index.html index 3f3f74e..17accd5 100644 --- a/src/index.html +++ b/src/index.html @@ -2517,6 +2517,25 @@ .toolOptions .buttonRow { margin:6px 0 0; } .toolOptHint { font-size:.7rem; color:var(--muted); margin:4px 0 0; line-height:1.35; } .penOverlay { position:absolute; inset:0; z-index:998; pointer-events:none; } + /* Marching-ants marquee box drawn while drag-selecting on the + canvas. CSS animation shifts four 1px dashed borders so the + outline reads as moving — same effect as Photoshop / Figma. */ + .marqueeSelectBox { + position: absolute; + pointer-events: none; + z-index: 10000; + background: + linear-gradient(90deg, rgba(15,23,42,0.95) 50%, rgba(255,255,255,0.95) 50%) 0 0/8px 1px repeat-x, + linear-gradient(90deg, rgba(15,23,42,0.95) 50%, rgba(255,255,255,0.95) 50%) 0 100%/8px 1px repeat-x, + linear-gradient(0deg, rgba(15,23,42,0.95) 50%, rgba(255,255,255,0.95) 50%) 0 0/1px 8px repeat-y, + linear-gradient(0deg, rgba(15,23,42,0.95) 50%, rgba(255,255,255,0.95) 50%) 100% 0/1px 8px repeat-y; + animation: marqueeAntsMarch 0.6s linear infinite; + } + @keyframes marqueeAntsMarch { + to { + background-position: 8px 0, -8px 100%, 0 -8px, 100% 8px; + } + } body.tool-drawing .penOverlay { pointer-events:auto; cursor:crosshair; } /* Direct Selection captures segment + marquee drags too, but individual anchor / handle dots still set their own cursors. */ @@ -18733,6 +18752,110 @@ setLayerSelection([]); } + // Marquee drag-select on an empty canvas region. Used by the + // Select (V) tool only — other tools own their canvas drags + // (pen creates anchors, hand pans, shape drags out a box, + // direct-select marquees over anchors). A short click without + // movement keeps the legacy "click empty area to deselect" + // behavior; a real drag draws a marching-ants rectangle and on + // release selects every layer whose bbox intersects it. + // Shift / Ctrl / Cmd held adds to the existing selection + // instead of replacing it. + function attachCanvasMarquee(host, isDesignerLayer) { + if (!host) return; + const MARQUEE_THRESHOLD = 4; // px before a click becomes a drag + host.addEventListener('pointerdown', (event) => { + if (event.button !== 0) return; // left button only + // Bail if the user is on a tool that owns canvas drags. + const tool = (typeof effectiveEditorTool === 'function') ? effectiveEditorTool() : 'select'; + if (tool !== 'select') return; + // freeLayer pointerdowns stopPropagation, so reaching here + // means the click missed every layer. But also bail if the + // event originated inside a layer overlay (e.g. selection box). + if (event.target.closest('.freeLayer')) return; + if (event.target.closest('.layerSelectionBox')) return; + + const hostRect = host.getBoundingClientRect(); + const unitW = isDesignerLayer ? discTemplate.discD : sheetW(); + const unitH = isDesignerLayer ? discTemplate.discD : sheetH(); + const additive = !!(event.shiftKey || event.ctrlKey || event.metaKey); + const startScreenX = event.clientX, startScreenY = event.clientY; + const startBaseIds = additive ? new Set(selectedLayerIds()) : new Set(); + + let box = null; + let dragging = false; + try { host.setPointerCapture(event.pointerId); } catch {} + + const screenToInch = (sx, sy) => ({ + x: (sx - hostRect.left) / hostRect.width * unitW, + y: (sy - hostRect.top) / hostRect.height * unitH, + }); + const screenToHostPct = (sx, sy) => ({ + x: (sx - hostRect.left) / hostRect.width * 100, + y: (sy - hostRect.top) / hostRect.height * 100, + }); + + const move = (e) => { + if (!dragging) { + const dx = e.clientX - startScreenX; + const dy = e.clientY - startScreenY; + if (dx * dx + dy * dy < MARQUEE_THRESHOLD * MARQUEE_THRESHOLD) return; + dragging = true; + box = document.createElement('div'); + box.className = 'marqueeSelectBox'; + host.appendChild(box); + } + const a = screenToHostPct(startScreenX, startScreenY); + const b = screenToHostPct(e.clientX, e.clientY); + const x0 = Math.min(a.x, b.x), x1 = Math.max(a.x, b.x); + const y0 = Math.min(a.y, b.y), y1 = Math.max(a.y, b.y); + box.style.left = `${x0}%`; + box.style.top = `${y0}%`; + box.style.width = `${x1 - x0}%`; + box.style.height = `${y1 - y0}%`; + }; + + const up = (e) => { + host.removeEventListener('pointermove', move); + host.removeEventListener('pointerup', up); + host.removeEventListener('pointercancel', up); + if (box && box.parentNode) box.parentNode.removeChild(box); + if (!dragging) { + // Plain click on empty canvas → legacy behaviour: deselect. + if (!additive) { clearLayerSelection(); render(); } + return; + } + // Compute marquee rect in inch space + find intersecting layers. + const a = screenToInch(startScreenX, startScreenY); + const b = screenToInch(e.clientX, e.clientY); + const r = { + x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), + w: Math.abs(b.x - a.x), h: Math.abs(b.y - a.y), + }; + const hits = state.layers.filter(l => { + if (!l || l.locked || l.hidden || l._generatedDiscLayer) return false; + const onDesigner = l.target === 'designer-disc'; + if (onDesigner !== isDesignerLayer) return false; + if (!isDesignerLayer && typeof isLayerForCurrentMode === 'function' + && !isLayerForCurrentMode(l)) return false; + const lx = Number(l.x) || 0, ly = Number(l.y) || 0; + const lw = Number(l.w) || 0, lh = Number(l.h) || 0; + // AABB intersection — fast and matches Illustrator's + // "loose" marquee behaviour (any touch selects). + return lx < r.x + r.w && lx + lw > r.x && ly < r.y + r.h && ly + lh > r.y; + }); + const idsAfter = additive + ? Array.from(new Set([...startBaseIds, ...hits.map(l => l.id)])) + : hits.map(l => l.id); + setLayerSelection(idsAfter, idsAfter[idsAfter.length - 1] || ''); + render(); + }; + host.addEventListener('pointermove', move); + host.addEventListener('pointerup', up); + host.addEventListener('pointercancel', up); + }); + } + let _layerPanelSelectionAnchor = null; let _layerPanelDrag = null; @@ -29964,10 +30087,12 @@ const addTextLayerBtn = $('addTextLayerBtn'); if (addTextLayerBtn) addTextLayerBtn.addEventListener('click', () => addTextLayer()); - $('sheet').addEventListener('click', () => { - clearLayerSelection(); - render(); - }); + // Empty-canvas pointerdown: marquee drag-select with the + // Select tool, plain click clears the selection. Layers + // stopPropagation on their own pointerdowns so this only fires + // when the click misses every layer. + attachCanvasMarquee($('sheet'), false); + attachCanvasMarquee($('designerDiscWrap'), true); // Clicks in the gutter between the sheet edge and the previewShell // edge also deselect — the existing #sheet handler misses those. // Layer .freeLayer clicks stopPropagation so they don't bubble up