@@ -204,6 +204,13 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const S = 1 / 1000 ; // mm → m
// ── Modus-Erkennung ──────────────────────────────────────────────────────────
const _urlParams = new URLSearchParams ( window . location . search ) ;
const IS _HOMING = _urlParams . get ( 'mode' ) === 'homing' ;
if ( IS _HOMING ) {
document . getElementById ( 'run-bar' ) . style . display = 'none' ;
}
// robot (x=right, y=backward, z=up) → Three.js (x=right, y=up, z=toward viewer)
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 ) ; }
@@ -243,13 +250,86 @@ const gCompare = new THREE.Group(); // Pos B Marker (nur fremd, orange)
const gCompareLines = new THREE . Group ( ) ; // Verbindungslinien Pos A↔Pos B
const gPositionC = new THREE . Group ( ) ; // Pos C Marker (nur fremd, cyan)
const gYAxis = new THREE . Group ( ) ; // Y-Achse Visualisierung (Kreismittelpunkte, Achse)
scene . add ( gPaper , gMarkers , gMeasured , gCameras , gCompare , gCompareLines , gPositionC , gYAxis ) ;
const gSkeleton = new THREE . Group ( ) ; // Roboter-Skeleton (FK, nur im Homing-Mode)
scene . add ( gPaper , gMarkers , gMeasured , gCameras , gCompare , gCompareLines , gPositionC , gYAxis , gSkeleton ) ;
// ── Zustand für Positionen ────────────────────────────────────────────────────
let _primaryFremdMarkers = [ ] ; // Pos A – [{marker_id, position_mm, num_cameras}]
let _compareFremdMarkers = [ ] ; // Pos B – [{marker_id, position_mm, num_cameras}]
let _positionCFremdMarkers = [ ] ; // Pos C – [{marker_id, position_mm, num_cameras}]
// ── Homing-Mode Zustand ───────────────────────────────────────────────────────
let _currentRobot = null ; // robot.json nach loadData()
let _homingAngles = null ; // { x, y, z, a, b, c, e } nach Homing-Run
// ── Roboter-Skeleton (Forward Kinematics) ─────────────────────────────────────
/**
* Zeichnet das Roboter-Skeleton mit vorwärts-kinematischer Berechnung.
* Nutzt skeleton.from/to aus robot.json (in lokalem Link-Frame).
* angles: { x_mm→x, y_deg→y, z_deg→z, a_deg→a, b_deg→b, c_deg→c, e_mm→e }
*/
function buildSkeletonFK ( robot , angles ) {
clearGroup ( gSkeleton ) ;
if ( ! robot ? . links ) return ;
const links = robot . links ;
const order = [ 'Base' , 'Arm1' , 'Ellbow' , 'Arm2' , 'Hand' , 'Palm' , 'FingerA' , 'FingerB' ] ;
// frames: Link-Name → Matrix4 (link-lokal → Welt)
const frames = { Board : new THREE . Matrix4 ( ) } ; // Board = Welt-Ursprung
for ( const linkName of order ) {
const link = links [ linkName ] ;
if ( ! link ? . jointToParent ) continue ;
const parentName = link . parent ? ? 'Board' ;
const parentFrame = frames [ parentName ] ? ? new THREE . Matrix4 ( ) ;
const jtp = link . jointToParent ;
// 1. Translation zum Gelenk-Ursprung (im Parent-Frame)
const [ ox , oy , oz ] = jtp . origin ? ? [ 0 , 0 , 0 ] ;
const T _origin = new THREE . Matrix4 ( ) . makeTranslation ( ox * S , oz * S , - oy * S ) ;
// 2. Gelenk-Transformation (Rotation/Translation je nach Typ)
const varName = jtp . variable ;
const q = angles ? . [ varName ] ? ? 0 ;
let T _joint = new THREE . Matrix4 ( ) ; // Einheitsmatrix bei q=0
if ( jtp . type === 'revolute' ) {
const [ ax , ay , az ] = jtp . axis ? ? [ 0 , 1 , 0 ] ;
// robot (x,y,z) → Three.js (x, z, -y)
const axisV = new THREE . Vector3 ( ax , az , - ay ) . normalize ( ) ;
T _joint . makeRotationAxis ( axisV , q * Math . PI / 180 ) ;
} else if ( jtp . type === 'linear' ) {
const [ ax , ay , az ] = jtp . axis ? ? [ 1 , 0 , 0 ] ;
T _joint . makeTranslation ( ax * q * S , az * q * S , - ay * q * S ) ;
}
// Child-Frame = Parent-Frame × T_origin × T_joint
const childFrame = parentFrame . clone ( ) . multiply ( T _origin ) . multiply ( T _joint ) ;
frames [ linkName ] = childFrame ;
// 3. Skeleton-Segment zeichnen
const skel = link . skeleton ;
if ( ! skel ? . from || ! skel ? . to ) continue ;
const [ fx , fy , fz ] = skel . from ;
const [ tx , ty , tz ] = skel . to ;
const fromW = new THREE . Vector3 ( fx * S , fz * S , - fy * S ) . applyMatrix4 ( childFrame ) ;
const toW = new THREE . Vector3 ( tx * S , tz * S , - ty * S ) . applyMatrix4 ( childFrame ) ;
const [ cr , cg , cb ] = skel . color ? ? [ 0.8 , 0.2 , 0.2 ] ;
const color = new THREE . Color ( cr , cg , cb ) ;
const rad = Math . max ( ( skel . radius ? ? 4 ) * S , 0.004 ) ;
gSkeleton . add ( makeLine ( fromW , toW , color , 0.9 ) ) ;
gSkeleton . add ( makeSphere ( fromW , rad , color ) ) ;
gSkeleton . add ( makeSphere ( toW , rad , color ) ) ;
// Gelenk-Mittelpunkt (Welt-Ursprung des Link-Frames)
const jointW = new THREE . Vector3 ( ) . applyMatrix4 ( childFrame ) ;
gSkeleton . add ( makeSphere ( jointW , 0.004 , 0xc8cdd8 ) ) ;
}
}
// ── Viewer-interner Logger ────────────────────────────────────────────────────
function vlog ( msg , kind = '' ) {
const el = document . getElementById ( 'viewer-log' ) ;
@@ -810,14 +890,21 @@ function computeAndShowYAxis() {
// ── Daten laden ───────────────────────────────────────────────────────────────
/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Ru n. */
/** Haupt-Run laden. Im Homing-Mode: immer neuester Homing-Run, kein Dropdow n. */
async function loadData ( ) {
const statusEl = document . getElementById ( 'status' ) ;
statusEl . textContent = 'Laden …' ;
const selRun = document . getElementById ( 'sel-run-primary' ) ? . value ? ? '' ;
cons t url = selRun
? ` /api/board/latest?run= ${ encodeURIComponent ( selRun ) } `
: '/api/board/latest' ;
le t url ;
if ( IS _HOMING ) {
url = '/api/board/latest?from=homing ' ;
} else {
const selRun = document . getElementById ( 'sel-run-primary' ) ? . value ? ? '' ;
url = selRun
? ` /api/board/latest?run= ${ encodeURIComponent ( selRun ) } `
: '/api/board/latest' ;
}
try {
const r = await fetch ( url ) ;
if ( ! r . ok ) throw new Error ( ` HTTP ${ r . status } ` ) ;
@@ -826,6 +913,7 @@ async function loadData() {
statusEl . textContent = 'Kein Board-Run vorhanden.' ;
document . getElementById ( 'stats' ) . textContent = '' ;
clearGroup ( gPaper ) ; clearGroup ( gMarkers ) ; clearGroup ( gMeasured ) ; clearGroup ( gCameras ) ;
if ( IS _HOMING ) clearGroup ( gSkeleton ) ;
return ;
}
buildScene ( data ) ;
@@ -839,6 +927,13 @@ async function loadData() {
buildCompareLines ( ) ;
const robotLabel = data . robotFile ? ` • Robot: ${ data . robotFile } ` : '' ;
statusEl . textContent = ` Run: ${ data . runDir } ${ robotLabel } • ${ new Date ( ) . toLocaleTimeString ( 'de-CH' ) } ` ;
// Skeleton im Homing-Mode
if ( IS _HOMING && data . robot ) {
_currentRobot = data . robot ;
const angles = _homingAngles ? ? data . robot . defaultPosition ? ? { } ;
buildSkeletonFK ( data . robot , angles ) ;
}
} catch ( err ) {
statusEl . textContent = ` Fehler: ${ err . message ? ? err } ` ;
}
@@ -965,6 +1060,11 @@ async function initRunSelectors() {
/** Vollständige Initialisierung: Selektoren → Pos A → Pos B → Pos C (sequenziell!) */
async function initAll ( ) {
if ( IS _HOMING ) {
// Homing-Mode: nur neuester Run laden, kein Dropdown, kein Vergleich
await loadData ( ) ;
return ;
}
await initRunSelectors ( ) ;
await loadData ( ) ; // setzt _primaryFremdMarkers
await loadCompareData ( ) ; // setzt _compareFremdMarkers + baut Linien + Y-Achse (no-op)
@@ -984,6 +1084,10 @@ document.getElementById('sel-run-compare')?.addEventListener('change', async ()
document . getElementById ( 'sel-run-c' ) ? . addEventListener ( 'change' , loadPositionC ) ;
window . addEventListener ( 'message' , async ( e ) => {
if ( e . data ? . type === 'reload' ) await initAll ( ) ;
if ( e . data ? . type === 'homing-state' && IS _HOMING ) {
_homingAngles = e . data . state ;
if ( _currentRobot ) buildSkeletonFK ( _currentRobot , _homingAngles ) ;
}
} ) ;
// ── Resize & Render-Loop ──────────────────────────────────────────────────────