@konitif/viewer-3d 0.285.3 → 0.285.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { createClockGraph, createInvalidationController, createTemporalTransport
6
6
  import { createViewerTemporalScheduler } from './ViewerTemporalScheduler.js';
7
7
  import { copyViewerMetadata } from './ViewerMetadata.js';
8
8
  import { resolveInteractionInputActions, resolveInteractionInputBindings } from '@konitif/tools/input';
9
- import { AxesHelper, Box3, BoxGeometry, BufferGeometry, CanvasTexture, Color, ConeGeometry, CylinderGeometry, DirectionalLight, DoubleSide, Float32BufferAttribute, FogExp2, GridHelper, Group, HalfFloatType, HemisphereLight, Line, LineBasicMaterial, Matrix4, MathUtils, Mesh, MeshBasicMaterial, MeshDepthMaterial, NoToneMapping, Object3D, OrthographicCamera, Plane, PlaneGeometry, PerspectiveCamera, VSMShadowMap, Quaternion, Raycaster, Scene, CircleGeometry, SphereGeometry, Sprite, SpriteMaterial, SRGBColorSpace, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from 'three';
9
+ import { AxesHelper, Box3, BoxGeometry, BufferGeometry, CanvasTexture, Color, ConeGeometry, CylinderGeometry, DirectionalLight, DoubleSide, Float32BufferAttribute, FogExp2, GridHelper, Group, HalfFloatType, HemisphereLight, Line, LineBasicMaterial, LineSegments, Matrix4, MathUtils, Mesh, MeshBasicMaterial, MeshDepthMaterial, NoToneMapping, Object3D, OrthographicCamera, Plane, PlaneGeometry, PerspectiveCamera, VSMShadowMap, Quaternion, Raycaster, Scene, CircleGeometry, SphereGeometry, Sprite, SpriteMaterial, SRGBColorSpace, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from 'three';
10
10
  import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls.js';
11
11
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
12
12
  import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
@@ -17,7 +17,7 @@ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
17
17
  import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
18
18
  import { HorizontalTiltShiftShader } from 'three/examples/jsm/shaders/HorizontalTiltShiftShader.js';
19
19
  import { VerticalTiltShiftShader } from 'three/examples/jsm/shaders/VerticalTiltShiftShader.js';
20
- import { getRobotMotionAnimationDuration, interpolateRobotMotionTrackValueAtTime, toRobotMotionJointValue, resolveRobotMotionPlaybackDuration } from './robotViewerMotion.js';
20
+ import { getRobotMotionAnimationDuration, interpolateRobotMotionTrackValueAtTime, sampleRobotMotionJointValuesAtTime, toRobotMotionJointValue, resolveRobotMotionPlaybackDuration } from './robotViewerMotion.js';
21
21
  import { createViewerProjectileLaunchSolution, VIEWER3D_PROJECTILE_PARTICLE_LIMIT, createViewerProjectileTrajectoryPoints, resolveViewerProjectileChargedLaunchProfile, resolveViewerProjectileImpulse, resolveViewerProjectileLauncherWorldScale, resolveViewerSlingshotReleasePull, stepViewerProjectileAgainstGround } from './ProjectileInteraction.js';
22
22
  import { createInteractionKeyboardSignal, DEFAULT_VIEWER_INPUT_BINDINGS, VIEWER_PROJECTILE_CHARGE_ACTION_ID, VIEWER_PROJECTILE_LAUNCH_ACTION_ID } from './ViewerInputBindings.js';
23
23
  import { ViewerProjectileParticleRenderer } from './ViewerProjectileParticleRenderer.js';
@@ -296,6 +296,7 @@ export class Viewer3D {
296
296
  projectilePointerInside = false;
297
297
  trajectoryAnimation = null;
298
298
  trajectorySignature = '';
299
+ trajectoryProjectionRevision = 0;
299
300
  trajectoryAnchors = new Map();
300
301
  themeObserver = null;
301
302
  animationLoop;
@@ -376,6 +377,8 @@ export class Viewer3D {
376
377
  poseHandleTargetTransitionState = null;
377
378
  poseHandleDragStarted = false;
378
379
  poseHandleControlsWereEnabled = false;
380
+ ledStateProjection = { visible: false, zones: {} };
381
+ ledMeshBaseColors = new WeakMap();
379
382
  suppressPoseHandleClickUntil = 0;
380
383
  poseEditBaseline = null;
381
384
  dirtyPose = false;
@@ -1176,6 +1179,7 @@ export class Viewer3D {
1176
1179
  this.resetTemporalProjection();
1177
1180
  if (this.options.physicsEnabled) {
1178
1181
  this.physicsVisualRootToRobotMatrix = null;
1182
+ this.invalidateTrajectoryPreviewProjection();
1179
1183
  if (!this.syncPhysicsKinematicPoseFromRobotPose({ holdFrames: PHYSICS_RESET_KINEMATIC_HOLD_FRAMES })) {
1180
1184
  this.syncPhysicsJointTargetsFromRobotPose();
1181
1185
  }
@@ -1201,6 +1205,7 @@ export class Viewer3D {
1201
1205
  }
1202
1206
  this.options.physicsMotorsCoupled = physicsMotorsCoupled;
1203
1207
  this.physicsVisualRootToRobotMatrix = null;
1208
+ this.invalidateTrajectoryPreviewProjection();
1204
1209
  this.resetPhysicsPostureCompensation();
1205
1210
  if (!physicsMotorsCoupled) {
1206
1211
  this.robot?.updateMatrixWorld(true);
@@ -1233,6 +1238,7 @@ export class Viewer3D {
1233
1238
  resolvePhysicsSourceString(source.metadata?.visualRootBodyName);
1234
1239
  this.physicsVisualBodyObjectNames = visualBodyObjectNames;
1235
1240
  this.physicsVisualRootToRobotMatrix = null;
1241
+ this.invalidateTrajectoryPreviewProjection();
1236
1242
  this.physicsResetPose = null;
1237
1243
  this.physicsResetRobotTransform = null;
1238
1244
  this.physicsResetBaselineCaptureFrames = 0;
@@ -1270,6 +1276,7 @@ export class Viewer3D {
1270
1276
  this.scrubPreviewState = null;
1271
1277
  this.playbackSyncSignature = '';
1272
1278
  this.physicsVisualRootToRobotMatrix = null;
1279
+ this.invalidateTrajectoryPreviewProjection();
1273
1280
  this.physicsAuthoredJointTargets.clear();
1274
1281
  this.resetPhysicsPostureCompensation();
1275
1282
  this.comparisonViewerSample = null;
@@ -1281,6 +1288,7 @@ export class Viewer3D {
1281
1288
  this.lastPhysicsStepAt = 0;
1282
1289
  this.resetTemporalProjection();
1283
1290
  this.physicsVisualRootToRobotMatrix = null;
1291
+ this.invalidateTrajectoryPreviewProjection();
1284
1292
  if (this.options.physicsMotorsCoupled) {
1285
1293
  if (!this.syncPhysicsKinematicPoseFromRobotPose({ holdFrames: PHYSICS_RESET_KINEMATIC_HOLD_FRAMES })) {
1286
1294
  this.syncPhysicsJointTargetsFromRobotPose();
@@ -1960,7 +1968,7 @@ export class Viewer3D {
1960
1968
  return;
1961
1969
  }
1962
1970
  this.options.showEyeRingDebug = showEyeRingDebug;
1963
- if (showEyeRingDebug || this.options.showEyeRingMire) {
1971
+ if (showEyeRingDebug || this.options.showEyeRingMire || this.ledStateProjection.visible) {
1964
1972
  this.updateEyeRingDebug();
1965
1973
  }
1966
1974
  else {
@@ -1973,7 +1981,7 @@ export class Viewer3D {
1973
1981
  return;
1974
1982
  }
1975
1983
  this.options.showEyeRingMire = showEyeRingMire;
1976
- if (showEyeRingMire || this.options.showEyeRingDebug) {
1984
+ if (showEyeRingMire || this.options.showEyeRingDebug || this.ledStateProjection.visible) {
1977
1985
  this.updateEyeRingDebug();
1978
1986
  }
1979
1987
  else {
@@ -1983,11 +1991,38 @@ export class Viewer3D {
1983
1991
  }
1984
1992
  setEyeRingCalibration(eyeRingCalibration) {
1985
1993
  this.options.eyeRingCalibration = eyeRingCalibration;
1986
- if (this.options.showEyeRingDebug || this.options.showEyeRingMire) {
1994
+ if (this.options.showEyeRingDebug || this.options.showEyeRingMire || this.ledStateProjection.visible) {
1987
1995
  this.updateEyeRingDebug();
1988
1996
  }
1989
1997
  this.requestRender();
1990
1998
  }
1999
+ setLedStateProjection(projection) {
2000
+ this.ledStateProjection = projection
2001
+ ? {
2002
+ visible: projection.visible === true,
2003
+ zones: Object.fromEntries(Object.entries(projection.zones).map(([zoneId, zone]) => [
2004
+ zoneId,
2005
+ zone
2006
+ ? {
2007
+ color: typeof zone.color === 'string' ? zone.color : undefined,
2008
+ colors: Array.isArray(zone.colors) ? [...zone.colors] : undefined,
2009
+ intensity: Number.isFinite(zone.intensity) ? Math.max(0, Math.min(1, zone.intensity ?? 1)) : 1
2010
+ }
2011
+ : undefined
2012
+ ]))
2013
+ }
2014
+ : { visible: false, zones: {} };
2015
+ if (this.ledStateProjection.visible || this.options.showEyeRingDebug || this.options.showEyeRingMire) {
2016
+ this.updateEyeRingDebug();
2017
+ }
2018
+ else {
2019
+ const robot = this.getPoseControlRobot();
2020
+ if (robot)
2021
+ this.updateEarMeshDiffusers(robot, []);
2022
+ this.updateEyeRingDebugVisibility();
2023
+ }
2024
+ this.requestRender();
2025
+ }
1991
2026
  setBalanceDebugVisible(showBalanceDebug) {
1992
2027
  if (this.options.showBalanceDebug === showBalanceDebug) {
1993
2028
  return;
@@ -2003,7 +2038,12 @@ export class Viewer3D {
2003
2038
  }
2004
2039
  setTrajectoryPreview(animation) {
2005
2040
  if (this.trajectoryAnimation === animation) {
2006
- this.updateTrajectoryGroupVisibility();
2041
+ if (this.options.showTrajectories) {
2042
+ this.rebuildTrajectoryPreview();
2043
+ }
2044
+ else {
2045
+ this.updateTrajectoryGroupVisibility();
2046
+ }
2007
2047
  return;
2008
2048
  }
2009
2049
  this.trajectoryAnimation = animation;
@@ -2027,6 +2067,7 @@ export class Viewer3D {
2027
2067
  this.captureMaterialBaselines(robot, this.robotMaterialBaselines);
2028
2068
  this.resetTemporalProjection();
2029
2069
  this.physicsVisualRootToRobotMatrix = null;
2070
+ this.invalidateTrajectoryPreviewProjection();
2030
2071
  this.physicsProjectionMinimumStateRevision = null;
2031
2072
  this.physicsResetPose = null;
2032
2073
  this.physicsResetRobotTransform = null;
@@ -2356,6 +2397,9 @@ export class Viewer3D {
2356
2397
  this.playbackState?.isPlaying &&
2357
2398
  this.playbackState.animation === animation &&
2358
2399
  this.playbackSyncSignature === syncSignature) {
2400
+ if (this.options.showTrajectories && animation) {
2401
+ this.rebuildTrajectoryPreview();
2402
+ }
2359
2403
  return;
2360
2404
  }
2361
2405
  const nextPlaybackState = {
@@ -2409,6 +2453,7 @@ export class Viewer3D {
2409
2453
  this.alignUnbufferedPlaybackPoseToSupportFloor();
2410
2454
  }
2411
2455
  }
2456
+ this.rebuildTrajectoryPreview();
2412
2457
  this.updateTrajectoryCurrentMarkers();
2413
2458
  if (nextPlaybackState.clockMode === 'realtime') {
2414
2459
  this.scheduleLoop();
@@ -2438,6 +2483,7 @@ export class Viewer3D {
2438
2483
  this.alignUnbufferedPlaybackPoseToSupportFloor();
2439
2484
  }
2440
2485
  }
2486
+ this.rebuildTrajectoryPreview();
2441
2487
  this.updateTrajectoryCurrentMarkers();
2442
2488
  if (shouldStartViewerPlaybackCompletionTransition({
2443
2489
  isPlaying,
@@ -3942,12 +3988,12 @@ export class Viewer3D {
3942
3988
  if (this.lastFrameAt > 0) {
3943
3989
  const frameMs = now - this.lastFrameAt;
3944
3990
  this.averageFrameMs = this.averageFrameMs * 0.9 + frameMs * 0.1;
3945
- this.tunePixelRatio(this.averageFrameMs);
3946
3991
  }
3947
3992
  this.lastFrameAt = now;
3948
3993
  const renderStartedAt = performance.now();
3949
3994
  this.render();
3950
3995
  this.averageRenderMs = this.updateProfilerAverage(this.averageRenderMs, performance.now() - renderStartedAt);
3996
+ this.tunePixelRatio(this.averageRenderMs);
3951
3997
  this.lastRenderCalls = this.renderer.info.render.calls;
3952
3998
  this.lastRenderTriangles = this.renderer.info.render.triangles;
3953
3999
  this.renderFramesSinceReport += 1;
@@ -4002,6 +4048,16 @@ export class Viewer3D {
4002
4048
  if (this.observedRobotGhostMirrorsSimulatedRobot) {
4003
4049
  this.syncObservedRobotGhostFromSimulatedRobot();
4004
4050
  }
4051
+ if (this.options.showTrajectories &&
4052
+ this.trajectoryAnimation?.motion.hasMotion &&
4053
+ this.trajectorySignature === '') {
4054
+ this.rebuildTrajectoryPreview();
4055
+ }
4056
+ // Every pose authority (authored playback, runtime-history replay and
4057
+ // physics projection) converges here. Refreshing the markers immediately
4058
+ // before drawing prevents a solver step or a root presentation offset
4059
+ // from leaving them one frame behind the rendered robot.
4060
+ this.updateTrajectoryCurrentMarkers();
4005
4061
  this.updatePoseHandleScreenScale();
4006
4062
  this.updateProjectileCameraProjection();
4007
4063
  const ground = this.visualGroundLayer;
@@ -6000,6 +6056,7 @@ export class Viewer3D {
6000
6056
  const previousPresentedY = this.robot.position.y + this.simulatedRobotPresentationRoot.position.y;
6001
6057
  if (!this.physicsVisualRootToRobotMatrix) {
6002
6058
  this.physicsVisualRootToRobotMatrix = this.resolvePhysicsVisualRootToRobotMatrix(physicsRootWorldMatrix, bodyTransforms);
6059
+ this.invalidateTrajectoryPreviewProjection();
6003
6060
  }
6004
6061
  const nextRobotWorldMatrix = physicsRootWorldMatrix.clone().multiply(this.physicsVisualRootToRobotMatrix);
6005
6062
  const parent = this.robot.parent;
@@ -6285,7 +6342,7 @@ export class Viewer3D {
6285
6342
  if (this.options.showContactPoints) {
6286
6343
  this.updateContactPoints();
6287
6344
  }
6288
- if (this.options.showEyeRingDebug || this.options.showEyeRingMire) {
6345
+ if (this.options.showEyeRingDebug || this.options.showEyeRingMire || this.ledStateProjection.visible) {
6289
6346
  this.updateEyeRingDebug();
6290
6347
  }
6291
6348
  if (this.options.showBalanceDebug) {
@@ -8391,6 +8448,7 @@ export class Viewer3D {
8391
8448
  if (!rootTransform)
8392
8449
  return;
8393
8450
  this.physicsVisualRootToRobotMatrix = this.resolvePhysicsVisualRootToRobotMatrix(this.createPhysicsBodyViewerWorldMatrix(rootTransform), bodyTransforms);
8451
+ this.invalidateTrajectoryPreviewProjection();
8394
8452
  }
8395
8453
  projectProjectilePhysicsObservation() {
8396
8454
  const bodyTransforms = this.physicsService.getBodyTransforms();
@@ -9236,7 +9294,7 @@ export class Viewer3D {
9236
9294
  : this.options.showRobot && this.robotComparisonAppearance.mode !== 'observed-only';
9237
9295
  this.eyeRingDebugGroup.visible =
9238
9296
  subjectVisible &&
9239
- (this.options.showEyeRingDebug || this.options.showEyeRingMire) &&
9297
+ (this.options.showEyeRingDebug || this.options.showEyeRingMire || this.ledStateProjection.visible) &&
9240
9298
  this.eyeRingDebugGroup.children.length > 0;
9241
9299
  }
9242
9300
  clearEyeRingDebug() {
@@ -9251,27 +9309,34 @@ export class Viewer3D {
9251
9309
  }
9252
9310
  updateEyeRingDebug() {
9253
9311
  const robot = this.getPoseControlRobot();
9254
- if ((!this.options.showEyeRingDebug && !this.options.showEyeRingMire) || !robot) {
9312
+ if ((!this.options.showEyeRingDebug && !this.options.showEyeRingMire && !this.ledStateProjection.visible) || !robot) {
9313
+ if (robot)
9314
+ this.updateEarMeshDiffusers(robot, []);
9255
9315
  this.updateEyeRingDebugVisibility();
9256
9316
  return;
9257
9317
  }
9258
9318
  robot.updateMatrixWorld(true);
9259
9319
  const calibration = this.resolveEyeRingCalibration();
9260
9320
  const ledRingPoses = this.resolveLedRingPoses(calibration);
9321
+ this.updateEarMeshDiffusers(robot, ledRingPoses.filter((pose) => pose.zoneId === 'ears'));
9261
9322
  if (ledRingPoses.length === 0) {
9262
9323
  this.clearEyeRingDebug();
9263
9324
  return;
9264
9325
  }
9265
9326
  const group = this.ensureEyeRingDebugGroup();
9266
9327
  this.clearEyeRingDebug();
9267
- if (this.options.showEyeRingDebug) {
9328
+ if (this.options.showEyeRingDebug || this.ledStateProjection.visible) {
9268
9329
  for (const ring of ledRingPoses) {
9269
9330
  const ledCount = Math.max(ring.calibration.layout === 'strip' ? 1 : 3, Math.min(64, Math.round(ring.calibration.ledCount)));
9270
- if (ring.calibration.mireVisible) {
9331
+ if (this.options.showEyeRingDebug && ring.calibration.mireVisible) {
9271
9332
  for (const [index, dimension] of this.resolveEyeRingDebugDimensions(ring.calibration).entries()) {
9272
9333
  group.add(this.createLedLayoutGuide(ring, dimension, index === 0 ? '#22c55e' : '#67e8f9', index === 0 ? 0.84 : 0.42));
9273
9334
  }
9274
9335
  }
9336
+ if (ring.zoneId === 'ears' && this.ledStateProjection.visible) {
9337
+ group.add(...this.createEarSpeakerGrille(ring));
9338
+ continue;
9339
+ }
9275
9340
  for (let index = 0; index < ledCount; index += 1) {
9276
9341
  group.add(this.createEyeRingLedMarker(ring, index, ledCount, ring.calibration.ledSize));
9277
9342
  }
@@ -9578,12 +9643,15 @@ export class Viewer3D {
9578
9643
  return line;
9579
9644
  }
9580
9645
  createEyeRingLedMarker(eye, index, count, ledSize) {
9581
- const color = index % 2 === 0 ? '#22c55e' : '#86efac';
9646
+ const zoneState = this.ledStateProjection.visible ? this.ledStateProjection.zones[eye.zoneId] : undefined;
9647
+ const projectedColor = zoneState?.colors?.[index % Math.max(1, zoneState.colors.length)] ?? zoneState?.color;
9648
+ const color = projectedColor ?? (this.ledStateProjection.visible ? '#111827' : index % 2 === 0 ? '#22c55e' : '#86efac');
9649
+ const intensity = zoneState?.intensity ?? (this.ledStateProjection.visible ? 0.3 : 0.92);
9582
9650
  const markerSize = Math.max(0.001, Math.min(0.02, ledSize));
9583
9651
  const marker = new Mesh(new SphereGeometry(markerSize, 12, 8), new MeshBasicMaterial({
9584
9652
  color,
9585
9653
  transparent: true,
9586
- opacity: 0.92,
9654
+ opacity: intensity,
9587
9655
  depthTest: false,
9588
9656
  depthWrite: false
9589
9657
  }));
@@ -9596,6 +9664,137 @@ export class Viewer3D {
9596
9664
  marker.renderOrder = 1000;
9597
9665
  return marker;
9598
9666
  }
9667
+ /** The speaker grille stays dark; only its peripheral diffuser receives LED light. */
9668
+ createEarSpeakerGrille(ear) {
9669
+ const normal = new Vector3(0, 1, 0).applyQuaternion(ear.orientation).normalize();
9670
+ const surface = new Mesh(new CircleGeometry(ear.radius * 0.76, 72), new MeshBasicMaterial({
9671
+ color: '#070a0d',
9672
+ transparent: true,
9673
+ opacity: 0.96,
9674
+ depthTest: true,
9675
+ depthWrite: false,
9676
+ side: DoubleSide
9677
+ }));
9678
+ surface.geometry.rotateX(-Math.PI * 0.5);
9679
+ surface.position.copy(ear.center).addScaledVector(normal, 0.00035);
9680
+ surface.quaternion.copy(ear.orientation);
9681
+ surface.name = `${ear.id}-speaker-grille-surface`;
9682
+ surface.renderOrder = 996;
9683
+ const grilleRadius = ear.radius * 0.70;
9684
+ const cellRadius = Math.max(0.0012, ear.radius * 0.082);
9685
+ const columnStep = Math.sqrt(3) * cellRadius;
9686
+ const rowStep = cellRadius * 1.5;
9687
+ const points = [];
9688
+ for (let row = -9; row <= 9; row += 1) {
9689
+ for (let column = -9; column <= 9; column += 1) {
9690
+ const centerX = column * columnStep + (Math.abs(row) % 2) * columnStep * 0.5;
9691
+ const centerZ = row * rowStep;
9692
+ if (Math.hypot(centerX, centerZ) + cellRadius > grilleRadius)
9693
+ continue;
9694
+ for (let edge = 0; edge < 6; edge += 1) {
9695
+ const angleA = Math.PI / 6 + edge * Math.PI / 3;
9696
+ const angleB = Math.PI / 6 + ((edge + 1) % 6) * Math.PI / 3;
9697
+ points.push(new Vector3(centerX + Math.cos(angleA) * cellRadius, 0, centerZ + Math.sin(angleA) * cellRadius), new Vector3(centerX + Math.cos(angleB) * cellRadius, 0, centerZ + Math.sin(angleB) * cellRadius));
9698
+ }
9699
+ }
9700
+ }
9701
+ const lattice = new LineSegments(new BufferGeometry().setFromPoints(points), new LineBasicMaterial({
9702
+ color: '#59616b',
9703
+ transparent: true,
9704
+ opacity: 0.9,
9705
+ depthTest: true,
9706
+ depthWrite: false
9707
+ }));
9708
+ lattice.position.copy(ear.center).addScaledVector(normal, 0.00055);
9709
+ lattice.quaternion.copy(ear.orientation);
9710
+ lattice.name = `${ear.id}-speaker-grille-lattice`;
9711
+ lattice.renderOrder = 997;
9712
+ return [surface, lattice];
9713
+ }
9714
+ /** Projects the timeline colors onto the authored outer ear diffuser band. */
9715
+ updateEarMeshDiffusers(robot, ears) {
9716
+ const zoneState = this.ledStateProjection.zones.ears;
9717
+ const intensity = Math.max(0, Math.min(1, zoneState?.intensity ?? 0.25));
9718
+ const sourceColors = zoneState?.colors?.length ? zoneState.colors : null;
9719
+ const earFrames = ears.map((ear) => {
9720
+ const count = Math.max(3, Math.min(32, Math.round(ear.calibration.ledCount)));
9721
+ return {
9722
+ ear,
9723
+ inverseOrientation: ear.orientation.clone().invert(),
9724
+ colors: Array.from({ length: count }, (_, index) => new Color(sourceColors?.[index % sourceColors.length] ?? zoneState?.color ?? '#071018'))
9725
+ };
9726
+ });
9727
+ const worldPosition = new Vector3();
9728
+ const localPosition = new Vector3();
9729
+ let found = false;
9730
+ robot.updateMatrixWorld(true);
9731
+ robot.traverse((object) => {
9732
+ const mesh = object;
9733
+ if (!mesh.isMesh)
9734
+ return;
9735
+ // Older hot-reloaded robot instances may still contain the source
9736
+ // speaker bulb that used to be extracted and incorrectly treated as a
9737
+ // segmented LED surface. Retire that legacy projection immediately;
9738
+ // the authored body and the grille projection own the ear appearance.
9739
+ if (mesh.name === 'viewer-led-diffuser-ears' || mesh.userData.viewerLedZone === 'ears') {
9740
+ mesh.visible = false;
9741
+ return;
9742
+ }
9743
+ const positions = mesh.geometry.getAttribute('position');
9744
+ const colors = mesh.geometry.getAttribute('color');
9745
+ if (!positions || !colors || colors.itemSize < 3)
9746
+ return;
9747
+ let baseline = this.ledMeshBaseColors.get(mesh.geometry);
9748
+ if (!baseline || baseline.length !== colors.array.length) {
9749
+ baseline = new Float32Array(colors.array);
9750
+ this.ledMeshBaseColors.set(mesh.geometry, baseline);
9751
+ }
9752
+ for (let index = 0; index < positions.count; index += 1) {
9753
+ const offset = index * colors.itemSize;
9754
+ colors.setXYZ(index, baseline[offset], baseline[offset + 1], baseline[offset + 2]);
9755
+ if (!this.ledStateProjection.visible || earFrames.length === 0)
9756
+ continue;
9757
+ worldPosition.fromBufferAttribute(positions, index).applyMatrix4(mesh.matrixWorld);
9758
+ let frame = earFrames[0];
9759
+ let distance = worldPosition.distanceToSquared(frame.ear.center);
9760
+ for (let candidateIndex = 1; candidateIndex < earFrames.length; candidateIndex += 1) {
9761
+ const candidate = earFrames[candidateIndex];
9762
+ const candidateDistance = worldPosition.distanceToSquared(candidate.ear.center);
9763
+ if (candidateDistance < distance) {
9764
+ frame = candidate;
9765
+ distance = candidateDistance;
9766
+ }
9767
+ }
9768
+ localPosition.copy(worldPosition).sub(frame.ear.center).applyQuaternion(frame.inverseOrientation);
9769
+ const radialDistance = Math.hypot(localPosition.x, localPosition.z);
9770
+ // Persisted calibrations created before the authored diffuser mapping
9771
+ // used a 16 mm marker radius. Never let that legacy presentation value
9772
+ // select the 25 mm inner speaker bulb again: the light-emitting plastic
9773
+ // is the outer annulus of the authored head shell.
9774
+ const innerRadius = Math.max(0.028, frame.ear.radius * 1.06);
9775
+ const outerRadius = Math.max(0.041, frame.ear.radius * 1.5);
9776
+ const surfaceDepth = Math.max(0.004, frame.ear.radius * 0.48);
9777
+ if (radialDistance < innerRadius ||
9778
+ radialDistance > outerRadius ||
9779
+ Math.abs(localPosition.y) > surfaceDepth) {
9780
+ continue;
9781
+ }
9782
+ found = true;
9783
+ let angle = Math.atan2(localPosition.x, localPosition.z);
9784
+ if (angle < 0)
9785
+ angle += Math.PI * 2;
9786
+ const colorPosition = angle / (Math.PI * 2) * frame.colors.length;
9787
+ const colorIndex = Math.floor(colorPosition) % frame.colors.length;
9788
+ const nextColorIndex = (colorIndex + 1) % frame.colors.length;
9789
+ const color = frame.colors[colorIndex].clone()
9790
+ .lerp(frame.colors[nextColorIndex], colorPosition - Math.floor(colorPosition))
9791
+ .multiplyScalar(intensity);
9792
+ colors.setXYZ(index, color.r, color.g, color.b);
9793
+ }
9794
+ colors.needsUpdate = true;
9795
+ });
9796
+ return found;
9797
+ }
9599
9798
  createEyeRingMireLines(eye) {
9600
9799
  const axisVertical = new Vector3(1, 0, 0).applyQuaternion(eye.orientation).normalize();
9601
9800
  const forward = new Vector3(0, 1, 0).applyQuaternion(eye.orientation).normalize();
@@ -10145,6 +10344,10 @@ export class Viewer3D {
10145
10344
  marker.name = 'balance-center-of-mass-ground-projection';
10146
10345
  return marker;
10147
10346
  }
10347
+ invalidateTrajectoryPreviewProjection() {
10348
+ this.trajectoryProjectionRevision += 1;
10349
+ this.trajectorySignature = '';
10350
+ }
10148
10351
  ensureTrajectoryGroup() {
10149
10352
  if (this.trajectoryGroup) {
10150
10353
  return this.trajectoryGroup;
@@ -10152,7 +10355,7 @@ export class Viewer3D {
10152
10355
  this.trajectoryGroup = new Group();
10153
10356
  this.trajectoryGroup.name = 'AnimationTrajectoryPreview';
10154
10357
  this.trajectoryGroup.visible = false;
10155
- this.scene.add(this.trajectoryGroup);
10358
+ this.simulatedRobotPresentationRoot.add(this.trajectoryGroup);
10156
10359
  return this.trajectoryGroup;
10157
10360
  }
10158
10361
  updateTrajectoryGroupVisibility() {
@@ -10201,18 +10404,31 @@ export class Viewer3D {
10201
10404
  this.clearTrajectoryPreview();
10202
10405
  this.trajectorySignature = nextSignature;
10203
10406
  const originalPose = this.captureJointPose();
10407
+ const originalRobotTransform = this.captureRobotTransform(this.robot);
10408
+ const temporalSampleResolver = this.playbackState?.animation === animation
10409
+ ? this.playbackState.resolveTemporalProjectionSample
10410
+ : null;
10411
+ let temporalRootAlignment = this.physicsVisualRootToRobotMatrix?.clone() ?? null;
10204
10412
  const previousAnimatedTargets = new Set(this.lastAnimatedJointTargets);
10205
10413
  const sampleCount = Math.max(ROBOT_VIEWER_TRAJECTORY_MIN_SAMPLES, Math.min(ROBOT_VIEWER_TRAJECTORY_MAX_SAMPLES, Math.ceil(duration * ROBOT_VIEWER_TRAJECTORY_SAMPLE_RATE)));
10206
10414
  const pointsByTarget = new Map(targets.map((target) => [target.id, []]));
10207
10415
  for (let sampleIndex = 0; sampleIndex <= sampleCount; sampleIndex += 1) {
10208
10416
  const sampleTime = (sampleIndex / sampleCount) * duration;
10209
- this.applyRobotPose(animation, sampleTime, [], duration);
10417
+ const temporalSample = temporalSampleResolver?.(sampleTime) ?? null;
10418
+ this.restoreRobotTransform(this.robot, originalRobotTransform);
10419
+ if (temporalSample?.viewer) {
10420
+ temporalRootAlignment = this.applyTrajectoryTemporalSamplePose(temporalSample, temporalRootAlignment);
10421
+ }
10422
+ else {
10423
+ this.applyTrajectorySamplePose(animation, sampleTime, duration);
10424
+ }
10210
10425
  this.robot.updateMatrixWorld(true);
10211
10426
  for (const target of targets) {
10212
- pointsByTarget.get(target.id)?.push(this.getTrajectoryPoint(target.id));
10427
+ pointsByTarget.get(target.id)?.push(this.getTrajectoryPointInProjectionFrame(target.id));
10213
10428
  }
10214
10429
  }
10215
- this.restoreJointPose(originalPose);
10430
+ this.restoreRobotTransform(this.robot, originalRobotTransform);
10431
+ this.restoreTrajectorySamplePose(originalPose);
10216
10432
  this.lastAnimatedJointTargets = previousAnimatedTargets;
10217
10433
  this.nextAnimatedJointTargets.clear();
10218
10434
  for (const target of targets) {
@@ -10222,26 +10438,100 @@ export class Viewer3D {
10222
10438
  }
10223
10439
  trajectoryGroup.add(this.createTrajectoryObject(target, points));
10224
10440
  }
10225
- this.updateTrajectoryCurrentMarkers();
10226
10441
  this.updateTrajectoryGroupVisibility();
10442
+ this.updateTrajectoryCurrentMarkers();
10227
10443
  this.requestRender();
10228
10444
  }
10445
+ applyTrajectorySamplePose(animation, time, duration) {
10446
+ const joints = this.getRobotJoints();
10447
+ for (const { target, value } of sampleRobotMotionJointValuesAtTime(animation, time, duration)) {
10448
+ const joint = joints[target];
10449
+ if (joint)
10450
+ this.applyDesiredProjectionJointValue(joint, value);
10451
+ }
10452
+ }
10453
+ applyTrajectoryTemporalSamplePose(sample, rootAlignment) {
10454
+ const viewer = sample.viewer;
10455
+ const robot = this.robot;
10456
+ if (!viewer || !robot) {
10457
+ return rootAlignment;
10458
+ }
10459
+ const joints = this.getRobotJoints();
10460
+ for (const [target, value] of Object.entries(viewer.joints ?? {})) {
10461
+ const joint = joints[target];
10462
+ if (joint && Number.isFinite(value)) {
10463
+ this.applyDesiredProjectionJointValue(joint, value);
10464
+ }
10465
+ }
10466
+ const bodyTransforms = (viewer.physics?.bodyTransforms ?? []).map((transform) => this.deserializeRuntimeHistoryTransform(transform, false));
10467
+ const rootTransform = bodyTransforms.find((transform) => transform.metadata?.visualRoot === true) ??
10468
+ bodyTransforms.find((transform) => transform.bodyName === this.physicsVisualRootBodyName);
10469
+ let nextRootAlignment = rootAlignment;
10470
+ if (rootTransform) {
10471
+ const physicsRootWorldMatrix = this.createPhysicsBodyViewerWorldMatrix(rootTransform);
10472
+ nextRootAlignment ??= this.resolvePhysicsVisualRootToRobotMatrix(physicsRootWorldMatrix, bodyTransforms);
10473
+ const nextRobotLocalMatrix = physicsRootWorldMatrix.clone().multiply(nextRootAlignment);
10474
+ const parent = robot.parent;
10475
+ if (parent && parent !== this.simulatedRobotPresentationRoot) {
10476
+ parent.updateMatrixWorld(true);
10477
+ nextRobotLocalMatrix.premultiply(parent.matrixWorld.clone().invert());
10478
+ }
10479
+ robot.matrix.copy(nextRobotLocalMatrix);
10480
+ robot.matrix.decompose(robot.position, robot.quaternion, robot.scale);
10481
+ }
10482
+ robot.updateMatrixWorld(true);
10483
+ return nextRootAlignment;
10484
+ }
10485
+ restoreTrajectorySamplePose(pose) {
10486
+ const joints = this.getRobotJoints();
10487
+ for (const [target, value] of Object.entries(pose)) {
10488
+ const joint = joints[target];
10489
+ if (joint)
10490
+ this.applyDesiredProjectionJointValue(joint, value);
10491
+ }
10492
+ this.robot?.updateMatrixWorld(true);
10493
+ }
10229
10494
  getTrajectorySignature(animation, targets, duration) {
10230
10495
  return JSON.stringify({
10231
10496
  duration: Number(duration.toFixed(3)),
10497
+ projectionRevision: this.trajectoryProjectionRevision,
10498
+ playbackProjection: this.getTrajectoryPlaybackProjectionSignature(animation),
10232
10499
  handsOnly: this.options.showHandTrajectoriesOnly,
10233
10500
  targets: targets.map((target) => target.id),
10501
+ temporalProjection: this.getTrajectoryTemporalProjectionSignature(animation, duration),
10234
10502
  tracks: animation.motion.tracks.map((track) => ({
10235
10503
  target: track.target,
10236
10504
  property: track.property,
10237
- keys: track.keys.length,
10238
- firstTime: track.keys[0]?.time ?? null,
10239
- lastTime: track.keys[track.keys.length - 1]?.time ?? null,
10240
- firstValue: track.keys[0]?.value ?? null,
10241
- lastValue: track.keys[track.keys.length - 1]?.value ?? null
10505
+ interpolation: track.interpolation ?? null,
10506
+ unitKind: track.unitKind ?? null,
10507
+ keys: track.keys.map((key) => [key.time, key.value])
10242
10508
  }))
10243
10509
  });
10244
10510
  }
10511
+ getTrajectoryPlaybackProjectionSignature(animation) {
10512
+ const playback = this.playbackState;
10513
+ if (playback?.animation !== animation) {
10514
+ return null;
10515
+ }
10516
+ return {
10517
+ mutedTargets: [...playback.mutedTrackTargetSet].sort(),
10518
+ startTransitionMinimumDurationSeconds: playback.startTransitionMinimumDurationSeconds,
10519
+ entryPose: this.createPoseSignature(playback.entryPose),
10520
+ endTransitionMinimumDurationSeconds: playback.endTransitionMinimumDurationSeconds,
10521
+ exitPose: this.createPoseSignature(playback.exitPose)
10522
+ };
10523
+ }
10524
+ getTrajectoryTemporalProjectionSignature(animation, duration) {
10525
+ const playback = this.playbackState;
10526
+ const resolver = playback?.animation === animation ? playback.resolveTemporalProjectionSample : null;
10527
+ if (!resolver) {
10528
+ return null;
10529
+ }
10530
+ return [0, duration * 0.5, duration].map((time) => {
10531
+ const sample = resolver(time);
10532
+ return sample ? [sample.id, Number(sample.timeSeconds.toFixed(4))] : null;
10533
+ });
10534
+ }
10245
10535
  getTrajectoryPreviewTargets(animation) {
10246
10536
  if (this.options.showHandTrajectoriesOnly) {
10247
10537
  return (this.options.handTipDefinitions ?? [])
@@ -10332,9 +10622,12 @@ export class Viewer3D {
10332
10622
  if (!target || !currentMarker) {
10333
10623
  continue;
10334
10624
  }
10335
- currentMarker.position.copy(this.getTrajectoryPoint(target));
10625
+ currentMarker.position.copy(this.getTrajectoryPointInProjectionFrame(target));
10336
10626
  }
10337
10627
  }
10628
+ getTrajectoryPointInProjectionFrame(target) {
10629
+ return projectViewerTrajectoryPointToFrame(this.getTrajectoryPoint(target), this.simulatedRobotPresentationRoot);
10630
+ }
10338
10631
  getTrajectoryPoint(target) {
10339
10632
  const handTip = (this.options.handTipDefinitions ?? []).find((definition) => definition.id === target);
10340
10633
  if (handTip) {
@@ -10413,10 +10706,10 @@ export class Viewer3D {
10413
10706
  this.trajectoryAnchors.set(target, anchor);
10414
10707
  return anchor;
10415
10708
  }
10709
+ joint.updateMatrixWorld?.(true);
10416
10710
  const jointInverse = joint.matrixWorld.clone().invert();
10417
10711
  const localBounds = new Box3();
10418
10712
  let hasBounds = false;
10419
- joint.updateMatrixWorld?.(true);
10420
10713
  joint.traverse((node) => {
10421
10714
  if (node !== joint && node.isURDFJoint) {
10422
10715
  return;
@@ -10710,6 +11003,11 @@ export class Viewer3D {
10710
11003
  function formatNavigationDistance(value) {
10711
11004
  return `${value >= 0 ? '+' : ''}${value.toFixed(2)}m`;
10712
11005
  }
11006
+ /** Projects a world-space trajectory sample into the robot presentation frame. */
11007
+ export function projectViewerTrajectoryPointToFrame(point, frame) {
11008
+ frame.updateWorldMatrix(true, false);
11009
+ return frame.worldToLocal(new Vector3(point.x, point.y, point.z));
11010
+ }
10713
11011
  function formatNavigationAngle(value) {
10714
11012
  const degrees = MathUtils.radToDeg(normalizeNavigationRadians(value));
10715
11013
  return `${degrees >= 0 ? '+' : ''}${degrees.toFixed(0)}°`;