mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-09 07:00:08 +00:00
marquee box-select on canvas with the Select tool
Click-and-drag on an empty area of the canvas (with the V tool active) now draws a marching-ants rectangle. On release, every layer whose bounding box intersects the rectangle is selected; Shift / Ctrl / Cmd held during the drag adds to the existing selection instead of replacing it. Plain clicks (no drag past the 4 px deadzone) still clear the selection, preserving the previous "click empty area to deselect" behaviour. Other tools own their own canvas drags (Pen creates anchors, Hand pans, Shape drags out a primitive, Direct Select marquees over anchors), so this only kicks in for V. Shift+click on a layer to add or remove it from the current selection was already wired — confirmed working through the existing additive path in startLayerDrag.
This commit is contained in:
1 parent
eb9aec19d8
commit
cd54d8357f
1 file changed
+129
-4
+129
-4
@@ -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
|
||||
|
||||
Reference in new issue
Block a user