Y-Axis checks
This commit is contained in:
@@ -149,6 +149,8 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- reine Berechnungslogik (kein DOM/Three.js) – auch von Jest-Tests genutzt -->
|
||||
<script src="/yAxisCompute.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -736,10 +738,6 @@ function buildCompareLines() {
|
||||
|
||||
// ── Y-Achsen-Berechnung aus drei Positionen ───────────────────────────────────
|
||||
|
||||
/** Marker, die sich weniger als diesen Wert bewegen, werden ignoriert.
|
||||
* Entspricht dem min_movement_mm-Parameter im Python-Skript. */
|
||||
const Y_AXIS_MIN_MOVEMENT_MM = 10.0;
|
||||
|
||||
function computeAndShowYAxis() {
|
||||
clearGroup(gYAxis);
|
||||
|
||||
@@ -751,134 +749,56 @@ function computeAndShowYAxis() {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapA = new Map(_primaryFremdMarkers .map(m => [m.marker_id, m]));
|
||||
const mapB = new Map(_compareFremdMarkers .map(m => [m.marker_id, m]));
|
||||
const mapC = new Map(_positionCFremdMarkers.map(m => [m.marker_id, m]));
|
||||
// ── Berechnung via yAxisCompute.js (kein DOM/Three.js) ───────────────────
|
||||
const result = YAxisCompute.computeYAxis(
|
||||
_primaryFremdMarkers,
|
||||
_compareFremdMarkers,
|
||||
_positionCFremdMarkers,
|
||||
);
|
||||
|
||||
function dist2(P, Q) { return (P[0]-Q[0])**2 + (P[1]-Q[1])**2 + (P[2]-Q[2])**2; }
|
||||
|
||||
const circumcenters = []; // [{id, C:[x,y,z]}] in mm
|
||||
const normals = []; // [[nx,ny,nz]] – Achsenrichtung je Marker
|
||||
const markerData = []; // [{markerId, posA, posB, posC, circumcenter, normal}] für Speicherung
|
||||
const skipped = []; // [{id, reason, maxMoveMm}]
|
||||
|
||||
for (const [id, ma] of mapA) {
|
||||
const mb = mapB.get(id);
|
||||
const mc = mapC.get(id);
|
||||
if (!mb || !mc) continue;
|
||||
|
||||
const P1 = ma.position_mm.map(Number);
|
||||
const P2 = mb.position_mm.map(Number);
|
||||
const P3 = mc.position_mm.map(Number);
|
||||
|
||||
// ── Mindest-Bewegungs-Filter ─────────────────────────────────────────────
|
||||
// Marker, die sich zwischen den drei Positionen kaum bewegen, liefern
|
||||
// degenerate Umkreismittelpunkte und korrumpieren die Achsenschätzung.
|
||||
// Dieselbe Logik wie im Python-Skript (min_movement_mm).
|
||||
const maxMoveMm = Math.max(
|
||||
Math.sqrt(dist2(P1, P2)),
|
||||
Math.sqrt(dist2(P2, P3)),
|
||||
Math.sqrt(dist2(P1, P3)),
|
||||
);
|
||||
if (maxMoveMm < Y_AXIS_MIN_MOVEMENT_MM) {
|
||||
vlog(`Y-Achse: Marker ${id} übersprungen – Bewegung zu gering` +
|
||||
` (${maxMoveMm.toFixed(1)} mm < ${Y_AXIS_MIN_MOVEMENT_MM} mm, kein rotierender Marker)`, 'warn');
|
||||
skipped.push({ id, reason: 'Bewegung zu gering', maxMoveMm: +maxMoveMm.toFixed(2) });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalenvektor der Kreisebene = Achsenrichtung
|
||||
const v1 = [P2[0]-P1[0], P2[1]-P1[1], P2[2]-P1[2]];
|
||||
const v2 = [P3[0]-P1[0], P3[1]-P1[1], P3[2]-P1[2]];
|
||||
const cross = [
|
||||
v1[1]*v2[2] - v1[2]*v2[1],
|
||||
v1[2]*v2[0] - v1[0]*v2[2],
|
||||
v1[0]*v2[1] - v1[1]*v2[0],
|
||||
];
|
||||
const crossLen = Math.sqrt(cross[0]**2 + cross[1]**2 + cross[2]**2);
|
||||
if (crossLen < 1e-3) {
|
||||
vlog(`Y-Achse: Marker ${id} degenerat (Punkte zu nahe / kollinear)`, 'warn');
|
||||
continue;
|
||||
}
|
||||
const n = cross.map(c => c / crossLen);
|
||||
normals.push(n);
|
||||
|
||||
// Umkreismittelpunkt (baryzentrischer Ansatz)
|
||||
const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
|
||||
const w1 = a2*(b2+c2-a2), w2 = b2*(a2+c2-b2), w3 = c2*(a2+b2-c2);
|
||||
const wSum = w1 + w2 + w3;
|
||||
if (Math.abs(wSum) < 1e-6) {
|
||||
vlog(`Y-Achse: Marker ${id} – Umkreis undefiniert`, 'warn');
|
||||
continue;
|
||||
}
|
||||
const C = [
|
||||
(w1*P1[0] + w2*P2[0] + w3*P3[0]) / wSum,
|
||||
(w1*P1[1] + w2*P2[1] + w3*P3[1]) / wSum,
|
||||
(w1*P1[2] + w2*P2[2] + w3*P3[2]) / wSum,
|
||||
];
|
||||
circumcenters.push({ id, C });
|
||||
markerData.push({ markerId: id, posA: P1, posB: P2, posC: P3, circumcenter: C, normal: n });
|
||||
|
||||
// Kreismittelpunkt (rose)
|
||||
gYAxis.add(makeSphere(r2vArr(C), 0.007, 0xfb7185));
|
||||
// Bogen-Linie B→C (cyan)
|
||||
gYAxis.add(makeLine(r2vArr(P2), r2vArr(P3), 0x22d3ee, 0.6));
|
||||
}
|
||||
|
||||
if (circumcenters.length === 0) {
|
||||
const why = skipped.length
|
||||
? `Alle ${skipped.length} Marker gefiltert (Bewegung < ${Y_AXIS_MIN_MOVEMENT_MM} mm)`
|
||||
: 'Keine gemeinsamen fremd-Marker in Pos A+B+C gefunden';
|
||||
vlog(`Y-Achse: ${why}`, 'warn');
|
||||
window.parent.postMessage({ type: 'yaxis-measurement', axisDir: null, skipped }, '*');
|
||||
if (!result.ok) {
|
||||
vlog(`Y-Achse: ${result.reason}`, 'warn');
|
||||
window.parent.postMessage({ type: 'yaxis-measurement', axisDir: null, skipped: result.skipped }, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
// Achsenrichtung: Mittlere Normale (Vorzeichen angleichen)
|
||||
const ref = normals[0];
|
||||
const aligned = normals.map(n => {
|
||||
const dot = n[0]*ref[0] + n[1]*ref[1] + n[2]*ref[2];
|
||||
return dot >= 0 ? n : n.map(c => -c);
|
||||
});
|
||||
const meanN = [0, 1, 2].map(i => aligned.reduce((s, n) => s + n[i], 0) / aligned.length);
|
||||
const meanNLen = Math.sqrt(meanN[0]**2 + meanN[1]**2 + meanN[2]**2);
|
||||
const axisDir = meanN.map(c => c / meanNLen); // Roboter-Koordinaten
|
||||
const { axisDir, axisPoint, tiltXY, tiltYZ, skipped, markerData } = result;
|
||||
|
||||
// Referenzpunkt: Schwerpunkt der Umkreismittelpunkte
|
||||
const axisPoint = [0, 1, 2].map(i =>
|
||||
circumcenters.reduce((s, c) => s + c.C[i], 0) / circumcenters.length
|
||||
);
|
||||
// ── Visualisierung (Three.js) ─────────────────────────────────────────────
|
||||
for (const { posB: P2, posC: P3, circumcenter: C } of markerData) {
|
||||
gYAxis.add(makeSphere(r2vArr(C), 0.007, 0xfb7185)); // Umkreismittelpunkt (rose)
|
||||
gYAxis.add(makeLine(r2vArr(P2), r2vArr(P3), 0x22d3ee, 0.6)); // Bogen B→C (cyan)
|
||||
}
|
||||
|
||||
// Achse als Linie ±500 mm visualisieren (magenta)
|
||||
const L = 500;
|
||||
const p1mm = axisPoint.map((v, i) => v - L * axisDir[i]);
|
||||
const p2mm = axisPoint.map((v, i) => v + L * axisDir[i]);
|
||||
gYAxis.add(makeLine(r2vArr(p1mm), r2vArr(p2mm), 0xe879f9, 0.9));
|
||||
gYAxis.add(makeSphere(r2vArr(axisPoint), 0.011, 0xe879f9));
|
||||
|
||||
// Abweichung von der idealen Y-Achse [0,1,0] in Roboter-Koordinaten
|
||||
const [ax, ay, az] = axisDir;
|
||||
const tiltXY = Math.atan2(ax, ay) * 180 / Math.PI; // Kippung in XY-Ebene
|
||||
const tiltYZ = Math.atan2(az, ay) * 180 / Math.PI; // Kippung in YZ-Ebene
|
||||
// ── Logging ───────────────────────────────────────────────────────────────
|
||||
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
|
||||
const good = Math.abs(tiltXY) < 0.5 && Math.abs(tiltYZ) < 0.5;
|
||||
|
||||
const usedIds = circumcenters.map(c => c.id);
|
||||
const usedIds = markerData.map(m => m.markerId);
|
||||
const skippedIds = skipped.map(s => s.id);
|
||||
|
||||
vlog(`Y-Achse: ${usedIds.length} Marker genutzt (${usedIds.join(', ')})` +
|
||||
(skippedIds.length ? ` · ${skippedIds.length} gefiltert (${skippedIds.join(', ')})` : ''));
|
||||
vlog(` dir=[${axisDir.map(v => v.toFixed(4)).join(', ')}]`);
|
||||
vlog(` Referenzpunkt: [${axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
|
||||
vlog(` Abw. von Y-Achse: XY ${fmt(tiltXY)} YZ ${fmt(tiltYZ)}`, good ? 'ok' : 'warn');
|
||||
if (skippedIds.length) {
|
||||
skipped.forEach(s => vlog(` ↳ Marker ${s.id} übersprungen: ${s.reason} (${s.maxMoveMm} mm)`, 'warn'));
|
||||
}
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'yaxis-measurement',
|
||||
type: 'yaxis-measurement',
|
||||
axisDir,
|
||||
axisPoint,
|
||||
tiltXY,
|
||||
tiltYZ,
|
||||
numMarkers: circumcenters.length,
|
||||
numMarkersCommon: circumcenters.length + skipped.length,
|
||||
numMarkers: result.numMarkers,
|
||||
numMarkersCommon: result.numMarkersCommon,
|
||||
skipped,
|
||||
// Für rotation_detection.json: Run-Referenzen und Marker-Rohdaten
|
||||
runA: document.getElementById('sel-run-primary')?.value ?? null,
|
||||
|
||||
@@ -556,15 +556,145 @@ function initArm(tab) {
|
||||
});
|
||||
}
|
||||
|
||||
// Achsen-Messung vom Viewer empfangen
|
||||
// ── Kalibrierungs-Aktionen (werden nach Rotation-Messung aktiv) ──────────
|
||||
// Tab-Name → Link-Name in robot.json
|
||||
const TAB_TO_LINK = { arm1: 'Arm1', arm2: 'Arm2', elbow: 'Ellbow', hand: 'Hand' };
|
||||
const robotLink = TAB_TO_LINK[tab] ?? tab;
|
||||
|
||||
const calibActionsEl = document.getElementById(`${tab}-calib-actions`);
|
||||
const assignFixedBtn = document.getElementById(`btn-${tab}-assign-fixed`);
|
||||
const assignFixedInfo = document.getElementById(`${tab}-assign-fixed-info`);
|
||||
const setOriginBtn = document.getElementById(`btn-${tab}-set-origin`);
|
||||
const setOriginInfo = document.getElementById(`${tab}-set-origin-info`);
|
||||
|
||||
let _lastYAxisMsg = null; // letztes gültiges yaxis-measurement
|
||||
|
||||
function enableCalibActions(msg) {
|
||||
if (!calibActionsEl) return;
|
||||
calibActionsEl.style.display = 'block';
|
||||
|
||||
// ── Button 1: Fixe Marker → Base ────────────────────────────────────────
|
||||
if (assignFixedBtn) {
|
||||
const skipped = msg.skipped ?? [];
|
||||
if (skipped.length > 0) {
|
||||
const ids = skipped.map(s => s.id).join(', ');
|
||||
assignFixedBtn.disabled = false;
|
||||
assignFixedBtn.style.opacity = '1';
|
||||
assignFixedBtn.style.cursor = 'pointer';
|
||||
assignFixedBtn.title =
|
||||
`Marker ${ids} in robot.json dem Link 'Base' zuordnen`;
|
||||
if (assignFixedInfo) {
|
||||
assignFixedInfo.textContent =
|
||||
`Kaum-bewegende Marker: ${ids} ` +
|
||||
`(Bewegung < ${msg.skipped.map(s => s.maxMoveMm + ' mm').join(', ')}) ` +
|
||||
`→ Link 'Base' in robot.json eintragen.`;
|
||||
}
|
||||
} else {
|
||||
assignFixedBtn.disabled = true;
|
||||
if (assignFixedInfo) assignFixedInfo.textContent = 'Alle erkannten Marker rotieren – kein fixer Marker gefunden.';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Button 2: Joint-Origin Y/Z ───────────────────────────────────────────
|
||||
if (setOriginBtn) {
|
||||
const [, ay, az] = msg.axisPoint;
|
||||
setOriginBtn.disabled = false;
|
||||
setOriginBtn.style.opacity = '1';
|
||||
setOriginBtn.style.cursor = 'pointer';
|
||||
setOriginBtn.title = `Joint '${robotLink}': origin[Y]=${ay.toFixed(1)} mm, origin[Z]=${az.toFixed(1)} mm`;
|
||||
if (setOriginInfo) {
|
||||
setOriginInfo.textContent =
|
||||
`Berechnete Achse: Y = ${ay.toFixed(1)} mm · Z = ${az.toFixed(1)} mm ` +
|
||||
`→ in robot.json links.${robotLink}.jointToParent.origin setzen.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function disableCalibActions() {
|
||||
if (assignFixedBtn) {
|
||||
assignFixedBtn.disabled = true;
|
||||
assignFixedBtn.style.opacity = '.45';
|
||||
assignFixedBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
if (setOriginBtn) {
|
||||
setOriginBtn.disabled = true;
|
||||
setOriginBtn.style.opacity = '.45';
|
||||
setOriginBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
if (assignFixedBtn) {
|
||||
assignFixedBtn.addEventListener('click', async () => {
|
||||
if (!_lastYAxisMsg) return;
|
||||
const skipped = _lastYAxisMsg.skipped ?? [];
|
||||
const markerIds = skipped.map(s => s.id);
|
||||
const measuredPositions = skipped
|
||||
.filter(s => Array.isArray(s.posA))
|
||||
.map(s => ({ id: s.id, position_mm: s.posA }));
|
||||
|
||||
log(`🔄 Ordne Marker [${markerIds.join(', ')}] dem Link 'Base' zu …`);
|
||||
assignFixedBtn.disabled = true;
|
||||
try {
|
||||
const r = await fetch('/api/robot/assign-fixed-markers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ markerIds, targetLink: 'Base', measuredPositions }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) { log(`❌ Fehler: ${data.error ?? r.status}`); return; }
|
||||
log(`✅ Zugeordnet: ${data.numAdded} neu, ${data.numAlreadyPresent} bereits vorhanden`);
|
||||
data.changes.forEach(c => {
|
||||
if (c.action === 'added') log(` + Marker ${c.markerId} → ${c.targetLink}`);
|
||||
else if (c.action === 'already-present') log(` ○ Marker ${c.markerId} bereits in '${c.existingLink}'`);
|
||||
else if (c.action === 'skipped-no-position') log(` ⚠ Marker ${c.markerId}: keine Positions-Daten`);
|
||||
});
|
||||
} catch (err) {
|
||||
log(`❌ Netzwerkfehler: ${err}`);
|
||||
} finally {
|
||||
assignFixedBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (setOriginBtn) {
|
||||
setOriginBtn.addEventListener('click', async () => {
|
||||
if (!_lastYAxisMsg) return;
|
||||
const [, ay, az] = _lastYAxisMsg.axisPoint;
|
||||
log(`🔄 Setze ${robotLink}.jointToParent.origin: Y=${ay.toFixed(1)} Z=${az.toFixed(1)} …`);
|
||||
setOriginBtn.disabled = true;
|
||||
try {
|
||||
const r = await fetch('/api/robot/set-joint-origin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ linkName: robotLink, y: ay, z: az }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) { log(`❌ Fehler: ${data.error ?? r.status}`); return; }
|
||||
log(`✅ Joint-Origin gesetzt: [${data.oldOrigin.join(', ')}] → [${data.newOrigin.join(', ')}]`);
|
||||
} catch (err) {
|
||||
log(`❌ Netzwerkfehler: ${err}`);
|
||||
} finally {
|
||||
setOriginBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Achsen-Messung vom Viewer empfangen ───────────────────────────────────
|
||||
window.addEventListener('message', (e) => {
|
||||
if (!frameEl || e.source !== frameEl.contentWindow) return;
|
||||
const msg = e.data;
|
||||
if (msg?.type === 'yaxis-measurement' && Array.isArray(msg.axisDir)) {
|
||||
if (msg?.type !== 'yaxis-measurement') return;
|
||||
|
||||
if (Array.isArray(msg.axisDir)) {
|
||||
_lastYAxisMsg = msg;
|
||||
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
|
||||
log(`📐 Achse (${msg.numMarkers} Marker): dir=[${msg.axisDir.map(v => v.toFixed(4)).join(', ')}]` +
|
||||
log(`📐 Achse (${msg.numMarkers}/${msg.numMarkersCommon} Marker): dir=[${msg.axisDir.map(v => v.toFixed(4)).join(', ')}]` +
|
||||
` XY=${fmt(msg.tiltXY)} YZ=${fmt(msg.tiltYZ)}`);
|
||||
log(` Referenzpunkt: [${msg.axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
|
||||
if ((msg.skipped ?? []).length) {
|
||||
log(` Gefiltert (zu geringe Bewegung): ${msg.skipped.map(s => `${s.id} (${s.maxMoveMm} mm)`).join(', ')}`);
|
||||
}
|
||||
enableCalibActions(msg);
|
||||
|
||||
// In rotation_detection.json speichern (anhängen)
|
||||
fetch('/api/xaxis/save-rotation-detection', {
|
||||
@@ -579,6 +709,11 @@ function initArm(tab) {
|
||||
}).then(r => r.json())
|
||||
.then(d => log(`💾 Gespeichert: ${d.file} (${d.total} Messungen)`))
|
||||
.catch(e => log(`⚠ Speichern fehlgeschlagen: ${e.message}`));
|
||||
|
||||
} else {
|
||||
// Kein gültiges Ergebnis → Buttons deaktivieren
|
||||
_lastYAxisMsg = null;
|
||||
disableCalibActions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<!-- ── Aktionen ───────────────────────────────────────────────────────────── -->
|
||||
<div class="section full">
|
||||
<h2>Aktionen</h2>
|
||||
|
||||
<!-- Roboter-Bewegung -->
|
||||
<div style="margin-top:14px;display:flex;align-items:center;gap:20px;flex-wrap:wrap">
|
||||
<button id="btn-arm1-ccw" style="font-size:18px;padding:6px 22px" title="Bieps rauf">
|
||||
⤴ Rauf
|
||||
@@ -32,6 +34,41 @@
|
||||
Runter ⤵
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Kalibrierungs-Aktionen (erscheinen wenn Rotation erkannt wurde) -->
|
||||
<div id="arm1-calib-actions" style="display:none;margin-top:20px;border-top:1px solid var(--border);padding-top:16px">
|
||||
<p style="font-size:11px;color:var(--muted);margin-bottom:12px">
|
||||
Aktionen aus erkannter Rotation (3 Positionen):
|
||||
</p>
|
||||
|
||||
<!-- Aktion 1: Fixe Marker dem Base-Link zuordnen -->
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:14px;flex-wrap:wrap">
|
||||
<button id="btn-arm1-assign-fixed" disabled
|
||||
style="min-width:220px;padding:6px 14px;opacity:.45;cursor:not-allowed"
|
||||
title="Marker, die sich kaum bewegen, dem Link 'Base' zuordnen">
|
||||
Fixe Marker → Link «Base»
|
||||
</button>
|
||||
<span id="arm1-assign-fixed-info"
|
||||
style="font-size:11px;color:var(--muted);max-width:360px;line-height:1.5">
|
||||
Marker, die sich kaum bewegen, sind physisch am Basis-Körper befestigt.
|
||||
Diese Aktion trägt sie in robot.json unter «Base» ein.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Aktion 2: Joint-Origin Y/Z aus Drehachse -->
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap">
|
||||
<button id="btn-arm1-set-origin" disabled
|
||||
style="min-width:220px;padding:6px 14px;opacity:.45;cursor:not-allowed"
|
||||
title="Y und Z des Schulter-Joints aus der berechneten Drehachse setzen">
|
||||
Joint-Origin Y/Z übernehmen
|
||||
</button>
|
||||
<span id="arm1-set-origin-info"
|
||||
style="font-size:11px;color:var(--muted);max-width:360px;line-height:1.5">
|
||||
Setzt Y und Z des Arm1-Joints (Schulter) in robot.json auf die
|
||||
berechnete Drehachsen-Position.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section full">
|
||||
|
||||
192
public/yAxisCompute.js
Normal file
192
public/yAxisCompute.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* yAxisCompute.js
|
||||
* ================
|
||||
* Reine Mathematik zur Rotationsachsen-Berechnung aus drei Marker-Positionen.
|
||||
* Keine DOM- oder Three.js-Abhängigkeit – läuft in Browser und Node.js.
|
||||
*
|
||||
* UMD-Pattern:
|
||||
* Browser: window.YAxisCompute.computeYAxis(...)
|
||||
* Node.js: const { computeYAxis } = require('./public/yAxisCompute')
|
||||
*
|
||||
* Eingabe (markersA/B/C):
|
||||
* Array von { marker_id: number, position_mm: [x, y, z] }
|
||||
* – entspricht den fremd-Markern (link !== 'Board') aus aruco_marker_poses.json
|
||||
*
|
||||
* Ausgabe bei Erfolg:
|
||||
* {
|
||||
* ok: true,
|
||||
* axisDir: [x, y, z], // Einheitsvektor der Rotationsachse
|
||||
* axisPoint: [x, y, z], // Referenzpunkt auf der Achse (mm)
|
||||
* tiltXY: degrees, // Abweichung von Y in XY-Ebene
|
||||
* tiltYZ: degrees, // Abweichung von Y in YZ-Ebene
|
||||
* numMarkers: number, // Anzahl genutzte Marker
|
||||
* numMarkersCommon: number, // Alle gemeinsamen Marker (inkl. gefiltert)
|
||||
* skipped: [{id, reason, maxMoveMm, posA?}],
|
||||
* markerData: [{markerId, posA, posB, posC, circumcenter, normal}],
|
||||
* }
|
||||
*
|
||||
* Ausgabe bei Fehler:
|
||||
* { ok: false, reason: string, skipped: [] }
|
||||
*/
|
||||
(function (exports) {
|
||||
'use strict';
|
||||
|
||||
/** Marker mit max. Zentren-Bewegung unter diesem Wert werden ignoriert. */
|
||||
const DEFAULT_MIN_MOVEMENT_MM = 10.0;
|
||||
|
||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
|
||||
|
||||
function dist2(P, Q) {
|
||||
return (P[0] - Q[0]) ** 2 + (P[1] - Q[1]) ** 2 + (P[2] - Q[2]) ** 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet:
|
||||
* - Umkreismittelpunkt C des Dreiecks P1-P2-P3 (Punkt auf der Rotationsachse)
|
||||
* - Normalenvektor n der Dreiecks-Ebene (= Richtung der Rotationsachse)
|
||||
*
|
||||
* Gibt null zurück wenn das Triplet degenerat ist (kollinear / zu nahe beieinander).
|
||||
*/
|
||||
function circumcenterAndNormal(P1, P2, P3) {
|
||||
const v1 = [P2[0] - P1[0], P2[1] - P1[1], P2[2] - P1[2]];
|
||||
const v2 = [P3[0] - P1[0], P3[1] - P1[1], P3[2] - P1[2]];
|
||||
const cross = [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
];
|
||||
const crossLen = Math.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2);
|
||||
if (crossLen < 1e-3) return null;
|
||||
|
||||
const n = cross.map(c => c / crossLen);
|
||||
|
||||
// Baryzentrische Gewichte → Umkreismittelpunkt (doc/04_y_achse.md)
|
||||
const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
|
||||
const w1 = a2 * (b2 + c2 - a2);
|
||||
const w2 = b2 * (a2 + c2 - b2);
|
||||
const w3 = c2 * (a2 + b2 - c2);
|
||||
const wSum = w1 + w2 + w3;
|
||||
if (Math.abs(wSum) < 1e-6) return null;
|
||||
|
||||
const C = [
|
||||
(w1 * P1[0] + w2 * P2[0] + w3 * P3[0]) / wSum,
|
||||
(w1 * P1[1] + w2 * P2[1] + w3 * P3[1]) / wSum,
|
||||
(w1 * P1[2] + w2 * P2[2] + w3 * P3[2]) / wSum,
|
||||
];
|
||||
return { C, n };
|
||||
}
|
||||
|
||||
// ── Kern-API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Berechnet die Rotationsachse aus drei Sätzen fremd-Marker.
|
||||
*
|
||||
* @param {Array} markersA Fremd-Marker aus Pos A (link !== 'Board')
|
||||
* @param {Array} markersB Fremd-Marker aus Pos B
|
||||
* @param {Array} markersC Fremd-Marker aus Pos C
|
||||
* @param {Object} options
|
||||
* @param {number} [options.minMovementMm=10] Mindest-Bewegung in mm
|
||||
*/
|
||||
function computeYAxis(markersA, markersB, markersC, options) {
|
||||
const minMovementMm = (options && options.minMovementMm != null)
|
||||
? options.minMovementMm
|
||||
: DEFAULT_MIN_MOVEMENT_MM;
|
||||
|
||||
const mapA = new Map(markersA.map(m => [m.marker_id, m]));
|
||||
const mapB = new Map(markersB.map(m => [m.marker_id, m]));
|
||||
const mapC = new Map(markersC.map(m => [m.marker_id, m]));
|
||||
|
||||
const circumcenters = []; // [{id, C:[x,y,z]}]
|
||||
const normals = []; // [[nx, ny, nz]]
|
||||
const markerData = []; // [{markerId, posA, posB, posC, circumcenter, normal}]
|
||||
const skipped = []; // [{id, reason, maxMoveMm, posA?}]
|
||||
|
||||
for (const [id, ma] of mapA) {
|
||||
const mb = mapB.get(id);
|
||||
const mc = mapC.get(id);
|
||||
if (!mb || !mc) continue;
|
||||
|
||||
const P1 = ma.position_mm.map(Number);
|
||||
const P2 = mb.position_mm.map(Number);
|
||||
const P3 = mc.position_mm.map(Number);
|
||||
|
||||
// ── Mindest-Bewegungs-Filter ──────────────────────────────────────────
|
||||
const maxMoveMm = Math.max(
|
||||
Math.sqrt(dist2(P1, P2)),
|
||||
Math.sqrt(dist2(P2, P3)),
|
||||
Math.sqrt(dist2(P1, P3)),
|
||||
);
|
||||
if (maxMoveMm < minMovementMm) {
|
||||
skipped.push({
|
||||
id,
|
||||
reason: 'Bewegung zu gering (kein rotierender Marker)',
|
||||
maxMoveMm: +maxMoveMm.toFixed(2),
|
||||
posA: P1, // Positions-Info für späteres Zuordnen zum Base-Link
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Umkreismittelpunkt + Normalenvektor ───────────────────────────────
|
||||
const result = circumcenterAndNormal(P1, P2, P3);
|
||||
if (!result) {
|
||||
skipped.push({ id, reason: 'degenerat (kollinear / identisch)', maxMoveMm: +maxMoveMm.toFixed(2) });
|
||||
continue;
|
||||
}
|
||||
|
||||
const { C, n } = result;
|
||||
circumcenters.push({ id, C });
|
||||
normals.push(n);
|
||||
markerData.push({ markerId: id, posA: P1, posB: P2, posC: P3, circumcenter: C, normal: n });
|
||||
}
|
||||
|
||||
if (circumcenters.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: skipped.length
|
||||
? `Alle ${skipped.length} gemeinsamen Marker gefiltert (Bewegung < ${minMovementMm} mm)`
|
||||
: 'Keine gemeinsamen fremd-Marker in Pos A+B+C gefunden',
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Achsenrichtung: Mittlere Normale (Vorzeichen angleichen) ─────────────
|
||||
const ref = normals[0];
|
||||
const aligned = normals.map(n => {
|
||||
const dot = n[0] * ref[0] + n[1] * ref[1] + n[2] * ref[2];
|
||||
return dot >= 0 ? n : n.map(c => -c);
|
||||
});
|
||||
const meanN = [0, 1, 2].map(i =>
|
||||
aligned.reduce((s, n) => s + n[i], 0) / aligned.length
|
||||
);
|
||||
const meanNLen = Math.sqrt(meanN[0] ** 2 + meanN[1] ** 2 + meanN[2] ** 2);
|
||||
const axisDir = meanN.map(c => c / meanNLen);
|
||||
|
||||
// ── Referenzpunkt: Schwerpunkt der Umkreismittelpunkte ───────────────────
|
||||
const axisPoint = [0, 1, 2].map(i =>
|
||||
circumcenters.reduce((s, c) => s + c.C[i], 0) / circumcenters.length
|
||||
);
|
||||
|
||||
// ── Kippwinkel gegen Y-Achse [0,1,0] ────────────────────────────────────
|
||||
const [ax, ay, az] = axisDir;
|
||||
const tiltXY = Math.atan2(ax, ay) * 180 / Math.PI;
|
||||
const tiltYZ = Math.atan2(az, ay) * 180 / Math.PI;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
axisDir,
|
||||
axisPoint,
|
||||
tiltXY,
|
||||
tiltYZ,
|
||||
numMarkers: circumcenters.length,
|
||||
numMarkersCommon: circumcenters.length + skipped.length,
|
||||
skipped,
|
||||
markerData,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Exports ──────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.computeYAxis = computeYAxis;
|
||||
exports.DEFAULT_MIN_MOVEMENT_MM = DEFAULT_MIN_MOVEMENT_MM;
|
||||
|
||||
}(typeof module !== 'undefined' ? module.exports : (self.YAxisCompute = {})));
|
||||
Reference in New Issue
Block a user