task(A4.5): migrate revcloud/hatch/measure tools to v2
This commit is contained in:
@@ -26,6 +26,8 @@ let arcCenter: Pt | null = null;
|
|||||||
let arcStartPt: Pt | null = null;
|
let arcStartPt: Pt | null = null;
|
||||||
/** Laufende Polyline/Polygon-Punkte; Commit per ENTER. */
|
/** Laufende Polyline/Polygon-Punkte; Commit per ENTER. */
|
||||||
let polyPoints: Pt[] = [];
|
let polyPoints: Pt[] = [];
|
||||||
|
/** Laufende RevCloud-Punkte (A4.5); Commit per ENTER. */
|
||||||
|
let cloudPoints: Pt[] = [];
|
||||||
|
|
||||||
function optsOf(ctx: FullToolContext): { stroke: string; layerId: string } {
|
function optsOf(ctx: FullToolContext): { stroke: string; layerId: string } {
|
||||||
return {
|
return {
|
||||||
@@ -354,16 +356,73 @@ function buildPoly(type: 'polyline' | 'polygon', pts: Pt[], ctx: FullToolContext
|
|||||||
} as unknown as CADElement;
|
} as unknown as CADElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** V2-Plugin: Kern-Zeichenwerkzeuge (Pilot: line; A4.2: rect; A4.3: circle/arc/poly). */
|
/**
|
||||||
|
* 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). */
|
||||||
export const coreDrawingPlugin: PluginV2 = {
|
export const coreDrawingPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-drawing',
|
id: 'core-drawing',
|
||||||
name: 'Zeichnen (Kern)',
|
name: 'Zeichnen (Kern)',
|
||||||
version: '1.2.0',
|
version: '1.3.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon).',
|
description: 'Zeichenwerkzeuge V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon, RevCloud.',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool],
|
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* core-measure – Built-in V2-Plugin (Task A4.5).
|
||||||
|
*
|
||||||
|
* measure: 2 Klicks → Distanz+Winkel-Anzeige im Status. KEIN Element-Commit
|
||||||
|
* (Legacy-Verhalten getreu — Messung ist Information, keine Zeichnung).
|
||||||
|
* Distanzformatierung via bestehendem formatDistance(unit, scaleFactor).
|
||||||
|
*/
|
||||||
|
import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types';
|
||||||
|
import { formatDistance } from '../../../utils/format';
|
||||||
|
|
||||||
|
type MeasurePhase = 0 | 1; // 0=wartet auf Start, 1=wartet auf Ende
|
||||||
|
let measureStart: import('../../../tools/modification/geometry').Pt | null = null;
|
||||||
|
|
||||||
|
export const measureTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'measure',
|
||||||
|
label: 'Messen',
|
||||||
|
icon: '\u2194',
|
||||||
|
ribbonTab: 'view',
|
||||||
|
tags: ['basic'],
|
||||||
|
description: 'Distanz und Winkel zwischen zwei Punkten messen',
|
||||||
|
},
|
||||||
|
optionsSchema: [],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!measureStart) {
|
||||||
|
measureStart = e.world;
|
||||||
|
ctx.setStatus('Messung: Endpunkt klicken');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Zweiter Klick → anzeigen (Legacy-Ausgabeformat)
|
||||||
|
const rawDist = Math.hypot(e.world.x - measureStart.x, e.world.y - measureStart.y);
|
||||||
|
const angle = (Math.atan2(e.world.y - measureStart.y, e.world.x - measureStart.x) * 180) / Math.PI;
|
||||||
|
const unit = (c.options.unit as string | undefined) ?? 'mm';
|
||||||
|
const scale = (c.options.scaleFactor as number | undefined) ?? 1;
|
||||||
|
const distStr = formatDistance(rawDist, unit as never, scale);
|
||||||
|
ctx.setStatus(`distance: ${distStr} angle: ${angle.toFixed(1)}\u00b0`);
|
||||||
|
measureStart = null;
|
||||||
|
},
|
||||||
|
cancel(ctx) {
|
||||||
|
measureStart = null;
|
||||||
|
ctx.setStatus('Messung abgebrochen');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** V2-Plugin: Messwerkzeuge. */
|
||||||
|
export const coreMeasurePlugin: PluginV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'core-measure',
|
||||||
|
name: 'Messen (Kern)',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: 'web-cad team',
|
||||||
|
description: 'Abstandswinkel-Messung ohne Element-Erzeugung.',
|
||||||
|
category: 'tools',
|
||||||
|
enabledByDefault: true,
|
||||||
|
},
|
||||||
|
tools: [measureTool],
|
||||||
|
};
|
||||||
@@ -48,16 +48,53 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** V2-Plugin: Kern-Bearbeitungswerkzeuge (Pilot: nur select). */
|
/**
|
||||||
|
* hatch-Werkzeug (A4.5): Klick auf geschlossenes Element (rect/circle/polygon)
|
||||||
|
* aktiviert Schraffur — Portierung aus Legacy handleHatchDown, aber jetzt via
|
||||||
|
* doc.updateElement (eigene Undo-Transaktion statt Callback-Direktaufruf).
|
||||||
|
*/
|
||||||
|
export const hatchTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'hatch', label: 'Schraffur', icon: '\u2593', ribbonTab: 'format', tags: ['pro'],
|
||||||
|
description: 'Geschlossenes Element anklicken zum Schraffieren',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'hatchPattern', label: 'Muster', type: 'select', options: [
|
||||||
|
{ value: 'lines', label: 'Linien' },
|
||||||
|
{ value: 'cross', label: 'Kreuz' },
|
||||||
|
] },
|
||||||
|
{ key: 'hatchSpacing', label: 'Abstand', type: 'number', min: 2, max: 40 },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as import('../../types').FullToolContext;
|
||||||
|
const bridge = c.selection;
|
||||||
|
if (!bridge || !c.doc) return;
|
||||||
|
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
if (!hit || !['rect', 'circle', 'polygon'].includes(hit.type)) {
|
||||||
|
ctx.setStatus('Schraffur: nur rect/circle/polygon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pattern = (c.options.hatchPattern as string | undefined) ?? 'lines';
|
||||||
|
const spacing = (c.options.hatchSpacing as number | undefined) ?? 8;
|
||||||
|
c.doc.updateElement(hit.id, {
|
||||||
|
properties: { hatch: true, hatchPattern: pattern, hatchSpacing: spacing },
|
||||||
|
});
|
||||||
|
ctx.setStatus(`Schraffur auf ${hit.id} (${pattern})`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** V2-Plugin: Kern-Bearbeitungswerkzeuge (Pilot: select; A4.5: hatch). */
|
||||||
export const coreModifyPlugin: PluginV2 = {
|
export const coreModifyPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-modify',
|
id: 'core-modify',
|
||||||
name: 'Bearbeiten (Kern)',
|
name: 'Bearbeiten (Kern)',
|
||||||
version: '1.0.0',
|
version: '1.1.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Auswahl und Bearbeitung (V2-Pilot: Selektion).',
|
description: 'Auswahl und Bearbeitung (V2: Selektion, Schraffur).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [selectTool],
|
tools: [selectTool, hatchTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,12 +30,14 @@ export { eventToolsPlugin } from './builtin/eventTools';
|
|||||||
export { coreDrawingPlugin } from './builtin/core-drawing';
|
export { coreDrawingPlugin } from './builtin/core-drawing';
|
||||||
export { coreModifyPlugin } from './builtin/core-modify';
|
export { coreModifyPlugin } from './builtin/core-modify';
|
||||||
export { coreAnnotatePlugin } from './builtin/core-annotate';
|
export { coreAnnotatePlugin } from './builtin/core-annotate';
|
||||||
|
export { coreMeasurePlugin } from './builtin/core-measure';
|
||||||
|
|
||||||
import { pluginRegistry } from './PluginRegistry';
|
import { pluginRegistry } from './PluginRegistry';
|
||||||
import { eventToolsPlugin } from './builtin/eventTools';
|
import { eventToolsPlugin } from './builtin/eventTools';
|
||||||
import { coreDrawingPlugin } from './builtin/core-drawing';
|
import { coreDrawingPlugin } from './builtin/core-drawing';
|
||||||
import { coreModifyPlugin } from './builtin/core-modify';
|
import { coreModifyPlugin } from './builtin/core-modify';
|
||||||
import { coreAnnotatePlugin } from './builtin/core-annotate';
|
import { coreAnnotatePlugin } from './builtin/core-annotate';
|
||||||
|
import { coreMeasurePlugin } from './builtin/core-measure';
|
||||||
|
|
||||||
/** Register all built-in plugins */
|
/** Register all built-in plugins */
|
||||||
export function registerBuiltinPlugins() {
|
export function registerBuiltinPlugins() {
|
||||||
@@ -44,4 +46,5 @@ export function registerBuiltinPlugins() {
|
|||||||
pluginRegistry.registerV2(coreDrawingPlugin);
|
pluginRegistry.registerV2(coreDrawingPlugin);
|
||||||
pluginRegistry.registerV2(coreModifyPlugin);
|
pluginRegistry.registerV2(coreModifyPlugin);
|
||||||
pluginRegistry.registerV2(coreAnnotatePlugin);
|
pluginRegistry.registerV2(coreAnnotatePlugin);
|
||||||
|
pluginRegistry.registerV2(coreMeasurePlugin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing';
|
import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing';
|
||||||
import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify';
|
import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify';
|
||||||
|
import { coreMeasurePlugin } from '../src/plugins/builtin/core-measure';
|
||||||
import { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate';
|
import { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate';
|
||||||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||||
@@ -260,6 +261,93 @@ describe('A4.4 pilot: annotate tools', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('A4.5 pilot: revcloud tool', () => {
|
||||||
|
it('committet per ENTER ab 3 Punkten mit Legacy-Konvention arcHeight=8', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const ctx = makeCtx({ doc });
|
||||||
|
const cloud = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'revcloud')!;
|
||||||
|
|
||||||
|
cloud.handlers.down!(pe(0, 0), ctx);
|
||||||
|
cloud.handlers.down!(pe(100, 0), ctx);
|
||||||
|
cloud.handlers.down!(pe(100, 50), ctx);
|
||||||
|
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||||||
|
const consumed = cloud.handlers.key!(kev, ctx);
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
|
||||||
|
const all = doc.getAllElements();
|
||||||
|
expect(all.length).toBe(1);
|
||||||
|
expect(all[0].type).toBe('revcloud');
|
||||||
|
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||||||
|
expect(all[0].properties.arcHeight).toBe(8); // Legacy-Konvention
|
||||||
|
expect(all[0].properties.fill).toBe('none');
|
||||||
|
|
||||||
|
expect(doc.undo()).toBe(true);
|
||||||
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('A4.5 pilot: measure tool', () => {
|
||||||
|
it('2 Klicks zeigen Distanz+Winkel im Status; erzeugt KEIN Element', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const ctx = makeCtx({ doc });
|
||||||
|
const meas = coreMeasurePlugin.tools!.find((t) => t.manifest.id === 'measure')!;
|
||||||
|
|
||||||
|
meas.handlers.down!(pe(0, 0), ctx);
|
||||||
|
expect(ctx.statuses.some((s) => s.includes('Endpunkt'))).toBe(true);
|
||||||
|
|
||||||
|
meas.handlers.down!(pe(30, 40), ctx); // dist=50, angle≈53.13°
|
||||||
|
const last = ctx.statuses[ctx.statuses.length - 1];
|
||||||
|
expect(last).toContain('distance:');
|
||||||
|
expect(last).toContain('angle:');
|
||||||
|
expect(last).toContain('53.1'); // atan2(40,30)≈53.13°
|
||||||
|
// KernLegacy-Verhalten: Messung erzeugt kein Element
|
||||||
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('A4.5 pilot: hatch tool', () => {
|
||||||
|
it('setzt hatch-Properties via updateElement; undo macht rückgängig', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
// Seed-Rect im Doc, das beim hitTest zurückkommt:
|
||||||
|
const rectEl = { id: 'r1', type: 'rect', layerId: 'l', x: 50, y: 50, width: 10, height: 10, properties: {} } as unknown as CADElement;
|
||||||
|
doc.addElement(rectEl);
|
||||||
|
const bridge = {
|
||||||
|
getIds: () => [],
|
||||||
|
setIds: () => {},
|
||||||
|
hitTest: () => rectEl,
|
||||||
|
};
|
||||||
|
const ctx = makeCtx({ doc, selection: bridge, options: { hatchPattern: 'cross', hatchSpacing: 12 } });
|
||||||
|
const hatch = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'hatch')!;
|
||||||
|
|
||||||
|
hatch.handlers.down!(pe(50, 50), ctx);
|
||||||
|
|
||||||
|
const updated = doc.getElement('r1')!;
|
||||||
|
expect(updated.properties.hatch).toBe(true);
|
||||||
|
expect(updated.properties.hatchPattern).toBe('cross'); // aus Options
|
||||||
|
expect(updated.properties.hatchSpacing).toBe(12);
|
||||||
|
|
||||||
|
expect(doc.undo()).toBe(true); // A2b-Update ist selbst Undo-Schritt
|
||||||
|
expect(doc.getElement('r1')!.properties.hatch).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lehnt offene Elementtypen (line) mit Hinweis ab', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const lineEl = { id: 'l1', type: 'line', layerId: 'l', x: 0, y: 0, width: 5, height: 5, properties: { x1: 0, y1: 0, x2: 5, y2: 5 } } as unknown as CADElement;
|
||||||
|
doc.addElement(lineEl);
|
||||||
|
const bridge = { getIds: () => [], setIds: () => {}, hitTest: () => lineEl };
|
||||||
|
const ctx = makeCtx({ doc, selection: bridge });
|
||||||
|
const hatch = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'hatch')!;
|
||||||
|
|
||||||
|
hatch.handlers.down!(pe(2, 2), ctx);
|
||||||
|
expect(doc.getElement('l1')!.properties.hatch).toBeUndefined(); // nichts geändert
|
||||||
|
expect(ctx.statuses.some((s) => s.includes('rect/circle/polygon'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('A3 pilot: select tool', () => {
|
describe('A3 pilot: select tool', () => {
|
||||||
function makeBridge(elements: CADElement[]) {
|
function makeBridge(elements: CADElement[]) {
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user