task(D9): grip editing - vertex/center grips in select tool (line endpoints, polyline vertices, circle/arc center), drag modifies single point in one undo transaction, corner scaling preserved
This commit is contained in:
@@ -82,6 +82,109 @@ function hitHandle(
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task D9: GripEditing — Vertex-/Center-Grips (reine Funktion).
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GripPoint {
|
||||
kind: 'vertex' | 'center' | 'corner';
|
||||
point: Pt;
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element-spezifische Griff-Punkte (Task D9):
|
||||
* line = beide Endpunkte, polyline/polygon = alle Vertices,
|
||||
* circle/arc = Zentrum, rect/default = 4 BBox-Ecken (C3fin-Skalierung).
|
||||
*/
|
||||
export function getGripPoints(el: CADElement): GripPoint[] {
|
||||
const props = el.properties as Record<string, unknown>;
|
||||
const type = String(el.type);
|
||||
if (
|
||||
type === 'line' &&
|
||||
props.x1 !== undefined && props.y1 !== undefined &&
|
||||
props.x2 !== undefined && props.y2 !== undefined
|
||||
) {
|
||||
return [
|
||||
{ kind: 'vertex', point: { x: props.x1 as number, y: props.y1 as number }, index: 0 },
|
||||
{ kind: 'vertex', point: { x: props.x2 as number, y: props.y2 as number }, index: 1 },
|
||||
];
|
||||
}
|
||||
if (type === 'polyline' || type === 'polygon') {
|
||||
const pts = (props.points as Pt[] | undefined) ?? [];
|
||||
return pts.map((p, i) => ({ kind: 'vertex' as const, point: { x: p.x, y: p.y }, index: i }));
|
||||
}
|
||||
if (type === 'circle' || type === 'arc') {
|
||||
return [{ kind: 'center', point: { x: el.x, y: el.y }, index: 0 }];
|
||||
}
|
||||
// rect und alle anderen: BBox-Ecken (Corner-Drag/Skalierung, C3fin)
|
||||
const minX = el.x - el.width / 2;
|
||||
const minY = el.y - el.height / 2;
|
||||
const maxX = el.x + el.width / 2;
|
||||
const maxY = el.y + el.height / 2;
|
||||
return [
|
||||
{ kind: 'corner', point: { x: minX, y: minY }, index: 0 },
|
||||
{ kind: 'corner', point: { x: maxX, y: minY }, index: 1 },
|
||||
{ kind: 'corner', point: { x: maxX, y: maxY }, index: 2 },
|
||||
{ kind: 'corner', point: { x: minX, y: maxY }, index: 3 },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet eine Vertex-/Center-Drag-Position an (Task D9) und liefert
|
||||
* das aktualisierte Element (reine Funktion — für Preview UND Commit).
|
||||
*/
|
||||
function applyVertexDrag(
|
||||
el: CADElement,
|
||||
drag: { el: CADElement; gripKind: 'vertex' | 'center'; index: number },
|
||||
wx: number,
|
||||
wy: number,
|
||||
): CADElement {
|
||||
const props = { ...(el.properties as Record<string, unknown>) };
|
||||
const type = String(el.type);
|
||||
if (drag.gripKind === 'center') {
|
||||
return { ...el, x: wx, y: wy, properties: props as CADElement['properties'] } as CADElement;
|
||||
}
|
||||
if (type === 'line') {
|
||||
if (drag.index === 0) { props.x1 = wx; props.y1 = wy; }
|
||||
else { props.x2 = wx; props.y2 = wy; }
|
||||
// BBox aktualisieren
|
||||
const x1 = props.x1 as number, y1 = props.y1 as number, x2 = props.x2 as number, y2 = props.y2 as number;
|
||||
return {
|
||||
...el,
|
||||
x: (x1 + x2) / 2,
|
||||
y: (y1 + y2) / 2,
|
||||
width: Math.abs(x2 - x1),
|
||||
height: Math.abs(y2 - y1),
|
||||
properties: props as CADElement['properties'],
|
||||
} as CADElement;
|
||||
}
|
||||
if (type === 'polyline' || type === 'polygon') {
|
||||
const pts = [...((props.points as Pt[] | undefined) ?? [])];
|
||||
if (drag.index < pts.length) pts[drag.index] = { x: wx, y: wy };
|
||||
props.points = pts;
|
||||
const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
|
||||
const minX = Math.min(...xs), minY = Math.min(...ys);
|
||||
const maxX = Math.max(...xs), maxY = Math.max(...ys);
|
||||
return {
|
||||
...el,
|
||||
x: minX + (maxX - minX) / 2,
|
||||
y: minY + (maxY - minY) / 2,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
properties: props as CADElement['properties'],
|
||||
} as CADElement;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/** Vertex-/Center-Drag-Sitzung (Task D9). */
|
||||
let vertexDrag: {
|
||||
el: CADElement;
|
||||
gripKind: 'vertex' | 'center';
|
||||
index: number;
|
||||
} | null = null;
|
||||
|
||||
export const selectTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'select',
|
||||
@@ -101,6 +204,28 @@ export const selectTool: ToolExtensionV2 = {
|
||||
const subtractive = e.ctrl;
|
||||
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
||||
|
||||
// ── Task D9: Vertex-/Center-Grip-Prüfung VOR BBox-Corners ──
|
||||
// (spezifischere Grips gewinnen gegenüber Corner-Skalierung)
|
||||
if (!additive && !subtractive && c.doc) {
|
||||
const GRIP_TOL = 8;
|
||||
for (const id of bridge.getIds()) {
|
||||
const el = c.doc.getElement(id);
|
||||
if (!el) continue;
|
||||
for (const g of getGripPoints(el)) {
|
||||
if (g.kind === 'corner') continue; // Corner unten (C3fin)
|
||||
if (
|
||||
Math.abs(e.world.x - g.point.x) <= GRIP_TOL &&
|
||||
Math.abs(e.world.y - g.point.y) <= GRIP_TOL
|
||||
) {
|
||||
vertexDrag = { el, gripKind: g.kind, index: g.index };
|
||||
const what = g.kind === 'center' ? 'Zentrum' : `Punkt ${g.index + 1}`;
|
||||
ctx.setStatus(`${what} gegriffen — ziehen zum Verschieben`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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.
|
||||
@@ -133,6 +258,15 @@ export const selectTool: ToolExtensionV2 = {
|
||||
},
|
||||
|
||||
move(e, ctx) {
|
||||
// ── Task D9: Vertex-Drag-Preview (nur der Punkt aktualisiert) ──
|
||||
if (vertexDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
const el = c.doc?.getElement(vertexDrag.el.id);
|
||||
if (!el) return;
|
||||
const updated = applyVertexDrag(el, vertexDrag, e.world.x, e.world.y);
|
||||
c.setPreview?.(updated);
|
||||
return;
|
||||
}
|
||||
// Live-Skalierung während Handle-Drag:
|
||||
if (!handleDrag) return;
|
||||
const c = ctx as FullToolContext;
|
||||
@@ -174,6 +308,26 @@ export const selectTool: ToolExtensionV2 = {
|
||||
},
|
||||
|
||||
up(e, ctx) {
|
||||
// ── Task D9: Vertex-Commit (nur der Punkt, 1 Undo-Schritt) ──
|
||||
if (vertexDrag) {
|
||||
const c = ctx as FullToolContext;
|
||||
const el = c.doc?.getElement(vertexDrag.el.id);
|
||||
if (el && c.doc) {
|
||||
const updated = applyVertexDrag(el, vertexDrag, e.world.x, e.world.y);
|
||||
c.doc.transact(() => {
|
||||
c.doc!.updateElement(el.id, {
|
||||
x: updated.x,
|
||||
y: updated.y,
|
||||
properties: updated.properties as CADElement['properties'],
|
||||
});
|
||||
});
|
||||
const what = vertexDrag.gripKind === 'center' ? 'Zentrum' : `Punkt ${vertexDrag.index + 1}`;
|
||||
ctx.setStatus(`${what} verschoben`);
|
||||
}
|
||||
vertexDrag = null;
|
||||
ctx.setPreview?.(null);
|
||||
return;
|
||||
}
|
||||
if (!handleDrag) return;
|
||||
const c = ctx as FullToolContext;
|
||||
if (!c.doc) { handleDrag = null; return; }
|
||||
@@ -224,6 +378,7 @@ export const selectTool: ToolExtensionV2 = {
|
||||
|
||||
cancel(ctx) {
|
||||
handleDrag = null;
|
||||
vertexDrag = null;
|
||||
ctx.setPreview?.(null);
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user