@konitif/viewer-3d 0.285.4 → 0.285.6

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.
Files changed (29) hide show
  1. package/README.md +40 -0
  2. package/dist/renderer/IncarnationMaterialProjection.d.ts +16 -0
  3. package/dist/renderer/IncarnationMaterialProjection.d.ts.map +1 -0
  4. package/dist/renderer/IncarnationMaterialProjection.js +45 -0
  5. package/dist/renderer/IncarnationMaterialProjection.js.map +1 -0
  6. package/dist/renderer/ObservedRobotPresentationBuffer.d.ts +15 -4
  7. package/dist/renderer/ObservedRobotPresentationBuffer.d.ts.map +1 -1
  8. package/dist/renderer/ObservedRobotPresentationBuffer.js +136 -8
  9. package/dist/renderer/ObservedRobotPresentationBuffer.js.map +1 -1
  10. package/dist/renderer/ObservedSoleStabilization.d.ts +41 -0
  11. package/dist/renderer/ObservedSoleStabilization.d.ts.map +1 -0
  12. package/dist/renderer/ObservedSoleStabilization.js +140 -0
  13. package/dist/renderer/ObservedSoleStabilization.js.map +1 -0
  14. package/dist/renderer/Viewer3DRenderer.d.ts +31 -0
  15. package/dist/renderer/Viewer3DRenderer.d.ts.map +1 -1
  16. package/dist/renderer/Viewer3DRenderer.js +331 -18
  17. package/dist/renderer/Viewer3DRenderer.js.map +1 -1
  18. package/dist/renderer/index.d.ts +1 -0
  19. package/dist/renderer/index.d.ts.map +1 -1
  20. package/dist/renderer/index.js +1 -0
  21. package/dist/renderer/index.js.map +1 -1
  22. package/dist/renderer/robotViewerMotion.d.ts +10 -0
  23. package/dist/renderer/robotViewerMotion.d.ts.map +1 -1
  24. package/dist/renderer/robotViewerMotion.js +19 -0
  25. package/dist/renderer/robotViewerMotion.js.map +1 -1
  26. package/dist/supportDefinition.d.ts +5 -0
  27. package/dist/supportDefinition.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/reference/catalog.json +3 -0
