@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.
- package/README.md +40 -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/Viewer3DRenderer.d.ts +17 -0
- package/dist/renderer/Viewer3DRenderer.d.ts.map +1 -1
- package/dist/renderer/Viewer3DRenderer.js +188 -3
- package/dist/renderer/Viewer3DRenderer.js.map +1 -1
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +1 -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 +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';
|
|
@@ -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 = [];
|