@konitif/viewer-3d 0.285.5 → 0.285.7
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.
- package/README.md +55 -0
- package/dist/renderer/IncarnationMaterialProjection.d.ts +16 -0
- package/dist/renderer/IncarnationMaterialProjection.d.ts.map +1 -0
- package/dist/renderer/IncarnationMaterialProjection.js +45 -0
- package/dist/renderer/IncarnationMaterialProjection.js.map +1 -0
- package/dist/renderer/ObservedRobotPresentationBuffer.d.ts +15 -4
- package/dist/renderer/ObservedRobotPresentationBuffer.d.ts.map +1 -1
- package/dist/renderer/ObservedRobotPresentationBuffer.js +136 -8
- package/dist/renderer/ObservedRobotPresentationBuffer.js.map +1 -1
- package/dist/renderer/ObservedSoleStabilization.d.ts +41 -0
- package/dist/renderer/ObservedSoleStabilization.d.ts.map +1 -0
- package/dist/renderer/ObservedSoleStabilization.js +140 -0
- package/dist/renderer/ObservedSoleStabilization.js.map +1 -0
- package/dist/renderer/PerformanceObservation.d.ts +8 -0
- package/dist/renderer/PerformanceObservation.d.ts.map +1 -0
- package/dist/renderer/PerformanceObservation.js +21 -0
- package/dist/renderer/PerformanceObservation.js.map +1 -0
- package/dist/renderer/Viewer3DRenderer.d.ts +23 -0
- package/dist/renderer/Viewer3DRenderer.d.ts.map +1 -1
- package/dist/renderer/Viewer3DRenderer.js +203 -5
- package/dist/renderer/Viewer3DRenderer.js.map +1 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/index.js.map +1 -1
- package/dist/supportDefinition.d.ts +5 -0
- package/dist/supportDefinition.d.ts.map +1 -1
- package/package.json +1 -1
- package/reference/catalog.json +13 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { measureViewerOperation } from './PerformanceObservation.js';
|
|
1
2
|
import { createViewerPhysicsFrameQuaternion, resolveViewerSceneYaw, projectViewerImpulseByFrame, projectViewerPointByFrame } from './physics/ViewerPhysicsFrame.js';
|
|
2
3
|
import { resolveViewerJointMapping, selectDirectViewerJointMappings } from './physics/ViewerJointResolution.js';
|
|
3
4
|
import { createPhysicsJointTargetValue, createVisualJointValueFromPhysics, createVisualJointValueFromSource } from './physics/ViewerPhysicsJointMapping.js';
|
|
@@ -5,6 +6,7 @@ import { resolvePhysicsSourceString, resolvePhysicsSourceStringListMap, resolveP
|
|
|
5
6
|
import { createClockGraph, createInvalidationController, createTemporalTransport } from '@konitif/temporal';
|
|
6
7
|
import { createViewerTemporalScheduler } from './ViewerTemporalScheduler.js';
|
|
7
8
|
import { copyViewerMetadata } from './ViewerMetadata.js';
|
|
9
|
+
import { IncarnationMaterialProjector, normalizeIncarnationMaterialProjection } from './IncarnationMaterialProjection.js';
|
|
8
10
|
import { resolveInteractionInputActions, resolveInteractionInputBindings } from '@konitif/tools/input';
|
|
9
11
|
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
12
|
import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls.js';
|
|
@@ -30,6 +32,8 @@ import { resolveViewerIncarnationHealth } from './ViewerIncarnationHealth.js';
|
|
|
30
32
|
import { PerformanceMonitor } from './PerformanceMonitor.js';
|
|
31
33
|
import { ViewerTemporalProjectionBuffer } from './TemporalProjectionModel.js';
|
|
32
34
|
import { ObservedRobotPresentationBuffer } from './ObservedRobotPresentationBuffer.js';
|
|
35
|
+
import { ObservedSoleAnchors, projectBoundedSoleCorrection } from './ObservedSoleStabilization.js';
|
|
36
|
+
import { ConvexHull } from 'three/examples/jsm/math/ConvexHull.js';
|
|
33
37
|
import { resolveViewerFrameSignalIntervalMs, VIEWER_FRAME_SIGNAL_CATALOG } from './ViewerFrameSignals.js';
|
|
34
38
|
import { DEFAULT_DISPLAY_FRAME_RATE_HZ, normalizeConfiguredDisplayFrameRate, observeDisplayFrameRateCeiling, resolveDisplayFrameRateTarget } from './displayFrameRate.js';
|
|
35
39
|
import { ViewerTemporalProjectionExperimentAdapter } from './ViewerTemporalProjectionExperimentAdapter.js';
|
|
@@ -219,6 +223,8 @@ export class Viewer3D {
|
|
|
219
223
|
poseControlTarget = 'simulated';
|
|
220
224
|
observedPoseLiveControlEnabled = false;
|
|
221
225
|
robotMaterialBaselines = new Map();
|
|
226
|
+
incarnationMaterialProjector = new IncarnationMaterialProjector();
|
|
227
|
+
incarnationMaterialProjections = {};
|
|
222
228
|
observedRobotGhostMaterialBaselines = new Map();
|
|
223
229
|
observedRobotGhostRenderOrderBaselines = new Map();
|
|
224
230
|
observedRobotGhostShadowMaterials = new Map();
|
|
@@ -229,6 +235,11 @@ export class Viewer3D {
|
|
|
229
235
|
observedRobotGhostBaseQuaternion = new Quaternion();
|
|
230
236
|
observedRobotGhostBaseScale = new Vector3(1, 1, 1);
|
|
231
237
|
observedRobotTemporalSupportAnchorState = null;
|
|
238
|
+
observedSoleAnchors = new ObservedSoleAnchors();
|
|
239
|
+
observedSoleConnectionId = '';
|
|
240
|
+
observedSoleStrength = 0;
|
|
241
|
+
observedSoleBaseline = new Map();
|
|
242
|
+
observedSoleSurfaceVertices = new WeakMap();
|
|
232
243
|
observedRobotGrounding = resolveObservedRobotGroundingProjection({
|
|
233
244
|
enabled: false,
|
|
234
245
|
floorY: ROBOT_VIEWER_SUPPORT_FLOOR_Y,
|
|
@@ -1051,11 +1062,17 @@ export class Viewer3D {
|
|
|
1051
1062
|
sampleId: input.frameId,
|
|
1052
1063
|
connectionId: input.connectionId,
|
|
1053
1064
|
sourceObservedAtSeconds: input.observedAtSeconds,
|
|
1065
|
+
sourceClock: input.sourceClock,
|
|
1054
1066
|
receivedAtMs,
|
|
1055
1067
|
jointValues: input.jointValues
|
|
1056
1068
|
});
|
|
1057
1069
|
if (!accepted)
|
|
1058
1070
|
return false;
|
|
1071
|
+
if (this.observedSoleConnectionId !== input.connectionId) {
|
|
1072
|
+
this.restoreObservedSoleBaseline();
|
|
1073
|
+
this.observedSoleAnchors.reset();
|
|
1074
|
+
this.observedSoleConnectionId = input.connectionId;
|
|
1075
|
+
}
|
|
1059
1076
|
const frame = this.observedRobotPresentation.project(receivedAtMs);
|
|
1060
1077
|
if (frame)
|
|
1061
1078
|
this.applyObservedRobotGhostJointValues(frame.jointValues);
|
|
@@ -1063,14 +1080,35 @@ export class Viewer3D {
|
|
|
1063
1080
|
return true;
|
|
1064
1081
|
}
|
|
1065
1082
|
resetObservedRobotGhostJointObservation() {
|
|
1083
|
+
this.restoreObservedSoleBaseline();
|
|
1084
|
+
this.observedSoleAnchors.reset();
|
|
1066
1085
|
this.observedRobotPresentation.reset('discontinuity');
|
|
1067
1086
|
}
|
|
1087
|
+
setObservedRobotSoleStabilizationStrength(value) {
|
|
1088
|
+
this.observedSoleAnchors ??= new ObservedSoleAnchors();
|
|
1089
|
+
const strength = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
1090
|
+
if (strength === this.observedSoleStrength)
|
|
1091
|
+
return;
|
|
1092
|
+
this.restoreObservedSoleBaseline();
|
|
1093
|
+
this.observedSoleAnchors.reset();
|
|
1094
|
+
this.observedSoleStrength = strength;
|
|
1095
|
+
this.applyObservedRobotGhostGrounding();
|
|
1096
|
+
this.requestRender();
|
|
1097
|
+
}
|
|
1098
|
+
restoreObservedSoleBaseline() {
|
|
1099
|
+
this.observedSoleBaseline ??= new Map();
|
|
1100
|
+
for (const [joint, value] of this.observedSoleBaseline)
|
|
1101
|
+
joint.setJointValue(value);
|
|
1102
|
+
this.observedSoleBaseline.clear();
|
|
1103
|
+
}
|
|
1068
1104
|
getObservedRobotPresentationStatus() {
|
|
1069
1105
|
return this.observedRobotPresentation.getStatus();
|
|
1070
1106
|
}
|
|
1071
1107
|
applyObservedRobotGhostJointValues(values) {
|
|
1072
1108
|
if (!this.observedRobotGhost)
|
|
1073
1109
|
return false;
|
|
1110
|
+
const hadSoleCorrection = (this.observedSoleBaseline?.size ?? 0) > 0;
|
|
1111
|
+
this.restoreObservedSoleBaseline();
|
|
1074
1112
|
const joints = this.observedRobotGhost.joints ?? {};
|
|
1075
1113
|
let changed = false;
|
|
1076
1114
|
for (const [target, value] of Object.entries(values)) {
|
|
@@ -1082,7 +1120,7 @@ export class Viewer3D {
|
|
|
1082
1120
|
continue;
|
|
1083
1121
|
changed = joint.setJointValue(value) !== false || changed;
|
|
1084
1122
|
}
|
|
1085
|
-
if (changed) {
|
|
1123
|
+
if (changed || hadSoleCorrection) {
|
|
1086
1124
|
this.applyObservedRobotGhostGrounding();
|
|
1087
1125
|
if (this.poseControlTarget === 'observed') {
|
|
1088
1126
|
this.refreshPoseHandleProjection();
|
|
@@ -1145,10 +1183,28 @@ export class Viewer3D {
|
|
|
1145
1183
|
// invalidate that anchor.
|
|
1146
1184
|
if (spatialLayoutChanged) {
|
|
1147
1185
|
this.observedRobotTemporalSupportAnchorState = null;
|
|
1186
|
+
this.observedSoleAnchors?.reset();
|
|
1148
1187
|
}
|
|
1149
1188
|
this.applyRobotComparisonAppearance();
|
|
1150
1189
|
this.requestRender();
|
|
1151
1190
|
}
|
|
1191
|
+
/** Tune one visual incarnation independently; source meshes and telemetry remain unchanged. */
|
|
1192
|
+
setIncarnationMaterialProjection(target, projection) {
|
|
1193
|
+
if (target !== 'simulated' && target !== 'observed')
|
|
1194
|
+
throw new Error('Invalid material incarnation target');
|
|
1195
|
+
this.incarnationMaterialProjections[target] = normalizeIncarnationMaterialProjection(projection);
|
|
1196
|
+
this.applyIncarnationMaterialProjections();
|
|
1197
|
+
this.requestRender();
|
|
1198
|
+
}
|
|
1199
|
+
applyIncarnationMaterialProjections() {
|
|
1200
|
+
for (const target of ['simulated', 'observed']) {
|
|
1201
|
+
const projection = this.incarnationMaterialProjections[target];
|
|
1202
|
+
if (!projection)
|
|
1203
|
+
continue;
|
|
1204
|
+
const materials = target === 'simulated' ? this.robotMaterialBaselines : this.observedRobotGhostMaterialBaselines;
|
|
1205
|
+
this.incarnationMaterialProjector.apply(materials.keys(), projection);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1152
1208
|
async setPhysicsEngine(physicsEngine, physicsEnabled = physicsEngine !== 'none') {
|
|
1153
1209
|
if (this.destroyed)
|
|
1154
1210
|
return;
|
|
@@ -1398,6 +1454,11 @@ export class Viewer3D {
|
|
|
1398
1454
|
getPerformanceSummary() {
|
|
1399
1455
|
return this.performanceMonitor.getSummary();
|
|
1400
1456
|
}
|
|
1457
|
+
performanceObserver = null;
|
|
1458
|
+
/** Borrowed, optional sink. Does not allocate a loop, timer or browser observer. */
|
|
1459
|
+
setPerformanceObserver(observer) {
|
|
1460
|
+
this.performanceObserver = observer;
|
|
1461
|
+
}
|
|
1401
1462
|
setTemporalProjectionMode(mode) {
|
|
1402
1463
|
if (this.temporalProjection.getMode() === mode) {
|
|
1403
1464
|
return;
|
|
@@ -3089,6 +3150,7 @@ export class Viewer3D {
|
|
|
3089
3150
|
this.requestRender();
|
|
3090
3151
|
}
|
|
3091
3152
|
destroy(options = {}) {
|
|
3153
|
+
this.performanceObserver = null;
|
|
3092
3154
|
if (this.destroyed) {
|
|
3093
3155
|
return;
|
|
3094
3156
|
}
|
|
@@ -3881,6 +3943,9 @@ export class Viewer3D {
|
|
|
3881
3943
|
if (!this.animationLoop.beginFrame()) {
|
|
3882
3944
|
return;
|
|
3883
3945
|
}
|
|
3946
|
+
measureViewerOperation(this.performanceObserver, 'viewer.loop.cpu', () => this.runFrame());
|
|
3947
|
+
};
|
|
3948
|
+
runFrame() {
|
|
3884
3949
|
const loopStartedAt = performance.now();
|
|
3885
3950
|
const rafNow = loopStartedAt;
|
|
3886
3951
|
this.temporalScheduler.frame(rafNow / 1000);
|
|
@@ -3957,7 +4022,7 @@ export class Viewer3D {
|
|
|
3957
4022
|
const controlsStartedAt = performance.now();
|
|
3958
4023
|
const shouldUpdateControls = this.controls.enabled && (this.controlsActive || this.controlsSettlingFrames > 0);
|
|
3959
4024
|
if (shouldUpdateControls) {
|
|
3960
|
-
this.controls.update();
|
|
4025
|
+
measureViewerOperation(this.performanceObserver, 'viewer.controls.cpu', () => this.controls.update());
|
|
3961
4026
|
}
|
|
3962
4027
|
if (shouldUpdateControls) {
|
|
3963
4028
|
this.markFrameSignal('controls');
|
|
@@ -4042,8 +4107,11 @@ export class Viewer3D {
|
|
|
4042
4107
|
if (shouldContinueLoop && this.animationLoop.isIdle()) {
|
|
4043
4108
|
this.scheduleLoop();
|
|
4044
4109
|
}
|
|
4045
|
-
}
|
|
4110
|
+
}
|
|
4046
4111
|
render() {
|
|
4112
|
+
measureViewerOperation(this.performanceObserver, 'viewer.draw.cpu', () => this.renderScene());
|
|
4113
|
+
}
|
|
4114
|
+
renderScene() {
|
|
4047
4115
|
this.projectComparisonSimulation();
|
|
4048
4116
|
if (this.observedRobotGhostMirrorsSimulatedRobot) {
|
|
4049
4117
|
this.syncObservedRobotGhostFromSimulatedRobot();
|
|
@@ -5150,6 +5218,10 @@ export class Viewer3D {
|
|
|
5150
5218
|
return;
|
|
5151
5219
|
const ghostMaterials = sourceMaterials.map((source) => {
|
|
5152
5220
|
const material = source.clone();
|
|
5221
|
+
// Three material cloning does not retain consumer-owned shader hooks.
|
|
5222
|
+
// Preserve the observed model's own hooks, never the simulated state.
|
|
5223
|
+
material.onBeforeCompile = source.onBeforeCompile;
|
|
5224
|
+
material.customProgramCacheKey = source.customProgramCacheKey;
|
|
5153
5225
|
this.captureMaterialBaseline(material, this.observedRobotGhostMaterialBaselines);
|
|
5154
5226
|
return material;
|
|
5155
5227
|
});
|
|
@@ -5189,6 +5261,8 @@ export class Viewer3D {
|
|
|
5189
5261
|
}
|
|
5190
5262
|
}
|
|
5191
5263
|
clearObservedRobotGhost() {
|
|
5264
|
+
this.restoreObservedSoleBaseline();
|
|
5265
|
+
this.observedSoleAnchors?.reset();
|
|
5192
5266
|
this.cancelRobotComparisonVisibilityTransition();
|
|
5193
5267
|
this.observedRobotPresentation.reset('discontinuity');
|
|
5194
5268
|
this.simulatedRobotPresentationRoot.position.x = 0;
|
|
@@ -5232,6 +5306,7 @@ export class Viewer3D {
|
|
|
5232
5306
|
this.applyBalanceDebugPresentationOffset();
|
|
5233
5307
|
}
|
|
5234
5308
|
applyRobotComparisonAppearance() {
|
|
5309
|
+
this.applyIncarnationMaterialProjections();
|
|
5235
5310
|
this.applyMaterialAppearance(this.robotMaterialBaselines, this.robotComparisonAppearance.simulated);
|
|
5236
5311
|
this.applyMaterialAppearance(this.observedRobotGhostMaterialBaselines, this.robotComparisonAppearance.observed);
|
|
5237
5312
|
this.applyObservedRobotGhostRenderOrder(this.robotComparisonAppearance.observed.opacity < 0.999);
|
|
@@ -5268,6 +5343,7 @@ export class Viewer3D {
|
|
|
5268
5343
|
}
|
|
5269
5344
|
}
|
|
5270
5345
|
applyObservedRobotGhostGrounding() {
|
|
5346
|
+
this.restoreObservedSoleBaseline();
|
|
5271
5347
|
if (this.comparisonProfileId) {
|
|
5272
5348
|
this.projectComparisonSimulation();
|
|
5273
5349
|
return;
|
|
@@ -5295,6 +5371,12 @@ export class Viewer3D {
|
|
|
5295
5371
|
robot.updateMatrixWorld(true);
|
|
5296
5372
|
const supportContacts = this.resolveObservedRobotSupportContacts(robot);
|
|
5297
5373
|
const supportPoints = supportContacts.supportPoints;
|
|
5374
|
+
const lowestRawSupport = Math.min(...supportPoints.map(point => point.y));
|
|
5375
|
+
const soleEligible = new Map();
|
|
5376
|
+
for (const group of new Set(supportContacts.supportGroupIds)) {
|
|
5377
|
+
const minimum = Math.min(...supportPoints.filter((_, index) => supportContacts.supportGroupIds[index] === group).map(point => point.y));
|
|
5378
|
+
soleEligible.set(group, minimum - lowestRawSupport <= 0.008);
|
|
5379
|
+
}
|
|
5298
5380
|
const activeSupport = resolveObservedRobotActiveSupportProjection({
|
|
5299
5381
|
supportPoints,
|
|
5300
5382
|
contactIds: supportContacts.contactIds,
|
|
@@ -5343,18 +5425,134 @@ export class Viewer3D {
|
|
|
5343
5425
|
robot.position.x += temporalSupportAnchor.offsetX;
|
|
5344
5426
|
robot.position.z += temporalSupportAnchor.offsetZ;
|
|
5345
5427
|
robot.updateMatrixWorld(true);
|
|
5428
|
+
const stabilizedSurfaceMinimumY = this.applyObservedSoleStabilization(robot, soleEligible, footSupportEligibility.eligible);
|
|
5346
5429
|
const observedSupport = this.resolveObservedRobotSupport(robot, activeSupport.activeContactCount);
|
|
5347
5430
|
this.observedRobotGrounding = resolveObservedRobotGroundingProjection({
|
|
5348
5431
|
enabled: this.options.groundObservedRobotGhost,
|
|
5349
5432
|
floorY: ROBOT_VIEWER_SUPPORT_FLOOR_Y,
|
|
5350
|
-
supportMinimumY: observedSupport.supportMinimumY,
|
|
5433
|
+
supportMinimumY: stabilizedSurfaceMinimumY ?? observedSupport.supportMinimumY,
|
|
5351
5434
|
supportContactCount: observedSupport.supportContactCount,
|
|
5352
|
-
source: observedSupport.source
|
|
5435
|
+
source: stabilizedSurfaceMinimumY === null || stabilizedSurfaceMinimumY === undefined ? observedSupport.source : 'foot-geometry'
|
|
5353
5436
|
});
|
|
5354
5437
|
robot.position.y += this.observedRobotGrounding.offsetY;
|
|
5355
5438
|
robot.updateMatrixWorld(true);
|
|
5356
5439
|
this.applyNavigationPreviewPresentationTransform();
|
|
5357
5440
|
}
|
|
5441
|
+
applyObservedSoleStabilization(robot, eligible, supportEligible) {
|
|
5442
|
+
if (!(this.observedSoleStrength > 0) || !supportEligible)
|
|
5443
|
+
return null;
|
|
5444
|
+
const status = this.observedRobotPresentation?.getStatus();
|
|
5445
|
+
if (status?.mode === 'raw' || status?.fallbackReason === 'sample_gap') {
|
|
5446
|
+
this.observedSoleAnchors.reset();
|
|
5447
|
+
return null;
|
|
5448
|
+
}
|
|
5449
|
+
const chains = this.options.supportDefinition?.soleStabilizationChains ?? [];
|
|
5450
|
+
const joints = robot.joints ?? {};
|
|
5451
|
+
const pose = (groupId, foot) => {
|
|
5452
|
+
const contacts = this.resolveObservedRobotSupportContacts(robot);
|
|
5453
|
+
const points = contacts.supportPoints.filter((_, index) => contacts.supportGroupIds[index] === groupId);
|
|
5454
|
+
const center = points.reduce((sum, point) => sum.add(point), new Vector3()).multiplyScalar(1 / Math.max(1, points.length));
|
|
5455
|
+
const rotation = foot.getWorldQuaternion(new Quaternion());
|
|
5456
|
+
const supportMinimumY = this.resolvePreciseObservedSoleMinimumY(foot) ?? Math.min(...points.map(point => point.y));
|
|
5457
|
+
return { position: [center.x, center.y, center.z], orientation: [rotation.x, rotation.y, rotation.z, rotation.w], supportMinimumY };
|
|
5458
|
+
};
|
|
5459
|
+
const candidates = chains.flatMap(chain => {
|
|
5460
|
+
const selected = chain.jointNames.map(name => joints[name]);
|
|
5461
|
+
const foot = selected[selected.length - 1];
|
|
5462
|
+
if (!foot || typeof foot.getWorldQuaternion !== 'function' || selected.some(joint => !joint || joint.mimicJoint))
|
|
5463
|
+
return [];
|
|
5464
|
+
const baseline = selected.map(joint => this.readJointValue(joint));
|
|
5465
|
+
if (baseline.some(value => typeof value !== 'number' || !Number.isFinite(value)))
|
|
5466
|
+
return [];
|
|
5467
|
+
return [{ chain, selected: selected, foot, baseline: baseline, raw: pose(chain.groupId, foot) }];
|
|
5468
|
+
});
|
|
5469
|
+
const targets = this.observedSoleAnchors.resolve(this.observedSoleConnectionId, candidates.map(candidate => ({
|
|
5470
|
+
id: candidate.chain.groupId, position: candidate.raw.position,
|
|
5471
|
+
eligible: this.options.groundObservedRobotGhost && supportEligible && eligible.get(candidate.chain.groupId) === true
|
|
5472
|
+
})), this.observedSoleStrength);
|
|
5473
|
+
// Both eligible soles share a surface before the final root grounding.
|
|
5474
|
+
// Preserve lifted/released feet rather than treating every chain as contact.
|
|
5475
|
+
const supportPlaneY = Math.min(...candidates.filter(candidate => targets.some(target => target.id === candidate.chain.groupId)).map(candidate => candidate.raw.supportMinimumY));
|
|
5476
|
+
for (const target of targets) {
|
|
5477
|
+
const candidate = candidates.find(entry => entry.chain.groupId === target.id);
|
|
5478
|
+
const verticalOffset = supportPlaneY - candidate.raw.supportMinimumY;
|
|
5479
|
+
const targetPosition = [target.target[0], candidate.raw.position[1] + verticalOffset, target.target[2]];
|
|
5480
|
+
if (Math.hypot(...targetPosition.map((value, index) => value - candidate.raw.position[index])) < 0.00001)
|
|
5481
|
+
continue;
|
|
5482
|
+
const apply = (values) => {
|
|
5483
|
+
candidate.selected.forEach((joint, index) => joint.setJointValue(values[index]));
|
|
5484
|
+
robot.updateMatrixWorld(true);
|
|
5485
|
+
};
|
|
5486
|
+
try {
|
|
5487
|
+
const correction = projectBoundedSoleCorrection({
|
|
5488
|
+
values: candidate.baseline,
|
|
5489
|
+
limits: candidate.selected.map(joint => [joint.limit?.lower ?? -Math.PI, joint.limit?.upper ?? Math.PI]),
|
|
5490
|
+
targetPosition, targetOrientation: candidate.raw.orientation,
|
|
5491
|
+
targetSupportMinimumY: supportPlaneY,
|
|
5492
|
+
evaluate: values => { apply(values); return pose(target.id, candidate.foot); }
|
|
5493
|
+
});
|
|
5494
|
+
apply(correction.values);
|
|
5495
|
+
candidate.selected.forEach((joint, index) => this.observedSoleBaseline.set(joint, candidate.baseline[index]));
|
|
5496
|
+
}
|
|
5497
|
+
catch {
|
|
5498
|
+
// Invalid/unreachable authored chains must leave the observed copy intact.
|
|
5499
|
+
apply(candidate.baseline);
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
if (!targets.length)
|
|
5503
|
+
return null;
|
|
5504
|
+
// An unanchored foot must still not be pushed through the floor by root grounding.
|
|
5505
|
+
const visibleMinima = candidates.map(candidate => this.resolvePreciseObservedSoleMinimumY(candidate.foot))
|
|
5506
|
+
.filter((value) => value !== null);
|
|
5507
|
+
const minimum = Math.min(...visibleMinima);
|
|
5508
|
+
return Number.isFinite(minimum) ? minimum : null;
|
|
5509
|
+
}
|
|
5510
|
+
/** Exact directional support of rigid visible meshes, with cached convex vertices. */
|
|
5511
|
+
resolvePreciseObservedSoleMinimumY(foot) {
|
|
5512
|
+
this.observedSoleSurfaceVertices ??= new WeakMap();
|
|
5513
|
+
let minimum = Infinity;
|
|
5514
|
+
foot.traverse(node => {
|
|
5515
|
+
const mesh = node;
|
|
5516
|
+
if (!mesh.isMesh || mesh.isURDFCollider || !mesh.geometry)
|
|
5517
|
+
return;
|
|
5518
|
+
const attribute = mesh.geometry.getAttribute('position');
|
|
5519
|
+
if (!attribute)
|
|
5520
|
+
return;
|
|
5521
|
+
const version = 'version' in attribute ? Number(attribute.version) : attribute.data.version;
|
|
5522
|
+
let cached = this.observedSoleSurfaceVertices.get(mesh.geometry);
|
|
5523
|
+
if (!cached || cached.attribute !== attribute || cached.version !== version) {
|
|
5524
|
+
const points = [];
|
|
5525
|
+
for (let index = 0; index < attribute.count; index++) {
|
|
5526
|
+
const point = new Vector3().fromBufferAttribute(attribute, index);
|
|
5527
|
+
if ([point.x, point.y, point.z].every(Number.isFinite))
|
|
5528
|
+
points.push(point);
|
|
5529
|
+
}
|
|
5530
|
+
let vertices = points;
|
|
5531
|
+
if (points.length > 4) {
|
|
5532
|
+
try {
|
|
5533
|
+
const hull = new ConvexHull().setFromPoints(points);
|
|
5534
|
+
const unique = new Set();
|
|
5535
|
+
for (const face of hull.faces) {
|
|
5536
|
+
let edge = face.edge;
|
|
5537
|
+
do {
|
|
5538
|
+
unique.add(edge.vertex.point);
|
|
5539
|
+
edge = edge.next;
|
|
5540
|
+
} while (edge !== face.edge);
|
|
5541
|
+
}
|
|
5542
|
+
if (unique.size)
|
|
5543
|
+
vertices = [...unique];
|
|
5544
|
+
}
|
|
5545
|
+
catch { /* Degenerate meshes retain their exact original vertices. */ }
|
|
5546
|
+
}
|
|
5547
|
+
cached = { attribute, version, points: vertices };
|
|
5548
|
+
this.observedSoleSurfaceVertices.set(mesh.geometry, cached);
|
|
5549
|
+
}
|
|
5550
|
+
const matrix = mesh.matrixWorld.elements;
|
|
5551
|
+
for (const point of cached.points)
|
|
5552
|
+
minimum = Math.min(minimum, matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13]);
|
|
5553
|
+
});
|
|
5554
|
+
return Number.isFinite(minimum) ? minimum : null;
|
|
5555
|
+
}
|
|
5358
5556
|
resolveObservedRobotSupportContacts(robot) {
|
|
5359
5557
|
const supportPoints = [];
|
|
5360
5558
|
const contactIds = [];
|