Marker mit einer Kamera - Gaurds
This commit is contained in:
@@ -216,6 +216,16 @@ function r2v(rx, ry, rz) { return new THREE.Vector3(rx * S, rz * S, -ry * S
|
||||
function r2vArr([rx, ry, rz]) { return r2v(rx, ry, rz); }
|
||||
function r2dir([dx, dy, dz]) { return new THREE.Vector3(dx, dz, -dy).normalize(); }
|
||||
|
||||
/**
|
||||
* True wenn `arr` ein nutzbares [x,y,z] ist (z. B. position_mm). Marker, die
|
||||
* 3b nicht triangulieren konnte (z. B. nur 1 Kamera), haben dieses Feld nicht
|
||||
* — solche Marker werden an allen Stellen, die diese Funktion nutzen, einfach
|
||||
* ignoriert statt einen Crash auszulösen.
|
||||
*/
|
||||
function hasXYZ(arr) {
|
||||
return Array.isArray(arr) && arr.length >= 3 && arr.slice(0, 3).every(Number.isFinite);
|
||||
}
|
||||
|
||||
// ── Renderer ──────────────────────────────────────────────────────────────────
|
||||
const canvas = document.getElementById('cv');
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
@@ -403,6 +413,7 @@ function buildSkeletonFK(robot, angles) {
|
||||
}
|
||||
|
||||
for (const obs of _measuredMarkers.markers) {
|
||||
if (!hasXYZ(obs.position_mm)) continue; // z.B. 1-Kamera-Marker, nicht trianguliert
|
||||
const obsLink = (obs.link && obs.link !== 'Board')
|
||||
? obs.link
|
||||
: markerIdToLink[obs.marker_id];
|
||||
@@ -602,6 +613,7 @@ function buildScene(data) {
|
||||
);
|
||||
|
||||
for (const m of a0markers) {
|
||||
if (!hasXYZ(m.position_mm)) continue; // z.B. 1-Kamera-Marker, nicht trianguliert
|
||||
nTriangulated++;
|
||||
// Kein künstlicher Offset – Kugelmittelpunkt exakt an triangulierter Position
|
||||
const mpos = r2vArr(m.position_mm);
|
||||
@@ -625,6 +637,7 @@ function buildScene(data) {
|
||||
!boardMarkers.some(bm => bm.id === m.marker_id)
|
||||
);
|
||||
for (const m of unknownTriangulated) {
|
||||
if (!hasXYZ(m.position_mm)) continue; // z.B. 1-Kamera-Marker, nicht trianguliert
|
||||
nUnknown++;
|
||||
const mpos = r2vArr(m.position_mm);
|
||||
gMeasured.add(makeSphere(mpos, 0.0055, 0x3b82f6));
|
||||
@@ -733,9 +746,9 @@ function buildTable(data) {
|
||||
let dist = null, dz = null, edge = null;
|
||||
let state = 'none'; // 'tri', '1cam', 'unk'
|
||||
|
||||
if (meas) {
|
||||
if (meas && hasXYZ(meas.position_mm)) {
|
||||
[x, y, z] = meas.position_mm;
|
||||
[nx, ny, nz] = meas.normal;
|
||||
if (Array.isArray(meas.normal)) [nx, ny, nz] = meas.normal;
|
||||
edge = meas.edge_length_mm;
|
||||
state = model ? 'tri' : 'unk';
|
||||
|
||||
@@ -745,7 +758,10 @@ function buildTable(data) {
|
||||
dist = Math.sqrt(dx*dx + dy*dy + ddz*ddz);
|
||||
dz = ddz;
|
||||
}
|
||||
} else if (cameras.length > 0) {
|
||||
} else if (cameras.length > 0 || meas) {
|
||||
// meas ohne position_mm (z.B. 1-Kamera-Marker, noch nicht trianguliert)
|
||||
// zählt wie "gesehen, aber nicht trianguliert" — gleicher Stand wie
|
||||
// cameras.length>0, nur eben aus 3b statt aus den rohen Detektionen.
|
||||
state = '1cam';
|
||||
}
|
||||
|
||||
@@ -1049,7 +1065,8 @@ async function loadData(specificRunDir = null) {
|
||||
buildTable(data);
|
||||
// Fremd-Marker für Verbindungslinien merken (Marker, die nicht in Board-Link stehen)
|
||||
const bIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
|
||||
_primaryFremdMarkers = (data.measuredMarkers?.markers ?? []).filter(m => !bIds.has(m.marker_id));
|
||||
_primaryFremdMarkers = (data.measuredMarkers?.markers ?? [])
|
||||
.filter(m => !bIds.has(m.marker_id) && hasXYZ(m.position_mm));
|
||||
const measTotal = data.measuredMarkers?.markers?.length ?? 0;
|
||||
vlog(`Basis: run=${data.runDir} gesamt=${measTotal} fremd=${_primaryFremdMarkers.length} boardIDs=${bIds.size}` +
|
||||
(_primaryFremdMarkers.length ? ` (${_primaryFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
|
||||
@@ -1087,7 +1104,7 @@ async function loadCompareData() {
|
||||
// Board-Marker-IDs aus Robot.json dieses Runs
|
||||
const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
|
||||
for (const m of markers) {
|
||||
if (!boardIds.has(m.marker_id)) {
|
||||
if (!boardIds.has(m.marker_id) && hasXYZ(m.position_mm)) {
|
||||
_compareFremdMarkers.push(m); // für Linien
|
||||
gCompare.add(makeSphere(r2vArr(m.position_mm), 0.006, 0xf97316)); // orange Kugel
|
||||
}
|
||||
@@ -1120,7 +1137,7 @@ async function loadPositionC() {
|
||||
const markers = data.measuredMarkers?.markers ?? [];
|
||||
const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
|
||||
for (const m of markers) {
|
||||
if (!boardIds.has(m.marker_id)) {
|
||||
if (!boardIds.has(m.marker_id) && hasXYZ(m.position_mm)) {
|
||||
_positionCFremdMarkers.push(m);
|
||||
gPositionC.add(makeSphere(r2vArr(m.position_mm), 0.006, 0x22d3ee)); // cyan
|
||||
}
|
||||
|
||||
@@ -106,6 +106,13 @@
|
||||
const mc = mapC.get(id);
|
||||
if (!mb || !mc) continue;
|
||||
|
||||
// Fehlende position_mm (z.B. Einzelkamera-Marker, von 3b nicht
|
||||
// trianguliert) ignorieren statt crashen — Marker bleibt im skipped-Log.
|
||||
if (!Array.isArray(ma.position_mm) || !Array.isArray(mb.position_mm) || !Array.isArray(mc.position_mm)) {
|
||||
skipped.push({ id, reason: 'fehlende position_mm (z.B. Einzelkamera-Marker, nicht trianguliert)' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const P1 = ma.position_mm.map(Number);
|
||||
const P2 = mb.position_mm.map(Number);
|
||||
const P3 = mc.position_mm.map(Number);
|
||||
|
||||
59
test/boardViewerHasXYZ.test.js
Normal file
59
test/boardViewerHasXYZ.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* boardViewerHasXYZ.test.js
|
||||
* =========================
|
||||
* Unit-Test für die reine Hilfsfunktion `hasXYZ()` aus public/boardViewer.html.
|
||||
*
|
||||
* boardViewer.html ist kein ladbares JS-Modul (Inline-<script type="module">
|
||||
* mit THREE.js/DOM/fetch-Abhängigkeiten) — ein normales require() ist daher
|
||||
* nicht möglich, ohne die Datei in Module aufzuteilen (nicht Teil dieser
|
||||
* Änderung). Stattdessen wird die `hasXYZ`-Funktionsdefinition per Regex aus
|
||||
* der Datei extrahiert und isoliert ausgewertet — testet exakt die Guard-Logik,
|
||||
* die an allen position_mm-Zugriffsstellen in boardViewer.html verwendet wird,
|
||||
* ohne Three.js/DOM laden zu müssen.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SRC = fs.readFileSync(path.join(__dirname, '../public/boardViewer.html'), 'utf8');
|
||||
|
||||
const match = SRC.match(/function hasXYZ\(arr\)\s*\{[^}]*\}/);
|
||||
if (!match) {
|
||||
throw new Error('hasXYZ() nicht in boardViewer.html gefunden — Guard wurde entfernt/umbenannt?');
|
||||
}
|
||||
// eslint-disable-next-line no-eval
|
||||
const hasXYZ = eval(`(${match[0]})`);
|
||||
|
||||
describe('boardViewer.html: hasXYZ() (Guard für fehlende position_mm)', () => {
|
||||
test('gültiges [x,y,z] → true', () => {
|
||||
expect(hasXYZ([1, 2, 3])).toBe(true);
|
||||
expect(hasXYZ([0, 0, 0])).toBe(true);
|
||||
expect(hasXYZ([-1.5, 200.25, 0])).toBe(true);
|
||||
});
|
||||
|
||||
test('undefined/null (fehlendes position_mm) → false, kein Crash', () => {
|
||||
expect(() => hasXYZ(undefined)).not.toThrow();
|
||||
expect(hasXYZ(undefined)).toBe(false);
|
||||
expect(hasXYZ(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('zu kurzes Array → false', () => {
|
||||
expect(hasXYZ([1, 2])).toBe(false);
|
||||
expect(hasXYZ([])).toBe(false);
|
||||
});
|
||||
|
||||
test('nicht-numerische/NaN-Werte → false', () => {
|
||||
expect(hasXYZ([1, NaN, 3])).toBe(false);
|
||||
expect(hasXYZ(['a', 'b', 'c'])).toBe(false);
|
||||
expect(hasXYZ([1, Infinity, 3])).toBe(false);
|
||||
});
|
||||
|
||||
test('kein Array (z.B. Objekt oder String) → false', () => {
|
||||
expect(hasXYZ({ x: 1, y: 2, z: 3 })).toBe(false);
|
||||
expect(hasXYZ('1,2,3')).toBe(false);
|
||||
});
|
||||
|
||||
test('Array mit mehr als 3 Werten (z.B. mit Zusatzfeld) → true (erste 3 zählen)', () => {
|
||||
expect(hasXYZ([1, 2, 3, 4])).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -193,3 +193,42 @@ describe('computeYAxis – Edge Cases', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user