@konitif/viewer-3d 0.285.5 → 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.
@@ -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';
@@ -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,
@@ -1051,11 +1061,17 @@ export class Viewer3D {
1051
1061
  sampleId: input.frameId,
1052
1062
  connectionId: input.connectionId,
1053
1063
  sourceObservedAtSeconds: input.observedAtSeconds,
1064
+ sourceClock: input.sourceClock,
1054
1065
  receivedAtMs,
1055
1066
  jointValues: input.jointValues
1056
1067
  });
1057
1068
  if (!accepted)
1058
1069
  return false;
1070
+ if (this.observedSoleConnectionId !== input.connectionId) {
1071
+ this.restoreObservedSoleBaseline();
1072
+ this.observedSoleAnchors.reset();
1073
+ this.observedSoleConnectionId = input.connectionId;
1074
+ }
1059
1075
  const frame = this.observedRobotPresentation.project(receivedAtMs);
1060
1076
  if (frame)
1061
1077
  this.applyObservedRobotGhostJointValues(frame.jointValues);
@@ -1063,14 +1079,35 @@ export class Viewer3D {
1063
1079
  return true;
1064
1080
  }
1065
1081
  resetObservedRobotGhostJointObservation() {
1082
+ this.restoreObservedSoleBaseline();
1083
+ this.observedSoleAnchors.reset();
1066
1084
  this.observedRobotPresentation.reset('discontinuity');
1067
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
+ }
1068
1103
  getObservedRobotPresentationStatus() {
1069
1104
  return this.observedRobotPresentation.getStatus();
1070
1105
  }
1071
1106
  applyObservedRobotGhostJointValues(values) {
1072
1107
  if (!this.observedRobotGhost)
1073
1108
  return false;
1109
+ const hadSoleCorrection = (this.observedSoleBaseline?.size ?? 0) > 0;
1110
+ this.restoreObservedSoleBaseline();
1074
1111
  const joints = this.observedRobotGhost.joints ?? {};
1075
1112
  let changed = false;
1076
1113
  for (const [target, value] of Object.entries(values)) {
@@ -1082,7 +1119,7 @@ export class Viewer3D {
1082
1119
  continue;
1083
1120
  changed = joint.setJointValue(value) !== false || changed;
1084
1121
  }
1085
- if (changed) {
1122
+ if (changed || hadSoleCorrection) {
1086
1123
  this.applyObservedRobotGhostGrounding();
1087
1124
  if (this.poseControlTarget === 'observed') {
1088
1125
  this.refreshPoseHandleProjection();
@@ -1145,10 +1182,28 @@ export class Viewer3D {
1145
1182
  // invalidate that anchor.
1146
1183
  if (spatialLayoutChanged) {
1147
1184
  this.observedRobotTemporalSupportAnchorState = null;
1185
+ this.observedSoleAnchors?.reset();
1148
1186
  }
1149
1187
  this.applyRobotComparisonAppearance();
1150
1188
  this.requestRender();
1151
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
+ }
1152
1207
  async setPhysicsEngine(physicsEngine, physicsEnabled = physicsEngine !== 'none') {
1153
1208
  if (this.destroyed)
1154
1209
  return;
@@ -5150,6 +5205,10 @@ export class Viewer3D {
5150
5205
  return;
5151
5206
  const ghostMaterials = sourceMaterials.map((source) => {
5152
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;
5153
5212
  this.captureMaterialBaseline(material, this.observedRobotGhostMaterialBaselines);
5154
5213
  return material;
5155
5214
  });
@@ -5189,6 +5248,8 @@ export class Viewer3D {
5189
5248
  }
5190
5249
  }
5191
5250
  clearObservedRobotGhost() {
5251
+ this.restoreObservedSoleBaseline();
5252
+ this.observedSoleAnchors?.reset();
5192
5253
  this.cancelRobotComparisonVisibilityTransition();
5193
5254
  this.observedRobotPresentation.reset('discontinuity');
5194
5255
  this.simulatedRobotPresentationRoot.position.x = 0;
@@ -5232,6 +5293,7 @@ export class Viewer3D {
5232
5293
  this.applyBalanceDebugPresentationOffset();
5233
5294
  }
5234
5295
  applyRobotComparisonAppearance() {
5296
+ this.applyIncarnationMaterialProjections();
5235
5297
  this.applyMaterialAppearance(this.robotMaterialBaselines, this.robotComparisonAppearance.simulated);
5236
5298
  this.applyMaterialAppearance(this.observedRobotGhostMaterialBaselines, this.robotComparisonAppearance.observed);
5237
5299
  this.applyObservedRobotGhostRenderOrder(this.robotComparisonAppearance.observed.opacity < 0.999);
@@ -5268,6 +5330,7 @@ export class Viewer3D {
5268
5330
  }
5269
5331
  }
5270
5332
  applyObservedRobotGhostGrounding() {
5333
+ this.restoreObservedSoleBaseline();
5271
5334
  if (this.comparisonProfileId) {
5272
5335
  this.projectComparisonSimulation();
5273
5336
  return;
@@ -5295,6 +5358,12 @@ export class Viewer3D {
5295
5358
  robot.updateMatrixWorld(true);
5296
5359
  const supportContacts = this.resolveObservedRobotSupportContacts(robot);
5297
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
+ }
5298
5367
  const activeSupport = resolveObservedRobotActiveSupportProjection({
5299
5368
  supportPoints,
5300
5369
  contactIds: supportContacts.contactIds,
@@ -5343,18 +5412,134 @@ export class Viewer3D {
5343
5412
  robot.position.x += temporalSupportAnchor.offsetX;
5344
5413
  robot.position.z += temporalSupportAnchor.offsetZ;
5345
5414
  robot.updateMatrixWorld(true);
5415
+ const stabilizedSurfaceMinimumY = this.applyObservedSoleStabilization(robot, soleEligible, footSupportEligibility.eligible);
5346
5416
  const observedSupport = this.resolveObservedRobotSupport(robot, activeSupport.activeContactCount);
5347
5417
  this.observedRobotGrounding = resolveObservedRobotGroundingProjection({
5348
5418
  enabled: this.options.groundObservedRobotGhost,
5349
5419
  floorY: ROBOT_VIEWER_SUPPORT_FLOOR_Y,
5350
- supportMinimumY: observedSupport.supportMinimumY,
5420
+ supportMinimumY: stabilizedSurfaceMinimumY ?? observedSupport.supportMinimumY,
5351
5421
  supportContactCount: observedSupport.supportContactCount,
5352
- source: observedSupport.source
5422
+ source: stabilizedSurfaceMinimumY === null || stabilizedSurfaceMinimumY === undefined ? observedSupport.source : 'foot-geometry'
5353
5423
  });
5354
5424
  robot.position.y += this.observedRobotGrounding.offsetY;
5355
5425
  robot.updateMatrixWorld(true);
5356
5426
  this.applyNavigationPreviewPresentationTransform();
5357
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
+ }
5358
5543
  resolveObservedRobotSupportContacts(robot) {
5359
5544
  const supportPoints = [];
5360
5545
  const contactIds = [];