diff --git a/public/boardViewer.html b/public/boardViewer.html
index 8c0fa52..a653815 100644
--- a/public/boardViewer.html
+++ b/public/boardViewer.html
@@ -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
}
diff --git a/public/yAxisCompute.js b/public/yAxisCompute.js
index cc1244a..08e5d4e 100644
--- a/public/yAxisCompute.js
+++ b/public/yAxisCompute.js
@@ -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);
diff --git a/test/boardViewerHasXYZ.test.js b/test/boardViewerHasXYZ.test.js
new file mode 100644
index 0000000..7f8bdd5
--- /dev/null
+++ b/test/boardViewerHasXYZ.test.js
@@ -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-