/** * yAxisComputeJs.test.js * ======================= * Unit-Test für public/yAxisCompute.js (reine JS-Berechnungslogik). * * Dieselben drei Timestamps wie der Python-Test: * test/y-axis-finder-examples/20260612_190019 (Pos A) * test/y-axis-finder-examples/20260612_190104 (Pos B) * test/y-axis-finder-examples/20260612_190241 (Pos C) * * Erwartete Werte (aus verifiziertem Python-Lauf und boardViewer-Logik): * Marker 197, 218, 219 → rotieren (3 Marker genutzt) * Marker 201, 204, 242 → gefiltert (Bewegung < 10 mm) * axisDir ≈ [0.9995, 0.029, -0.015] (fast reine X-Achse des Roboters) * * Hinweis: Im Gegensatz zum Python-Skript verwendet computeYAxis() nur die * Marker-Zentren (position_mm), nicht alle vier Ecken. Deshalb: * numMarkers = 3 (nicht 5 wie Python) * Residuen etwas größer (Python hat 4× mehr Punkte je Marker) */ const fs = require('fs'); const path = require('path'); const { computeYAxis, DEFAULT_MIN_MOVEMENT_MM } = require('../public/yAxisCompute'); // ── Hilfsfunktionen ─────────────────────────────────────────────────────────── const EXAMPLES = path.join(__dirname, 'y-axis-finder-examples'); /** Lädt aruco_marker_poses.json und gibt die fremd-Marker zurück (link !== 'Board'). */ function loadFremdMarkers(timestamp) { const file = path.join(EXAMPLES, timestamp, 'aruco_marker_poses.json'); const data = JSON.parse(fs.readFileSync(file, 'utf8')); return (data.markers ?? []).filter(m => m.link !== 'Board'); } const fremdA = loadFremdMarkers('20260612_190019'); const fremdB = loadFremdMarkers('20260612_190104'); const fremdC = loadFremdMarkers('20260612_190241'); // ── Haupt-Suite ─────────────────────────────────────────────────────────────── describe('computeYAxis – Arm1-Testdaten (190019 / 190104 / 190241)', () => { let r; beforeAll(() => { r = computeYAxis(fremdA, fremdB, fremdC); }); // Grundstatus test('Berechnung erfolgreich (ok: true)', () => { expect(r.ok).toBe(true); }); // Marker-Anzahlen test('numMarkersCommon erfasst alle gemeinsamen fremd-Marker', () => { // Alle drei Timestamps haben dieselben 6 fremd-Marker in common: // 197, 201, 204, 218, 219, 242 expect(r.numMarkersCommon).toBeGreaterThanOrEqual(4); }); test('3 Marker tatsächlich genutzt (197, 218, 219)', () => { expect(r.numMarkers).toBe(3); const usedIds = r.markerData.map(m => m.markerId).sort((a, b) => a - b); expect(usedIds).toEqual([197, 218, 219]); }); // Filterung test('kaum-bewegende Marker werden gefiltert (201, 204 mindestens)', () => { const skippedIds = r.skipped.map(s => s.id).sort((a, b) => a - b); expect(skippedIds).toContain(201); expect(skippedIds).toContain(204); }); test('gefilterte Marker haben Bewegung < DEFAULT_MIN_MOVEMENT_MM', () => { r.skipped.forEach(s => { expect(s.maxMoveMm).toBeLessThan(DEFAULT_MIN_MOVEMENT_MM); expect(s.reason).toMatch(/Bewegung zu gering/); }); }); test('gefilterte Marker enthalten posA (für späteres Base-Zuordnen)', () => { r.skipped.forEach(s => { expect(Array.isArray(s.posA)).toBe(true); expect(s.posA).toHaveLength(3); }); }); // Achsenrichtung test('axisDir ist Einheitsvektor (|axisDir| ≈ 1)', () => { const [ax, ay, az] = r.axisDir; const len = Math.sqrt(ax * ax + ay * ay + az * az); expect(len).toBeCloseTo(1.0, 4); }); test('Achse liegt fast auf X-Achse des Roboters (axisDir[0] > 0.99)', () => { // Arm1 schwingt auf/ab → Drehachse = X-Richtung expect(r.axisDir[0]).toBeGreaterThan(0.99); }); test('axisDir hat 3 Komponenten', () => { expect(r.axisDir).toHaveLength(3); }); test('axisPoint hat 3 Komponenten', () => { expect(r.axisPoint).toHaveLength(3); }); // Kippwinkel test('tiltXY und tiltYZ sind finite Zahlen', () => { expect(Number.isFinite(r.tiltXY)).toBe(true); expect(Number.isFinite(r.tiltYZ)).toBe(true); }); // markerData-Struktur test('jeder markerData-Eintrag hat posA/posB/posC/circumcenter/normal', () => { r.markerData.forEach(md => { expect(md.posA).toHaveLength(3); expect(md.posB).toHaveLength(3); expect(md.posC).toHaveLength(3); expect(md.circumcenter).toHaveLength(3); expect(md.normal).toHaveLength(3); }); }); }); // ── Parametertest: minMovementMm ────────────────────────────────────────────── describe('computeYAxis – minMovementMm-Parameter', () => { test('minMovementMm = 0 → alle gemeinsamen Marker werden genutzt, keine Skips', () => { const r = computeYAxis(fremdA, fremdB, fremdC, { minMovementMm: 0 }); expect(r.ok).toBe(true); // Kein Marker sollte wegen Bewegung gefiltert sein const movementSkips = r.skipped.filter(s => s.reason.includes('Bewegung zu gering')); expect(movementSkips).toHaveLength(0); // Mehr Marker als im Standardfall expect(r.numMarkers).toBeGreaterThanOrEqual(3); }); test('minMovementMm = 9999 → alle Marker gefiltert, ok: false', () => { const r = computeYAxis(fremdA, fremdB, fremdC, { minMovementMm: 9999 }); expect(r.ok).toBe(false); expect(r.reason).toMatch(/gefiltert|gefunden/i); expect(Array.isArray(r.skipped)).toBe(true); }); test('DEFAULT_MIN_MOVEMENT_MM ist 10', () => { expect(DEFAULT_MIN_MOVEMENT_MM).toBe(10.0); }); }); // ── Edge-Cases ──────────────────────────────────────────────────────────────── describe('computeYAxis – Edge Cases', () => { test('leere Eingaben → ok: false', () => { const r = computeYAxis([], [], []); expect(r.ok).toBe(false); }); test('keine gemeinsamen Marker → ok: false', () => { const a = [{ marker_id: 1, position_mm: [0, 0, 0] }]; const b = [{ marker_id: 2, position_mm: [1, 0, 0] }]; const c = [{ marker_id: 3, position_mm: [2, 0, 0] }]; const r = computeYAxis(a, b, c); expect(r.ok).toBe(false); }); test('drei kollineare Punkte → degenerat, ok: false (einzelner Marker)', () => { // Marker bewegt sich auf einer Linie → Kreisebene undefiniert const a = [{ marker_id: 99, position_mm: [0, 0, 100] }]; const b = [{ marker_id: 99, position_mm: [0, 100, 100] }]; // nur Y ändert sich const c = [{ marker_id: 99, position_mm: [0, 200, 100] }]; // weiterhin nur Y // Bewegung ist 200 mm (> 10 mm Threshold), aber Punkte sind kollinear const r = computeYAxis(a, b, c, { minMovementMm: 0 }); expect(r.ok).toBe(false); }); test('ein einzelner gültiger Marker → Achse berechenbar', () => { // Punkt bewegt sich auf einem Kreisbogen (einfache 2D-Rotation um Z-Achse) const R = 100; // Radius 100 mm const angle = (deg) => deg * Math.PI / 180; const mk = (deg) => ({ marker_id: 42, position_mm: [R * Math.cos(angle(deg)), R * Math.sin(angle(deg)), 50], }); const r = computeYAxis([mk(0)], [mk(90)], [mk(180)]); expect(r.ok).toBe(true); expect(r.numMarkers).toBe(1); // Achse sollte in Z-Richtung zeigen [0, 0, ±1] expect(Math.abs(r.axisDir[2])).toBeGreaterThan(0.99); }); });