spin Marker Callibration

This commit is contained in:
chk
2026-06-15 09:23:21 +02:00
parent 375ee4cf69
commit 15d4175fd1
18 changed files with 1191 additions and 239 deletions

View File

@@ -338,18 +338,34 @@ function buildSkeletonFK(robot, angles) {
gSkeleton.add(makeSphere(jointW, 0.004, 0xc8cdd8));
}
// 4. Arm-Marker zeichnen (Modellposition via FK, als orientiertes Quadrat)
// 4. Arm-Marker zeichnen (Modellposition via FK, orientiertes Quadrat + spin)
if (link.markers?.length > 0) {
const col = LINK_COLORS[linkName] ?? 0xffffff;
for (const m of link.markers) {
if (!m.position) continue;
const [lx, ly, lz] = m.position;
const posWorld = new THREE.Vector3(lx * S, lz * S, -ly * S).applyMatrix4(childFrame);
const posWorld = new THREE.Vector3(lx * S, lz * S, -ly * S).applyMatrix4(childFrame);
const markerSizeM = (m.size ?? 25) * S;
const [nx, ny, nz] = m.normal ?? [0, 0, 1];
const normalWorld = new THREE.Vector3(nx, nz, -ny).transformDirection(childFrame);
gArmMarkers.add(makeMarkerSquareOriented(posWorld, normalWorld, markerSizeM, col));
const normalW = new THREE.Vector3(nx, nz, -ny).transformDirection(childFrame).normalize();
// P1: Quadrat mit spin-Rotation (um die Marker-Normale in Welt-Koordinaten)
const markerMesh = makeMarkerSquareOriented(posWorld, normalW, markerSizeM, col);
const spinRad = ((m.spin ?? 0) * Math.PI) / 180;
if (Math.abs(spinRad) > 1e-6) {
markerMesh.quaternion.premultiply(
new THREE.Quaternion().setFromAxisAngle(normalW, spinRad)
);
}
gArmMarkers.add(markerMesh);
gArmMarkers.add(makeSphere(posWorld, 0.0006, col));
// P3b (Modell-Seite): Orientierungszeiger zur Ecke 0 (top-left bei spin=0)
// markerMesh.quaternion kodiert bereits Q_normal ∘ Q_spin
const ptrDir = new THREE.Vector3(1, 1, 0).normalize().applyQuaternion(markerMesh.quaternion);
const corner0W = posWorld.clone().add(ptrDir.multiplyScalar(markerSizeM * Math.SQRT1_2));
gArmMarkers.add(makeLine(posWorld, corner0W, col, 0.9));
gArmMarkers.add(makeSphere(corner0W, 0.0008, col));
}
}
}

View File

@@ -24,6 +24,7 @@
<button class="tab-btn" data-tab="board" data-src="/calibration_board.html">Board</button>
<button class="tab-btn" data-tab="robot-x-axis" data-src="/calibration_xaxis.html">Robot X Axis</button>
<button class="tab-btn" data-tab="arm1" data-src="/calibration_arm.html">Arm1 Y</button>
<button class="tab-btn" data-tab="marker" data-src="/calibration_marker.html">Marker</button>
</nav>
<!-- CONTENT (Panels werden lazy per fetch befüllt) -->
@@ -32,6 +33,7 @@
<div class="tab-panel" id="tab-board"></div>
<div class="tab-panel" id="tab-robot-x-axis"></div>
<div class="tab-panel" id="tab-arm1"></div>
<div class="tab-panel" id="tab-marker"></div>
</div>
</div><!-- /.calib-body -->

View File

