2026-08-26 21:30:54 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* core-drawing – Built-in V2-Plugin (Task A3 Pilot).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Werkzeuge: line (Migrations-Pilot aus dem Legacy-Switch).
|
|
|
|
|
|
* Elemente werden über CADDocument committed → kollaborativ korrektes Undo.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Sitzungszustand (Startpunkt der aktuellen Linie) lebt als Modul-Member,
|
|
|
|
|
|
* NICHT in options: Options sind geteilter UI-Store-Zustand.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types';
|
|
|
|
|
|
import type { CADElement } from '../../../types/cad.types';
|
|
|
|
|
|
import type { Pt } from '../../../tools/modification/geometry';
|
|
|
|
|
|
|
|
|
|
|
|
const uid = (prefix: string): string =>
|
|
|
|
|
|
`${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
|
|
/** Startpunkt der laufenden Linie (nur innerhalb einer down→up-Sitzung gesetzt). */
|
|
|
|
|
|
let lineStart: Pt | null = null;
|
2026-08-26 23:14:26 +02:00
|
|
|
|
/** Startpunkt des laufenden Rechtecks (A4.2). */
|
|
|
|
|
|
let rectStart: Pt | null = null;
|
2026-08-27 00:39:30 +02:00
|
|
|
|
/** 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[] = [];
|
2026-08-27 00:48:28 +02:00
|
|
|
|
/** Laufende RevCloud-Punkte (A4.5); Commit per ENTER. */
|
|
|
|
|
|
let cloudPoints: Pt[] = [];
|
2026-08-27 00:39:30 +02:00
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-08-26 21:30:54 +02:00
|
|
|
|
|
|
|
|
|
|
function buildLine(start: Pt, end: Pt, ctx: FullToolContext): CADElement {
|
|
|
|
|
|
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
|
|
|
|
|
const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0';
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: uid('line'),
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
layerId,
|
|
|
|
|
|
x: (start.x + end.x) / 2,
|
|
|
|
|
|
y: (start.y + end.y) / 2,
|
|
|
|
|
|
width: Math.abs(end.x - start.x),
|
|
|
|
|
|
height: Math.abs(end.y - start.y),
|
|
|
|
|
|
properties: { x1: start.x, y1: start.y, x2: end.x, y2: end.y, stroke, strokeWidth: 1 },
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 23:14:26 +02:00
|
|
|
|
function buildRect(cornerA: Pt, cornerB: Pt, ctx: FullToolContext): CADElement {
|
|
|
|
|
|
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
|
|
|
|
|
const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0';
|
|
|
|
|
|
const minX = Math.min(cornerA.x, cornerB.x);
|
|
|
|
|
|
const minY = Math.min(cornerA.y, cornerB.y);
|
|
|
|
|
|
const w = Math.abs(cornerB.x - cornerA.x);
|
|
|
|
|
|
const h = Math.abs(cornerB.y - cornerA.y);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: uid('rect'),
|
|
|
|
|
|
type: 'rect',
|
|
|
|
|
|
layerId,
|
|
|
|
|
|
x: minX + w / 2, // BBox-Zentrum (Legacy-Konvention)
|
|
|
|
|
|
y: minY + h / 2,
|
|
|
|
|
|
width: w,
|
|
|
|
|
|
height: h,
|
|
|
|
|
|
properties: { stroke, strokeWidth: 1 },
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 21:30:54 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* line-Werkzeug: down setzt Startpunkt, move zeigt Live-Vorschau,
|
|
|
|
|
|
* up committet via doc.transact (1 Linie = 1 Undo-Schritt).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const lineTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'line',
|
|
|
|
|
|
label: 'Linie',
|
|
|
|
|
|
icon: '\u2571',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Zeichnet eine Linie (Klick Start, Klick Ende)',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [
|
|
|
|
|
|
{ key: 'strokeColor', label: 'Farbe', type: 'color' },
|
|
|
|
|
|
],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
lineStart = e.world;
|
|
|
|
|
|
ctx.setStatus(`Linie ab (${lineStart.x.toFixed(0)}, ${lineStart.y.toFixed(0)}) – Ende wählen`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
if (!lineStart) return;
|
|
|
|
|
|
ctx.setPreview?.(buildLine(lineStart, e.world, ctx));
|
|
|
|
|
|
},
|
|
|
|
|
|
up(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!lineStart || !c.doc) {
|
|
|
|
|
|
lineStart = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const el = buildLine(lineStart, e.world, c);
|
|
|
|
|
|
c.doc.transact(() => {
|
|
|
|
|
|
// addElement öffnet inneres transact — Yjs verschachtelt sie → EIN Undo-Schritt
|
|
|
|
|
|
c.doc!.addElement(el);
|
|
|
|
|
|
});
|
|
|
|
|
|
lineStart = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`Linie erstellt (${el.id})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
lineStart = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('Linie abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-26 23:14:26 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* rect-Werkzeug (A4.2): down setzt Ecke, move zeigt BBox-Vorschau,
|
|
|
|
|
|
* up committet via doc.transact. Portiert aus Legacy case 'rect'
|
|
|
|
|
|
* (BBox-Konvention: x/y = Zentrum, width/height absolut).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const rectTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'rect',
|
|
|
|
|
|
label: 'Rechteck',
|
|
|
|
|
|
icon: '\u25ad',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Zieht ein Rechteck auf (Klick Ecke, Klick Gegenecke)',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [
|
|
|
|
|
|
{ key: 'strokeColor', label: 'Farbe', type: 'color' },
|
|
|
|
|
|
],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
rectStart = e.world;
|
|
|
|
|
|
ctx.setStatus(`Rechteck ab (${rectStart.x.toFixed(0)}, ${rectStart.y.toFixed(0)}) – Gegenecke wählen`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
if (!rectStart) return;
|
|
|
|
|
|
ctx.setPreview?.(buildRect(rectStart, e.world, ctx));
|
|
|
|
|
|
},
|
|
|
|
|
|
up(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!rectStart || !c.doc) {
|
|
|
|
|
|
rectStart = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const el = buildRect(rectStart, e.world, c);
|
|
|
|
|
|
c.doc.transact(() => {
|
|
|
|
|
|
c.doc!.addElement(el);
|
|
|
|
|
|
});
|
|
|
|
|
|
rectStart = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`Rechteck erstellt (${el.id})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
rectStart = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('Rechteck abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-27 00:39:30 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 00:48:28 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* revcloud-Werkzeug (A4.5): Mehrklick-Revisionswolke wie Poly-Muster;
|
|
|
|
|
|
* ENTER committet ab 3 Punkten. Legacy-Konvention: arcHeight 8.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const revcloudTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'revcloud', label: 'RevCloud', icon: '\u2611', ribbonTab: 'format', tags: ['pro'],
|
|
|
|
|
|
description: 'Revisionswolke: Punkte klicken, ENTER beendet',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
cloudPoints.push(e.world);
|
|
|
|
|
|
ctx.setStatus(`RevCloud: ${cloudPoints.length} Punkte \u2013 ENTER beendet`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
if (cloudPoints.length === 0) return;
|
|
|
|
|
|
ctx.setPreview?.(buildRevCloud([...cloudPoints, e.world], ctx));
|
|
|
|
|
|
},
|
|
|
|
|
|
up() { /* commit via ENTER */ },
|
|
|
|
|
|
key(e, ctx) {
|
|
|
|
|
|
if (e.key !== 'Enter') return false;
|
|
|
|
|
|
if (cloudPoints.length < 3) {
|
|
|
|
|
|
ctx.setStatus('RevCloud: mindestens 3 Punkte n\u00f6tig');
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!c.doc) { cloudPoints = []; return true; }
|
|
|
|
|
|
const el = buildRevCloud(cloudPoints, c);
|
|
|
|
|
|
c.doc.transact(() => c.doc!.addElement(el));
|
|
|
|
|
|
cloudPoints = [];
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`RevCloud erstellt (${el.id})`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
cloudPoints = [];
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('RevCloud abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function buildRevCloud(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('revcloud'), type: 'revcloud', 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 })), arcHeight: 8, fill: 'none', stroke: '#e0e0e0', strokeWidth: 1.5 },
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** V2-Plugin: Kern-Zeichenwerkzeuge (A4.x schrittweise migriert). */
|
2026-08-29 03:12:12 +02:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-29 03:14:45 +02:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Task D2: Spline (CV-basiert) — Kontrollpunkt-Klick-Sequenz,
|
|
|
|
|
|
// ENTER committet ab 3 CVs. Catmull-Rom-Interpolation (Kurve läuft
|
|
|
|
|
|
// DURCH alle CVs, schwingt an Ecken = echte Glättung).
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
let splinePoints: Pt[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Catmull-Rom-Spline-Interpolation: Für n CVs werden n-1 Segmente
|
|
|
|
|
|
* mit je SEG Samples erzeugt. Tangenten aus Nachbarn (Endpunkte
|
|
|
|
|
|
* geklemmt). Ergibt eine glatte Kurve durch alle CVs.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function catmullRom(cvs: Pt[], seg = 16): Pt[] {
|
|
|
|
|
|
if (cvs.length < 2) return [...cvs];
|
|
|
|
|
|
const out: Pt[] = [];
|
|
|
|
|
|
const n = cvs.length;
|
|
|
|
|
|
for (let i = 0; i < n - 1; i++) {
|
|
|
|
|
|
const p0 = cvs[Math.max(0, i - 1)];
|
|
|
|
|
|
const p1 = cvs[i];
|
|
|
|
|
|
const p2 = cvs[i + 1];
|
|
|
|
|
|
const p3 = cvs[Math.min(n - 1, i + 2)];
|
|
|
|
|
|
for (let s = (i === 0 ? 0 : 1); s <= seg; s++) {
|
|
|
|
|
|
const t = s / seg;
|
|
|
|
|
|
const t2 = t * t;
|
|
|
|
|
|
const t3 = t2 * t;
|
|
|
|
|
|
const x = 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
|
|
|
|
|
|
const y = 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
|
|
|
|
|
|
out.push({ x, y });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildSpline(cvs: Pt[], ctx: FullToolContext): CADElement {
|
|
|
|
|
|
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
|
|
|
|
|
const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0';
|
|
|
|
|
|
const pts = catmullRom(cvs);
|
|
|
|
|
|
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 {
|
|
|
|
|
|
id: uid('spline'),
|
|
|
|
|
|
type: 'polyline',
|
|
|
|
|
|
layerId,
|
|
|
|
|
|
x: minX + (maxX - minX) / 2,
|
|
|
|
|
|
y: minY + (maxY - minY) / 2,
|
|
|
|
|
|
width: maxX - minX,
|
|
|
|
|
|
height: maxY - minY,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
points: pts,
|
|
|
|
|
|
controlPoints: [...cvs],
|
|
|
|
|
|
stroke,
|
|
|
|
|
|
strokeWidth: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const splineTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'spline',
|
|
|
|
|
|
label: 'Spline',
|
|
|
|
|
|
icon: '\u2248',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Spline zeichnen: Kontrollpunkte klicken, ENTER beendet',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
splinePoints.push(e.world);
|
|
|
|
|
|
ctx.setStatus(`Spline: ${splinePoints.length} Kontrollpunkte – ENTER beendet`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (splinePoints.length === 0) return;
|
|
|
|
|
|
const pts = [...splinePoints, e.world];
|
|
|
|
|
|
ctx.setPreview?.(buildSpline(pts, c));
|
|
|
|
|
|
},
|
|
|
|
|
|
up() { /* commit nur via ENTER */ },
|
|
|
|
|
|
key(e, ctx) {
|
|
|
|
|
|
if (e.key !== 'Enter' && e.key !== 'ENTER') return false;
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (splinePoints.length < 3) {
|
|
|
|
|
|
ctx.setStatus(`Spline benötigt mindestens 3 Kontrollpunkte (aktuell ${splinePoints.length})`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!c.doc) return false;
|
|
|
|
|
|
const el = buildSpline(splinePoints, c);
|
|
|
|
|
|
c.doc.transact(() => c.doc!.addElement(el));
|
|
|
|
|
|
const count = splinePoints.length;
|
|
|
|
|
|
splinePoints = [];
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`Spline erstellt (${count} CVs, ${el.id})`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
splinePoints = [];
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('Spline abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-29 03:17:38 +02:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Task D3: Point/XLine/Ray — Konstruktions-Elemente auf defpoints.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** Auto-Anlegen des defpoints-Layers (Task D3) falls fehlt. */
|
|
|
|
|
|
function ensureDefpointsLayer(ctx: FullToolContext): void {
|
|
|
|
|
|
const doc = ctx.doc as unknown as {
|
|
|
|
|
|
getLayer(id: string): unknown;
|
|
|
|
|
|
addLayer(l: unknown): void;
|
|
|
|
|
|
} | undefined;
|
|
|
|
|
|
if (!doc || doc.getLayer('defpoints')) return;
|
|
|
|
|
|
doc.addLayer({
|
|
|
|
|
|
id: 'defpoints',
|
|
|
|
|
|
name: 'defpoints',
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
locked: false,
|
|
|
|
|
|
color: '#8a8f98',
|
|
|
|
|
|
lineType: 'dotted',
|
|
|
|
|
|
transparency: 0.5,
|
|
|
|
|
|
sortOrder: 999,
|
|
|
|
|
|
parentId: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Konstruktions-Element-Basis-Props (construction-Tags, Task D3). */
|
|
|
|
|
|
function constructionProps(): Record<string, unknown> {
|
|
|
|
|
|
return { stroke: '#8a8f98', strokeWidth: 1, construction: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const pointTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'point',
|
|
|
|
|
|
label: 'Punkt',
|
|
|
|
|
|
icon: '\u2022',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Konstruktionspunkt platzieren (defpoints-Layer)',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!c.doc) return;
|
|
|
|
|
|
ensureDefpointsLayer(c);
|
|
|
|
|
|
const r = 1.5;
|
|
|
|
|
|
const el: CADElement = {
|
|
|
|
|
|
id: uid('point'),
|
|
|
|
|
|
type: 'circle',
|
|
|
|
|
|
layerId: 'defpoints',
|
|
|
|
|
|
x: e.world.x,
|
|
|
|
|
|
y: e.world.y,
|
|
|
|
|
|
width: r * 2,
|
|
|
|
|
|
height: r * 2,
|
|
|
|
|
|
properties: { ...constructionProps(), radius: r, point: true },
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
c.doc.transact(() => c.doc!.addElement(el));
|
|
|
|
|
|
ctx.setStatus(`Punkt platziert (${el.id})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** XLine: sehr lange Linie in BEIDE Richtungen um den Basispunkt. */
|
|
|
|
|
|
let xlineBase: Pt | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
export const xlineTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'xline',
|
|
|
|
|
|
label: 'Konstruktionslinie',
|
|
|
|
|
|
icon: '\u2194',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Unendliche Konstruktionslinie: Basispunkt + Richtung klicken',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!c.doc) return;
|
|
|
|
|
|
if (!xlineBase) {
|
|
|
|
|
|
xlineBase = e.world;
|
|
|
|
|
|
ctx.setStatus('Konstruktionslinie: Basis gesetzt – Richtung klicken');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
ensureDefpointsLayer(c);
|
|
|
|
|
|
const dx = e.world.x - xlineBase.x;
|
|
|
|
|
|
const dy = e.world.y - xlineBase.y;
|
|
|
|
|
|
const len = Math.hypot(dx, dy);
|
|
|
|
|
|
if (len < 0.001) { xlineBase = null; return; }
|
|
|
|
|
|
const ux = dx / len, uy = dy / len;
|
|
|
|
|
|
const L = 100000;
|
|
|
|
|
|
const el: CADElement = {
|
|
|
|
|
|
id: uid('xline'),
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
layerId: 'defpoints',
|
|
|
|
|
|
x: xlineBase.x,
|
|
|
|
|
|
y: xlineBase.y,
|
|
|
|
|
|
width: 0,
|
|
|
|
|
|
height: 0,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...constructionProps(),
|
|
|
|
|
|
x1: xlineBase.x - ux * L,
|
|
|
|
|
|
y1: xlineBase.y - uy * L,
|
|
|
|
|
|
x2: xlineBase.x + ux * L,
|
|
|
|
|
|
y2: xlineBase.y + uy * L,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
c.doc.transact(() => c.doc!.addElement(el));
|
|
|
|
|
|
xlineBase = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`Konstruktionslinie erstellt (${el.id})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!xlineBase) return;
|
|
|
|
|
|
const dx = e.world.x - xlineBase.x;
|
|
|
|
|
|
const dy = e.world.y - xlineBase.y;
|
|
|
|
|
|
const len = Math.hypot(dx, dy);
|
|
|
|
|
|
if (len < 0.001) return;
|
|
|
|
|
|
const ux = dx / len, uy = dy / len;
|
|
|
|
|
|
const L = 100000;
|
|
|
|
|
|
ctx.setPreview?.({
|
|
|
|
|
|
id: '__preview_xline__',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
layerId: 'defpoints',
|
|
|
|
|
|
x: xlineBase.x,
|
|
|
|
|
|
y: xlineBase.y,
|
|
|
|
|
|
width: 0,
|
|
|
|
|
|
height: 0,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...constructionProps(),
|
|
|
|
|
|
x1: xlineBase.x - ux * L,
|
|
|
|
|
|
y1: xlineBase.y - uy * L,
|
|
|
|
|
|
x2: xlineBase.x + ux * L,
|
|
|
|
|
|
y2: xlineBase.y + uy * L,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as unknown as CADElement);
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
xlineBase = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('Konstruktionslinie abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** Ray: Linie ab Basispunkt NUR in Klickrichtung. */
|
|
|
|
|
|
let rayBase: Pt | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
export const rayTool: ToolExtensionV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'ray',
|
|
|
|
|
|
label: 'Strahl',
|
|
|
|
|
|
icon: '\u2192',
|
|
|
|
|
|
ribbonTab: 'canvas',
|
|
|
|
|
|
tags: ['basic'],
|
|
|
|
|
|
description: 'Strahl ab Basispunkt in Klickrichtung',
|
|
|
|
|
|
},
|
|
|
|
|
|
optionsSchema: [],
|
|
|
|
|
|
handlers: {
|
|
|
|
|
|
down(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!c.doc) return;
|
|
|
|
|
|
if (!rayBase) {
|
|
|
|
|
|
rayBase = e.world;
|
|
|
|
|
|
ctx.setStatus('Strahl: Basis gesetzt – Richtung klicken');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
ensureDefpointsLayer(c);
|
|
|
|
|
|
const dx = e.world.x - rayBase.x;
|
|
|
|
|
|
const dy = e.world.y - rayBase.y;
|
|
|
|
|
|
const len = Math.hypot(dx, dy);
|
|
|
|
|
|
if (len < 0.001) { rayBase = null; return; }
|
|
|
|
|
|
const ux = dx / len, uy = dy / len;
|
|
|
|
|
|
const L = 100000;
|
|
|
|
|
|
const el: CADElement = {
|
|
|
|
|
|
id: uid('ray'),
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
layerId: 'defpoints',
|
|
|
|
|
|
x: rayBase.x,
|
|
|
|
|
|
y: rayBase.y,
|
|
|
|
|
|
width: 0,
|
|
|
|
|
|
height: 0,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...constructionProps(),
|
|
|
|
|
|
x1: rayBase.x,
|
|
|
|
|
|
y1: rayBase.y,
|
|
|
|
|
|
x2: rayBase.x + ux * L,
|
|
|
|
|
|
y2: rayBase.y + uy * L,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as unknown as CADElement;
|
|
|
|
|
|
c.doc.transact(() => c.doc!.addElement(el));
|
|
|
|
|
|
rayBase = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus(`Strahl erstellt (${el.id})`);
|
|
|
|
|
|
},
|
|
|
|
|
|
move(e, ctx) {
|
|
|
|
|
|
const c = ctx as FullToolContext;
|
|
|
|
|
|
if (!rayBase) return;
|
|
|
|
|
|
const dx = e.world.x - rayBase.x;
|
|
|
|
|
|
const dy = e.world.y - rayBase.y;
|
|
|
|
|
|
const len = Math.hypot(dx, dy);
|
|
|
|
|
|
if (len < 0.001) return;
|
|
|
|
|
|
const ux = dx / len, uy = dy / len;
|
|
|
|
|
|
const L = 100000;
|
|
|
|
|
|
ctx.setPreview?.({
|
|
|
|
|
|
id: '__preview_ray__',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
layerId: 'defpoints',
|
|
|
|
|
|
x: rayBase.x,
|
|
|
|
|
|
y: rayBase.y,
|
|
|
|
|
|
width: 0,
|
|
|
|
|
|
height: 0,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...constructionProps(),
|
|
|
|
|
|
x1: rayBase.x,
|
|
|
|
|
|
y1: rayBase.y,
|
|
|
|
|
|
x2: rayBase.x + ux * L,
|
|
|
|
|
|
y2: rayBase.y + uy * L,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as unknown as CADElement);
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel(ctx) {
|
|
|
|
|
|
rayBase = null;
|
|
|
|
|
|
ctx.setPreview?.(null);
|
|
|
|
|
|
ctx.setStatus('Strahl abgebrochen');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-26 21:30:54 +02:00
|
|
|
|
export const coreDrawingPlugin: PluginV2 = {
|
|
|
|
|
|
manifest: {
|
|
|
|
|
|
id: 'core-drawing',
|
|
|
|
|
|
name: 'Zeichnen (Kern)',
|
2026-08-29 03:17:38 +02:00
|
|
|
|
version: '1.5.0',
|
2026-08-26 21:30:54 +02:00
|
|
|
|
author: 'web-cad team',
|
2026-08-27 00:48:28 +02:00
|
|
|
|
description: 'Zeichenwerkzeuge V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon, RevCloud.',
|
2026-08-26 21:30:54 +02:00
|
|
|
|
category: 'tools',
|
|
|
|
|
|
enabledByDefault: true,
|
|
|
|
|
|
},
|
2026-08-29 03:17:38 +02:00
|
|
|
|
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool, splineTool, pointTool, xlineTool, rayTool],
|
2026-08-26 21:30:54 +02:00
|
|
|
|
};
|