task(CAD-3): box selection - window/crossing by drag direction, preview rectangle, shift-additive, defensive state hygiene
This commit is contained in:
@@ -178,6 +178,71 @@ function applyVertexDrag(
|
||||
return el;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task CAD-3: Fenster-/Kreuz-Selektion (Box-Select).
|
||||
// Links-nach-rechts ziehen = 'window' (Element muss VOLLSTAENDIG
|
||||
// in der Box liegen), rechts-nach-links = 'crossing' (Beruehrung
|
||||
// reicht). Leerer down startet Box-Drag statt Selektion zu leeren.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Richtung bestimmt den Modus: to.x >= from.x → window, sonst crossing. */
|
||||
export function boxModeFor(from: Pt, to: Pt): 'window' | 'crossing' {
|
||||
return to.x >= from.x ? 'window' : 'crossing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Selektiert alle Elemente, die die Box erfuellen.
|
||||
* window: BBox des Elements VOLLSTAENDIG in der Box.
|
||||
* crossing: BBox des Elements ueberschneidet/beruehrt die Box.
|
||||
*/
|
||||
export function boxSelectElements(
|
||||
elements: CADElement[],
|
||||
box: { x1: number; y1: number; x2: number; y2: number },
|
||||
mode: 'window' | 'crossing',
|
||||
): string[] {
|
||||
const minX = Math.min(box.x1, box.x2);
|
||||
const maxX = Math.max(box.x1, box.x2);
|
||||
const minY = Math.min(box.y1, box.y2);
|
||||
const maxY = Math.max(box.y1, box.y2);
|
||||
const hits: string[] = [];
|
||||
for (const el of elements) {
|
||||
const b = elementBBox(el);
|
||||
if (!b) continue;
|
||||
if (mode === 'window') {
|
||||
if (b.minX >= minX && b.maxX <= maxX && b.minY >= minY && b.maxY <= maxY) {
|
||||
hits.push(el.id);
|
||||
}
|
||||
} else {
|
||||
// crossing: Ueberschneidung (Beruehrung inklusive)
|
||||
if (b.minX <= maxX && b.maxX >= minX && b.minY <= maxY && b.maxY >= minY) {
|
||||
hits.push(el.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/** BBox eines Elements (line aus Koordinaten, sonst x/y±w/2). */
|
||||
function elementBBox(el: CADElement): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
||||
const props = el.properties as Record<string, unknown>;
|
||||
if (
|
||||
props.x1 !== undefined && props.y1 !== undefined &&
|
||||
props.x2 !== undefined && props.y2 !== undefined
|
||||
) {
|
||||
const x1 = props.x1 as number, y1 = props.y1 as number;
|
||||
const x2 = props.x2 as number, y2 = props.y2 as number;
|
||||
return { minX: Math.min(x1, x2), minY: Math.min(y1, y2), maxX: Math.max(x1, x2), maxY: Math.max(y1, y2) };
|
||||
}
|
||||
const w = el.width ?? 0, h = el.height ?? 0;
|
||||
return { minX: el.x - w / 2, minY: el.y - h / 2, maxX: el.x + w / 2, maxY: el.y + h / 2 };
|
||||
}
|
||||
|
||||
/** Box-Drag-Sitzung (Task CAD-3). */
|
||||
let boxDrag: { start: Pt; shift: boolean } | null = null;
|
||||
|
||||
/** Mindestgroesse (Welt-Einheiten) ab der ein Zug als Box gilt. */
|
||||
const BOX_MIN_SIZE = 3;
|
||||
|
||||
/** Vertex-/Center-Drag-Sitzung (Task D9). */
|
||||
let vertexDrag: {
|
||||
el: CADElement;
|
||||
@@ -200,6 +265,9 @@ export const selectTool: ToolExtensionV2 = {
|
||||
const bridge = (ctx as FullToolContext).selection;
|
||||
const c = ctx as FullToolContext;
|
||||
if (!bridge) return;
|
||||
// CAD-3: verlorene Box-Sitzungen abräumen (defensive State-Hygiene,
|
||||
// schützt auch vor verlorenen up-Events); wird unten bei Leerraum neu gesetzt.
|
||||
boxDrag = null;
|
||||
const additive = e.shift;
|
||||
const subtractive = e.ctrl;
|
||||
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
||||
@@ -243,7 +311,10 @@ export const selectTool: ToolExtensionV2 = {
|
||||
}
|
||||
|
||||
if (!hit) {
|
||||
if (!additive && !subtractive) bridge.setIds([]);
|
||||
// ── Task CAD-3: Leerraum-Klick startet Box-Select-Drag ──
|
||||
// (statt sofort zu leeren; das Leeren passiert nur bei Klick OHNE Zug)
|
||||
boxDrag = { start: e.world, shift: e.shift };
|
||||
ctx.setStatus('Box-Auswahl: ziehen (rechts=fenster, links=kreuzend)');
|
||||
return;
|
||||
}
|
||||
let ids = new Set(bridge.getIds());
|
||||
@@ -258,6 +329,31 @@ export const selectTool: ToolExtensionV2 = {
|
||||
},
|
||||
|
||||
move(e, ctx) {
|
||||
// ── Task CAD-3: Box-Select-Preview ──
|
||||
if (boxDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
const x = Math.min(boxDrag.start.x, e.world.x);
|
||||
const y = Math.min(boxDrag.start.y, e.world.y);
|
||||
const w = Math.abs(e.world.x - boxDrag.start.x);
|
||||
const h = Math.abs(e.world.y - boxDrag.start.y);
|
||||
const mode = boxModeFor(boxDrag.start, e.world);
|
||||
c.setPreview?.({
|
||||
id: '__preview_boxselect__',
|
||||
type: 'rect',
|
||||
layerId: 'layer-0',
|
||||
x: x + w / 2,
|
||||
y: y + h / 2,
|
||||
width: w,
|
||||
height: h,
|
||||
properties: {
|
||||
stroke: mode === 'window' ? '#4a90d9' : '#22c55e',
|
||||
strokeWidth: 1,
|
||||
fill: mode === 'window' ? 'rgba(74,144,217,0.08)' : 'rgba(34,197,94,0.08)',
|
||||
boxSelectMode: mode,
|
||||
},
|
||||
} as unknown as CADElement);
|
||||
return;
|
||||
}
|
||||
// ── Task D9: Vertex-Drag-Preview (nur der Punkt aktualisiert) ──
|
||||
if (vertexDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
@@ -308,6 +404,40 @@ export const selectTool: ToolExtensionV2 = {
|
||||
},
|
||||
|
||||
up(e, ctx) {
|
||||
// ── Task CAD-3: Box-Select-Abschluss ──
|
||||
if (boxDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
const start = boxDrag.start;
|
||||
const shift = boxDrag.shift || e.shift;
|
||||
boxDrag = null;
|
||||
ctx.setPreview?.(null);
|
||||
const w = Math.abs(e.world.x - start.x);
|
||||
const h = Math.abs(e.world.y - start.y);
|
||||
// Winzige Box = Klick ohne Zug → Legacy: Selektion leeren
|
||||
if (w < BOX_MIN_SIZE && h < BOX_MIN_SIZE) {
|
||||
if (!shift) (c.selection ?? { setIds: () => {} }).setIds([]);
|
||||
ctx.setStatus('Selektion geleert');
|
||||
return;
|
||||
}
|
||||
const mode = boxModeFor(start, e.world);
|
||||
const all = c.doc ? c.doc.getAllElements() : [];
|
||||
const hits = boxSelectElements(all, {
|
||||
x1: start.x, y1: start.y, x2: e.world.x, y2: e.world.y,
|
||||
}, mode);
|
||||
const bridge = (ctx as FullToolContext).selection;
|
||||
if (!bridge) return;
|
||||
if (shift) {
|
||||
const merged = new Set([...bridge.getIds(), ...hits]);
|
||||
bridge.setIds(Array.from(merged));
|
||||
} else {
|
||||
bridge.setIds(hits);
|
||||
}
|
||||
const count = shift
|
||||
? new Set([...(bridge.getIds())]).size
|
||||
: hits.length;
|
||||
ctx.setStatus(`Box-Auswahl (${mode === 'window' ? 'Fenster' : 'Kreuzend'}): ${count} Element(e)`);
|
||||
return;
|
||||
}
|
||||
// ── Task D9: Vertex-Commit (nur der Punkt, 1 Undo-Schritt) ──
|
||||
if (vertexDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
@@ -379,6 +509,7 @@ export const selectTool: ToolExtensionV2 = {
|
||||
cancel(ctx) {
|
||||
handleDrag = null;
|
||||
vertexDrag = null;
|
||||
boxDrag = null;
|
||||
ctx.setPreview?.(null);
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user