@@ -5,6 +5,7 @@ import { resolvePhysicsSourceString, resolvePhysicsSourceStringListMap, resolveP
5
5
  import { createClockGraph, createInvalidationController, createTemporalTransport } from '@konitif/temporal';
6
6
  import { createViewerTemporalScheduler } from './ViewerTemporalScheduler.js';
7
7
  import { copyViewerMetadata } from './ViewerMetadata.js';
8
+ import { IncarnationMaterialProjector, normalizeIncarnationMaterialProjection } from './IncarnationMaterialProjection.js';
8
9
  import { resolveInteractionInputActions, resolveInteractionInputBindings } from '@konitif/tools/input';
9
10
  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
11
  import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls.js';
@@ -17,7 +18,7 @@ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
17
18
  import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
18
19
  import { HorizontalTiltShiftShader } from 'three/examples/jsm/shaders/HorizontalTiltShiftShader.js';
19
20
  import { VerticalTiltShiftShader } from 'three/examples/jsm/shaders/VerticalTiltShiftShader.js';
20
- import { getRobotMotionAnimationDuration, interpolateRobotMotionTrackValueAtTime, toRobotMotionJointValue, resolveRobotMotionPlaybackDuration } from './robotViewerMotion.js';
21
+ import { getRobotMotionAnimationDuration, interpolateRobotMotionTrackValueAtTime, sampleRobotMotionJointValuesAtTime, toRobotMotionJointValue, resolveRobotMotionPlaybackDuration } from './robotViewerMotion.js';
21
22
  import { createViewerProjectileLaunchSolution, VIEWER3D_PROJECTILE_PARTICLE_LIMIT, createViewerProjectileTrajectoryPoints, resolveViewerProjectileChargedLaunchProfile, resolveViewerProjectileImpulse, resolveViewerProjectileLauncherWorldScale, resolveViewerSlingshotReleasePull, stepViewerProjectileAgainstGround } from './ProjectileInteraction.js';
22
23
  import { createInteractionKeyboardSignal, DEFAULT_VIEWER_INPUT_BINDINGS, VIEWER_PROJECTILE_CHARGE_ACTION_ID, VIEWER_PROJECTILE_LAUNCH_ACTION_ID } from './ViewerInputBindings.js';
23
24
  import { ViewerProjectileParticleRenderer } from './ViewerProjectileParticleRenderer.js';
@@ -30,6 +31,8 @@ import { resolveViewerIncarnationHealth } from './ViewerIncarnationHealth.js';
30
31
  import { PerformanceMonitor } from './PerformanceMonitor.js';
31
32
  import { ViewerTemporalProjectionBuffer } from './TemporalProjectionModel.js';
32
33
  import { ObservedRobotPresentationBuffer } from './ObservedRobotPresentationBuffer.js';
34
+ import { ObservedSoleAnchors, projectBoundedSoleCorrection } from './ObservedSoleStabilization.js';
35
+ import { ConvexHull } from 'three/examples/jsm/math/ConvexHull.js';
33
36
  import { resolveViewerFrameSignalIntervalMs, VIEWER_FRAME_SIGNAL_CATALOG } from './ViewerFrameSignals.js';
34
37
  import { DEFAULT_DISPLAY_FRAME_RATE_HZ, normalizeConfiguredDisplayFrameRate, observeDisplayFrameRateCeiling, resolveDisplayFrameRateTarget } from './displayFrameRate.js';
35
38
  import { ViewerTemporalProjectionExperimentAdapter } from './ViewerTemporalProjectionExperimentAdapter.js';
@@ -219,6 +222,8 @@ export class Viewer3D {
219
222
  poseControlTarget = 'simulated';
220
223
  observedPoseLiveControlEnabled = false;
221
224
  robotMaterialBaselines = new Map();
225
+ incarnationMaterialProjector = new IncarnationMaterialProjector();
226
+ incarnationMaterialProjections = {};
222
227
  observedRobotGhostMaterialBaselines = new Map();
223
228
  observedRobotGhostRenderOrderBaselines = new Map();
224
229
  observedRobotGhostShadowMaterials = new Map();
@@ -229,6 +234,11 @@ export class Viewer3D {
229
234
  observedRobotGhostBaseQuaternion = new Quaternion();
230
235
  observedRobotGhostBaseScale = new Vector3(1, 1, 1);
231
236
  observedRobotTemporalSupportAnchorState = null;
237
+ observedSoleAnchors = new ObservedSoleAnchors();
238
+ observedSoleConnectionId = '';
239
+ observedSoleStrength = 0;
240
+ observedSoleBaseline = new Map();
241
+ observedSoleSurfaceVertices = new WeakMap();
232
242
  observedRobotGrounding = resolveObservedRobotGroundingProjection({
233
243
  enabled: false,
234
244
  floorY: ROBOT_VIEWER_SUPPORT_FLOOR_Y,
@@ -296,6 +306,7 @@ export class Viewer3D {
296
306
  projectilePointerInside = false;
297
307
  trajectoryAnimation = null;
298
308
  trajectorySignature = '';
309
+ trajectoryProjectionRevision = 0;
299
310
  trajectoryAnchors = new Map();
300
311
  themeObserver = null;
301
312
  animationLoop;
@@ -1050,11 +1061,17 @@ export class Viewer3D {
1050
1061
  sampleId: input.frameId,
1051
1062
  connectionId: input.connectionId,
1052
1063
  sourceObservedAtSeconds: input.observedAtSeconds,
1064
+ sourceClock: input.sourceClock,
1053
1065
  receivedAtMs,
1054
1066
  jointValues: input.jointValues
1055
1067
  });
1056
1068
  if (!accepted)
1057
1069
  return false;
1070
+ if (this.observedSoleConnectionId !== input.connectionId) {
1071
+ this.restoreObservedSoleBaseline();
1072
+ this.observedSoleAnchors.reset();
1073
+ this.observedSoleConnectionId = input.connectionId;
1074
+ }
1058
1075
  const frame = this.observedRobotPresentation.project(receivedAtMs);
1059
1076
  if (frame)
1060
1077
  this.applyObservedRobotGhostJointValues(frame.jointValues);
@@ -1062,14 +1079,35 @@ export class Viewer3D {
1062
1079
  return true;
1063
1080
  }
1064
1081
  resetObservedRobotGhostJointObservation() {
1082
+ this.restoreObservedSoleBaseline();
1083
+ this.observedSoleAnchors.reset();
1065
1084
  this.observedRobotPresentation.reset('discontinuity');
1066
1085
  }
1086
+ setObservedRobotSoleStabilizationStrength(value) {
1087
+ this.observedSoleAnchors ??= new ObservedSoleAnchors();
1088
+ const strength = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
1089
+ if (strength === this.observedSoleStrength)
1090
+ return;
1091
+ this.restoreObservedSoleBaseline();
1092
+ this.observedSoleAnchors.reset();
1093
+ this.observedSoleStrength = strength;
1094
+ this.applyObservedRobotGhostGrounding();
1095
+ this.requestRender();
1096
+ }
1097
+ restoreObservedSoleBaseline() {
1098
+ this.observedSoleBaseline ??= new Map();
1099
+ for (const [joint, value] of this.observedSoleBaseline)
1100
+ joint.setJointValue(value);
1101
+ this.observedSoleBaseline.clear();
1102
+ }
1067
1103
  getObservedRobotPresentationStatus() {
1068
1104
  return this.observedRobotPresentation.getStatus();
1069
1105
  }
1070
1106
  applyObservedRobotGhostJointValues(values) {
1071
1107
  if (!this.observedRobotGhost)
1072
1108
  return false;
1109
+ const hadSoleCorrection = (this.observedSoleBaseline?.size ?? 0) > 0;
1110
+ this.restoreObservedSoleBaseline();
1073
1111
  const joints = this.observedRobotGhost.joints ?? {};
1074
1112
  let changed = false;
1075
1113
  for (const [target, value] of Object.entries(values)) {
@@ -1081,7 +1119,7 @@ export class Viewer3D {
1081
1119
  continue;
1082
1120
  changed = joint.setJointValue(value) !== false || changed;
1083
1121
  }
1084
- if (changed) {
1122
+ if (changed || hadSoleCorrection) {
1085
1123
  this.applyObservedRobotGhostGrounding();
1086
1124
  if (this.poseControlTarget === 'observed') {
1087
1125
  this.refreshPoseHandleProjection();
@@ -1144,10 +1182,28 @@ export class Viewer3D {
1144
1182
  // invalidate that anchor.
1145
1183
  if (spatialLayoutChanged) {
1146
1184
  this.observedRobotTemporalSupportAnchorState = null;
1185
+ this.observedSoleAnchors?.reset();
1147
1186
  }
1148
1187
  this.applyRobotComparisonAppearance();
1149
1188
  this.requestRender();
1150
1189
  }
1190
+ /** Tune one visual incarnation independently; source meshes and telemetry remain unchanged. */
1191
+ setIncarnationMaterialProjection(target, projection) {
1192
+ if (target !== 'simulated' && target !== 'observed')
1193
+ throw new Error('Invalid material incarnation target');
1194
+ this.incarnationMaterialProjections[target] = normalizeIncarnationMaterialProjection(projection);
1195
+ this.applyIncarnationMaterialProjections();
1196
+ this.requestRender();
1197
+ }
1198
+ applyIncarnationMaterialProjections() {
1199
+ for (const target of ['simulated', 'observed']) {
1200
+ const projection = this.incarnationMaterialProjections[target];
1201
+ if (!projection)
1202
+ continue;
1203
+ const materials = target === 'simulated' ? this.robotMaterialBaselines : this.observedRobotGhostMaterialBaselines;
1204
+ this.incarnationMaterialProjector.apply(materials.keys(), projection);
1205
+ }
1206
+ }
1151
1207
  async setPhysicsEngine(physicsEngine, physicsEnabled = physicsEngine !== 'none') {
1152
1208
  if (this.destroyed)
1153
1209
  return;
@@ -1178,6 +1234,7 @@ export class Viewer3D {
1178
1234
  this.resetTemporalProjection();
1179
1235
  if (this.options.physicsEnabled) {
1180
1236
  this.physicsVisualRootToRobotMatrix = null;
1237
+ this.invalidateTrajectoryPreviewProjection();
1181
1238
  if (!this.syncPhysicsKinematicPoseFromRobotPose({ holdFrames: PHYSICS_RESET_KINEMATIC_HOLD_FRAMES })) {
1182
1239
  this.syncPhysicsJointTargetsFromRobotPose();
1183
1240
  }
@@ -1203,6 +1260,7 @@ export class Viewer3D {
1203
1260
  }
1204
1261
  this.options.physicsMotorsCoupled = physicsMotorsCoupled;
1205
1262
  this.physicsVisualRootToRobotMatrix = null;
1263
+ this.invalidateTrajectoryPreviewProjection();
1206
1264
  this.resetPhysicsPostureCompensation();
1207
1265
  if (!physicsMotorsCoupled) {
1208
1266
  this.robot?.updateMatrixWorld(true);
@@ -1235,6 +1293,7 @@ export class Viewer3D {
1235
1293
  resolvePhysicsSourceString(source.metadata?.visualRootBodyName);
1236
1294
  this.physicsVisualBodyObjectNames = visualBodyObjectNames;
1237
1295
  this.physicsVisualRootToRobotMatrix = null;
1296
+ this.invalidateTrajectoryPreviewProjection();
1238
1297
  this.physicsResetPose = null;
1239
1298
  this.physicsResetRobotTransform = null;
1240
1299
  this.physicsResetBaselineCaptureFrames = 0;
@@ -1272,6 +1331,7 @@ export class Viewer3D {
1272
1331
  this.scrubPreviewState = null;
1273
1332
  this.playbackSyncSignature = '';
1274
1333
  this.physicsVisualRootToRobotMatrix = null;
1334
+ this.invalidateTrajectoryPreviewProjection();
1275
1335
  this.physicsAuthoredJointTargets.clear();
1276
1336
  this.resetPhysicsPostureCompensation();
1277
1337
  this.comparisonViewerSample = null;
@@ -1283,6 +1343,7 @@ export class Viewer3D {
1283
1343
  this.lastPhysicsStepAt = 0;
1284
1344
  this.resetTemporalProjection();
1285
1345
  this.physicsVisualRootToRobotMatrix = null;
1346
+ this.invalidateTrajectoryPreviewProjection();
1286
1347
  if (this.options.physicsMotorsCoupled) {
1287
1348
  if (!this.syncPhysicsKinematicPoseFromRobotPose({ holdFrames: PHYSICS_RESET_KINEMATIC_HOLD_FRAMES })) {
1288
1349
  this.syncPhysicsJointTargetsFromRobotPose();
@@ -2032,7 +2093,12 @@ export class Viewer3D {
2032
2093
  }
2033
2094
  setTrajectoryPreview(animation) {
2034
2095
  if (this.trajectoryAnimation === animation) {
2035
- this.updateTrajectoryGroupVisibility();
2096
+ if (this.options.showTrajectories) {
2097
+ this.rebuildTrajectoryPreview();
2098
+ }
2099
+ else {
2100
+ this.updateTrajectoryGroupVisibility();
2101
+ }
2036
2102
  return;
2037
2103
  }
2038
2104
  this.trajectoryAnimation = animation;
@@ -2056,6 +2122,7 @@ export class Viewer3D {
2056
2122
  this.captureMaterialBaselines(robot, this.robotMaterialBaselines);
2057
2123
  this.resetTemporalProjection();
2058
2124
  this.physicsVisualRootToRobotMatrix = null;
2125
+ this.invalidateTrajectoryPreviewProjection();
2059
2126
  this.physicsProjectionMinimumStateRevision = null;
2060
2127
  this.physicsResetPose = null;
2061
2128
  this.physicsResetRobotTransform = null;
@@ -2385,6 +2452,9 @@ export class Viewer3D {
2385
2452
  this.playbackState?.isPlaying &&
2386
2453
  this.playbackState.animation === animation &&
2387
2454
  this.playbackSyncSignature === syncSignature) {
2455
+ if (this.options.showTrajectories && animation) {
2456
+ this.rebuildTrajectoryPreview();
2457
+ }
2388
2458
  return;
2389
2459
  }
2390
2460
  const nextPlaybackState = {
@@ -2438,6 +2508,7 @@ export class Viewer3D {
2438
2508
  this.alignUnbufferedPlaybackPoseToSupportFloor();
2439
2509
  }
2440
2510
  }
2511
+ this.rebuildTrajectoryPreview();
2441
2512
  this.updateTrajectoryCurrentMarkers();
2442
2513
  if (nextPlaybackState.clockMode === 'realtime') {
2443
2514
  this.scheduleLoop();
@@ -2467,6 +2538,7 @@ export class Viewer3D {
2467
2538
  this.alignUnbufferedPlaybackPoseToSupportFloor();
2468
2539
  }
2469
2540
  }
2541
+ this.rebuildTrajectoryPreview();
2470
2542
  this.updateTrajectoryCurrentMarkers();
2471
2543
  if (shouldStartViewerPlaybackCompletionTransition({
2472
2544
  isPlaying,
@@ -3971,12 +4043,12 @@ export class Viewer3D {
3971
4043
  if (this.lastFrameAt > 0) {
3972
4044
  const frameMs = now - this.lastFrameAt;
3973
4045
  this.averageFrameMs = this.averageFrameMs * 0.9 + frameMs * 0.1;
3974
- this.tunePixelRatio(this.averageFrameMs);
3975
4046
  }
3976
4047
  this.lastFrameAt = now;
3977
4048
  const renderStartedAt = performance.now();
3978
4049
  this.render();
3979
4050
  this.averageRenderMs = this.updateProfilerAverage(this.averageRenderMs, performance.now() - renderStartedAt);
4051
+ this.tunePixelRatio(this.averageRenderMs);
3980
4052
  this.lastRenderCalls = this.renderer.info.render.calls;
3981
4053
  this.lastRenderTriangles = this.renderer.info.render.triangles;
3982
4054
  this.renderFramesSinceReport += 1;
@@ -4031,6 +4103,16 @@ export class Viewer3D {
4031
4103
  if (this.observedRobotGhostMirrorsSimulatedRobot) {
4032
4104
  this.syncObservedRobotGhostFromSimulatedRobot();
4033
4105
  }
4106
+ if (this.options.showTrajectories &&
4107
+ this.trajectoryAnimation?.motion.hasMotion &&
4108
+ this.trajectorySignature === '') {
4109
+ this.rebuildTrajectoryPreview();
4110
+ }
4111
+ // Every pose authority (authored playback, runtime-history replay and
4112
+ // physics projection) converges here. Refreshing the markers immediately
4113
+ // before drawing prevents a solver step or a root presentation offset
4114
+ // from leaving them one frame behind the rendered robot.
4115
+ this.updateTrajectoryCurrentMarkers();
4034
4116
  this.updatePoseHandleScreenScale();
4035
4117
  this.updateProjectileCameraProjection();
4036
4118
  const ground = this.visualGroundLayer;
@@ -5123,6 +5205,10 @@ export class Viewer3D {
5123
5205
  return;
5124
5206
  const ghostMaterials = sourceMaterials.map((source) => {
5125
5207
  const material = source.clone();
5208
+ // Three material cloning does not retain consumer-owned shader hooks.
5209
+ // Preserve the observed model's own hooks, never the simulated state.
5210
+ material.onBeforeCompile = source.onBeforeCompile;
5211
+ material.customProgramCacheKey = source.customProgramCacheKey;
5126
5212
  this.captureMaterialBaseline(material, this.observedRobotGhostMaterialBaselines);
5127
5213
  return material;
5128
5214
  });
@@ -5162,6 +5248,8 @@ export class Viewer3D {
5162
5248
  }
5163
5249
  }
5164
5250
  clearObservedRobotGhost() {
5251
+ this.restoreObservedSoleBaseline();
5252
+ this.observedSoleAnchors?.reset();
5165
5253
  this.cancelRobotComparisonVisibilityTransition();
5166
5254
  this.observedRobotPresentation.reset('discontinuity');
5167
5255
  this.simulatedRobotPresentationRoot.position.x = 0;
@@ -5205,6 +5293,7 @@ export class Viewer3D {
5205
5293
  this.applyBalanceDebugPresentationOffset();
5206
5294
  }
5207
5295
  applyRobotComparisonAppearance() {
5296
+ this.applyIncarnationMaterialProjections();
5208
5297
  this.applyMaterialAppearance(this.robotMaterialBaselines, this.robotComparisonAppearance.simulated);
5209
5298
  this.applyMaterialAppearance(this.observedRobotGhostMaterialBaselines, this.robotComparisonAppearance.observed);
5210
5299
  this.applyObservedRobotGhostRenderOrder(this.robotComparisonAppearance.observed.opacity < 0.999);
@@ -5241,6 +5330,7 @@ export class Viewer3D {
5241
5330
  }
5242
5331
  }
5243
5332
  applyObservedRobotGhostGrounding() {
5333
+ this.restoreObservedSoleBaseline();
5244
5334
  if (this.comparisonProfileId) {
5245
5335
  this.projectComparisonSimulation();
5246
5336
  return;
@@ -5268,6 +5358,12 @@ export class Viewer3D {
5268
5358
  robot.updateMatrixWorld(true);
5269
5359
  const supportContacts = this.resolveObservedRobotSupportContacts(robot);
5270
5360
  const supportPoints = supportContacts.supportPoints;
5361
+ const lowestRawSupport = Math.min(...supportPoints.map(point => point.y));
5362
+ const soleEligible = new Map();
5363
+ for (const group of new Set(supportContacts.supportGroupIds)) {
5364
+ const minimum = Math.min(...supportPoints.filter((_, index) => supportContacts.supportGroupIds[index] === group).map(point => point.y));
5365
+ soleEligible.set(group, minimum - lowestRawSupport <= 0.008);
5366
+ }
5271
5367
  const activeSupport = resolveObservedRobotActiveSupportProjection({
5272
5368
  supportPoints,
5273
5369
  contactIds: supportContacts.contactIds,
@@ -5316,18 +5412,134 @@ export class Viewer3D {
5316
5412
  robot.position.x += temporalSupportAnchor.offsetX;
5317
5413
  robot.position.z += temporalSupportAnchor.offsetZ;
5318
5414
  robot.updateMatrixWorld(true);
5415
+ const stabilizedSurfaceMinimumY = this.applyObservedSoleStabilization(robot, soleEligible, footSupportEligibility.eligible);
5319
5416
  const observedSupport = this.resolveObservedRobotSupport(robot, activeSupport.activeContactCount);
5320
5417
  this.observedRobotGrounding = resolveObservedRobotGroundingProjection({
5321
5418
  enabled: this.options.groundObservedRobotGhost,
5322
5419
  floorY: ROBOT_VIEWER_SUPPORT_FLOOR_Y,
5323
- supportMinimumY: observedSupport.supportMinimumY,
5420
+ supportMinimumY: stabilizedSurfaceMinimumY ?? observedSupport.supportMinimumY,
5324
5421
  supportContactCount: observedSupport.supportContactCount,
5325
- source: observedSupport.source
5422
+ source: stabilizedSurfaceMinimumY === null || stabilizedSurfaceMinimumY === undefined ? observedSupport.source : 'foot-geometry'
5326
5423
  });
5327
5424
  robot.position.y += this.observedRobotGrounding.offsetY;
5328
5425
  robot.updateMatrixWorld(true);
5329
5426
  this.applyNavigationPreviewPresentationTransform();
5330
5427
  }
5428
+ applyObservedSoleStabilization(robot, eligible, supportEligible) {
5429
+ if (!(this.observedSoleStrength > 0) || !supportEligible)
5430
+ return null;
5431
+ const status = this.observedRobotPresentation?.getStatus();
5432
+ if (status?.mode === 'raw' || status?.fallbackReason === 'sample_gap') {
5433
+ this.observedSoleAnchors.reset();
5434
+ return null;
5435
+ }
5436
+ const chains = this.options.supportDefinition?.soleStabilizationChains ?? [];
5437
+ const joints = robot.joints ?? {};
5438
+ const pose = (groupId, foot) => {
5439
+ const contacts = this.resolveObservedRobotSupportContacts(robot);
5440
+ const points = contacts.supportPoints.filter((_, index) => contacts.supportGroupIds[index] === groupId);
5441
+ const center = points.reduce((sum, point) => sum.add(point), new Vector3()).multiplyScalar(1 / Math.max(1, points.length));
5442
+ const rotation = foot.getWorldQuaternion(new Quaternion());
5443
+ const supportMinimumY = this.resolvePreciseObservedSoleMinimumY(foot) ?? Math.min(...points.map(point => point.y));
5444
+ return { position: [center.x, center.y, center.z], orientation: [rotation.x, rotation.y, rotation.z, rotation.w], supportMinimumY };
5445
+ };
5446
+ const candidates = chains.flatMap(chain => {
5447
+ const selected = chain.jointNames.map(name => joints[name]);
5448
+ const foot = selected[selected.length - 1];
5449
+ if (!foot || typeof foot.getWorldQuaternion !== 'function' || selected.some(joint => !joint || joint.mimicJoint))
5450
+ return [];
5451
+ const baseline = selected.map(joint => this.readJointValue(joint));
5452
+ if (baseline.some(value => typeof value !== 'number' || !Number.isFinite(value)))
5453
+ return [];
5454
+ return [{ chain, selected: selected, foot, baseline: baseline, raw: pose(chain.groupId, foot) }];
5455
+ });
5456
+ const targets = this.observedSoleAnchors.resolve(this.observedSoleConnectionId, candidates.map(candidate => ({
5457
+ id: candidate.chain.groupId, position: candidate.raw.position,
5458
+ eligible: this.options.groundObservedRobotGhost && supportEligible && eligible.get(candidate.chain.groupId) === true
5459
+ })), this.observedSoleStrength);
5460
+ // Both eligible soles share a surface before the final root grounding.
5461
+ // Preserve lifted/released feet rather than treating every chain as contact.
5462
+ const supportPlaneY = Math.min(...candidates.filter(candidate => targets.some(target => target.id === candidate.chain.groupId)).map(candidate => candidate.raw.supportMinimumY));
5463
+ for (const target of targets) {
5464
+ const candidate = candidates.find(entry => entry.chain.groupId === target.id);
5465
+ const verticalOffset = supportPlaneY - candidate.raw.supportMinimumY;
5466
+ const targetPosition = [target.target[0], candidate.raw.position[1] + verticalOffset, target.target[2]];
5467
+ if (Math.hypot(...targetPosition.map((value, index) => value - candidate.raw.position[index])) < 0.00001)
5468
+ continue;
5469
+ const apply = (values) => {
5470
+ candidate.selected.forEach((joint, index) => joint.setJointValue(values[index]));
5471
+ robot.updateMatrixWorld(true);
5472
+ };
5473
+ try {
5474
+ const correction = projectBoundedSoleCorrection({
5475
+ values: candidate.baseline,
5476
+ limits: candidate.selected.map(joint => [joint.limit?.lower ?? -Math.PI, joint.limit?.upper ?? Math.PI]),
5477
+ targetPosition, targetOrientation: candidate.raw.orientation,
5478
+ targetSupportMinimumY: supportPlaneY,
5479
+ evaluate: values => { apply(values); return pose(target.id, candidate.foot); }
5480
+ });
5481
+ apply(correction.values);
5482
+ candidate.selected.forEach((joint, index) => this.observedSoleBaseline.set(joint, candidate.baseline[index]));
5483
+ }
5484
+ catch {
5485
+ // Invalid/unreachable authored chains must leave the observed copy intact.
5486
+ apply(candidate.baseline);
5487
+ }
5488
+ }
5489
+ if (!targets.length)
5490
+ return null;
5491
+ // An unanchored foot must still not be pushed through the floor by root grounding.
5492
+ const visibleMinima = candidates.map(candidate => this.resolvePreciseObservedSoleMinimumY(candidate.foot))
5493
+ .filter((value) => value !== null);
5494
+ const minimum = Math.min(...visibleMinima);
5495
+ return Number.isFinite(minimum) ? minimum : null;
5496
+ }
5497
+ /** Exact directional support of rigid visible meshes, with cached convex vertices. */
5498
+ resolvePreciseObservedSoleMinimumY(foot) {
5499
+ this.observedSoleSurfaceVertices ??= new WeakMap();
5500
+ let minimum = Infinity;
5501
+ foot.traverse(node => {
5502
+ const mesh = node;
5503
+ if (!mesh.isMesh || mesh.isURDFCollider || !mesh.geometry)
5504
+ return;
5505
+ const attribute = mesh.geometry.getAttribute('position');
5506
+ if (!attribute)
5507
+ return;
5508
+ const version = 'version' in attribute ? Number(attribute.version) : attribute.data.version;
5509
+ let cached = this.observedSoleSurfaceVertices.get(mesh.geometry);
5510
+ if (!cached || cached.attribute !== attribute || cached.version !== version) {
5511
+ const points = [];
5512
+ for (let index = 0; index < attribute.count; index++) {
5513
+ const point = new Vector3().fromBufferAttribute(attribute, index);
5514
+ if ([point.x, point.y, point.z].every(Number.isFinite))
5515
+ points.push(point);
5516
+ }
5517
+ let vertices = points;
5518
+ if (points.length > 4) {
5519
+ try {
5520
+ const hull = new ConvexHull().setFromPoints(points);
5521
+ const unique = new Set();
5522
+ for (const face of hull.faces) {
5523
+ let edge = face.edge;
5524
+ do {
5525
+ unique.add(edge.vertex.point);
5526
+ edge = edge.next;
5527
+ } while (edge !== face.edge);
5528
+ }
5529
+ if (unique.size)
5530
+ vertices = [...unique];
5531
+ }
5532
+ catch { /* Degenerate meshes retain their exact original vertices. */ }
5533
+ }
5534
+ cached = { attribute, version, points: vertices };
5535
+ this.observedSoleSurfaceVertices.set(mesh.geometry, cached);
5536
+ }
5537
+ const matrix = mesh.matrixWorld.elements;
5538
+ for (const point of cached.points)
5539
+ minimum = Math.min(minimum, matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13]);
5540
+ });
5541
+ return Number.isFinite(minimum) ? minimum : null;
5542
+ }
5331
5543
  resolveObservedRobotSupportContacts(robot) {
5332
5544
  const supportPoints = [];
5333
5545
  const contactIds = [];
@@ -6029,6 +6241,7 @@ export class Viewer3D {
6029
6241
  const previousPresentedY = this.robot.position.y + this.simulatedRobotPresentationRoot.position.y;
6030
6242
  if (!this.physicsVisualRootToRobotMatrix) {
6031
6243
  this.physicsVisualRootToRobotMatrix = this.resolvePhysicsVisualRootToRobotMatrix(physicsRootWorldMatrix, bodyTransforms);
6244
+ this.invalidateTrajectoryPreviewProjection();
6032
6245
  }
6033
6246
  const nextRobotWorldMatrix = physicsRootWorldMatrix.clone().multiply(this.physicsVisualRootToRobotMatrix);
6034
6247
  const parent = this.robot.parent;
@@ -8420,6 +8633,7 @@ export class Viewer3D {
8420
8633
  if (!rootTransform)
8421
8634
  return;
8422
8635
  this.physicsVisualRootToRobotMatrix = this.resolvePhysicsVisualRootToRobotMatrix(this.createPhysicsBodyViewerWorldMatrix(rootTransform), bodyTransforms);
8636
+ this.invalidateTrajectoryPreviewProjection();
8423
8637
  }
8424
8638
  projectProjectilePhysicsObservation() {
8425
8639
  const bodyTransforms = this.physicsService.getBodyTransforms();
@@ -10315,6 +10529,10 @@ export class Viewer3D {
10315
10529
  marker.name = 'balance-center-of-mass-ground-projection';
10316
10530
  return marker;
10317
10531
  }
10532
+ invalidateTrajectoryPreviewProjection() {
10533
+ this.trajectoryProjectionRevision += 1;
10534
+ this.trajectorySignature = '';
10535
+ }
10318
10536
  ensureTrajectoryGroup() {
10319
10537
  if (this.trajectoryGroup) {
10320
10538
  return this.trajectoryGroup;
@@ -10322,7 +10540,7 @@ export class Viewer3D {
10322
10540
  this.trajectoryGroup = new Group();
10323
10541
  this.trajectoryGroup.name = 'AnimationTrajectoryPreview';
10324
10542
  this.trajectoryGroup.visible = false;
10325
- this.scene.add(this.trajectoryGroup);
10543
+ this.simulatedRobotPresentationRoot.add(this.trajectoryGroup);
10326
10544
  return this.trajectoryGroup;
10327
10545
  }
10328
10546
  updateTrajectoryGroupVisibility() {
@@ -10371,18 +10589,31 @@ export class Viewer3D {
10371
10589
  this.clearTrajectoryPreview();
10372
10590
  this.trajectorySignature = nextSignature;
10373
10591
  const originalPose = this.captureJointPose();
10592
+ const originalRobotTransform = this.captureRobotTransform(this.robot);
10593
+ const temporalSampleResolver = this.playbackState?.animation === animation
10594
+ ? this.playbackState.resolveTemporalProjectionSample
10595
+ : null;
10596
+ let temporalRootAlignment = this.physicsVisualRootToRobotMatrix?.clone() ?? null;
10374
10597
  const previousAnimatedTargets = new Set(this.lastAnimatedJointTargets);
10375
10598
  const sampleCount = Math.max(ROBOT_VIEWER_TRAJECTORY_MIN_SAMPLES, Math.min(ROBOT_VIEWER_TRAJECTORY_MAX_SAMPLES, Math.ceil(duration * ROBOT_VIEWER_TRAJECTORY_SAMPLE_RATE)));
10376
10599
  const pointsByTarget = new Map(targets.map((target) => [target.id, []]));
10377
10600
  for (let sampleIndex = 0; sampleIndex <= sampleCount; sampleIndex += 1) {
10378
10601
  const sampleTime = (sampleIndex / sampleCount) * duration;
10379
- this.applyRobotPose(animation, sampleTime, [], duration);
10602
+ const temporalSample = temporalSampleResolver?.(sampleTime) ?? null;
10603
+ this.restoreRobotTransform(this.robot, originalRobotTransform);
10604
+ if (temporalSample?.viewer) {
10605
+ temporalRootAlignment = this.applyTrajectoryTemporalSamplePose(temporalSample, temporalRootAlignment);
10606
+ }
10607
+ else {
10608
+ this.applyTrajectorySamplePose(animation, sampleTime, duration);
10609
+ }
10380
10610
  this.robot.updateMatrixWorld(true);
10381
10611
  for (const target of targets) {
10382
- pointsByTarget.get(target.id)?.push(this.getTrajectoryPoint(target.id));
10612
+ pointsByTarget.get(target.id)?.push(this.getTrajectoryPointInProjectionFrame(target.id));
10383
10613
  }
10384
10614
  }
10385
- this.restoreJointPose(originalPose);
10615
+ this.restoreRobotTransform(this.robot, originalRobotTransform);
10616
+ this.restoreTrajectorySamplePose(originalPose);
10386
10617
  this.lastAnimatedJointTargets = previousAnimatedTargets;
10387
10618
  this.nextAnimatedJointTargets.clear();
10388
10619
  for (const target of targets) {
@@ -10392,26 +10623,100 @@ export class Viewer3D {
10392
10623
  }
10393
10624
  trajectoryGroup.add(this.createTrajectoryObject(target, points));
10394
10625
  }
10395
- this.updateTrajectoryCurrentMarkers();
10396
10626
  this.updateTrajectoryGroupVisibility();
10627
+ this.updateTrajectoryCurrentMarkers();
10397
10628
  this.requestRender();
10398
10629
  }
10630
+ applyTrajectorySamplePose(animation, time, duration) {
10631
+ const joints = this.getRobotJoints();
10632
+ for (const { target, value } of sampleRobotMotionJointValuesAtTime(animation, time, duration)) {
10633
+ const joint = joints[target];
10634
+ if (joint)
10635
+ this.applyDesiredProjectionJointValue(joint, value);
10636
+ }
10637
+ }
10638
+ applyTrajectoryTemporalSamplePose(sample, rootAlignment) {
10639
+ const viewer = sample.viewer;
10640
+ const robot = this.robot;
10641
+ if (!viewer || !robot) {
10642
+ return rootAlignment;
10643
+ }
10644
+ const joints = this.getRobotJoints();
10645
+ for (const [target, value] of Object.entries(viewer.joints ?? {})) {
10646
+ const joint = joints[target];
10647
+ if (joint && Number.isFinite(value)) {
10648
+ this.applyDesiredProjectionJointValue(joint, value);
10649
+ }
10650
+ }
10651
+ const bodyTransforms = (viewer.physics?.bodyTransforms ?? []).map((transform) => this.deserializeRuntimeHistoryTransform(transform, false));
10652
+ const rootTransform = bodyTransforms.find((transform) => transform.metadata?.visualRoot === true) ??
10653
+ bodyTransforms.find((transform) => transform.bodyName === this.physicsVisualRootBodyName);
10654
+ let nextRootAlignment = rootAlignment;
10655
+ if (rootTransform) {
10656
+ const physicsRootWorldMatrix = this.createPhysicsBodyViewerWorldMatrix(rootTransform);
10657
+ nextRootAlignment ??= this.resolvePhysicsVisualRootToRobotMatrix(physicsRootWorldMatrix, bodyTransforms);
10658
+ const nextRobotLocalMatrix = physicsRootWorldMatrix.clone().multiply(nextRootAlignment);
10659
+ const parent = robot.parent;
10660
+ if (parent && parent !== this.simulatedRobotPresentationRoot) {
10661
+ parent.updateMatrixWorld(true);
10662
+ nextRobotLocalMatrix.premultiply(parent.matrixWorld.clone().invert());
10663
+ }
10664
+ robot.matrix.copy(nextRobotLocalMatrix);
10665
+ robot.matrix.decompose(robot.position, robot.quaternion, robot.scale);
10666
+ }
10667
+ robot.updateMatrixWorld(true);
10668
+ return nextRootAlignment;
10669
+ }
10670
+ restoreTrajectorySamplePose(pose) {
10671
+ const joints = this.getRobotJoints();
10672
+ for (const [target, value] of Object.entries(pose)) {
10673
+ const joint = joints[target];
10674
+ if (joint)
10675
+ this.applyDesiredProjectionJointValue(joint, value);
10676
+ }
10677
+ this.robot?.updateMatrixWorld(true);
10678
+ }
10399
10679
  getTrajectorySignature(animation, targets, duration) {
10400
10680
  return JSON.stringify({
10401
10681
  duration: Number(duration.toFixed(3)),
10682
+ projectionRevision: this.trajectoryProjectionRevision,
10683
+ playbackProjection: this.getTrajectoryPlaybackProjectionSignature(animation),
10402
10684
  handsOnly: this.options.showHandTrajectoriesOnly,
10403
10685
  targets: targets.map((target) => target.id),
10686
+ temporalProjection: this.getTrajectoryTemporalProjectionSignature(animation, duration),
10404
10687
  tracks: animation.motion.tracks.map((track) => ({
10405
10688
  target: track.target,
10406
10689
  property: track.property,
10407
- keys: track.keys.length,
10408
- firstTime: track.keys[0]?.time ?? null,
10409
- lastTime: track.keys[track.keys.length - 1]?.time ?? null,
10410
- firstValue: track.keys[0]?.value ?? null,
10411
- lastValue: track.keys[track.keys.length - 1]?.value ?? null
10690
+ interpolation: track.interpolation ?? null,
10691
+ unitKind: track.unitKind ?? null,
10692
+ keys: track.keys.map((key) => [key.time, key.value])
10412
10693
  }))
10413
10694
  });
10414
10695
  }
10696
+ getTrajectoryPlaybackProjectionSignature(animation) {
10697
+ const playback = this.playbackState;
10698
+ if (playback?.animation !== animation) {
10699
+ return null;
10700
+ }
10701
+ return {
10702
+ mutedTargets: [...playback.mutedTrackTargetSet].sort(),
10703
+ startTransitionMinimumDurationSeconds: playback.startTransitionMinimumDurationSeconds,
10704
+ entryPose: this.createPoseSignature(playback.entryPose),
10705
+ endTransitionMinimumDurationSeconds: playback.endTransitionMinimumDurationSeconds,
10706
+ exitPose: this.createPoseSignature(playback.exitPose)
10707
+ };
10708
+ }
10709
+ getTrajectoryTemporalProjectionSignature(animation, duration) {
10710
+ const playback = this.playbackState;
10711
+ const resolver = playback?.animation === animation ? playback.resolveTemporalProjectionSample : null;
10712
+ if (!resolver) {
10713
+ return null;
10714
+ }
10715
+ return [0, duration * 0.5, duration].map((time) => {
10716
+ const sample = resolver(time);
10717
+ return sample ? [sample.id, Number(sample.timeSeconds.toFixed(4))] : null;
10718
+ });
10719
+ }
10415
10720
  getTrajectoryPreviewTargets(animation) {
10416
10721
  if (this.options.showHandTrajectoriesOnly) {
10417
10722
  return (this.options.handTipDefinitions ?? [])
@@ -10502,9 +10807,12 @@ export class Viewer3D {
10502
10807
  if (!target || !currentMarker) {
10503
10808
  continue;
10504
10809
  }
10505
- currentMarker.position.copy(this.getTrajectoryPoint(target));
10810
+ currentMarker.position.copy(this.getTrajectoryPointInProjectionFrame(target));
10506
10811
  }
10507
10812
  }
10813
+ getTrajectoryPointInProjectionFrame(target) {
10814
+ return projectViewerTrajectoryPointToFrame(this.getTrajectoryPoint(target), this.simulatedRobotPresentationRoot);
10815
+ }
10508
10816
  getTrajectoryPoint(target) {
10509
10817
  const handTip = (this.options.handTipDefinitions ?? []).find((definition) => definition.id === target);
10510
10818
  if (handTip) {
@@ -10583,10 +10891,10 @@ export class Viewer3D {
10583
10891
  this.trajectoryAnchors.set(target, anchor);
10584
10892
  return anchor;
10585
10893
  }
10894
+ joint.updateMatrixWorld?.(true);
10586
10895
  const jointInverse = joint.matrixWorld.clone().invert();
10587
10896
  const localBounds = new Box3();
10588
10897
  let hasBounds = false;
10589
- joint.updateMatrixWorld?.(true);
10590
10898
  joint.traverse((node) => {
10591
10899
  if (node !== joint && node.isURDFJoint) {
10592
10900
  return;
@@ -10880,6 +11188,11 @@ export class Viewer3D {
10880
11188
  function formatNavigationDistance(value) {
10881
11189
  return `${value >= 0 ? '+' : ''}${value.toFixed(2)}m`;
10882
11190
  }
11191
+ /** Projects a world-space trajectory sample into the robot presentation frame. */
11192
+ export function projectViewerTrajectoryPointToFrame(point, frame) {
11193
+ frame.updateWorldMatrix(true, false);
11194
+ return frame.worldToLocal(new Vector3(point.x, point.y, point.z));
11195
+ }
10883
11196
  function formatNavigationAngle(value) {
10884
11197
  const degrees = MathUtils.radToDeg(normalizeNavigationRadians(value));
10885
11198
  return `${degrees >= 0 ? '+' : ''}${degrees.toFixed(0)}°`;