/** * editRobot.js * Hilfsfunktionen zum Bearbeiten von robot_xxx.json. * Alle Schreibvorgänge machen ein Backup-Kommentar in der Datei (nein, aber * atomisches Write per Temp-Datei ist hier nicht nötig – die Datei wird direkt * überschrieben; bei Bedarf Backup-Strategie ergänzen). */ import fsPromises from 'fs/promises'; // ── I/O ─────────────────────────────────────────────────────────────────────── async function readRobot(robotPath) { return JSON.parse(await fsPromises.readFile(robotPath, 'utf8')); } async function writeRobot(robotPath, data) { await fsPromises.writeFile(robotPath, JSON.stringify(data, null, 2), 'utf8'); } // ── Aktion 1: Marker nach Z-Bereich zuordnen ───────────────────────────────── /** * Weist allen Markern, deren z-Position (mm) zwischen zMin und zMax liegt, * das angegebene Set und/oder den angegebenen Link zu. * * - set (optional): Setzt den set-Wert des Markers. * - link (optional): Verschiebt den Marker in diesen Link (wird ggf. angelegt). * - extraMarkers (optional): Triangulierte Marker aus aruco_marker_poses.json – * werden als neue Einträge in robot.json hinzugefügt, * wenn sie noch nicht vorhanden sind. * * Gibt { numChanged, changes[] } zurück. * changes[]: { markerId, action, oldLink, newLink, oldSet, newSet } * action: 'updated' | 'added' */ export async function assignByZRange(robotPath, { zMin, zMax, set, link, extraMarkers = [] }) { const robot = await readRobot(robotPath); const links = robot.links ?? {}; const changes = []; const zLo = Number(zMin); const zHi = Number(zMax); // ── Teil 1: Bestehende robot.json-Marker aktualisieren / verschieben ────────── const snapshot = []; for (const [linkName, linkData] of Object.entries(links)) { for (const marker of (linkData.markers ?? [])) { if (Array.isArray(marker.position)) snapshot.push({ id: marker.id, currentLink: linkName }); } } for (const { id, currentLink } of snapshot) { const srcLinkData = links[currentLink]; const idx = (srcLinkData?.markers ?? []).findIndex(m => Number(m.id) === id); if (idx === -1) continue; const marker = srcLinkData.markers[idx]; const z = Number(marker.position[2]); if (z < zLo || z > zHi) continue; const change = { action: 'updated', markerId: marker.id, oldLink: currentLink, oldSet: marker.set ?? '', newLink: currentLink, newSet: marker.set ?? '', }; if (set !== undefined && set !== '') { marker.set = set; change.newSet = set; } if (link && link !== currentLink) { srcLinkData.markers.splice(idx, 1); if (!links[link]) links[link] = { markers: [] }; if (!links[link].markers) links[link].markers = []; links[link].markers.push(marker); change.newLink = link; } changes.push(change); } // ── Teil 2: Neue Marker aus 3b-Triangulation einfügen (noch nicht in robot.json) ── // Diese haben keine Position in robot.json – wir übernehmen die gemessene Position. if (link && extraMarkers.length > 0) { const knownIds = new Set(); for (const ld of Object.values(links)) { for (const m of (ld.markers ?? [])) knownIds.add(Number(m.id)); } for (const em of extraMarkers) { const emId = Number(em.marker_id); const emPos = em.position_mm; // [x_mm, y_mm, z_mm] robot-Koordinaten if (knownIds.has(emId)) continue; // bereits in robot.json (evtl. gerade hinzugefügt) if (!Array.isArray(emPos) || emPos.length < 3) continue; const z = Number(emPos[2]); if (z < zLo || z > zHi) continue; const newMarker = { id: emId, position: emPos.map(v => Math.round(Number(v) * 100) / 100), // 2 Dezimalstellen }; if (set) newMarker.set = set; if (!links[link]) links[link] = { markers: [] }; if (!links[link].markers) links[link].markers = []; links[link].markers.push(newMarker); knownIds.add(emId); changes.push({ action: 'added', markerId: emId, oldLink: null, oldSet: '', newLink: link, newSet: set ?? '', }); } } if (changes.length > 0) { robot.links = links; await writeRobot(robotPath, robot); } return { numChanged: changes.length, changes }; } // ── Aktion 2: Set oder Link-Zuordnung entfernen ─────────────────────────────── /** * Entfernt die Set- oder Link-Zuordnung eines Markers. * * removeFrom: 'set' → löscht nur den set-Wert (Marker bleibt im Link) * removeFrom: 'link' → entfernt Marker komplett aus dem Link * (nur wenn set bereits leer/nicht gesetzt) * * Gibt { changed, markerId, action, link, [oldSet], [error] } zurück. */ export async function removeMarkerAssignment(robotPath, { markerId, removeFrom }) { const id = Number(markerId); const robot = await readRobot(robotPath); const links = robot.links ?? {}; for (const [linkName, linkData] of Object.entries(links)) { const markers = linkData.markers ?? []; const idx = markers.findIndex(m => Number(m.id) === id); if (idx === -1) continue; const marker = markers[idx]; if (removeFrom === 'set') { const oldSet = marker.set ?? ''; delete marker.set; await writeRobot(robotPath, robot); return { changed: true, markerId: id, action: 'set-removed', link: linkName, oldSet }; } if (removeFrom === 'link') { if (marker.set) { return { changed: false, markerId: id, error: `Marker ${id} hat noch Set "${marker.set}". Bitte zuerst Set entfernen.`, }; } markers.splice(idx, 1); await writeRobot(robotPath, robot); return { changed: true, markerId: id, action: 'link-removed', link: linkName }; } return { changed: false, error: `Unbekannte Aktion: "${removeFrom}"` }; } return { changed: false, error: `Marker-ID ${id} nicht gefunden.` }; } // ── Aktion 3: Sets untereinander justieren (2D+Z Kabsch) ───────────────────── /** * Optimale 2D-Rotation (um Z-Achse) + 3D-Translation, die model→measured minimiert. * Gibt { tx, ty, tz, theta, thetaDeg } zurück. * modelPts / measuredPts: Arrays von [x, y, z] in mm. */ function computeRigid2DZ(modelPts, measuredPts) { const n = modelPts.length; if (n === 0) return { tx: 0, ty: 0, tz: 0, theta: 0, thetaDeg: 0 }; // Schwerpunkte let mCx = 0, mCy = 0, mCz = 0, pCx = 0, pCy = 0, pCz = 0; for (let i = 0; i < n; i++) { mCx += modelPts[i][0]; mCy += modelPts[i][1]; mCz += modelPts[i][2]; pCx += measuredPts[i][0]; pCy += measuredPts[i][1]; pCz += measuredPts[i][2]; } mCx /= n; mCy /= n; mCz /= n; pCx /= n; pCy /= n; pCz /= n; // 2D-Kreuzkovarianz für Kabsch-Rotation in XY-Ebene let H00 = 0, H01 = 0, H10 = 0, H11 = 0; for (let i = 0; i < n; i++) { const ax = modelPts[i][0] - mCx, ay = modelPts[i][1] - mCy; const bx = measuredPts[i][0] - pCx, by = measuredPts[i][1] - pCy; H00 += ax * bx; H01 += ax * by; H10 += ay * bx; H11 += ay * by; } // Optimaler Drehwinkel (2D-Sonderfall von Kabsch / SVD → atan2) const theta = Math.atan2(H10 - H01, H00 + H11); const cos = Math.cos(theta); const sin = Math.sin(theta); // Translation: gemessener Schwerpunkt − R × Modell-Schwerpunkt const tx = pCx - (cos * mCx - sin * mCy); const ty = pCy - (sin * mCx + cos * mCy); const tz = pCz - mCz; return { tx, ty, tz, theta, thetaDeg: theta * 180 / Math.PI }; } function applyRigid2DZ([x, y, z], { tx, ty, tz, theta }) { const cos = Math.cos(theta); const sin = Math.sin(theta); return [ Math.round((cos * x - sin * y + tx) * 100) / 100, Math.round((sin * x + cos * y + ty) * 100) / 100, Math.round((z + tz) * 100) / 100, ]; } /** * Richtet alle Marker des Sets `setToMove` rigid an ihren triangulieren 3b-Positionen aus. * Das andere Set bleibt unberührt – die Transformation wird aus den Paaren * (Modellposition, Messposition) der matching Marker berechnet. * * setToMove: Name des zu verschiebenden Sets (z.B. "rail") * extraMarkers: Marker aus aruco_marker_poses.json (mit marker_id, position_mm) * * Gibt { numChanged, numMatchingPts, transform: {tx, ty, tz, thetaDeg} } zurück, * oder { error } bei Fehler. */ export async function alignSetToMeasured(robotPath, { setToMove, extraMarkers = [] }) { const robot = await readRobot(robotPath); const links = robot.links ?? {}; // Alle Marker des zu verschiebenden Sets sammeln const setMarkers = []; for (const [linkName, linkData] of Object.entries(links)) { for (const marker of (linkData.markers ?? [])) { if (marker.set === setToMove) { setMarkers.push({ marker, linkName }); } } } if (setMarkers.length === 0) { return { error: `Set "${setToMove}" nicht gefunden oder leer.` }; } // Gemessene Positionen aus 3b indizieren const measuredMap = new Map(); for (const em of extraMarkers) { if (Array.isArray(em.position_mm) && em.position_mm.length >= 3) { measuredMap.set(Number(em.marker_id), em.position_mm.map(Number)); } } // Matching-Paare für Kabsch (nur Marker, die sowohl Modellposition als auch Messung haben) const modelPts = [], measuredPts = []; for (const { marker } of setMarkers) { if (!Array.isArray(marker.position) || marker.position.length < 3) continue; const mpos = measuredMap.get(Number(marker.id)); if (!mpos) continue; modelPts.push(marker.position.map(Number)); measuredPts.push(mpos); } if (modelPts.length < 2) { return { error: `Zu wenig Messpunkte für Set "${setToMove}" (${modelPts.length} von ${setMarkers.length} Markern gemessen). ` + `Bitte Board-Run mit ≥2 Kameras durchführen.`, }; } const transform = computeRigid2DZ(modelPts, measuredPts); // Transformation auf ALLE Marker des Sets anwenden (auch nicht gemessene) let numChanged = 0; for (const { marker } of setMarkers) { if (!Array.isArray(marker.position) || marker.position.length < 3) continue; marker.position = applyRigid2DZ(marker.position.map(Number), transform); numChanged++; } robot.links = links; await writeRobot(robotPath, robot); return { numChanged, numMatchingPts: modelPts.length, transform: { tx: Math.round(transform.tx * 100) / 100, ty: Math.round(transform.ty * 100) / 100, tz: Math.round(transform.tz * 100) / 100, thetaDeg: Math.round(transform.thetaDeg * 100) / 100, }, }; } // ── Aktion 4: Einzelnen Marker per ID hinzufügen / aktualisieren ────────────── /** * Fügt einen Marker per ID zu Set und Link hinzu oder aktualisiert einen bestehenden. * * - Existiert der Marker bereits in robot.json: set und/oder link werden aktualisiert. * - Existiert er noch nicht: Position wird aus extraMarkers (3b-Output) geholt und * der Marker wird dem angegebenen Link hinzugefügt. * * Gibt { changed, change } oder { changed: false, error } zurück. */ export async function assignMarkerId(robotPath, { markerId, set, link, extraMarkers = [] }) { const id = Number(markerId); if (!Number.isFinite(id) || id < 0) return { changed: false, error: 'Ungültige Marker-ID.' }; const robot = await readRobot(robotPath); const links = robot.links ?? {}; // Marker in robot.json suchen let foundMarker = null, foundLink = null; outer: for (const [linkName, linkData] of Object.entries(links)) { for (const m of (linkData.markers ?? [])) { if (Number(m.id) === id) { foundMarker = m; foundLink = linkName; break outer; } } } if (foundMarker) { // Bestehenden Marker: Set und ggf. Link aktualisieren const change = { action: 'updated', markerId: id, oldLink: foundLink, oldSet: foundMarker.set ?? '', newLink: foundLink, newSet: foundMarker.set ?? '', }; if (set !== undefined && set !== '') { foundMarker.set = set; change.newSet = set; } if (link && link !== foundLink) { // In neuen Link verschieben links[foundLink].markers = links[foundLink].markers.filter(m => Number(m.id) !== id); if (!links[link]) links[link] = { markers: [] }; if (!links[link].markers) links[link].markers = []; links[link].markers.push(foundMarker); change.newLink = link; } robot.links = links; await writeRobot(robotPath, robot); return { changed: true, change }; } // Neuer Marker: Position aus 3b-Output holen const em = extraMarkers.find(m => Number(m.marker_id) === id); if (!em) { return { changed: false, error: `Marker ${id} ist nicht in robot.json und nicht im letzten 3b-Run vorhanden.`, }; } if (!link) { return { changed: false, error: 'Link muss angegeben werden, um einen neuen Marker hinzuzufügen.' }; } const newMarker = { id, position: em.position_mm.map(v => Math.round(Number(v) * 100) / 100), }; if (set) newMarker.set = set; if (!links[link]) links[link] = { markers: [] }; if (!links[link].markers) links[link].markers = []; links[link].markers.push(newMarker); robot.links = links; await writeRobot(robotPath, robot); return { changed: true, change: { action: 'added', markerId: id, oldLink: null, oldSet: '', newLink: link, newSet: set ?? '' }, }; } // ── Aktion 5: X-Achse übernehmen ───────────────────────────────────────────── /** * Rotiert alle Marker-Positionen in robot.json so, dass die übergebene Richtung * zur neuen X-Achse [1,0,0] wird. Rotation um den Schwerpunkt aller A0-Marker * (Origin bleibt erhalten). * * direction: [vx, vy, vz] – gemessene Ist-X-Richtung in Roboter-Koordinaten * * Algorithmus: * 1. Normalisiere direction, ggf. Vorzeichen so dass vx > 0 * 2. Baue Orthonormalbasis: x_new = v, * z_new = Gram-Schmidt([0,0,1] gegen v), * y_new = cross(z_new, x_new) * 3. Rotationsmatrix R (old→new): Zeilen = [x_new, y_new, z_new] * 4. p_new = origin + R * (p_old − origin) */ export async function adoptXAxis(robotPath, { direction }) { const [vx, vy, vz] = direction.map(Number); const len = Math.sqrt(vx * vx + vy * vy + vz * vz); if (len < 1e-9) throw new Error('Richtungsvektor zu klein (fast Null-Vektor).'); // Normalisieren, immer positive X-Komponente let nx = vx / len, ny = vy / len, nz = vz / len; if (nx < 0) { nx = -nx; ny = -ny; nz = -nz; } // ── Orthonormalbasis ────────────────────────────────────────────────────── // Z_new: Gram-Schmidt von [0,0,1] gegen x_new const dotZ = nz; // dot([0,0,1], [nx,ny,nz]) let zx = -dotZ * nx, zy = -dotZ * ny, zz = 1 - dotZ * nz; let zlen = Math.sqrt(zx * zx + zy * zy + zz * zz); if (zlen < 1e-9) { // Sonderfall: x_new fast parallel zu Z – Fallback auf [0,1,0] const dotY = ny; zx = -dotY * nx; zy = 1 - dotY * ny; zz = -dotY * nz; zlen = Math.sqrt(zx * zx + zy * zy + zz * zz); } zx /= zlen; zy /= zlen; zz /= zlen; // Y_new = cross(z_new, x_new) [rechte-Hand-Regel: ẑ × x̂ = ŷ] const yx = zy * nz - zz * ny; const yy = zz * nx - zx * nz; const yz = zx * ny - zy * nx; // Rotationsfunktion: p_rot = R * p (R hat Zeilen = neue Achsen in alten Koordinaten) function rotVec(px, py, pz) { return [ nx * px + ny * py + nz * pz, // neue X-Komponente yx * px + yy * py + yz * pz, // neue Y-Komponente zx * px + zy * py + zz * pz, // neue Z-Komponente ]; } const robot = await readRobot(robotPath); const links = robot.links ?? {}; // ── Ursprung: Schwerpunkt aller A0-Marker ──────────────────────────────── const a0Pos = []; for (const ld of Object.values(links)) { for (const m of (ld.markers ?? [])) { if (m.set === 'A0' && Array.isArray(m.position) && m.position.length >= 3) { a0Pos.push(m.position.map(Number)); } } } let ox = 0, oy = 0, oz = 0; if (a0Pos.length > 0) { for (const [px, py, pz] of a0Pos) { ox += px; oy += py; oz += pz; } ox /= a0Pos.length; oy /= a0Pos.length; oz /= a0Pos.length; } // ── Alle Marker rotieren ────────────────────────────────────────────────── let numChanged = 0; for (const ld of Object.values(links)) { for (const m of (ld.markers ?? [])) { if (!Array.isArray(m.position) || m.position.length < 3) continue; const [px, py, pz] = m.position.map(Number); const [rx, ry, rz] = rotVec(px - ox, py - oy, pz - oz); m.position = [ Math.round((ox + rx) * 100) / 100, Math.round((oy + ry) * 100) / 100, Math.round((oz + rz) * 100) / 100, ]; numChanged++; } } robot.links = links; await writeRobot(robotPath, robot); return { numChanged, origin: [ox, oy, oz].map(v => Math.round(v * 10) / 10), newXAxis: [nx, ny, nz].map(v => Math.round(v * 10000) / 10000), angleXYdeg: Math.round(Math.atan2(ny, nx) * 18000 / Math.PI) / 100, angleXZdeg: Math.round(Math.atan2(nz, nx) * 18000 / Math.PI) / 100, }; } // ── Aktion 6: Fixe Marker einem Link zuordnen ───────────────────────────────── /** * Ordnet Marker, die sich bei einer Gelenk-Rotation kaum bewegen, dem angegebenen * Link zu. Typischer Anwendungsfall: Marker auf dem Basis-Körper werden als * "Base"-Marker in robot.json registriert. * * - markerIds: Array von Marker-IDs (number[]) * - targetLink: Ziel-Link-Name (z. B. 'Base') * - measuredPositions: Array von { id, position_mm:[x,y,z] } aus Pos A * * Gibt { numAdded, numAlreadyPresent, changes[] } zurück. * Bestehende Einträge werden NICHT verschoben (überschreiben wäre destruktiv). */ export async function assignFixedMarkersToLink(robotPath, { markerIds, targetLink, measuredPositions = [] }) { if (!targetLink) throw new Error('targetLink muss angegeben werden.'); const robot = await readRobot(robotPath); const links = robot.links ?? {}; // Index: markerId → aktueller Link const existingMap = new Map(); for (const [linkName, linkData] of Object.entries(links)) { for (const m of (linkData.markers ?? [])) { existingMap.set(Number(m.id), linkName); } } // Pos-A-Positionen indizieren const posMap = new Map(); for (const p of measuredPositions) { posMap.set(Number(p.id), p.position_mm); } if (!links[targetLink]) links[targetLink] = { markers: [] }; if (!links[targetLink].markers) links[targetLink].markers = []; const changes = []; let numAdded = 0, numAlreadyPresent = 0; for (const rawId of markerIds) { const id = Number(rawId); if (existingMap.has(id)) { numAlreadyPresent++; changes.push({ action: 'already-present', markerId: id, existingLink: existingMap.get(id) }); continue; } const pos = posMap.get(id); if (!pos || pos.length < 3) { changes.push({ action: 'skipped-no-position', markerId: id }); continue; } links[targetLink].markers.push({ id, position: pos.map(v => Math.round(Number(v) * 10) / 10), // 1 Dezimalstelle (mm) positionSource: 'calibration-fixed-detection', }); numAdded++; changes.push({ action: 'added', markerId: id, targetLink }); } if (numAdded > 0) { robot.links = links; await writeRobot(robotPath, robot); } return { numAdded, numAlreadyPresent, changes }; } // ── Aktion 7: Joint-Origin Y/Z aus Drehachse übernehmen ────────────────────── /** * Setzt die Y- und Z-Koordinaten des jointToParent.origin eines Links. * * Der Ursprung der Drehachse (axisPoint_mm) liegt im Board-Koordinatensystem. * Y und Z sind davon direkt auf den Joint-Origin übertragbar (X wird vom * Slider bestimmt und bleibt unverändert). * * - linkName: Name des Links, dessen Joint aktualisiert wird (z. B. 'Arm1') * - y: Neuer Y-Wert des Joint-Origins (mm) * - z: Neuer Z-Wert des Joint-Origins (mm) * * Gibt { changed, linkName, oldOrigin, newOrigin } zurück. */ export async function setJointOriginYZ(robotPath, { linkName, y, z }) { if (!linkName) throw new Error('linkName muss angegeben werden.'); const robot = await readRobot(robotPath); const link = (robot.links ?? {})[linkName]; if (!link) return { changed: false, error: `Link '${linkName}' nicht in robot.json gefunden.` }; const joint = link.jointToParent; if (!joint) return { changed: false, error: `Link '${linkName}' hat kein jointToParent.` }; const oldOrigin = Array.isArray(joint.origin) ? [...joint.origin] : [null, null, null]; if (!Array.isArray(joint.origin) || joint.origin.length < 3) { joint.origin = [oldOrigin[0] ?? 0, 0, 0]; } joint.origin[1] = Math.round(Number(y) * 10) / 10; // Y (1 Dezimalstelle) joint.origin[2] = Math.round(Number(z) * 10) / 10; // Z // Quelle dokumentieren if (!joint.originSource) joint.originSource = [null, null, null]; while (joint.originSource.length < 3) joint.originSource.push(null); joint.originSource[1] = 'calibration-yaxis'; joint.originSource[2] = 'calibration-yaxis'; await writeRobot(robotPath, robot); return { changed: true, linkName, oldOrigin, newOrigin: [...joint.origin], }; }