task(D-ext2): chamfer and lengthen tools for v2
This commit is contained in:
@@ -388,6 +388,213 @@ function makeArrayTool(mode: 'array-rect' | 'array-polar'): ToolExtensionV2 {
|
||||
export const arrayRectTool: ToolExtensionV2 = makeArrayTool('array-rect');
|
||||
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
|
||||
// findIntersection-Geometrie (B2). Legacy-Ablauf:
|
||||
@@ -581,11 +788,11 @@ export const coreModifyPlugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'core-modify',
|
||||
name: 'Bearbeiten (Kern)',
|
||||
version: '1.6.0',
|
||||
version: '1.7.0',
|
||||
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',
|
||||
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],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user