235 lines
9.0 KiB
JavaScript
235 lines
9.0 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
|
||
// ── Fehlende position_mm (z.B. 1-Kamera-Marker, von 3b nicht trianguliert) ───
|
||
|
||
describe('computeYAxis – Marker ohne position_mm werden ignoriert statt zu crashen', () => {
|
||
|
||
test('position_mm fehlt bei Pos A → Marker landet in skipped, kein Crash', () => {
|
||
const a = [{ marker_id: 5 }]; // keine position_mm
|
||
const b = [{ marker_id: 5, position_mm: [0, 0, 0] }];
|
||
const c = [{ marker_id: 5, position_mm: [0, 1, 0] }];
|
||
expect(() => computeYAxis(a, b, c)).not.toThrow();
|
||
const r = computeYAxis(a, b, c);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.skipped.some(s => s.id === 5)).toBe(true);
|
||
});
|
||
|
||
test('position_mm fehlt bei Pos B/C → ebenfalls kein Crash', () => {
|
||
const a = [{ marker_id: 6, position_mm: [0, 0, 0] }];
|
||
const b = [{ marker_id: 6, position_mm: null }];
|
||
const c = [{ marker_id: 6 }];
|
||
expect(() => computeYAxis(a, b, c)).not.toThrow();
|
||
});
|
||
|
||
test('Mix aus gültigen und ungültigen Markern: gültige werden trotzdem genutzt', () => {
|
||
const R = 100;
|
||
const angle = (deg) => deg * Math.PI / 180;
|
||
const mk = (id, deg) => ({
|
||
marker_id: id,
|
||
position_mm: [R * Math.cos(angle(deg)), R * Math.sin(angle(deg)), 50],
|
||
});
|
||
const a = [mk(1, 0), { marker_id: 2 /* fehlt */ }];
|
||
const b = [mk(1, 90), { marker_id: 2, position_mm: [1, 2, 3] }];
|
||
const c = [mk(1, 180), { marker_id: 2 }]; // bei C wieder fehlend
|
||
const r = computeYAxis(a, b, c);
|
||
expect(r.ok).toBe(true);
|
||
expect(r.numMarkers).toBe(1);
|
||
expect(r.markerData.map(m => m.markerId)).toEqual([1]);
|
||
expect(r.skipped.some(s => s.id === 2)).toBe(true);
|
||
});
|
||
});
|