@@ -22,6 +22,7 @@ async function loadPanel(tab, src) {
else if (tab === 'board') initBoard();
else if (tab === 'robot-x-axis') initXAxis();
else if (tab === 'arm1') initArm('arm1');
else if (tab === 'marker') initMarker();
} catch (err) {
document.getElementById('tab-' + tab).innerHTML =
@@ -950,3 +951,163 @@ function initBoard() {
}
});
}
// ── Tab: Marker ───────────────────────────────────────────────────────────────
function initMarker() {
const logEl = document.getElementById('log-marker');
const tableWrap = document.getElementById('marker-table-wrap');
const linkSel = document.getElementById('marker-action-link');
const idSel = document.getElementById('marker-action-id');
const spinLabel = document.getElementById('marker-spin-current');
const resultEl = document.getElementById('marker-action-result');
const frameEl = document.getElementById('marker-viewer-frame');
const ARM_LINKS = ['Arm1', 'Ellbow', 'Arm2', 'Hand', 'Palm', 'FingerA', 'FingerB'];
let _robot = null;
function logM(msg) {
const ts = new Date().toLocaleTimeString('de-CH');
logEl.value += `[${ts}] ${msg}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
// ── Marker-Tabelle rendern ────────────────────────────────────────────────
function renderTable(robot) {
if (!tableWrap) return;
const links = robot?.links ?? {};
const th = (a) => `style="text-align:${a};padding:3px 8px;border-bottom:1px solid #2a2d35;white-space:nowrap;background:#1e293b;color:#555b6e;font-weight:normal"`;
const td = (a, x = '') => `style="padding:2px 8px;border-bottom:1px solid #111418;text-align:${a};white-space:nowrap;${x}"`;
let rows = '';
let total = 0;
for (const linkName of ARM_LINKS) {
const markers = links[linkName]?.markers ?? [];
for (const m of markers) {
total++;
const pos = m.position ? m.position.map(v => Number(v).toFixed(1)).join(', ') : '';
const norm = m.normal ? m.normal.map(v => Number(v).toFixed(2)).join(', ') : '';
rows += `<tr>
<td ${td('left', 'color:#4a9eff')}>${linkName}</td>
<td ${td('right')}>${m.id}</td>
<td ${td('left', 'color:#888')}>${m.name ?? ''}</td>
<td ${td('right')}>${pos}</td>
<td ${td('right', 'color:#aaa')}>${norm}</td>
<td ${td('right')}>${m.size ?? ''}</td>
<td ${td('right', 'color:#f0a500;font-weight:bold')}>${m.spin ?? 0}°</td>
</tr>`;
}
}
if (total === 0) {
tableWrap.innerHTML = '<p style="font-size:12px;color:var(--muted)">Keine Arm-Marker in robot.json eingetragen.</p>';
return;
}
tableWrap.innerHTML = `
<p style="font-size:10px;color:#555b6e;margin-bottom:4px">${total} Marker in Arm-Links</p>
<table style="border-collapse:collapse;font-size:11px;font-family:inherit;width:100%">
<thead><tr>
<th ${th('left')}>Link</th>
<th ${th('right')}>ID</th>
<th ${th('left')}>Name</th>
<th ${th('right')}>Position [x,y,z] mm</th>
<th ${th('right')}>Normal [nx,ny,nz]</th>
<th ${th('right')}>Size mm</th>
<th ${th('right')}>Spin</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
// ── Marker-Dropdown für gewählten Link befüllen ───────────────────────────
function updateMarkerDropdown() {
if (!idSel || !_robot) return;
const linkName = linkSel?.value;
const markers = _robot.links?.[linkName]?.markers ?? [];
const prev = idSel.value;
idSel.innerHTML = '<option value=""> wählen </option>' +
markers.map(m => `<option value="${m.id}">${m.id}${m.name ? ' ' + m.name : ''}</option>`).join('');
if (markers.some(m => String(m.id) === prev)) idSel.value = prev;
updateSpinLabel();
}
function updateSpinLabel() {
if (!spinLabel || !_robot) { if (spinLabel) spinLabel.textContent = ''; return; }
const linkName = linkSel?.value;
const markerId = idSel?.value;
if (!markerId) { spinLabel.textContent = ''; return; }
const markers = _robot.links?.[linkName]?.markers ?? [];
const m = markers.find(mm => String(mm.id) === String(markerId));
spinLabel.textContent = m ? `Aktuell: spin = ${m.spin ?? 0}°` : '';
}
// ── Robot laden ───────────────────────────────────────────────────────────
async function loadRobot() {
try {
const r = await fetch('/api/robot');
if (!r.ok) throw new Error(`HTTP ${r.status}`);
_robot = await r.json();
renderTable(_robot);
updateMarkerDropdown();
} catch (err) {
if (tableWrap) tableWrap.innerHTML = `<p style="color:#f87171;font-size:12px">Fehler: ${err}</p>`;
logM(`❌ robot.json konnte nicht geladen werden: ${err}`);
}
}
// ── Spin-Aktion ausführen ─────────────────────────────────────────────────
async function applySpin(delta) {
if (!resultEl) return;
const linkName = linkSel?.value;
const markerId = idSel?.value;
if (!markerId) {
resultEl.innerHTML = '<span style="color:#f87171">⚠ Bitte zuerst einen Marker wählen.</span>';
return;
}
const markers = _robot?.links?.[linkName]?.markers ?? [];
const current = markers.find(m => String(m.id) === String(markerId));
const oldSpin = current?.spin ?? 0;
const newSpin = ((oldSpin + delta) % 360 + 360) % 360;
resultEl.innerHTML = '<span style="color:#555b6e">Speichern …</span>';
try {
const r = await fetch('/api/robot/set-arm-marker-spin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ linkName, markerId: Number(markerId), spin: newSpin }),
});
const data = await r.json();
if (!r.ok || !data.changed) {
resultEl.innerHTML = `<span style="color:#f87171">❌ ${data.error ?? `HTTP ${r.status}`}</span>`;
return;
}
resultEl.innerHTML =
`<span style="color:#22c55e">✅ ${linkName} #${markerId}: spin ${data.oldSpin}° → ${data.newSpin}°</span>`;
logM(`Spin ${linkName}#${markerId}: ${data.oldSpin}° → ${data.newSpin}°`);
// Lokales Modell aktualisieren
if (current) current.spin = data.newSpin;
updateSpinLabel();
renderTable(_robot);
// Viewer neu laden
if (frameEl?.contentWindow) frameEl.contentWindow.postMessage({ type: 'reload' }, '*');
} catch (err) {
resultEl.innerHTML = `<span style="color:#f87171">❌ ${err}</span>`;
}
}
// ── Event-Listener ────────────────────────────────────────────────────────
document.getElementById('btn-marker-reload')?.addEventListener('click', () => loadRobot());
linkSel?.addEventListener('change', () => updateMarkerDropdown());
idSel?.addEventListener('change', () => updateSpinLabel());
document.getElementById('btn-spin-minus90')?.addEventListener('click', () => applySpin(-90));
document.getElementById('btn-spin-plus90')?.addEventListener('click', () => applySpin(+90));
document.getElementById('btn-spin-180')?.addEventListener('click', () => applySpin(+180));
// Init
loadRobot();
}

