task(D1): ellipse tool - 3-click center/radiusX/radiusY with rotation from drag direction, full ellipse as 64-point polygon, arc via start/end angle options
This commit is contained in:
@@ -414,6 +414,150 @@ function buildRevCloud(pts: Pt[], ctx: FullToolContext): CADElement {
|
||||
}
|
||||
|
||||
/** V2-Plugin: Kern-Zeichenwerkzeuge (A4.x schrittweise migriert). */
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task D1: Ellipse (3-Klick: Zentrum→RadiusX→RadiusY) +
|
||||
// Ellipsenbogen via startAngle/endAngle-Options.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Ellipsen-Sitzung: Phase 0=nichts, 1=Zentrum, 2=RadiusX fixiert. */
|
||||
let ellipsePhase = 0;
|
||||
let ellipseCenter: Pt | null = null;
|
||||
let ellipseRadiusX = 0;
|
||||
/** Rotation aus der Klick2-Zugrichtung (Grad, 0°=+x, CW-positiv). */
|
||||
let ellipseRotationDeg = 0;
|
||||
|
||||
/**
|
||||
* Ellipse als Polyline-Approximation (F5-Parser-Konvention):
|
||||
* Voll-Ellipse → 64 Punkte geschlossen (polygon), Bogen → proportional
|
||||
* offene Polyline. rotation aus der Klick2-Zugrichtung (Grad, 0°=+x).
|
||||
*/
|
||||
function buildEllipse(
|
||||
cx: number,
|
||||
cy: number,
|
||||
rx: number,
|
||||
ry: number,
|
||||
rotationDeg: number,
|
||||
ctx: FullToolContext,
|
||||
startAngle = 0,
|
||||
endAngle = 360,
|
||||
): CADElement {
|
||||
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
||||
const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0';
|
||||
const phi = (rotationDeg * Math.PI) / 180;
|
||||
const cosP = Math.cos(phi), sinP = Math.sin(phi);
|
||||
const FULL = 360;
|
||||
const span = Math.min(endAngle, FULL) - Math.min(startAngle, FULL);
|
||||
const isFull = span >= FULL - 0.001 || span <= 0.001;
|
||||
const n = Math.max(2, Math.round(64 * (Math.max(span, 1) / FULL)));
|
||||
const pts: Pt[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = ((startAngle + (span * i) / (isFull ? n : (n - 1))) * Math.PI) / 180;
|
||||
const x = rx * Math.cos(t);
|
||||
const y = ry * Math.sin(t);
|
||||
pts.push({
|
||||
x: cx + x * cosP - y * sinP,
|
||||
y: cy + x * sinP + y * cosP,
|
||||
});
|
||||
}
|
||||
const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
|
||||
const minX = Math.min(...xs), minY = Math.min(...ys);
|
||||
const type = isFull ? 'polygon' : 'polyline';
|
||||
return {
|
||||
id: uid('ellipse'),
|
||||
type,
|
||||
layerId,
|
||||
x: minX + (Math.max(...xs) - minX) / 2,
|
||||
y: minY + (Math.max(...ys) - minY) / 2,
|
||||
width: Math.max(...xs) - minX,
|
||||
height: Math.max(...ys) - minY,
|
||||
properties: {
|
||||
points: pts,
|
||||
stroke,
|
||||
strokeWidth: 1,
|
||||
cx, cy, rx, ry,
|
||||
rotation: rotationDeg,
|
||||
...(isFull ? {} : { startAngle, endAngle }),
|
||||
},
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
export const ellipseTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'ellipse',
|
||||
label: 'Ellipse',
|
||||
icon: '\u2b2d',
|
||||
ribbonTab: 'canvas',
|
||||
tags: ['basic'],
|
||||
description: 'Ellipse zeichnen: Zentrum klicken, RadiusX, RadiusY (Bogen via Optionen)',
|
||||
},
|
||||
optionsSchema: [
|
||||
{ key: 'strokeColor', label: 'Farbe', type: 'color' },
|
||||
{ key: 'startAngle', label: 'Bogen-Start (°)', type: 'number', min: 0, max: 360 },
|
||||
{ key: 'endAngle', label: 'Bogen-Ende (°)', type: 'number', min: 0, max: 360 },
|
||||
],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (ellipsePhase === 0) {
|
||||
ellipseCenter = e.world;
|
||||
ellipsePhase = 1;
|
||||
ctx.setStatus('Ellipse: Zentrum gesetzt – RadiusX klicken (Richtung = Rotation)');
|
||||
} else if (ellipsePhase === 1) {
|
||||
if (!ellipseCenter) { ellipsePhase = 0; return; }
|
||||
const dx = e.world.x - ellipseCenter.x;
|
||||
const dy = e.world.y - ellipseCenter.y;
|
||||
ellipseRadiusX = Math.hypot(dx, dy);
|
||||
ellipseRotationDeg = ((Math.atan2(dy, dx) * 180) / Math.PI + 360) % 360;
|
||||
ellipsePhase = 2;
|
||||
ctx.setStatus('Ellipse: RadiusX gesetzt – RadiusY klicken');
|
||||
} else {
|
||||
// Phase 2: Commit
|
||||
if (!ellipseCenter || !c.doc) { ellipsePhase = 0; ellipseCenter = null; return; }
|
||||
const dx = e.world.x - ellipseCenter.x;
|
||||
const dy = e.world.y - ellipseCenter.y;
|
||||
const ry = Math.hypot(dx, dy);
|
||||
// Rotation: Winkel des Klick2-Vektors gegenüber der +x-Achse
|
||||
const startAngle = (ctx.options.startAngle as number | undefined) ?? 0;
|
||||
const endAngle = (ctx.options.endAngle as number | undefined) ?? 360;
|
||||
const el = buildEllipse(
|
||||
ellipseCenter.x, ellipseCenter.y,
|
||||
ellipseRadiusX, ry,
|
||||
ellipseRotationDeg,
|
||||
c, startAngle, endAngle,
|
||||
);
|
||||
c.doc.transact(() => c.doc!.addElement(el));
|
||||
ellipsePhase = 0;
|
||||
ellipseCenter = null;
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`Ellipse erstellt (${el.id})`);
|
||||
}
|
||||
},
|
||||
move(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (ellipsePhase === 1 && ellipseCenter) {
|
||||
// RadiusX-Linie als Preview
|
||||
const r = Math.hypot(e.world.x - ellipseCenter.x, e.world.y - ellipseCenter.y);
|
||||
const rotDeg = ((Math.atan2(e.world.y - ellipseCenter.y, e.world.x - ellipseCenter.x) * 180) / Math.PI + 360) % 360;
|
||||
const startAngle = (ctx.options.startAngle as number | undefined) ?? 0;
|
||||
const endAngle = (ctx.options.endAngle as number | undefined) ?? 360;
|
||||
ctx.setPreview?.(buildEllipse(ellipseCenter.x, ellipseCenter.y, r, r, rotDeg, c, startAngle, endAngle));
|
||||
} else if (ellipsePhase === 2 && ellipseCenter) {
|
||||
const ry = Math.hypot(e.world.x - ellipseCenter.x, e.world.y - ellipseCenter.y);
|
||||
const rotDeg = ellipseRotationDeg;
|
||||
const startAngle = (ctx.options.startAngle as number | undefined) ?? 0;
|
||||
const endAngle = (ctx.options.endAngle as number | undefined) ?? 360;
|
||||
ctx.setPreview?.(buildEllipse(ellipseCenter.x, ellipseCenter.y, ellipseRadiusX, ry, rotDeg, c, startAngle, endAngle));
|
||||
}
|
||||
},
|
||||
cancel(ctx) {
|
||||
ellipsePhase = 0;
|
||||
ellipseCenter = null;
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Ellipse abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const coreDrawingPlugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'core-drawing',
|
||||
@@ -424,5 +568,5 @@ export const coreDrawingPlugin: PluginV2 = {
|
||||
category: 'tools',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool],
|
||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user