task(A4.3): migrate circle/arc/polyline/polygon tools to v2
This commit is contained in:
@@ -18,6 +18,21 @@ const uid = (prefix: string): string =>
|
||||
let lineStart: Pt | null = null;
|
||||
/** Startpunkt des laufenden Rechtecks (A4.2). */
|
||||
let rectStart: Pt | null = null;
|
||||
/** Zentrum des laufenden Kreises (A4.3). */
|
||||
let circleCenter: Pt | null = null;
|
||||
/** Laufender Bogen: Phase 0=nichts, 1=Zentrum gesetzt, 2=Radius+Startwinkel gesetzt. */
|
||||
let arcPhase = 0;
|
||||
let arcCenter: Pt | null = null;
|
||||
let arcStartPt: Pt | null = null;
|
||||
/** Laufende Polyline/Polygon-Punkte; Commit per ENTER. */
|
||||
let polyPoints: Pt[] = [];
|
||||
|
||||
function optsOf(ctx: FullToolContext): { stroke: string; layerId: string } {
|
||||
return {
|
||||
stroke: (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0',
|
||||
layerId: (ctx.options.layerId as string | undefined) ?? 'layer-0',
|
||||
};
|
||||
}
|
||||
|
||||
function buildLine(start: Pt, end: Pt, ctx: FullToolContext): CADElement {
|
||||
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
||||
@@ -149,16 +164,206 @@ export const rectTool: ToolExtensionV2 = {
|
||||
},
|
||||
};
|
||||
|
||||
/** V2-Plugin: Kern-Zeichenwerkzeuge (Pilot: line; A4.2: rect). */
|
||||
/**
|
||||
* circle-Werkzeug (A4.3): Zentrum→Radius wie Legacy (Radius = Distanz).
|
||||
*/
|
||||
export const circleTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'circle', label: 'Kreis', icon: '\u25cb', ribbonTab: 'canvas', tags: ['basic'],
|
||||
description: 'Kreis: Zentrum klicken, Radius ziehen',
|
||||
},
|
||||
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
||||
handlers: {
|
||||
down(e, ctx) { circleCenter = e.world; ctx.setStatus('Kreiszentrum gesetzt – Radius ziehen'); },
|
||||
move(e, ctx) {
|
||||
if (!circleCenter) return;
|
||||
const r = Math.hypot(e.world.x - circleCenter.x, e.world.y - circleCenter.y);
|
||||
ctx.setPreview?.(buildCircle(circleCenter, r, ctx));
|
||||
},
|
||||
up(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (!circleCenter || !c.doc) { circleCenter = null; return; }
|
||||
const r = Math.hypot(e.world.x - circleCenter.x, e.world.y - circleCenter.y);
|
||||
const el = buildCircle(circleCenter, r, c);
|
||||
c.doc.transact(() => c.doc!.addElement(el));
|
||||
circleCenter = null;
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`Kreis erstellt (${el.id})`);
|
||||
},
|
||||
cancel(ctx) { circleCenter = null; ctx.setPreview?.(null); ctx.setStatus('Kreis abgebrochen'); },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* arc-Werkzeug (A4.3): 3-Klick wie Legacy — Zentrum, Startpunkt (Radius+
|
||||
* Startwinkel), Endpunkt (Endwinkel). Winkel in Grad CCW.
|
||||
*/
|
||||
export const arcTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'arc', label: 'Bogen', icon: '\u2322', ribbonTab: 'canvas', tags: ['basic'],
|
||||
description: 'Bogen: Zentrum, Startpunkt, Endpunkt',
|
||||
},
|
||||
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (arcPhase === 0) {
|
||||
arcCenter = e.world;
|
||||
arcPhase = 1;
|
||||
ctx.setStatus('Bogenzentrum gesetzt \u2013 Startpunkt w\u00e4hlen');
|
||||
} else if (arcPhase === 1) {
|
||||
arcStartPt = e.world;
|
||||
arcPhase = 2;
|
||||
ctx.setStatus('Bogen: Endpunkt w\u00e4hlen');
|
||||
} else if (arcPhase === 2 && arcCenter && arcStartPt) {
|
||||
// Dritter Klick committet (klick-progressiv wie Legacy — up feuert im
|
||||
// echten Browser immer direkt nach down, darf deshalb NICHT committen)
|
||||
if (!c.doc) { arcPhase = 0; arcCenter = null; arcStartPt = null; return; }
|
||||
const el = buildArc(arcCenter, arcStartPt, e.world, c);
|
||||
c.doc.transact(() => c.doc!.addElement(el));
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`Bogen erstellt (${el.id})`);
|
||||
arcPhase = 0;
|
||||
arcCenter = null;
|
||||
arcStartPt = null;
|
||||
}
|
||||
},
|
||||
move(e, ctx) {
|
||||
if (arcPhase === 1 && arcCenter) {
|
||||
const r = Math.hypot(e.world.x - arcCenter.x, e.world.y - arcCenter.y);
|
||||
// Vollkreis-Guide in Phasen-Preview (Legacy-Konvention)
|
||||
ctx.setPreview?.(buildArcFullGuide(arcCenter, r, ctx));
|
||||
} else if (arcPhase === 2 && arcCenter && arcStartPt) {
|
||||
ctx.setPreview?.(buildArc(arcCenter, arcStartPt, e.world, ctx));
|
||||
}
|
||||
},
|
||||
cancel(ctx) {
|
||||
arcPhase = 0; arcCenter = null; arcStartPt = null;
|
||||
ctx.setPreview?.(null); ctx.setStatus('Bogen abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* polyline-Werkzeug (A4.3): Mehrklick fügt Punkte; ENTER committet ab 2 Punkten.
|
||||
*/
|
||||
export const polylineTool: ToolExtensionV2 = makePolyTool('polyline', 'Polylinie', 2, '\u2500');
|
||||
|
||||
/**
|
||||
* polygon-Werkzeug (A4.3): wie Polylinie, aber geschlossen; ENTER ab 3 Punkten.
|
||||
*/
|
||||
export const polygonTool: ToolExtensionV2 = makePolyTool('polygon', 'Polygon', 3, '\u2b21');
|
||||
|
||||
function makePolyTool(type: 'polyline' | 'polygon', label: string, minPoints: number, icon: string): ToolExtensionV2 {
|
||||
return {
|
||||
manifest: { id: type, label, icon, ribbonTab: 'canvas', tags: ['basic'], description: `${label}: Punkte klicken, ENTER beendet` },
|
||||
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
polyPoints.push(e.world);
|
||||
if (!isPolyType(type)) return;
|
||||
ctx.setStatus(`${label}: ${polyPoints.length} Punkte – ENTER beendet`);
|
||||
},
|
||||
move(e, ctx) {
|
||||
if (polyPoints.length === 0) return;
|
||||
const pts = [...polyPoints, e.world];
|
||||
ctx.setPreview?.(buildPoly(type, pts, ctx));
|
||||
},
|
||||
up() { /* commit nur via ENTER */ },
|
||||
key(e, ctx) {
|
||||
if (e.key !== 'Enter') return false;
|
||||
if (polyPoints.length < minPoints) {
|
||||
ctx.setStatus(`${label}: mindestens ${minPoints} Punkte nötig`);
|
||||
return true; // konsumiert
|
||||
}
|
||||
const el = buildPoly(type, polyPoints, ctx as FullToolContext);
|
||||
const doc = (ctx as FullToolContext).doc;
|
||||
if (!doc) { resetPoly(); return true; }
|
||||
doc.transact(() => doc.addElement(el));
|
||||
resetPoly();
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`${label} erstellt (${el.id})`);
|
||||
return true;
|
||||
},
|
||||
cancel(ctx) {
|
||||
resetPoly();
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`${label} abgebrochen`);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isPolyType(t: string): boolean {
|
||||
return t === 'polyline' || t === 'polygon';
|
||||
}
|
||||
|
||||
function resetPoly(): void {
|
||||
polyPoints = [];
|
||||
}
|
||||
|
||||
// ─── Build-Helfer A4.3 ───────────────────────────────────────
|
||||
|
||||
function buildCircle(center: Pt, radius: number, ctx: FullToolContext): CADElement {
|
||||
const o = optsOf(ctx);
|
||||
return {
|
||||
id: uid('circle'), type: 'circle', layerId: o.layerId,
|
||||
x: center.x, y: center.y, width: radius * 2, height: radius * 2,
|
||||
properties: { radius, stroke: o.stroke, strokeWidth: 1 },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
function anglesOf(center: Pt, start: Pt, end: Pt): { radius: number; startAngle: number; endAngle: number } {
|
||||
const radius = Math.hypot(start.x - center.x, start.y - center.y);
|
||||
const startAngle = (Math.atan2(start.y - center.y, start.x - center.x) * 180) / Math.PI;
|
||||
const endAngle = (Math.atan2(end.y - center.y, end.x - center.x) * 180) / Math.PI;
|
||||
return { radius, startAngle, endAngle };
|
||||
}
|
||||
|
||||
function buildArc(center: Pt, startPt: Pt, endPt: Pt, ctx: FullToolContext): CADElement {
|
||||
const o = optsOf(ctx);
|
||||
const a = anglesOf(center, startPt, endPt);
|
||||
return {
|
||||
id: uid('arc'), type: 'arc', layerId: o.layerId,
|
||||
x: center.x, y: center.y, width: a.radius * 2, height: a.radius * 2,
|
||||
properties: { radius: a.radius, startAngle: a.startAngle, endAngle: a.endAngle, stroke: o.stroke, strokeWidth: 1 },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
function buildArcFullGuide(center: Pt, radius: number, ctx: FullToolContext): CADElement {
|
||||
// Vollkreis-Vorschau als Radien-Leitfaden (Legacy-Verhalten Phase 1)
|
||||
const o = optsOf(ctx);
|
||||
return {
|
||||
id: uid('arc'), type: 'circle', layerId: o.layerId,
|
||||
x: center.x, y: center.y, width: radius * 2, height: radius * 2,
|
||||
properties: { radius, stroke: o.stroke, strokeWidth: 1, guideOnly: true },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
function buildPoly(type: 'polyline' | 'polygon', pts: Pt[], ctx: FullToolContext): CADElement {
|
||||
const o = optsOf(ctx);
|
||||
const xs = pts.map((p) => p.x);
|
||||
const ys = pts.map((p) => p.y);
|
||||
return {
|
||||
id: uid(type), type, layerId: o.layerId,
|
||||
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
||||
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys),
|
||||
properties: { points: pts.map((p) => ({ x: p.x, y: p.y })), stroke: o.stroke, strokeWidth: 1 },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
/** V2-Plugin: Kern-Zeichenwerkzeuge (Pilot: line; A4.2: rect; A4.3: circle/arc/poly). */
|
||||
export const coreDrawingPlugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'core-drawing',
|
||||
name: 'Zeichnen (Kern)',
|
||||
version: '1.1.0',
|
||||
version: '1.2.0',
|
||||
author: 'web-cad team',
|
||||
description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck).',
|
||||
description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon).',
|
||||
category: 'tools',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [lineTool, rectTool],
|
||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user