View File

@@ -0,0 +1,96 @@
<div class="sections">
<!-- ── Aktuelle Marker ──────────────────────────────────────────────────── -->
<div class="section full">
<h2>Aktuelle Marker <span class="status-badge open">aus robot.json</span></h2>
<p style="font-size:12px;color:var(--muted);margin-top:8px;margin-bottom:10px">
Arm-Marker aller Links (Board-Marker ausgeblendet). Spin = Drehung um die Marker-Normale in Grad.
</p>
<div id="marker-table-wrap">
<p style="font-size:12px;color:var(--muted)">(wird geladen …)</p>
</div>
<div class="controls" style="margin-top:12px">
<button id="btn-marker-reload">Neu laden</button>
</div>
</div>
<!-- ── Aktionen ─────────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Aktionen</h2>
<!-- Auswahl Link / Marker -->
<div style="margin-top:14px;display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-size:12px;color:var(--text)">
<label>Link
<select id="marker-action-link"
style="margin-left:6px;background:#1e293b;border:1px solid #334155;color:#c8cdd8;border-radius:3px;padding:3px 8px;font:inherit;font-size:12px;cursor:pointer">
<option value="Arm1">Arm1</option>
<option value="Ellbow">Ellbow</option>
<option value="Arm2">Arm2</option>
<option value="Hand">Hand</option>
<option value="Palm">Palm</option>
<option value="FingerA">FingerA</option>
<option value="FingerB">FingerB</option>
</select>
</label>
<label>Marker-ID
<select id="marker-action-id"
style="margin-left:6px;background:#1e293b;border:1px solid #334155;color:#c8cdd8;border-radius:3px;padding:3px 8px;font:inherit;font-size:12px;cursor:pointer">
<option value=""> wählen </option>
</select>
</label>
<span id="marker-spin-current" style="color:var(--muted);font-size:11px"></span>
</div>
<!-- Spin-Aktionen -->
<div style="margin-top:12px">
<p style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px">
Spin (Drehung um Marker-Normale)
</p>
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center">
<button id="btn-spin-minus90"
style="background:#1e293b;color:#c8cdd8;border:1px solid #334155;border-radius:3px;padding:5px 14px;cursor:pointer;font:inherit;font-size:12px">
90°
</button>
<button id="btn-spin-plus90"
style="background:#1e293b;color:#c8cdd8;border:1px solid #334155;border-radius:3px;padding:5px 14px;cursor:pointer;font:inherit;font-size:12px">
+90°
</button>
<button id="btn-spin-180"
style="background:#1e293b;color:#c8cdd8;border:1px solid #334155;border-radius:3px;padding:5px 14px;cursor:pointer;font:inherit;font-size:12px">
180°
</button>
<span style="font-size:10px;color:var(--muted)">→ wird sofort in robot.json gespeichert und im Viewer aktualisiert</span>
</div>
</div>
<!-- Ergebnis -->
<div id="marker-action-result" style="margin-top:10px;font-size:11px;min-height:18px;color:var(--muted)"></div>
</div>
<!-- ── Log ──────────────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Log</h2>
<textarea id="log-marker" readonly placeholder="(Aktionen erscheinen hier)"
style="height:100px"></textarea>
</div>
<!-- ── Viewer ───────────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
Zeigt das Roboter-Modell mit den Arm-Markern in der aktuellen robot.json-Konfiguration.
Sind Homing-Messwerte vorhanden (aus letztem Homing-Run), werden auch die beobachteten Marker
als Kugeln und die Abweichungs-Linien dargestellt.
Nach einer Spin-Änderung wird der Viewer automatisch neu geladen.
</p>
<iframe
id="marker-viewer-frame"
src="/boardViewer.html?mode=homing"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Marker-Viewer"
></iframe>
</div>
</div>