Files
web-cad/frontend/tests/otrackDivide.test.ts

223 lines
9.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Task CAD-4 OTRACK + DIVIDE/MEASURE.
*
* OTRACK: Tracking-Vektoren von Fang-Punkten (horizontal/vertikal/
* Winkelmultiples), SnapEngine-Erweiterung.
* DIVIDE: n gleichmaeßige Punkte auf einem Pfad.
* MEASURE: Punkte im festen Abstand auf einem Pfad.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { SnapEngine, computeTrackPoint } from '../src/canvas/SnapEngine';
import { divideTool, measureTool, pointsAlongPath } from '../src/plugins/builtin/core-modify';
import { CADDocument } from '../src/kernel/document/CADDocument';
import { createInMemoryDoc } from './helpers/inMemoryDoc';
import type { CADElement, Pt } from '../src/types/cad.types';
function mkCtx(elements: CADElement[], options: Record<string, unknown> = {}) {
const { ydoc } = createInMemoryDoc();
const doc = new CADDocument(ydoc);
for (const el of elements) doc.addElement(el);
const selectedIds = new Set<string>();
const statuses: string[] = [];
const ctx: any = {
doc,
options,
setStatus: (m: string) => statuses.push(m),
setPreview: (_el: CADElement | null) => {},
selection: {
getIds: () => Array.from(selectedIds),
setIds: (ids: string[]) => {
selectedIds.clear();
for (const id of ids) selectedIds.add(id);
},
hitTest: (x: number, y: number, _tol: number) => {
for (const el of elements) {
const p = el.properties as Record<string, unknown>;
if (Array.isArray(p.points)) {
const pts = p.points as Pt[];
for (let i = 0; i < pts.length - 1; i++) {
const dx = pts[i + 1].x - pts[i].x;
const dy = pts[i + 1].y - pts[i].y;
const len = Math.hypot(dx, dy) || 1;
const t = Math.max(0, Math.min(1, ((x - pts[i].x) * dx + (y - pts[i].y) * dy) / (len * len)));
const px = pts[i].x + t * dx;
const py = pts[i].y + t * dy;
if (Math.hypot(x - px, y - py) < 10) return el;
}
}
if (
x >= el.x - el.width / 2 - 5 && x <= el.x + el.width / 2 + 5 &&
y >= el.y - el.height / 2 - 5 && y <= el.y + el.height / 2 + 5
) return el;
}
return null;
},
},
};
return { ctx, doc, selectedIds, statuses };
}
function pe(x: number, y: number, mods: { shift?: boolean } = {}): { world: Pt; shift: boolean; ctrl: boolean } {
return { world: { x, y }, shift: mods.shift ?? false, ctrl: false } as never;
}
function path(pts: Array<[number, number]>): CADElement {
return {
id: 'path-1', type: 'polyline', layerId: 'l',
x: pts.reduce((s, p) => s + p[0], 0) / pts.length,
y: pts.reduce((s, p) => s + p[1], 0) / pts.length,
width: Math.max(...pts.map(p => p[0])) - Math.min(...pts.map(p => p[0])),
height: Math.max(...pts.map(p => p[1])) - Math.min(...pts.map(p => p[1])),
properties: { points: pts.map(([x, y]) => ({ x, y })) },
} as unknown as CADElement;
}
beforeEach(() => {
divideTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
measureTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
});
// ─── OTRACK: computeTrackPoint ────────────────────────────────
describe('CAD-4: computeTrackPoint (OTRACK)', () => {
it('horizontal von Basis (100,100) bei Cursor (200,90): Track-Punkt (200,100)', () => {
const result = computeTrackPoint({ x: 100, y: 100 }, { x: 200, y: 90 });
expect(result).not.toBeNull();
expect(result!.x).toBeCloseTo(200, 6);
expect(result!.y).toBeCloseTo(100, 6);
});
it('vertikal von Basis (100,100) bei Cursor (90,200): Track-Punkt (100,200)', () => {
const result = computeTrackPoint({ x: 100, y: 100 }, { x: 90, y: 200 });
expect(result).not.toBeNull();
expect(result!.x).toBeCloseTo(100, 6);
expect(result!.y).toBeCloseTo(200, 6);
});
it('45-Grad-Richtung wird verfolgt', () => {
// Basis (0,0), Cursor (100,80): 45 Grad heißt y=x → Track (100,100)
const result = computeTrackPoint({ x: 0, y: 0 }, { x: 100, y: 80 }, [0, 45, 90, 135, 180, 225, 270, 315]);
expect(result).not.toBeNull();
expect(result!.x).toBeCloseTo(100, 6);
expect(result!.y).toBeCloseTo(100, 6);
});
it('Cursor genau auf der Basis: kein Track (null)', () => {
expect(computeTrackPoint({ x: 100, y: 100 }, { x: 100, y: 100 })).toBeNull();
});
it('ohne Winkel-Option: nur 0/90/180/270 (Standard-Tracking)', () => {
// 30-Grad-Cursor nahe horizontal: snappt auf horizontal
const result = computeTrackPoint({ x: 0, y: 0 }, { x: 100, y: 15 });
expect(result).not.toBeNull();
expect(result!.x).toBeCloseTo(100, 6);
expect(result!.y).toBeCloseTo(0, 6);
});
});
// ─── pointsAlongPath (reine Funktion) ─────────────────────────
describe('CAD-4: pointsAlongPath', () => {
it('DIVIDE: 4 Segmente auf 100-Einheiten-Pfad → 5 Punkte (inkl. Enden)', () => {
const pts = [{ x: 0, y: 0 }, { x: 100, y: 0 }];
const result = pointsAlongPath(pts, 4);
expect(result).toHaveLength(5);
expect(result[0]).toEqual({ x: 0, y: 0 });
expect(result[2]).toEqual({ x: 50, y: 0 });
expect(result[4]).toEqual({ x: 100, y: 0 });
});
it('MEASURE: Abstand 25 auf 100-Einheiten-Pfad → 5 Punkte', () => {
const pts = [{ x: 0, y: 0 }, { x: 100, y: 0 }];
const result = pointsAlongPath(pts, 0, 25);
expect(result).toHaveLength(5);
expect(result[3]).toEqual({ x: 75, y: 0 });
});
it('MEASURE mit Rest: 30 auf 100 → 4 Punkte (Rest 10 verworfen)', () => {
const pts = [{ x: 0, y: 0 }, { x: 100, y: 0 }];
const result = pointsAlongPath(pts, 0, 30);
expect(result).toHaveLength(4);
expect(result[3]).toEqual({ x: 90, y: 0 });
});
it('gebogener L-Pfad: Punkte folgen der Geometrie', () => {
const pts = [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 50 }];
const result = pointsAlongPath(pts, 3); // 3 Segmente auf 100 Gesamtlänge → 4 Punkte
expect(result).toHaveLength(4);
expect(result[0]).toEqual({ x: 0, y: 0 });
expect(result[2].x).toBeCloseTo(50, 0);
expect(result[3]).toEqual({ x: 50, y: 50 });
});
});
// ─── divideTool ────────────────────────────────────────────
describe('CAD-4: divideTool', () => {
it('Klick auf Pfad + count=4 platziert 5 Punkte', () => {
const p = path([[0, 0], [100, 0]]);
const { ctx, doc } = mkCtx([p], { count: 4 });
divideTool.handlers.down!(pe(50, 0), ctx);
const points = doc.getAllElements().filter((e) => e.type === 'circle' && e.properties.point);
expect(points).toHaveLength(5);
});
it('Punkte liegen auf defpoints-Layer mit construction:true', () => {
const p = path([[0, 0], [100, 0]]);
const { ctx, doc } = mkCtx([p], { count: 2 });
divideTool.handlers.down!(pe(50, 0), ctx);
const points = doc.getAllElements().filter((e) => e.properties.construction);
expect(points.length).toBeGreaterThanOrEqual(3);
expect(points.every((e) => e.layerId === 'defpoints')).toBe(true);
});
it('Klick ohne Pfad-Treffer: kein Element, Status erklaert', () => {
const { ctx, doc, statuses } = mkCtx([], { count: 2 });
divideTool.handlers.down!(pe(500, 500), ctx);
expect(doc.getAllElements()).toHaveLength(0);
expect(statuses[statuses.length - 1]).toContain('Polyline');
});
it('ALLES in EINER Transaktion (1 Undo entfernt alle Punkte)', () => {
const p = path([[0, 0], [100, 0]]);
const { ctx, doc } = mkCtx([p], { count: 4 });
divideTool.handlers.down!(pe(50, 0), ctx);
expect(doc.getAllElements().length).toBeGreaterThan(1);
doc.undo();
expect(doc.getAllElements()).toHaveLength(1); // Nur der Pfad bleibt
});
});
// ─── measureTool ────────────────────────────────────────────
describe('CAD-4: measureTool', () => {
it('Klick auf Pfad + Abstand 25 platziert 5 Punkte', () => {
const p = path([[0, 0], [100, 0]]);
const { ctx, doc } = mkCtx([p], { spacing: 25 });
measureTool.handlers.down!(pe(50, 0), ctx);
const points = doc.getAllElements().filter((e) => e.type === 'circle' && e.properties.point);
expect(points).toHaveLength(5);
});
it('count=0 (keine Segmente) mit spacing nutzt Abstand', () => {
const p = path([[0, 0], [100, 0]]);
const { ctx, doc } = mkCtx([p], { spacing: 20 });
measureTool.handlers.down!(pe(50, 0), ctx);
const points = doc.getAllElements().filter((e) => e.type === 'circle' && e.properties.point);
expect(points).toHaveLength(6); // 0, 20, 40, 60, 80, 100
});
});
// ─── Manifeste ──────────────────────────────────────────────
describe('CAD-4: Manifeste', () => {
it('ids und Optionen korrekt', () => {
expect(divideTool.manifest.id).toBe('divide');
expect(measureTool.manifest.id).toBe('measure');
const divKeys = (divideTool.optionsSchema ?? []).map((f) => f.key);
expect(divKeys).toContain('count');
const meaKeys = (measureTool.optionsSchema ?? []).map((f) => f.key);
expect(meaKeys).toContain('spacing');
});
});