task(C3fin-int): interactive handle drag for select tool (scale by corner)

This commit is contained in:
Agent Zero
2026-08-27 20:00:33 +02:00
parent 8223d159f8
commit 5605993462
2 changed files with 225 additions and 1 deletions
@@ -31,8 +31,57 @@ const uid = (prefix: string): string =>
`${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
// ─────────────────────────────────────────────────────────────
// select (Task A3 Pilot)
// select (Task A3 Pilot) + Handle-Drag (Task C3fin interaktiv)
// ─────────────────────────────────────────────────────────────
/** Handle-Drag-Sitzung: aktives Element + gegenüberliegende Ecke. */
let handleDrag: {
el: CADElement;
anchorCorner: Pt;
handleIndex: number;
} | null = null;
/** Prüft, ob (wx,wy) eine BBox-Ecke des Elements trifft (Toleranz tol). */
function hitHandle(
el: CADElement,
wx: number,
wy: number,
tol: number,
): { handleIndex: number; anchorCorner: Pt } | null {
const props = el.properties as Record<string, unknown>;
let minX: number, minY: number, maxX: number, maxY: number;
if (
props.x1 !== undefined && props.y1 !== undefined &&
props.x2 !== undefined && props.y2 !== undefined
) {
minX = Math.min(props.x1 as number, props.x2 as number);
minY = Math.min(props.y1 as number, props.y2 as number);
maxX = Math.max(props.x1 as number, props.x2 as number);
maxY = Math.max(props.y1 as number, props.y2 as number);
} else {
minX = el.x - el.width / 2;
minY = el.y - el.height / 2;
maxX = el.x + el.width / 2;
maxY = el.y + el.height / 2;
}
const corners: Array<{ x: number; y: number }> = [
{ x: minX, y: minY },
{ x: maxX, y: minY },
{ x: maxX, y: maxY },
{ x: minX, y: maxY },
];
for (let i = 0; i < corners.length; i++) {
if (
Math.abs(wx - corners[i].x) <= tol &&
Math.abs(wy - corners[i].y) <= tol
) {
// Gegenüberliegende Ecke = Anker (bleibt fix beim Skalieren)
const anchor = corners[(i + 2) % 4];
return { handleIndex: i, anchorCorner: anchor };
}
}
return null;
}
export const selectTool: ToolExtensionV2 = {
manifest: {
id: 'select',
@@ -46,10 +95,28 @@ export const selectTool: ToolExtensionV2 = {
handlers: {
down(e, ctx) {
const bridge = (ctx as FullToolContext).selection;
const c = ctx as FullToolContext;
if (!bridge) return;
const additive = e.shift;
const subtractive = e.ctrl;
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
// ── Handle-Drag-Start (Task C3fin interaktiv) ──
// Wenn bereits Elemente selektiert sind und der Klick auf eine Ecke
// eines davon trifft → Handle-Drag starten statt neu zu wählen.
if (!additive && !subtractive && bridge.getIds().length > 0 && c.doc) {
for (const id of bridge.getIds()) {
const el = c.doc.getElement(id);
if (!el) continue;
const h = hitHandle(el, e.world.x, e.world.y, 8);
if (h) {
handleDrag = { el, anchorCorner: h.anchorCorner, handleIndex: h.handleIndex };
ctx.setStatus(`Handle ${h.handleIndex + 1} — skalieren durch Ziehen`);
return;
}
}
}
if (!hit) {
if (!additive && !subtractive) bridge.setIds([]);
return;
@@ -64,6 +131,101 @@ export const selectTool: ToolExtensionV2 = {
bridge.setIds(Array.from(ids));
ctx.setStatus(`Selektion: ${ids.size} Element(e)`);
},
move(e, ctx) {
// Live-Skalierung während Handle-Drag:
if (!handleDrag) return;
const c = ctx as FullToolContext;
if (!c.doc) return;
const el = c.doc.getElement(handleDrag.el.id);
if (!el) return;
const props = el.properties as Record<string, unknown>;
// BBox aus Element abrufen (mit aktuellen Koordinaten):
let minX: number, minY: number, maxX: number, maxY: number;
if (
props.x1 !== undefined && props.y1 !== undefined &&
props.x2 !== undefined && props.y2 !== undefined
) {
minX = Math.min(props.x1 as number, props.x2 as number);
minY = Math.min(props.y1 as number, props.y2 as number);
maxX = Math.max(props.x1 as number, props.x2 as number);
maxY = Math.max(props.y1 as number, props.y2 as number);
} else {
minX = el.x - el.width / 2;
minY = el.y - el.height / 2;
maxX = el.x + el.width / 2;
maxY = el.y + el.height / 2;
}
// Neue BBox = Anker fix, gezogene Ecke = Maus:
const anchor = handleDrag.anchorCorner;
const newMinX = Math.min(anchor.x, e.world.x);
const newMinY = Math.min(anchor.y, e.world.y);
const newMaxX = Math.max(anchor.x, e.world.x);
const newMaxY = Math.max(anchor.y, e.world.y);
// Preview-Linie von Anker zur Maus (vereinfachtes Feedback):
c.setPreview?.({
...el,
x: (newMinX + newMaxX) / 2,
y: (newMinY + newMaxY) / 2,
width: newMaxX - newMinX,
height: newMaxY - newMinY,
} as CADElement);
void minX; void minY; void maxX; void maxY;
},
up(e, ctx) {
if (!handleDrag) return;
const c = ctx as FullToolContext;
if (!c.doc) { handleDrag = null; return; }
const el = c.doc.getElement(handleDrag.el.id);
const anchor = handleDrag.anchorCorner;
if (!el) { handleDrag = null; return; }
const props = el.properties as Record<string, unknown>;
// Neue BBox berechnen:
const newMinX = Math.min(anchor.x, e.world.x);
const newMinY = Math.min(anchor.y, e.world.y);
const newMaxX = Math.max(anchor.x, e.world.x);
const newMaxY = Math.max(anchor.y, e.world.y);
const newW = newMaxX - newMinX;
const newH = newMaxY - newMinY;
const oldW = el.width || 1;
const oldH = el.height || 1;
const scaleX = oldW !== 0 ? newW / oldW : 1;
const scaleY = oldH !== 0 ? newH / oldH : 1;
// Neues Zentrum:
const newCx = (newMinX + newMaxX) / 2;
const newCy = (newMinY + newMaxY) / 2;
c.doc.transact(() => {
// Element in neuer BBox skalieren (shallow properties überschrieben):
const newProps = { ...(el.properties as Record<string, unknown>) };
// Für line: Koordinaten skalieren relativ zur neuen BBox
if (
newProps.x1 !== undefined && newProps.y1 !== undefined &&
newProps.x2 !== undefined && newProps.y2 !== undefined
) {
newProps.x1 = newMinX + ((newProps.x1 as number) - (el.x - el.width / 2)) * scaleX;
newProps.y1 = newMinY + ((newProps.y1 as number) - (el.y - el.height / 2)) * scaleY;
newProps.x2 = newMinX + ((newProps.x2 as number) - (el.x - el.width / 2)) * scaleX;
newProps.y2 = newMinY + ((newProps.y2 as number) - (el.y - el.height / 2)) * scaleY;
}
c.doc!.updateElement(el.id, {
x: newCx,
y: newCy,
width: newW,
height: newH,
properties: newProps,
});
});
ctx.setStatus(`Skaliert auf ${newW.toFixed(0)}×${newH.toFixed(0)}`);
handleDrag = null;
ctx.setPreview?.(null);
},
cancel(ctx) {
handleDrag = null;
ctx.setPreview?.(null);
},
},
};