task(D-ext2): chamfer and lengthen tools for v2

This commit is contained in:
Agent Zero
2026-08-27 18:56:26 +02:00
parent 6a1d0db7a1
commit eb1b8692a8
@@ -388,6 +388,213 @@ function makeArrayTool(mode: 'array-rect' | 'array-polar'): ToolExtensionV2 {
export const arrayRectTool: ToolExtensionV2 = makeArrayTool('array-rect'); export const arrayRectTool: ToolExtensionV2 = makeArrayTool('array-rect');
export const arrayPolarTool: ToolExtensionV2 = makeArrayTool('array-polar'); export const arrayPolarTool: ToolExtensionV2 = makeArrayTool('array-polar');
// ─────────────────────────────────────────────────────────────
// Chamfer (Task D-Ext): schneidet zwei sich schneidende Linien an einer
// festen Distanz vom Schnittpunkt und verbindet die Enden mit einer
// geraden Fase. Legacy nutzt trimElement + Verbindungslinie.
// Klick 1: erste Linie, Klick 2: zweite Linie → 3 Elemente (2 gekürzt + 1 Fase).
// ─────────────────────────────────────────────────────────────
let chamferFirst: CADElement | null = null;
export const chamferTool: ToolExtensionV2 = {
manifest: {
id: 'chamfer', label: 'Chamfer', icon: '\u25e4', ribbonTab: 'canvas', tags: ['pro'],
description: 'Zwei Linien anklicken — Fase wird eingefügt',
},
optionsSchema: [
{ key: 'chamferDist', label: 'Fase-Distanz', type: 'number', min: 1, max: 100 },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc || !c.selection) return;
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
if (!hit) return;
if (!chamferFirst) {
chamferFirst = hit;
ctx.setStatus('Chamfer: zweite Linie wählen');
return;
}
const dist = (c.options.chamferDist as number | undefined) ?? 10;
// Beide Linien auf dist vom Schnittpunkt kürzen:
const t1 = trimElement(chamferFirst, hit);
const t2 = trimElement(hit, chamferFirst);
if (!t1 || !t2) {
ctx.setStatus('Chamfer: kein Schnittpunkt');
chamferFirst = null;
return;
}
// Endpunkte nahe am Schnittpunkt holen und um dist zurücksetzen:
const props1 = t1.properties as { x1?: number; y1?: number; x2?: number; y2?: number };
const props2 = t2.properties as { x1?: number; y1?: number; x2?: number; y2?: number };
if (props1.x1 === undefined || props1.y1 === undefined || props1.x2 === undefined || props1.y2 === undefined ||
props2.x1 === undefined || props2.y1 === undefined || props2.x2 === undefined || props2.y2 === undefined) {
chamferFirst = null;
return;
}
// Nahe Enden (Schnittpunkt-Seite) identifizieren via Schnittpunkt:
const isectPt = findIsectPoint(chamferFirst, hit);
if (!isectPt) { chamferFirst = null; return; }
const near1 = nearEnd(props1 as { x1: number; y1: number; x2: number; y2: number }, isectPt);
const near2 = nearEnd(props2 as { x1: number; y1: number; x2: number; y2: number }, isectPt);
// Kürze nahe Enden auf dist vom Schnittpunkt:
const shrink1 = shrinkTowards(props1 as { x1: number; y1: number; x2: number; y2: number }, near1, isectPt, dist);
const shrink2 = shrinkTowards(props2 as { x1: number; y1: number; x2: number; y2: number }, near2, isectPt, dist);
// Fase: Linie zwischen den neuen Enden:
const chamferLine: CADElement = {
...chamferFirst,
id: uid('cham'),
type: 'line',
x: (shrink1.near.x + shrink2.near.x) / 2,
y: (shrink1.near.y + shrink2.near.y) / 2,
width: Math.abs(shrink2.near.x - shrink1.near.x),
height: Math.abs(shrink2.near.y - shrink1.near.y),
properties: {
x1: shrink1.near.x, y1: shrink1.near.y,
x2: shrink2.near.x, y2: shrink2.near.y,
stroke: '#e0e0e0', strokeWidth: 1,
},
};
c.doc.transact(() => {
c.doc!.updateElement(chamferFirst!.id, {
x: shrink1.cx, y: shrink1.cy,
width: Math.abs(props1.x2! - props1.x1!),
height: Math.abs(props1.y2! - props1.y1!),
properties: props1,
});
c.doc!.updateElement(hit.id, {
x: shrink2.cx, y: shrink2.cy,
width: Math.abs(props2.x2! - props2.x1!),
height: Math.abs(props2.y2! - props2.y1!),
properties: props2,
});
c.doc!.addElement(chamferLine);
});
ctx.setStatus('Chamfer erstellt');
chamferFirst = null;
},
cancel(ctx) {
chamferFirst = null;
ctx.setStatus('Chamfer abgebrochen');
},
},
};
function findIsectPoint(a: CADElement, b: CADElement): Pt | null {
const pa = a.properties as { x1?: number; y1?: number; x2?: number; y2?: number };
const pb = b.properties as { x1?: number; y1?: number; x2?: number; y2?: number };
if (pa.x1 === undefined || pb.x1 === undefined) return null;
const denom = (pa.x1! - pa.x2!) * (pb.y1! - pb.y2!) - (pa.y1! - pa.y2!) * (pb.x1! - pb.x2!);
if (Math.abs(denom) < 1e-10) return null;
const t = ((pa.x1! - pb.x1!) * (pb.y1! - pb.y2!) - (pa.y1! - pb.y1!) * (pb.x1! - pb.x2!)) / denom;
return { x: pa.x1! + t * (pa.x2! - pa.x1!), y: pa.y1! + t * (pa.y2! - pa.y1!) };
}
function nearEnd(props: { x1: number; y1: number; x2: number; y2: number }, pt: Pt): 'start' | 'end' {
const d1 = Math.hypot(props.x1 - pt.x, props.y1 - pt.y);
const d2 = Math.hypot(props.x2 - pt.x, props.y2 - pt.y);
return d1 < d2 ? 'start' : 'end';
}
function shrinkTowards(
props: { x1: number; y1: number; x2: number; y2: number },
near: 'start' | 'end',
isectPt: Pt,
dist: number,
): { near: Pt; cx: number; cy: number } {
if (near === 'start') {
const len = Math.hypot(props.x2 - props.x1, props.y2 - props.y1);
if (len === 0) return { near: { x: props.x1, y: props.y1 }, cx: props.x1, cy: props.y1 };
const ratio = Math.min(dist / len, 1);
const nx = props.x1 + (props.x2 - props.x1) * ratio;
const ny = props.y1 + (props.y2 - props.y1) * ratio;
return { near: { x: nx, y: ny }, cx: (nx + props.x2) / 2, cy: (ny + props.y2) / 2 };
}
const len = Math.hypot(props.x2 - props.x1, props.y2 - props.y1);
if (len === 0) return { near: { x: props.x2, y: props.y2 }, cx: props.x2, cy: props.y2 };
const ratio = Math.min(dist / len, 1);
const nx = props.x2 + (props.x1 - props.x2) * ratio;
const ny = props.y2 + (props.y1 - props.y2) * ratio;
return { near: { x: nx, y: ny }, cx: (nx + props.x1) / 2, cy: (ny + props.y1) / 2 };
}
// ─────────────────────────────────────────────────────────────
// Lengthen (Task D-Ext): verlängert/verkürzt eine Linie am geklickten
// Ende um einen Delta-Wert. Klick 1: Linie, Klick 2: Seite.
// ─────────────────────────────────────────────────────────────
let lengthenEl: CADElement | null = null;
export const lengthenTool: ToolExtensionV2 = {
manifest: {
id: 'lengthen', label: 'Lengthen', icon: '\u2194', ribbonTab: 'canvas', tags: ['pro'],
description: 'Linie anklicken, dann Delta-Ziel klicken (Options: lengthenDelta)',
},
optionsSchema: [
{ key: 'lengthenDelta', label: 'Delta', type: 'number' },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc || !c.selection) return;
if (!lengthenEl) {
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
if (!hit || hit.type !== 'line') {
ctx.setStatus('Lengthen: nur Linien');
return;
}
lengthenEl = hit;
ctx.setStatus('Lengthen: Klick in Verlängerungsrichtung');
return;
}
const el = c.doc.getElement(lengthenEl.id);
if (!el) { lengthenEl = null; return; }
const props = el.properties as { x1?: number; y1?: number; x2?: number; y2?: number };
if (props.x1 === undefined || props.y1 === undefined || props.x2 === undefined || props.y2 === undefined) {
lengthenEl = null;
return;
}
const delta = (c.options.lengthenDelta as number | undefined) ?? 10;
// Wähle das Ende, das dem Klick am nächsten ist:
const dStart = Math.hypot(e.world.x - (props.x1 as number), e.world.y - (props.y1 as number));
const dEnd = Math.hypot(e.world.x - (props.x2 as number), e.world.y - (props.y2 as number));
const nearIsStart = dStart < dEnd;
// Richtungsvektor (vom fernen Ende zum nahen Ende):
const dirX = nearIsStart ? (props.x1! - props.x2!) : (props.x2! - props.x1!);
const dirY = nearIsStart ? (props.y1! - props.y2!) : (props.y2! - props.y1!);
const len = Math.hypot(dirX, dirY);
if (len === 0) { lengthenEl = null; return; }
const ux = dirX / len;
const uy = dirY / len;
if (nearIsStart) {
props.x1 = (props.x1 as number) + ux * delta;
props.y1 = (props.y1 as number) + uy * delta;
} else {
props.x2 = (props.x2 as number) + ux * delta;
props.y2 = (props.y2 as number) + uy * delta;
}
const nx1 = props.x1 as number;
const ny1 = props.y1 as number;
const nx2 = props.x2 as number;
const ny2 = props.y2 as number;
c.doc.transact(() => {
c.doc!.updateElement(el.id, {
x: (nx1 + nx2) / 2,
y: (ny1 + ny2) / 2,
width: Math.abs(nx2 - nx1),
height: Math.abs(ny2 - ny1),
properties: props,
});
});
ctx.setStatus(`Lengthen: Linie um ${delta} angepasst`);
lengthenEl = null;
},
cancel(ctx) {
lengthenEl = null;
ctx.setStatus('Lengthen abgebrochen');
},
},
};
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Trim / Extend / Fillet (Task A4.10) — baut auf BUG-2-reparierter // Trim / Extend / Fillet (Task A4.10) — baut auf BUG-2-reparierter
// findIntersection-Geometrie (B2). Legacy-Ablauf: // findIntersection-Geometrie (B2). Legacy-Ablauf:
@@ -581,11 +788,11 @@ export const coreModifyPlugin: PluginV2 = {
manifest: { manifest: {
id: 'core-modify', id: 'core-modify',
name: 'Bearbeiten (Kern)', name: 'Bearbeiten (Kern)',
version: '1.6.0', version: '1.7.0',
author: 'web-cad team', author: 'web-cad team',
description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays (V2).', description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays/Chamfer/Lengthen (V2).',
category: 'tools', category: 'tools',
enabledByDefault: true, enabledByDefault: true,
}, },
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool], tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool],
}; };