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;
|
let lineStart: Pt | null = null;
|
||||||
/** Startpunkt des laufenden Rechtecks (A4.2). */
|
/** Startpunkt des laufenden Rechtecks (A4.2). */
|
||||||
let rectStart: Pt | null = null;
|
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 {
|
function buildLine(start: Pt, end: Pt, ctx: FullToolContext): CADElement {
|
||||||
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
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 = {
|
export const coreDrawingPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-drawing',
|
id: 'core-drawing',
|
||||||
name: 'Zeichnen (Kern)',
|
name: 'Zeichnen (Kern)',
|
||||||
version: '1.1.0',
|
version: '1.2.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck).',
|
description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [lineTool, rectTool],
|
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,46 +80,125 @@ describe('A3 pilot: line tool', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('A4.2 pilot: rect tool', () => {
|
describe('A4.3 pilot: circle tool', () => {
|
||||||
it('erzeugt Rechteck mit BBox-Konvention; ein undo entfernt es', () => {
|
it('erzeugt Kreis Zentrum+Radius; ein undo entfernt ihn', () => {
|
||||||
const { ydoc } = createInMemoryDoc();
|
const { ydoc } = createInMemoryDoc();
|
||||||
const doc = new CADDocument(ydoc);
|
const doc = new CADDocument(ydoc);
|
||||||
const ctx = makeCtx({ doc });
|
const ctx = makeCtx({ doc });
|
||||||
const rect = coreDrawingPlugin.tools?.find((t) => t.manifest.id === 'rect');
|
const circle = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'circle')!;
|
||||||
expect(rect).toBeDefined();
|
|
||||||
|
|
||||||
rect!.handlers.down!(pe(10, 10), ctx);
|
circle.handlers.down!(pe(100, 100), ctx);
|
||||||
rect!.handlers.move!(pe(110, 60), ctx);
|
circle.handlers.move!(pe(200, 100), ctx);
|
||||||
expect(ctx.previews.length).toBe(1); // Live-Vorschau
|
expect(ctx.previews.length).toBe(1);
|
||||||
rect!.handlers.up!(pe(110, 60), ctx);
|
circle.handlers.up!(pe(200, 100), ctx);
|
||||||
|
|
||||||
const all = doc.getAllElements();
|
const all = doc.getAllElements();
|
||||||
expect(all.length).toBe(1);
|
expect(all.length).toBe(1);
|
||||||
expect(all[0].type).toBe('rect');
|
expect(all[0].type).toBe('circle');
|
||||||
// Legacy-BBox-Konvention: Zentrum zwischen beiden Ecken
|
expect(all[0].x).toBe(100);
|
||||||
expect(all[0].x).toBe(60);
|
expect(all[0].properties.radius).toBe(100); // Distanz wie Legacy
|
||||||
expect(all[0].y).toBe(35);
|
|
||||||
expect(all[0].width).toBe(100);
|
expect(doc.undo()).toBe(true);
|
||||||
expect(all[0].height).toBe(50);
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('A4.3 pilot: arc tool', () => {
|
||||||
|
it('klick-progressiv: Zentrum→Start→Ende committet beim 3. Klick mit Grad-Winkeln', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const ctx = makeCtx({ doc });
|
||||||
|
const arc = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'arc')!;
|
||||||
|
|
||||||
|
// Klick 1: Zentrum (up gehört zum Klick, committet NICHT — klick-progressiv)
|
||||||
|
arc.handlers.down!(pe(0, 0), ctx);
|
||||||
|
expect(doc.getAllElements().length).toBe(0); // Phase 1/2 committet nie
|
||||||
|
|
||||||
|
// Klick 2: Startpunkt auf Radius 50 nach Osten (0°)
|
||||||
|
arc.handlers.down!(pe(50, 0), ctx);
|
||||||
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
|
||||||
|
// Klick 3: Endpunkt oben (90°) → JETZT committet der dritte down
|
||||||
|
arc.handlers.down!(pe(0, 50), ctx);
|
||||||
|
|
||||||
|
const all = doc.getAllElements();
|
||||||
|
expect(all.length).toBe(1);
|
||||||
|
expect(all[0].type).toBe('arc');
|
||||||
|
expect(all[0].x).toBe(0);
|
||||||
|
expect(all[0].y).toBe(0);
|
||||||
|
expect(all[0].properties.radius).toBe(50);
|
||||||
|
expect(all[0].properties.startAngle).toBeCloseTo(0);
|
||||||
|
expect(all[0].properties.endAngle).toBeCloseTo(90);
|
||||||
|
|
||||||
expect(doc.undo()).toBe(true);
|
expect(doc.undo()).toBe(true);
|
||||||
expect(doc.getAllElements().length).toBe(0);
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancel verwirft das laufende Rechteck', () => {
|
it('cancel bricht mehrphasige Sitzung vollstaendig ab', () => {
|
||||||
const { ydoc } = createInMemoryDoc();
|
const { ydoc } = createInMemoryDoc();
|
||||||
const doc = new CADDocument(ydoc);
|
const doc = new CADDocument(ydoc);
|
||||||
const ctx = makeCtx({ doc });
|
const ctx = makeCtx({ doc });
|
||||||
const rect = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'rect')!;
|
const arc = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'arc')!;
|
||||||
|
|
||||||
rect.handlers.down!(pe(5, 5), ctx);
|
arc.handlers.down!(pe(0, 0), ctx); // phase 1
|
||||||
rect.handlers.cancel!(ctx);
|
arc.handlers.down!(pe(30, 0), ctx); // phase 2
|
||||||
rect.handlers.up!(pe(50, 50), ctx);
|
arc.handlers.cancel!(ctx); // reset
|
||||||
|
arc.handlers.down!(pe(99, 99), ctx); // startet neu bei phase 1
|
||||||
|
|
||||||
|
// Kein Commit erfolgt (nur phase 1), nichts im Doc:
|
||||||
expect(doc.getAllElements().length).toBe(0);
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('A4.3 pilot: polyline/polygon tools', () => {
|
||||||
|
it('polyline sammelt Punkte und committet per ENTER ab 2', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const ctx = makeCtx({ doc });
|
||||||
|
const pl = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'polyline')!;
|
||||||
|
|
||||||
|
pl.handlers.down!(pe(0, 0), ctx);
|
||||||
|
pl.handlers.move!(pe(10, 10), ctx);
|
||||||
|
expect(ctx.previews.length).toBeGreaterThan(0);
|
||||||
|
pl.handlers.down!(pe(20, 0), ctx);
|
||||||
|
pl.handlers.down!(pe(30, 30), ctx);
|
||||||
|
|
||||||
|
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||||||
|
const consumed = pl.handlers.key!(kev, ctx);
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
|
||||||
|
const all = doc.getAllElements();
|
||||||
|
expect(all.length).toBe(1);
|
||||||
|
expect(all[0].type).toBe('polyline');
|
||||||
|
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||||||
|
expect(all[0].x).toBe(15); // BBox-Zentrum min0 max30
|
||||||
|
|
||||||
|
expect(doc.undo()).toBe(true);
|
||||||
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('polygon braucht mind. 3 Punkte; ENTER davor meldet Hinweis', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const pg = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'polygon')!;
|
||||||
|
const ctx = makeCtx({ doc });
|
||||||
|
|
||||||
|
pg.handlers.down!(pe(0, 0), ctx);
|
||||||
|
pg.handlers.down!(pe(10, 10), ctx);
|
||||||
|
const kev = { key: 'Enter', preventDefault: () => {} } as unknown as KeyboardEvent;
|
||||||
|
pg.handlers.key!(kev, ctx); // nur 2 Punkte → abgelehnt
|
||||||
|
expect(doc.getAllElements().length).toBe(0);
|
||||||
|
expect(ctx.statuses.some((s) => s.includes('mindestens 3'))).toBe(true);
|
||||||
|
|
||||||
|
pg.handlers.down!(pe(20, -5), ctx);
|
||||||
|
pg.handlers.key!(kev, ctx); // jetzt 3 Punkte → ok
|
||||||
|
const all = doc.getAllElements();
|
||||||
|
expect(all.length).toBe(1);
|
||||||
|
expect(all[0].type).toBe('polygon');
|
||||||
|
expect((all[0].properties.points as Pt[]).length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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