@relu-interactives/spatial-ecs 0.4.4 → 0.5.0
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/dist/{System-ln4Q7Eot.d.cts → System-V_mv07le.d.cts} +2 -2
- package/dist/{System-ln4Q7Eot.d.ts → System-V_mv07le.d.ts} +2 -2
- package/dist/{XRHandPose-CxPv-LBo.d.cts → XRHandPose-CQTLGxFC.d.cts} +218 -2
- package/dist/{XRHandPose-BImIa4ZS.d.ts → XRHandPose-CU3vNFuS.d.ts} +218 -2
- package/dist/{chunk-4AK4I3QF.js → chunk-BUEKOSR5.js} +102 -3
- package/dist/chunk-BUEKOSR5.js.map +1 -0
- package/dist/{chunk-SGADE55G.js → chunk-RXBLOO5B.js} +249 -90
- package/dist/chunk-RXBLOO5B.js.map +1 -0
- package/dist/{chunk-4DWZK4X4.js → chunk-WJD2VKEH.js} +3 -3
- package/dist/chunk-WJD2VKEH.js.map +1 -0
- package/dist/{chunk-OY2OONRR.js → chunk-Z6ZMR3CS.js} +1374 -30
- package/dist/chunk-Z6ZMR3CS.js.map +1 -0
- package/dist/components/index.cjs +99 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +3 -3
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.js +1 -1
- package/dist/docs/guide/mixed-reality.md +137 -3
- package/dist/docs/guide/spatial-ui.md +165 -0
- package/dist/docs/guide/vr-and-hand-poses.md +29 -0
- package/dist/docs/guide/world-api.md +1 -1
- package/dist/factories/index.cjs +372 -114
- package/dist/factories/index.cjs.map +1 -1
- package/dist/factories/index.d.cts +13 -3
- package/dist/factories/index.d.ts +13 -3
- package/dist/factories/index.js +3 -3
- package/dist/{index-CuGqxuk-.d.ts → index-C3ik3sA1.d.ts} +16 -3
- package/dist/{index-B2oHfqkj.d.ts → index-CppKDxbu.d.ts} +187 -3
- package/dist/{index-CQsHUez3.d.cts → index-Dlr-7-4m.d.cts} +16 -3
- package/dist/{index-7YCk4cmQ.d.cts → index-I2IsocYW.d.cts} +187 -3
- package/dist/index.cjs +1977 -366
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -9
- package/dist/index.d.ts +42 -9
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/systems/index.cjs +1465 -25
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +2 -2
- package/dist/systems/index.d.ts +2 -2
- package/dist/systems/index.js +3 -3
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/worldSnapshotUtils.d.cts +1 -1
- package/dist/worldSnapshotUtils.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-4AK4I3QF.js.map +0 -1
- package/dist/chunk-4DWZK4X4.js.map +0 -1
- package/dist/chunk-OY2OONRR.js.map +0 -1
- package/dist/chunk-SGADE55G.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-
|
|
2
|
-
import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, createAttachPoint, XRHandPoseComponent, createHandPoseEntry, XRAnchorComponent, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, XREnvironmentComponent, handPoseDefinitionFor, getHandPosePreset } from './chunk-
|
|
1
|
+
import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-WJD2VKEH.js';
|
|
2
|
+
import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, createAttachPoint, XRHandPoseComponent, createHandPoseEntry, XRAnchorComponent, UIPanelComponent, UITextComponent, UIButtonComponent, UIToggleComponent, UISliderComponent, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, XREnvironmentComponent, handPoseDefinitionFor, getHandPosePreset } from './chunk-BUEKOSR5.js';
|
|
3
3
|
import { __name } from './chunk-OY7ZODZP.js';
|
|
4
4
|
import * as THREE19 from 'three';
|
|
5
5
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
@@ -2848,6 +2848,11 @@ function buildComponentValueMaps(snapshot) {
|
|
|
2848
2848
|
"XRHandPoseComponent"
|
|
2849
2849
|
),
|
|
2850
2850
|
xrAnchorValues: readByComponent(snapshot.components, "XRAnchorComponent"),
|
|
2851
|
+
uiPanelValues: readByComponent(snapshot.components, "UIPanelComponent"),
|
|
2852
|
+
uiTextValues: readByComponent(snapshot.components, "UITextComponent"),
|
|
2853
|
+
uiButtonValues: readByComponent(snapshot.components, "UIButtonComponent"),
|
|
2854
|
+
uiToggleValues: readByComponent(snapshot.components, "UIToggleComponent"),
|
|
2855
|
+
uiSliderValues: readByComponent(snapshot.components, "UISliderComponent"),
|
|
2851
2856
|
xrInteractorSideValues: readByComponent(
|
|
2852
2857
|
snapshot.components,
|
|
2853
2858
|
"XRInteractorSideComponent"
|
|
@@ -3169,6 +3174,49 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
|
|
|
3169
3174
|
}
|
|
3170
3175
|
}
|
|
3171
3176
|
}
|
|
3177
|
+
{
|
|
3178
|
+
const restoreUi = /* @__PURE__ */ __name((map, Ctor, existing) => {
|
|
3179
|
+
const source = asRecord(map.get(sourceEntityId));
|
|
3180
|
+
if (!source) return;
|
|
3181
|
+
let component = existing;
|
|
3182
|
+
if (!component) {
|
|
3183
|
+
component = new Ctor();
|
|
3184
|
+
world.addComponent(targetEntityId, component);
|
|
3185
|
+
}
|
|
3186
|
+
const target = component;
|
|
3187
|
+
for (const [key, value] of Object.entries(source)) {
|
|
3188
|
+
const current = target[key];
|
|
3189
|
+
if ((typeof value === "string" || typeof value === "number" || typeof value === "boolean") && typeof current === typeof value) {
|
|
3190
|
+
target[key] = value;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
}, "restoreUi");
|
|
3194
|
+
restoreUi(
|
|
3195
|
+
values.uiPanelValues,
|
|
3196
|
+
UIPanelComponent,
|
|
3197
|
+
world.getComponent(targetEntityId, UIPanelComponent)
|
|
3198
|
+
);
|
|
3199
|
+
restoreUi(
|
|
3200
|
+
values.uiTextValues,
|
|
3201
|
+
UITextComponent,
|
|
3202
|
+
world.getComponent(targetEntityId, UITextComponent)
|
|
3203
|
+
);
|
|
3204
|
+
restoreUi(
|
|
3205
|
+
values.uiButtonValues,
|
|
3206
|
+
UIButtonComponent,
|
|
3207
|
+
world.getComponent(targetEntityId, UIButtonComponent)
|
|
3208
|
+
);
|
|
3209
|
+
restoreUi(
|
|
3210
|
+
values.uiToggleValues,
|
|
3211
|
+
UIToggleComponent,
|
|
3212
|
+
world.getComponent(targetEntityId, UIToggleComponent)
|
|
3213
|
+
);
|
|
3214
|
+
restoreUi(
|
|
3215
|
+
values.uiSliderValues,
|
|
3216
|
+
UISliderComponent,
|
|
3217
|
+
world.getComponent(targetEntityId, UISliderComponent)
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3172
3220
|
{
|
|
3173
3221
|
const sourceInteractor = asRecord(
|
|
3174
3222
|
values.xrInteractorValues.get(sourceEntityId)
|
|
@@ -5134,6 +5182,23 @@ var _Behaviour = class _Behaviour {
|
|
|
5134
5182
|
/** Called when this entity is released. Optional XR hook. */
|
|
5135
5183
|
onRelease(input) {
|
|
5136
5184
|
}
|
|
5185
|
+
/** A UI button on this entity (or a descendant) was clicked. Clicks fire on
|
|
5186
|
+
* RELEASE while still pointing at the widget — sliding off cancels. */
|
|
5187
|
+
onUiPress(_event) {
|
|
5188
|
+
}
|
|
5189
|
+
/** A pointer began hovering a UI widget on this entity (or a descendant). */
|
|
5190
|
+
onUiHoverStart(_event) {
|
|
5191
|
+
}
|
|
5192
|
+
/** The pointer left the widget. */
|
|
5193
|
+
onUiHoverEnd(_event) {
|
|
5194
|
+
}
|
|
5195
|
+
/** A UI toggle flipped; `event.value` is the new boolean. */
|
|
5196
|
+
onUiToggle(_event) {
|
|
5197
|
+
}
|
|
5198
|
+
/** A UI slider moved; `event.value` is the new number. Fires on real value
|
|
5199
|
+
* changes during the drag and once more on release. */
|
|
5200
|
+
onUiChange(_event) {
|
|
5201
|
+
}
|
|
5137
5202
|
};
|
|
5138
5203
|
__name(_Behaviour, "Behaviour");
|
|
5139
5204
|
/**
|
|
@@ -6664,6 +6729,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6664
6729
|
this.grabs = /* @__PURE__ */ new Map();
|
|
6665
6730
|
this.hoveredEntityId = null;
|
|
6666
6731
|
/** Per-hand hover, with the hit distance so the ray can stop at the surface. */
|
|
6732
|
+
this.busyContributors = [];
|
|
6667
6733
|
this.hovers = /* @__PURE__ */ new Map();
|
|
6668
6734
|
/** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
|
|
6669
6735
|
this.elapsed = 0;
|
|
@@ -6672,6 +6738,8 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6672
6738
|
update(world, deltaTime) {
|
|
6673
6739
|
if (!world.isXrPresenting()) {
|
|
6674
6740
|
this.releaseAll(world);
|
|
6741
|
+
this.hovers.clear();
|
|
6742
|
+
this.hoveredEntityId = null;
|
|
6675
6743
|
return;
|
|
6676
6744
|
}
|
|
6677
6745
|
this.elapsed += Math.max(0, deltaTime);
|
|
@@ -7294,13 +7362,30 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
7294
7362
|
*/
|
|
7295
7363
|
isSourceBusy(world, input) {
|
|
7296
7364
|
if (this.grabs.has(input)) return true;
|
|
7365
|
+
for (const contributor of this.busyContributors) {
|
|
7366
|
+
try {
|
|
7367
|
+
if (contributor(world, input)) return true;
|
|
7368
|
+
} catch {
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7297
7371
|
const hover = this.hovers.get(input);
|
|
7298
7372
|
if (!hover) return false;
|
|
7299
7373
|
const interactable = world.getComponent(
|
|
7300
7374
|
hover.entityId,
|
|
7301
7375
|
XRInteractableComponent
|
|
7302
7376
|
);
|
|
7303
|
-
return Boolean(
|
|
7377
|
+
return Boolean(
|
|
7378
|
+
interactable?.enabled && (interactable.grabbable || interactable.pointable)
|
|
7379
|
+
);
|
|
7380
|
+
}
|
|
7381
|
+
/** OR-ed into isSourceBusy; see the comment there. */
|
|
7382
|
+
registerBusyContributor(contributor) {
|
|
7383
|
+
this.busyContributors.push(contributor);
|
|
7384
|
+
}
|
|
7385
|
+
/** Hover for a specific input source (handedness-free — Vision Pro's
|
|
7386
|
+
* transient pointers all report handedness "none"). */
|
|
7387
|
+
getHoverForInput(input) {
|
|
7388
|
+
return this.hovers.get(input) ?? null;
|
|
7304
7389
|
}
|
|
7305
7390
|
/** True when THIS hand is holding something. */
|
|
7306
7391
|
isGrabbingWith(handedness) {
|
|
@@ -7464,6 +7549,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
7464
7549
|
const rig = world.getComponent(rigEntityId, XRRigComponent);
|
|
7465
7550
|
const rigTransform = world.getComponent(rigEntityId, TransformComponent);
|
|
7466
7551
|
if (!rig || !rigTransform) return;
|
|
7552
|
+
if (world.isXrPassthrough()) return;
|
|
7467
7553
|
const inputs = this.inputSystem.getInputs();
|
|
7468
7554
|
if (inputs.length === 0) return;
|
|
7469
7555
|
if (rig.snapTurn) {
|
|
@@ -7726,6 +7812,32 @@ var _XRSessionSystem = class _XRSessionSystem {
|
|
|
7726
7812
|
}
|
|
7727
7813
|
if (presenting) {
|
|
7728
7814
|
this.dispatchInputEdges(world);
|
|
7815
|
+
this.muteDepthOcclusionColor(world);
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
/**
|
|
7819
|
+
* Stop three's depth-occlusion pass from writing COLOR.
|
|
7820
|
+
*
|
|
7821
|
+
* With depth sensing granted, three draws a full-screen, head-locked mesh
|
|
7822
|
+
* every frame whose fragment shader assigns ONLY `gl_FragDepth` — that is
|
|
7823
|
+
* the whole occlusion mechanism — but its material never sets
|
|
7824
|
+
* `colorWrite: false`, so the colour output is UNDEFINED per GLSL. Desktop
|
|
7825
|
+
* drivers happen to zero it, which is why this ships unnoticed; Quest's
|
|
7826
|
+
* mobile GPU frequently leaves the last-computed temporary in the register —
|
|
7827
|
+
* the sampled DEPTH value — so the pass paints the depth camera's view over
|
|
7828
|
+
* the screen as faint head-locked contour bands. On device that reads as "a
|
|
7829
|
+
* large wireframe dome that follows my head".
|
|
7830
|
+
*
|
|
7831
|
+
* The mesh is created lazily inside three's render, so this is re-checked
|
|
7832
|
+
* every frame rather than hooked once; the write is idempotent and the
|
|
7833
|
+
* check is two property reads. Depth-only is the mesh's entire purpose —
|
|
7834
|
+
* occlusion is unaffected.
|
|
7835
|
+
*/
|
|
7836
|
+
muteDepthOcclusionColor(world) {
|
|
7837
|
+
const xr = world.getRenderer().xr;
|
|
7838
|
+
const material = xr.getDepthSensingMesh?.()?.material;
|
|
7839
|
+
if (material && material.colorWrite !== false) {
|
|
7840
|
+
material.colorWrite = false;
|
|
7729
7841
|
}
|
|
7730
7842
|
}
|
|
7731
7843
|
handleSessionStart(world) {
|
|
@@ -7879,8 +7991,12 @@ function planeColliderFromPolygon(polygon) {
|
|
|
7879
7991
|
__name(planeColliderFromPolygon, "planeColliderFromPolygon");
|
|
7880
7992
|
function writeXrMeshGeometry(vertices, indices, existing) {
|
|
7881
7993
|
const geometry = existing ?? new THREE19.BufferGeometry();
|
|
7882
|
-
geometry.setAttribute(
|
|
7883
|
-
|
|
7994
|
+
geometry.setAttribute(
|
|
7995
|
+
"position",
|
|
7996
|
+
new THREE19.BufferAttribute(vertices.slice(), 3)
|
|
7997
|
+
);
|
|
7998
|
+
geometry.setIndex(new THREE19.BufferAttribute(indices.slice(), 1));
|
|
7999
|
+
geometry.deleteAttribute("normal");
|
|
7884
8000
|
geometry.computeBoundingSphere();
|
|
7885
8001
|
geometry.computeVertexNormals();
|
|
7886
8002
|
return geometry;
|
|
@@ -7904,20 +8020,151 @@ function decomposeXrPose(matrixElements, target) {
|
|
|
7904
8020
|
}
|
|
7905
8021
|
__name(decomposeXrPose, "decomposeXrPose");
|
|
7906
8022
|
|
|
8023
|
+
// core/xr/poseStability.ts
|
|
8024
|
+
var GLUED_REF_SPEED = 0.12;
|
|
8025
|
+
var STILL_SPEED = 0.05;
|
|
8026
|
+
var GLUED_FRAMES_TO_FREEZE = 3;
|
|
8027
|
+
var STATIC_FRAMES_TO_RECOVER = 10;
|
|
8028
|
+
var SNAP_SPEED = 3;
|
|
8029
|
+
var dist = /* @__PURE__ */ __name((a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z), "dist");
|
|
8030
|
+
var _SurfacePoseGate = class _SurfacePoseGate {
|
|
8031
|
+
constructor() {
|
|
8032
|
+
this.prevRef = null;
|
|
8033
|
+
this.prevView = null;
|
|
8034
|
+
this.gluedStreak = 0;
|
|
8035
|
+
this.staticStreak = 0;
|
|
8036
|
+
this.suspect = false;
|
|
8037
|
+
this.deltas = 0;
|
|
8038
|
+
}
|
|
8039
|
+
/** True while poses are being rejected as head-glued. */
|
|
8040
|
+
get frozen() {
|
|
8041
|
+
return this.suspect;
|
|
8042
|
+
}
|
|
8043
|
+
/**
|
|
8044
|
+
* True once enough motion has been observed to classify the pose at all.
|
|
8045
|
+
* Surface CREATION waits for this: an entity spawned from a glued pose
|
|
8046
|
+
* materializes wherever the head happens to be — a rotated ghost copy of
|
|
8047
|
+
* the room — and freezing it there is barely better than letting it follow.
|
|
8048
|
+
*/
|
|
8049
|
+
get warmedUp() {
|
|
8050
|
+
return this.deltas >= GLUED_FRAMES_TO_FREEZE;
|
|
8051
|
+
}
|
|
8052
|
+
/**
|
|
8053
|
+
* Feed this frame's surface position in reference space and in view
|
|
8054
|
+
* (viewer-relative) space. Returns true when the pose should be APPLIED,
|
|
8055
|
+
* false when it should be frozen out.
|
|
8056
|
+
*
|
|
8057
|
+
* `viewPos` may be null when the viewer pose is unavailable (older
|
|
8058
|
+
* runtimes) — the gate then passes everything through, degrading to the
|
|
8059
|
+
* flag-only behavior.
|
|
8060
|
+
*/
|
|
8061
|
+
update(refPos, viewPos, deltaTime) {
|
|
8062
|
+
if (!viewPos || deltaTime <= 0) {
|
|
8063
|
+
this.prevRef = { ...refPos };
|
|
8064
|
+
this.prevView = viewPos ? { ...viewPos } : null;
|
|
8065
|
+
return !this.suspect;
|
|
8066
|
+
}
|
|
8067
|
+
const prevRef = this.prevRef;
|
|
8068
|
+
const prevView = this.prevView;
|
|
8069
|
+
this.prevRef = { ...refPos };
|
|
8070
|
+
this.prevView = { ...viewPos };
|
|
8071
|
+
if (!prevRef || !prevView) return !this.suspect;
|
|
8072
|
+
this.deltas += 1;
|
|
8073
|
+
const refSpeed = dist(refPos, prevRef) / deltaTime;
|
|
8074
|
+
const viewSpeed = dist(viewPos, prevView) / deltaTime;
|
|
8075
|
+
const glued = refSpeed > GLUED_REF_SPEED && viewSpeed < STILL_SPEED;
|
|
8076
|
+
const worldStatic = refSpeed < STILL_SPEED && viewSpeed > GLUED_REF_SPEED;
|
|
8077
|
+
if (this.suspect) {
|
|
8078
|
+
if (refSpeed > SNAP_SPEED && viewSpeed > SNAP_SPEED) {
|
|
8079
|
+
this.suspect = false;
|
|
8080
|
+
this.gluedStreak = 0;
|
|
8081
|
+
this.staticStreak = 0;
|
|
8082
|
+
return true;
|
|
8083
|
+
}
|
|
8084
|
+
this.staticStreak = worldStatic ? this.staticStreak + 1 : 0;
|
|
8085
|
+
if (this.staticStreak >= STATIC_FRAMES_TO_RECOVER) {
|
|
8086
|
+
this.suspect = false;
|
|
8087
|
+
this.gluedStreak = 0;
|
|
8088
|
+
this.staticStreak = 0;
|
|
8089
|
+
return true;
|
|
8090
|
+
}
|
|
8091
|
+
return false;
|
|
8092
|
+
}
|
|
8093
|
+
this.gluedStreak = glued ? this.gluedStreak + 1 : 0;
|
|
8094
|
+
if (this.gluedStreak >= GLUED_FRAMES_TO_FREEZE) {
|
|
8095
|
+
this.suspect = true;
|
|
8096
|
+
this.staticStreak = 0;
|
|
8097
|
+
return false;
|
|
8098
|
+
}
|
|
8099
|
+
return true;
|
|
8100
|
+
}
|
|
8101
|
+
};
|
|
8102
|
+
__name(_SurfacePoseGate, "SurfacePoseGate");
|
|
8103
|
+
var SurfacePoseGate = _SurfacePoseGate;
|
|
8104
|
+
|
|
7907
8105
|
// core/systems/XREnvironmentSystem.ts
|
|
7908
8106
|
var DEBUG_COLOR = 5088255;
|
|
8107
|
+
function debugColorFor(kind, semanticLabel) {
|
|
8108
|
+
switch (semanticLabel) {
|
|
8109
|
+
case "floor":
|
|
8110
|
+
return 5100666;
|
|
8111
|
+
// green — the surface everything should rest on
|
|
8112
|
+
case "ceiling":
|
|
8113
|
+
return 11693055;
|
|
8114
|
+
// purple
|
|
8115
|
+
case "wall":
|
|
8116
|
+
case "wall face":
|
|
8117
|
+
return DEBUG_COLOR;
|
|
8118
|
+
// blue, the family's base colour
|
|
8119
|
+
case "global mesh":
|
|
8120
|
+
return 9079442;
|
|
8121
|
+
// dim gray — raw scan, not an object
|
|
8122
|
+
default:
|
|
8123
|
+
return semanticLabel ? 16747594 : DEBUG_COLOR;
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8126
|
+
__name(debugColorFor, "debugColorFor");
|
|
8127
|
+
var GLOBAL_MESH_DEBUG_OPACITY = 0.35;
|
|
8128
|
+
var DEBUG_RENDER_ORDER = 900;
|
|
7909
8129
|
var ROOM_CAPTURE_DELAY_SECONDS = 3;
|
|
8130
|
+
var SYNTHETIC_FLOOR_EXTENT = 50;
|
|
8131
|
+
var SYNTHETIC_FLOOR_PLANE = {
|
|
8132
|
+
planeSpace: {},
|
|
8133
|
+
polygon: [
|
|
8134
|
+
{ x: -SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: -SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
8135
|
+
{ x: SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: -SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
8136
|
+
{ x: SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
8137
|
+
{ x: -SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: SYNTHETIC_FLOOR_EXTENT / 2 }
|
|
8138
|
+
],
|
|
8139
|
+
orientation: "horizontal",
|
|
8140
|
+
semanticLabel: "floor",
|
|
8141
|
+
lastChangedTime: 0
|
|
8142
|
+
};
|
|
7910
8143
|
var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
7911
8144
|
constructor() {
|
|
7912
8145
|
/** Keyed by the XRPlane/XRMesh object itself — the runtime's own identity. */
|
|
7913
8146
|
this.planes = /* @__PURE__ */ new Map();
|
|
7914
8147
|
this.meshes = /* @__PURE__ */ new Map();
|
|
8148
|
+
/**
|
|
8149
|
+
* Surfaces observed but not yet SPAWNED — their gate is still proving the
|
|
8150
|
+
* pose is world-anchored. Creating from an unflagged head-glued pose (seen
|
|
8151
|
+
* on-device after a suspend/resume regenerated the scan identities) plants
|
|
8152
|
+
* a rotated ghost copy of the room wherever the head happens to be, which
|
|
8153
|
+
* the apply-gate then freezes there — wrong forever. So admission itself
|
|
8154
|
+
* is gated: a few frames of world-anchored behavior first.
|
|
8155
|
+
*/
|
|
8156
|
+
this.pendingPlanes = /* @__PURE__ */ new Map();
|
|
8157
|
+
this.pendingMeshes = /* @__PURE__ */ new Map();
|
|
7915
8158
|
this.presenting = false;
|
|
7916
8159
|
this.debugVisible = false;
|
|
7917
8160
|
/** Seconds presenting, used to decide the room has genuinely nothing in it. */
|
|
7918
8161
|
this.elapsed = 0;
|
|
7919
8162
|
/** `initiateRoomCapture` may be called only ONCE per session. */
|
|
7920
8163
|
this.roomCaptureRequested = false;
|
|
8164
|
+
/** This frame's inverse viewer matrix, or null when unavailable. */
|
|
8165
|
+
this.viewerInverse = null;
|
|
8166
|
+
this.tmpViewerMatrix = new THREE19.Matrix4();
|
|
8167
|
+
this.tmpViewPos = new THREE19.Vector3();
|
|
7921
8168
|
}
|
|
7922
8169
|
update(world, deltaTime = 0) {
|
|
7923
8170
|
const presenting = world.isXrPresenting();
|
|
@@ -7947,16 +8194,23 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
7947
8194
|
const frame = xr.getFrame?.();
|
|
7948
8195
|
const referenceSpace = xr.getReferenceSpace();
|
|
7949
8196
|
if (!frame || !referenceSpace) return;
|
|
8197
|
+
if (frame.detectedPlanes === void 0 && frame.detectedMeshes === void 0) {
|
|
8198
|
+
this.ensureSyntheticFloor(world);
|
|
8199
|
+
return;
|
|
8200
|
+
}
|
|
8201
|
+
this.removeSyntheticFloor(world);
|
|
7950
8202
|
const meshesAvailable = Boolean(frame.detectedMeshes);
|
|
7951
8203
|
const useMesh = (mode === "mesh" || mode === "auto") && meshesAvailable;
|
|
7952
8204
|
const usePlanes = mode === "planes" || mode === "auto" && !useMesh;
|
|
8205
|
+
const viewer = frame.getViewerPose?.(referenceSpace);
|
|
8206
|
+
this.viewerInverse = viewer ? this.tmpViewerMatrix.fromArray(viewer.transform.matrix).invert() : null;
|
|
7953
8207
|
if (usePlanes) {
|
|
7954
|
-
this.syncPlanes(world, frame, referenceSpace);
|
|
8208
|
+
this.syncPlanes(world, frame, referenceSpace, deltaTime);
|
|
7955
8209
|
} else if (this.planes.size > 0) {
|
|
7956
8210
|
this.clearMap(world, this.planes);
|
|
7957
8211
|
}
|
|
7958
8212
|
if (useMesh) {
|
|
7959
|
-
this.syncMeshes(world, frame, referenceSpace);
|
|
8213
|
+
this.syncMeshes(world, frame, referenceSpace, deltaTime);
|
|
7960
8214
|
} else if (this.meshes.size > 0) {
|
|
7961
8215
|
this.clearMap(world, this.meshes);
|
|
7962
8216
|
}
|
|
@@ -7993,12 +8247,57 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
7993
8247
|
getDetectedCount() {
|
|
7994
8248
|
return { planes: this.planes.size, meshes: this.meshes.size };
|
|
7995
8249
|
}
|
|
8250
|
+
/**
|
|
8251
|
+
* A census of every surface currently spawned: what it is, where it sits and
|
|
8252
|
+
* roughly how big it is. Built for the template's post-session readout, so a
|
|
8253
|
+
* mystery shape on the headset can be looked up by its label instead of
|
|
8254
|
+
* guessed at — a small-radius entry at head height IS the mystery shape, and
|
|
8255
|
+
* a clean list means whatever was seen did not come from this system.
|
|
8256
|
+
*/
|
|
8257
|
+
getDebugSnapshot(world) {
|
|
8258
|
+
const snapshot = [];
|
|
8259
|
+
for (const [plane, entry] of this.planes) {
|
|
8260
|
+
const transform = world.getComponent(entry.entityId, TransformComponent);
|
|
8261
|
+
const collider = planeColliderFromPolygon(plane.polygon);
|
|
8262
|
+
snapshot.push({
|
|
8263
|
+
kind: "plane",
|
|
8264
|
+
label: plane.semanticLabel ?? null,
|
|
8265
|
+
position: transform ? { ...transform.position } : { x: 0, y: 0, z: 0 },
|
|
8266
|
+
radius: collider ? Math.hypot(collider.size.x, collider.size.z) / 2 : 0,
|
|
8267
|
+
emulated: entry.emulated
|
|
8268
|
+
});
|
|
8269
|
+
}
|
|
8270
|
+
for (const [mesh, entry] of this.meshes) {
|
|
8271
|
+
const transform = world.getComponent(entry.entityId, TransformComponent);
|
|
8272
|
+
snapshot.push({
|
|
8273
|
+
kind: "mesh",
|
|
8274
|
+
label: mesh.semanticLabel ?? null,
|
|
8275
|
+
position: transform ? { ...transform.position } : { x: 0, y: 0, z: 0 },
|
|
8276
|
+
radius: entry.geometry?.boundingSphere?.radius ?? 0,
|
|
8277
|
+
emulated: entry.emulated
|
|
8278
|
+
});
|
|
8279
|
+
}
|
|
8280
|
+
return snapshot;
|
|
8281
|
+
}
|
|
8282
|
+
/** Create the no-scan floor once; it lives in the ordinary plane map. */
|
|
8283
|
+
ensureSyntheticFloor(world) {
|
|
8284
|
+
if (this.planes.has(SYNTHETIC_FLOOR_PLANE)) return;
|
|
8285
|
+
const created = this.createPlaneEntity(world, SYNTHETIC_FLOOR_PLANE);
|
|
8286
|
+
if (created) this.planes.set(SYNTHETIC_FLOOR_PLANE, created);
|
|
8287
|
+
}
|
|
8288
|
+
/** Real scene data exists (or arrived) — the stand-in must go. */
|
|
8289
|
+
removeSyntheticFloor(world) {
|
|
8290
|
+
const entry = this.planes.get(SYNTHETIC_FLOOR_PLANE);
|
|
8291
|
+
if (!entry) return;
|
|
8292
|
+
this.destroyEntry(world, entry);
|
|
8293
|
+
this.planes.delete(SYNTHETIC_FLOOR_PLANE);
|
|
8294
|
+
}
|
|
7996
8295
|
findRig(world) {
|
|
7997
8296
|
const ids = world.query(XRRigComponent);
|
|
7998
8297
|
if (ids.length === 0) return null;
|
|
7999
8298
|
return world.getComponent(ids[0], XRRigComponent) ?? null;
|
|
8000
8299
|
}
|
|
8001
|
-
syncPlanes(world, frame, referenceSpace) {
|
|
8300
|
+
syncPlanes(world, frame, referenceSpace, deltaTime) {
|
|
8002
8301
|
const detected = frame.detectedPlanes;
|
|
8003
8302
|
if (!detected) return;
|
|
8004
8303
|
for (const [plane, entry] of this.planes) {
|
|
@@ -8007,14 +8306,22 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8007
8306
|
this.planes.delete(plane);
|
|
8008
8307
|
}
|
|
8009
8308
|
}
|
|
8309
|
+
for (const plane of this.pendingPlanes.keys()) {
|
|
8310
|
+
if (!detected.has(plane)) this.pendingPlanes.delete(plane);
|
|
8311
|
+
}
|
|
8010
8312
|
for (const plane of detected) {
|
|
8011
8313
|
const pose = frame.getPose(plane.planeSpace, referenceSpace);
|
|
8012
8314
|
if (!pose) continue;
|
|
8315
|
+
const emulated = pose.emulatedPosition === true;
|
|
8013
8316
|
let entry = this.planes.get(plane);
|
|
8014
8317
|
const changed = plane.lastChangedTime ?? 0;
|
|
8015
8318
|
if (!entry) {
|
|
8319
|
+
if (emulated) continue;
|
|
8320
|
+
const gate = this.admit(this.pendingPlanes, plane, pose, deltaTime);
|
|
8321
|
+
if (!gate) continue;
|
|
8016
8322
|
const created = this.createPlaneEntity(world, plane);
|
|
8017
8323
|
if (!created) continue;
|
|
8324
|
+
created.gate = gate;
|
|
8018
8325
|
entry = created;
|
|
8019
8326
|
this.planes.set(plane, entry);
|
|
8020
8327
|
} else if (changed !== entry.lastChangedTime) {
|
|
@@ -8025,14 +8332,21 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8025
8332
|
if (collider) {
|
|
8026
8333
|
entry.geometry?.dispose();
|
|
8027
8334
|
entry.geometry = null;
|
|
8028
|
-
this.attachPlaneDebug(
|
|
8335
|
+
this.attachPlaneDebug(
|
|
8336
|
+
world,
|
|
8337
|
+
entry,
|
|
8338
|
+
collider,
|
|
8339
|
+
plane.semanticLabel ?? null
|
|
8340
|
+
);
|
|
8029
8341
|
}
|
|
8030
8342
|
}
|
|
8031
8343
|
}
|
|
8032
|
-
this.
|
|
8344
|
+
const allowed = !emulated && this.gateAllows(entry.gate, pose, deltaTime);
|
|
8345
|
+
entry.emulated = emulated || entry.gate.frozen;
|
|
8346
|
+
if (allowed) this.applyPose(world, entry.entityId, pose);
|
|
8033
8347
|
}
|
|
8034
8348
|
}
|
|
8035
|
-
syncMeshes(world, frame, referenceSpace) {
|
|
8349
|
+
syncMeshes(world, frame, referenceSpace, deltaTime) {
|
|
8036
8350
|
const detected = frame.detectedMeshes;
|
|
8037
8351
|
if (!detected) return;
|
|
8038
8352
|
for (const [mesh, entry] of this.meshes) {
|
|
@@ -8041,23 +8355,68 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8041
8355
|
this.meshes.delete(mesh);
|
|
8042
8356
|
}
|
|
8043
8357
|
}
|
|
8358
|
+
for (const mesh of this.pendingMeshes.keys()) {
|
|
8359
|
+
if (!detected.has(mesh)) this.pendingMeshes.delete(mesh);
|
|
8360
|
+
}
|
|
8044
8361
|
for (const mesh of detected) {
|
|
8045
8362
|
const pose = frame.getPose(mesh.meshSpace, referenceSpace);
|
|
8046
8363
|
if (!pose) continue;
|
|
8364
|
+
const emulated = pose.emulatedPosition === true;
|
|
8047
8365
|
let entry = this.meshes.get(mesh);
|
|
8048
8366
|
const changed = mesh.lastChangedTime ?? 0;
|
|
8049
8367
|
if (!entry) {
|
|
8368
|
+
if (emulated) continue;
|
|
8369
|
+
const gate = this.admit(this.pendingMeshes, mesh, pose, deltaTime);
|
|
8370
|
+
if (!gate) continue;
|
|
8050
8371
|
const created = this.createMeshEntity(world, mesh);
|
|
8051
8372
|
if (!created) continue;
|
|
8373
|
+
created.gate = gate;
|
|
8052
8374
|
entry = created;
|
|
8053
8375
|
this.meshes.set(mesh, entry);
|
|
8054
8376
|
} else if (changed !== entry.lastChangedTime) {
|
|
8055
8377
|
entry.lastChangedTime = changed;
|
|
8056
8378
|
writeXrMeshGeometry(mesh.vertices, mesh.indices, entry.geometry);
|
|
8057
8379
|
}
|
|
8058
|
-
this.
|
|
8380
|
+
const allowed = !emulated && this.gateAllows(entry.gate, pose, deltaTime);
|
|
8381
|
+
entry.emulated = emulated || entry.gate.frozen;
|
|
8382
|
+
if (allowed) this.applyPose(world, entry.entityId, pose);
|
|
8059
8383
|
}
|
|
8060
8384
|
}
|
|
8385
|
+
/**
|
|
8386
|
+
* Admission control for a NOT-yet-spawned surface. With a viewer pose
|
|
8387
|
+
* available, the surface must behave world-anchored for a few frames before
|
|
8388
|
+
* an entity is created for it; without one (older runtimes, tests) it is
|
|
8389
|
+
* admitted immediately — the gate can't classify anyway. Returns the
|
|
8390
|
+
* warmed-up gate (carried into the entry so its motion history survives),
|
|
8391
|
+
* or null while the surface must keep waiting.
|
|
8392
|
+
*/
|
|
8393
|
+
admit(pending, key, pose, deltaTime) {
|
|
8394
|
+
if (!this.viewerInverse) return new SurfacePoseGate();
|
|
8395
|
+
let gate = pending.get(key);
|
|
8396
|
+
if (!gate) {
|
|
8397
|
+
gate = new SurfacePoseGate();
|
|
8398
|
+
pending.set(key, gate);
|
|
8399
|
+
}
|
|
8400
|
+
const allowed = this.gateAllows(gate, pose, deltaTime);
|
|
8401
|
+
if (!allowed || !gate.warmedUp) return null;
|
|
8402
|
+
pending.delete(key);
|
|
8403
|
+
return gate;
|
|
8404
|
+
}
|
|
8405
|
+
/**
|
|
8406
|
+
* Run a surface's pose through its head-glue gate: position in reference
|
|
8407
|
+
* space vs position relative to the viewer. Without a viewer pose the gate
|
|
8408
|
+
* passes everything, degrading to the flag-only behavior.
|
|
8409
|
+
*/
|
|
8410
|
+
gateAllows(gate, pose, deltaTime) {
|
|
8411
|
+
const m = pose.transform.matrix;
|
|
8412
|
+
const refPos = { x: m[12], y: m[13], z: m[14] };
|
|
8413
|
+
let viewPos = null;
|
|
8414
|
+
if (this.viewerInverse) {
|
|
8415
|
+
const v = this.tmpViewPos.set(refPos.x, refPos.y, refPos.z).applyMatrix4(this.viewerInverse);
|
|
8416
|
+
viewPos = { x: v.x, y: v.y, z: v.z };
|
|
8417
|
+
}
|
|
8418
|
+
return gate.update(refPos, viewPos, deltaTime);
|
|
8419
|
+
}
|
|
8061
8420
|
createPlaneEntity(world, plane) {
|
|
8062
8421
|
const collider = planeColliderFromPolygon(plane.polygon);
|
|
8063
8422
|
if (!collider) return null;
|
|
@@ -8075,9 +8434,13 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8075
8434
|
entityId,
|
|
8076
8435
|
lastChangedTime: plane.lastChangedTime ?? 0,
|
|
8077
8436
|
geometry: null,
|
|
8078
|
-
material: null
|
|
8437
|
+
material: null,
|
|
8438
|
+
emulated: false,
|
|
8439
|
+
gate: new SurfacePoseGate()
|
|
8079
8440
|
};
|
|
8080
|
-
if (this.debugVisible)
|
|
8441
|
+
if (this.debugVisible) {
|
|
8442
|
+
this.attachPlaneDebug(world, entry, collider, plane.semanticLabel ?? null);
|
|
8443
|
+
}
|
|
8081
8444
|
return entry;
|
|
8082
8445
|
}
|
|
8083
8446
|
applyPlaneCollider(world, entityId, plane) {
|
|
@@ -8102,15 +8465,36 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8102
8465
|
createMeshEntity(world, mesh) {
|
|
8103
8466
|
if (!mesh.vertices?.length || !mesh.indices?.length) return null;
|
|
8104
8467
|
const geometry = writeXrMeshGeometry(mesh.vertices, mesh.indices);
|
|
8468
|
+
const label = mesh.semanticLabel ?? null;
|
|
8469
|
+
const isGlobalMesh = label === "global mesh";
|
|
8105
8470
|
const material = new THREE19.MeshBasicMaterial({
|
|
8106
|
-
color:
|
|
8107
|
-
wireframe: true
|
|
8471
|
+
color: debugColorFor("mesh", label),
|
|
8472
|
+
wireframe: true,
|
|
8473
|
+
// The global scan is a coarse triangle soup of the whole room; at full
|
|
8474
|
+
// strength it reads as phantom objects rather than as context.
|
|
8475
|
+
transparent: isGlobalMesh,
|
|
8476
|
+
opacity: isGlobalMesh ? GLOBAL_MESH_DEBUG_OPACITY : 1,
|
|
8477
|
+
// Depth-TESTED, deliberately. The overlay was X-ray for one release —
|
|
8478
|
+
// introduced on the theory that depth occlusion was carving it into a
|
|
8479
|
+
// head-locked dome — but that theory was falsified (the dome was the
|
|
8480
|
+
// depth mesh's unset colour output, plus emulated poses; both fixed).
|
|
8481
|
+
// X-ray had its own on-device failure: the global scan is DENSE, and a
|
|
8482
|
+
// dense wireframe at 0.35 opacity fuses into a translucent sheet — with
|
|
8483
|
+
// no depth test the walls' sheets drew through everything, stacked in
|
|
8484
|
+
// front of the user's eyes ("planes covering my face"). With the depth
|
|
8485
|
+
// buffer now holding CORRECT real-world depth, normal testing gives the
|
|
8486
|
+
// right picture: the near wall hides the far one. depthWrite stays off
|
|
8487
|
+
// so the overlay never occludes actual content.
|
|
8488
|
+
depthTest: true,
|
|
8489
|
+
depthWrite: false
|
|
8108
8490
|
});
|
|
8109
8491
|
const root = new THREE19.Group();
|
|
8110
8492
|
const child = new THREE19.Mesh(geometry, material);
|
|
8111
8493
|
child.visible = this.debugVisible;
|
|
8494
|
+
child.renderOrder = DEBUG_RENDER_ORDER;
|
|
8112
8495
|
root.add(child);
|
|
8113
8496
|
const entityId = world.createEntity();
|
|
8497
|
+
root.userData.entityId = entityId;
|
|
8114
8498
|
world.addComponents(entityId, [
|
|
8115
8499
|
new TransformComponent(),
|
|
8116
8500
|
new Object3DRef(root),
|
|
@@ -8124,29 +8508,38 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8124
8508
|
entityId,
|
|
8125
8509
|
lastChangedTime: mesh.lastChangedTime ?? 0,
|
|
8126
8510
|
geometry,
|
|
8127
|
-
material
|
|
8511
|
+
material,
|
|
8512
|
+
emulated: false,
|
|
8513
|
+
gate: new SurfacePoseGate()
|
|
8128
8514
|
};
|
|
8129
8515
|
}
|
|
8130
8516
|
/** A thin box so a detected plane can be SEEN, not just collided with. */
|
|
8131
|
-
attachPlaneDebug(world, entry, collider) {
|
|
8132
|
-
const
|
|
8517
|
+
attachPlaneDebug(world, entry, collider, semanticLabel) {
|
|
8518
|
+
const box = new THREE19.BoxGeometry(
|
|
8133
8519
|
collider.size.x,
|
|
8134
8520
|
collider.size.y,
|
|
8135
8521
|
collider.size.z
|
|
8136
8522
|
);
|
|
8137
|
-
const
|
|
8138
|
-
|
|
8139
|
-
|
|
8523
|
+
const geometry = new THREE19.EdgesGeometry(box);
|
|
8524
|
+
box.dispose();
|
|
8525
|
+
const material = new THREE19.LineBasicMaterial({
|
|
8526
|
+
color: debugColorFor("plane", semanticLabel),
|
|
8527
|
+
// Same depth policy as the mesh overlay — see createMeshEntity.
|
|
8528
|
+
depthTest: true,
|
|
8529
|
+
depthWrite: false
|
|
8140
8530
|
});
|
|
8141
8531
|
const root = new THREE19.Group();
|
|
8142
|
-
const child = new THREE19.
|
|
8532
|
+
const child = new THREE19.LineSegments(geometry, material);
|
|
8533
|
+
child.renderOrder = DEBUG_RENDER_ORDER;
|
|
8143
8534
|
child.position.set(collider.offset.x, collider.offset.y, collider.offset.z);
|
|
8144
8535
|
child.visible = this.debugVisible;
|
|
8145
8536
|
root.add(child);
|
|
8146
8537
|
entry.geometry = geometry;
|
|
8147
8538
|
entry.material = material;
|
|
8539
|
+
root.userData.entityId = entry.entityId;
|
|
8148
8540
|
const existing = world.getComponent(entry.entityId, Object3DRef);
|
|
8149
8541
|
if (existing) {
|
|
8542
|
+
existing.object.removeFromParent();
|
|
8150
8543
|
existing.object = root;
|
|
8151
8544
|
} else {
|
|
8152
8545
|
world.addComponent(entry.entityId, new Object3DRef(root));
|
|
@@ -8162,19 +8555,31 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8162
8555
|
for (const [plane, entry] of this.planes) {
|
|
8163
8556
|
if (entry.geometry) continue;
|
|
8164
8557
|
const collider = planeColliderFromPolygon(plane.polygon);
|
|
8165
|
-
if (collider)
|
|
8558
|
+
if (collider) {
|
|
8559
|
+
this.attachPlaneDebug(
|
|
8560
|
+
world,
|
|
8561
|
+
entry,
|
|
8562
|
+
collider,
|
|
8563
|
+
plane.semanticLabel ?? null
|
|
8564
|
+
);
|
|
8565
|
+
}
|
|
8166
8566
|
}
|
|
8167
8567
|
}
|
|
8168
8568
|
for (const map of [this.planes, this.meshes]) {
|
|
8169
8569
|
for (const entry of map.values()) {
|
|
8170
8570
|
const ref = world.getComponent(entry.entityId, Object3DRef);
|
|
8171
8571
|
ref?.object.traverse((child) => {
|
|
8172
|
-
|
|
8572
|
+
const drawable = child;
|
|
8573
|
+
if (drawable.isMesh || drawable.isLine) {
|
|
8574
|
+
child.visible = this.debugVisible;
|
|
8575
|
+
}
|
|
8173
8576
|
});
|
|
8174
8577
|
}
|
|
8175
8578
|
}
|
|
8176
8579
|
}
|
|
8177
8580
|
destroyEntry(world, entry) {
|
|
8581
|
+
const ref = world.getComponent(entry.entityId, Object3DRef);
|
|
8582
|
+
ref?.object.removeFromParent();
|
|
8178
8583
|
entry.geometry?.dispose();
|
|
8179
8584
|
entry.material?.dispose();
|
|
8180
8585
|
world.destroyEntity(entry.entityId);
|
|
@@ -8186,6 +8591,8 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
8186
8591
|
clearAll(world) {
|
|
8187
8592
|
this.clearMap(world, this.planes);
|
|
8188
8593
|
this.clearMap(world, this.meshes);
|
|
8594
|
+
this.pendingPlanes.clear();
|
|
8595
|
+
this.pendingMeshes.clear();
|
|
8189
8596
|
}
|
|
8190
8597
|
dispose(world) {
|
|
8191
8598
|
this.clearAll(world);
|
|
@@ -8197,6 +8604,918 @@ function normalizeOrientation(value) {
|
|
|
8197
8604
|
return value === "horizontal" || value === "vertical" ? value : null;
|
|
8198
8605
|
}
|
|
8199
8606
|
__name(normalizeOrientation, "normalizeOrientation");
|
|
8607
|
+
var MOUSE_CLICK_TRAVEL_PX = 6;
|
|
8608
|
+
var PRESS_TINT_MIN_SECONDS = 0.1;
|
|
8609
|
+
var PRESS_TINT = 0.72;
|
|
8610
|
+
var HOVER_TINT = 0.9;
|
|
8611
|
+
var _UIInteractionSystem = class _UIInteractionSystem {
|
|
8612
|
+
/**
|
|
8613
|
+
* @param interaction The XR interaction system, or null in a world with no
|
|
8614
|
+
* XR (flat 3D experiences) — the mouse path still works via
|
|
8615
|
+
* `options.inputSystem`.
|
|
8616
|
+
*/
|
|
8617
|
+
constructor(interaction, options) {
|
|
8618
|
+
this.xrPresses = /* @__PURE__ */ new Map();
|
|
8619
|
+
this.mousePress = null;
|
|
8620
|
+
this.mouseHover = null;
|
|
8621
|
+
/** entityId → seconds of pressed-tint left to display. */
|
|
8622
|
+
this.tintFloors = /* @__PURE__ */ new Map();
|
|
8623
|
+
this.tinted = /* @__PURE__ */ new Set();
|
|
8624
|
+
this.raycaster = new THREE19.Raycaster();
|
|
8625
|
+
this.interaction = interaction;
|
|
8626
|
+
this.inputSystem = options?.inputSystem ?? null;
|
|
8627
|
+
interaction?.registerBusyContributor(
|
|
8628
|
+
(_world, input) => this.xrPresses.has(input)
|
|
8629
|
+
);
|
|
8630
|
+
}
|
|
8631
|
+
update(world, deltaTime = 0) {
|
|
8632
|
+
if (world.isEditorMode()) return;
|
|
8633
|
+
if (world.isXrPresenting()) {
|
|
8634
|
+
this.updateXr(world);
|
|
8635
|
+
} else {
|
|
8636
|
+
this.updateMouse(world);
|
|
8637
|
+
}
|
|
8638
|
+
this.updateTints(world, deltaTime);
|
|
8639
|
+
}
|
|
8640
|
+
// ── XR path ────────────────────────────────────────────────────────────────
|
|
8641
|
+
updateXr(world) {
|
|
8642
|
+
const interaction = this.interaction;
|
|
8643
|
+
if (!interaction) return;
|
|
8644
|
+
const xrInput = interaction.inputSystem;
|
|
8645
|
+
const inputs = xrInput.getInputs();
|
|
8646
|
+
for (const input of inputs) {
|
|
8647
|
+
const hover = interaction.getHoverForInput(input);
|
|
8648
|
+
const hovered = hover ? this.widgetKind(world, hover.entityId) : null;
|
|
8649
|
+
if (input.selectStart && hover && hovered) {
|
|
8650
|
+
this.xrPresses.set(input, {
|
|
8651
|
+
entityId: hover.entityId,
|
|
8652
|
+
kind: hovered,
|
|
8653
|
+
travelPx: 0
|
|
8654
|
+
});
|
|
8655
|
+
this.beginPressTint(world, hover.entityId);
|
|
8656
|
+
}
|
|
8657
|
+
const press = this.xrPresses.get(input);
|
|
8658
|
+
if (press && press.kind === "slider") {
|
|
8659
|
+
this.dragSlider(world, press.entityId, input);
|
|
8660
|
+
}
|
|
8661
|
+
if (input.selectEnd || !input.connected) {
|
|
8662
|
+
if (press) {
|
|
8663
|
+
this.xrPresses.delete(input);
|
|
8664
|
+
const stillOn = hover?.entityId === press.entityId || // Vision Pro: the transient source is gone from the hover map on
|
|
8665
|
+
// its release frame — confirm against the last latched ray.
|
|
8666
|
+
this.rayHitsWidget(world, press.entityId, input);
|
|
8667
|
+
if (stillOn || press.kind === "slider") {
|
|
8668
|
+
this.fire(world, press, input);
|
|
8669
|
+
}
|
|
8670
|
+
this.endPressTint(press.entityId);
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
}
|
|
8674
|
+
for (const [input] of this.xrPresses) {
|
|
8675
|
+
if (!inputs.includes(input)) this.xrPresses.delete(input);
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
// ── Mouse path (flat preview) ──────────────────────────────────────────────
|
|
8679
|
+
updateMouse(world) {
|
|
8680
|
+
const input = this.inputSystem;
|
|
8681
|
+
if (!input) return;
|
|
8682
|
+
const hit = this.raycastMouse(world);
|
|
8683
|
+
const hovered = hit && this.widgetKind(world, hit) ? hit : null;
|
|
8684
|
+
if (hovered !== this.mouseHover) {
|
|
8685
|
+
if (this.mouseHover !== null) {
|
|
8686
|
+
this.setTint(world, this.mouseHover, 1);
|
|
8687
|
+
this.dispatchBubbling(
|
|
8688
|
+
world,
|
|
8689
|
+
this.mouseHover,
|
|
8690
|
+
(b, e) => b.onUiHoverEnd(e)
|
|
8691
|
+
);
|
|
8692
|
+
}
|
|
8693
|
+
if (hovered !== null) {
|
|
8694
|
+
this.setTint(world, hovered, HOVER_TINT);
|
|
8695
|
+
this.dispatchBubbling(world, hovered, (b, e) => b.onUiHoverStart(e));
|
|
8696
|
+
}
|
|
8697
|
+
this.mouseHover = hovered;
|
|
8698
|
+
}
|
|
8699
|
+
if (input.getMouseDown("left") && hovered) {
|
|
8700
|
+
const kind = this.widgetKind(world, hovered);
|
|
8701
|
+
if (kind) {
|
|
8702
|
+
this.mousePress = { entityId: hovered, kind, travelPx: 0 };
|
|
8703
|
+
this.beginPressTint(world, hovered);
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
if (this.mousePress) {
|
|
8707
|
+
const delta = input.getMouseDelta();
|
|
8708
|
+
this.mousePress.travelPx += Math.hypot(delta.x, delta.y);
|
|
8709
|
+
if (this.mousePress.kind === "slider") {
|
|
8710
|
+
this.dragSliderMouse(world, this.mousePress.entityId);
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8713
|
+
if (input.getMouseUp("left") && this.mousePress) {
|
|
8714
|
+
const press = this.mousePress;
|
|
8715
|
+
this.mousePress = null;
|
|
8716
|
+
const stillOn = hovered === press.entityId;
|
|
8717
|
+
const withinTravel = press.travelPx <= MOUSE_CLICK_TRAVEL_PX;
|
|
8718
|
+
if (press.kind === "slider" || stillOn && withinTravel) {
|
|
8719
|
+
this.fire(world, press, null);
|
|
8720
|
+
}
|
|
8721
|
+
this.endPressTint(press.entityId);
|
|
8722
|
+
}
|
|
8723
|
+
}
|
|
8724
|
+
raycastMouse(world) {
|
|
8725
|
+
const input = this.inputSystem;
|
|
8726
|
+
if (!input) return null;
|
|
8727
|
+
const dom = world.getRenderer().domElement;
|
|
8728
|
+
const rect = dom.getBoundingClientRect();
|
|
8729
|
+
let ndcX = 0;
|
|
8730
|
+
let ndcY = 0;
|
|
8731
|
+
if (input.isMousePointerLocked()) {
|
|
8732
|
+
ndcX = 0;
|
|
8733
|
+
ndcY = 0;
|
|
8734
|
+
} else {
|
|
8735
|
+
const pos = input.getMousePosition();
|
|
8736
|
+
ndcX = (pos.x - rect.left) / Math.max(1, rect.width) * 2 - 1;
|
|
8737
|
+
ndcY = -((pos.y - rect.top) / Math.max(1, rect.height) * 2 - 1);
|
|
8738
|
+
if (ndcX < -1 || ndcX > 1 || ndcY < -1 || ndcY > 1) return null;
|
|
8739
|
+
}
|
|
8740
|
+
this.raycaster.setFromCamera(new THREE19.Vector2(ndcX, ndcY), world.getCamera());
|
|
8741
|
+
const hits = world.raycast(
|
|
8742
|
+
this.raycaster.ray.origin,
|
|
8743
|
+
this.raycaster.ray.direction
|
|
8744
|
+
);
|
|
8745
|
+
for (const hit of hits) {
|
|
8746
|
+
if (typeof hit.entityId !== "number") continue;
|
|
8747
|
+
if (this.widgetKind(world, hit.entityId)) return hit.entityId;
|
|
8748
|
+
return null;
|
|
8749
|
+
}
|
|
8750
|
+
return null;
|
|
8751
|
+
}
|
|
8752
|
+
// ── Shared machinery ───────────────────────────────────────────────────────
|
|
8753
|
+
widgetKind(world, entityId) {
|
|
8754
|
+
if (world.getComponent(entityId, UIButtonComponent)) return "button";
|
|
8755
|
+
if (world.getComponent(entityId, UIToggleComponent)) return "toggle";
|
|
8756
|
+
if (world.getComponent(entityId, UISliderComponent)) return "slider";
|
|
8757
|
+
return null;
|
|
8758
|
+
}
|
|
8759
|
+
fire(world, press, input) {
|
|
8760
|
+
if (press.kind === "button") {
|
|
8761
|
+
this.dispatchBubbling(world, press.entityId, (b, e) => b.onUiPress(e), input);
|
|
8762
|
+
return;
|
|
8763
|
+
}
|
|
8764
|
+
if (press.kind === "toggle") {
|
|
8765
|
+
const toggle = world.getComponent(press.entityId, UIToggleComponent);
|
|
8766
|
+
if (!toggle) return;
|
|
8767
|
+
toggle.value = !toggle.value;
|
|
8768
|
+
this.dispatchBubbling(
|
|
8769
|
+
world,
|
|
8770
|
+
press.entityId,
|
|
8771
|
+
(b, e) => b.onUiToggle(e),
|
|
8772
|
+
input,
|
|
8773
|
+
toggle.value
|
|
8774
|
+
);
|
|
8775
|
+
return;
|
|
8776
|
+
}
|
|
8777
|
+
const slider = world.getComponent(press.entityId, UISliderComponent);
|
|
8778
|
+
if (!slider) return;
|
|
8779
|
+
this.dispatchBubbling(
|
|
8780
|
+
world,
|
|
8781
|
+
press.entityId,
|
|
8782
|
+
(b, e) => b.onUiChange(e),
|
|
8783
|
+
input,
|
|
8784
|
+
slider.value
|
|
8785
|
+
);
|
|
8786
|
+
}
|
|
8787
|
+
dragSlider(world, entityId, input) {
|
|
8788
|
+
const point = this.rayPointOnWidget(world, entityId, input);
|
|
8789
|
+
if (point) this.applySliderPoint(world, entityId, point, input);
|
|
8790
|
+
}
|
|
8791
|
+
dragSliderMouse(world, entityId) {
|
|
8792
|
+
const object = this.widgetObject(world, entityId);
|
|
8793
|
+
if (!object) return;
|
|
8794
|
+
const hits = this.raycaster.intersectObject(object, true);
|
|
8795
|
+
if (hits.length > 0) this.applySliderPoint(world, entityId, hits[0].point, null);
|
|
8796
|
+
}
|
|
8797
|
+
applySliderPoint(world, entityId, worldPoint, input) {
|
|
8798
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
8799
|
+
const object = this.widgetObject(world, entityId);
|
|
8800
|
+
if (!slider || !object) return;
|
|
8801
|
+
const local = object.worldToLocal(worldPoint.clone());
|
|
8802
|
+
const inset = slider.height / 2;
|
|
8803
|
+
const usable = Math.max(1e-3, slider.width - inset * 2);
|
|
8804
|
+
const t = Math.min(1, Math.max(0, local.x / usable + 0.5));
|
|
8805
|
+
const next = slider.min + t * (slider.max - slider.min);
|
|
8806
|
+
if (Math.abs(next - slider.value) < 1e-4) return;
|
|
8807
|
+
slider.value = next;
|
|
8808
|
+
this.dispatchBubbling(
|
|
8809
|
+
world,
|
|
8810
|
+
entityId,
|
|
8811
|
+
(b, e) => b.onUiChange(e),
|
|
8812
|
+
input,
|
|
8813
|
+
next
|
|
8814
|
+
);
|
|
8815
|
+
}
|
|
8816
|
+
widgetObject(world, entityId) {
|
|
8817
|
+
return world.getComponent(entityId, Object3DRef)?.object;
|
|
8818
|
+
}
|
|
8819
|
+
rayHitsWidget(world, entityId, input) {
|
|
8820
|
+
const object = this.widgetObject(world, entityId);
|
|
8821
|
+
if (!object) return false;
|
|
8822
|
+
this.raycaster.set(
|
|
8823
|
+
input.rayOrigin.clone(),
|
|
8824
|
+
input.rayDirection.clone().normalize()
|
|
8825
|
+
);
|
|
8826
|
+
return this.raycaster.intersectObject(object, true).length > 0;
|
|
8827
|
+
}
|
|
8828
|
+
rayPointOnWidget(world, entityId, input) {
|
|
8829
|
+
const object = this.widgetObject(world, entityId);
|
|
8830
|
+
if (!object) return null;
|
|
8831
|
+
this.raycaster.set(
|
|
8832
|
+
input.rayOrigin.clone(),
|
|
8833
|
+
input.rayDirection.clone().normalize()
|
|
8834
|
+
);
|
|
8835
|
+
const hits = this.raycaster.intersectObject(object, true);
|
|
8836
|
+
return hits.length > 0 ? hits[0].point : null;
|
|
8837
|
+
}
|
|
8838
|
+
/**
|
|
8839
|
+
* Dispatch to the widget's Behaviours, then BUBBLE up the parent chain so a
|
|
8840
|
+
* panel script hears its children. Bubbling (not a world broadcast) keeps
|
|
8841
|
+
* two spawned copies of the same menu prefab independent.
|
|
8842
|
+
*/
|
|
8843
|
+
dispatchBubbling(world, entityId, visit, input = null, value) {
|
|
8844
|
+
const event = {
|
|
8845
|
+
targetEntityId: entityId,
|
|
8846
|
+
actionId: this.actionIdOf(world, entityId),
|
|
8847
|
+
input,
|
|
8848
|
+
...value === void 0 ? {} : { value }
|
|
8849
|
+
};
|
|
8850
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8851
|
+
let current = entityId;
|
|
8852
|
+
for (; ; ) {
|
|
8853
|
+
if (visited.has(current)) break;
|
|
8854
|
+
visited.add(current);
|
|
8855
|
+
const script = world.getComponent(current, ScriptComponent);
|
|
8856
|
+
const instances = script?.scriptInstances;
|
|
8857
|
+
if (instances) {
|
|
8858
|
+
for (const instance of instances.values()) {
|
|
8859
|
+
if (!(instance instanceof Behaviour)) continue;
|
|
8860
|
+
try {
|
|
8861
|
+
visit(instance, event);
|
|
8862
|
+
} catch (error) {
|
|
8863
|
+
console.error("[UIInteractionSystem] hook failed:", error);
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
const parentId = world.getComponent(current, ParentId)?.value;
|
|
8868
|
+
if (typeof parentId !== "number") break;
|
|
8869
|
+
current = parentId;
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
actionIdOf(world, entityId) {
|
|
8873
|
+
const button = world.getComponent(entityId, UIButtonComponent);
|
|
8874
|
+
if (button?.actionId) return button.actionId;
|
|
8875
|
+
const toggle = world.getComponent(entityId, UIToggleComponent);
|
|
8876
|
+
if (toggle?.actionId) return toggle.actionId;
|
|
8877
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
8878
|
+
if (slider?.actionId) return slider.actionId;
|
|
8879
|
+
const name = world.getComponent(entityId, Name)?.value;
|
|
8880
|
+
return typeof name === "string" ? name : String(entityId);
|
|
8881
|
+
}
|
|
8882
|
+
// ── Pressed/hover tint ─────────────────────────────────────────────────────
|
|
8883
|
+
beginPressTint(world, entityId) {
|
|
8884
|
+
this.setTint(world, entityId, PRESS_TINT);
|
|
8885
|
+
this.tintFloors.set(entityId, PRESS_TINT_MIN_SECONDS);
|
|
8886
|
+
}
|
|
8887
|
+
endPressTint(entityId) {
|
|
8888
|
+
if (!this.tintFloors.has(entityId)) {
|
|
8889
|
+
this.tintFloors.set(entityId, 0);
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8892
|
+
updateTints(world, deltaTime) {
|
|
8893
|
+
for (const [entityId, remaining] of this.tintFloors) {
|
|
8894
|
+
const next = remaining - deltaTime;
|
|
8895
|
+
const stillHeld = [...this.xrPresses.values()].some((p) => p.entityId === entityId) || this.mousePress?.entityId === entityId;
|
|
8896
|
+
if (stillHeld) continue;
|
|
8897
|
+
if (next <= 0) {
|
|
8898
|
+
this.tintFloors.delete(entityId);
|
|
8899
|
+
this.setTint(
|
|
8900
|
+
world,
|
|
8901
|
+
entityId,
|
|
8902
|
+
this.mouseHover === entityId ? HOVER_TINT : 1
|
|
8903
|
+
);
|
|
8904
|
+
} else {
|
|
8905
|
+
this.tintFloors.set(entityId, next);
|
|
8906
|
+
}
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
setTint(world, entityId, scalar) {
|
|
8910
|
+
const object = this.widgetObject(world, entityId);
|
|
8911
|
+
if (!object) return;
|
|
8912
|
+
if (scalar === 1) this.tinted.delete(entityId);
|
|
8913
|
+
else this.tinted.add(entityId);
|
|
8914
|
+
object.traverse((child) => {
|
|
8915
|
+
const material = child.material;
|
|
8916
|
+
if (material?.color) material.color.setScalar(scalar);
|
|
8917
|
+
});
|
|
8918
|
+
}
|
|
8919
|
+
};
|
|
8920
|
+
__name(_UIInteractionSystem, "UIInteractionSystem");
|
|
8921
|
+
var UIInteractionSystem = _UIInteractionSystem;
|
|
8922
|
+
|
|
8923
|
+
// core/ui/uiCanvas.ts
|
|
8924
|
+
var DEFAULT_PIXELS_PER_METER = 1024;
|
|
8925
|
+
var MIN_PIXELS_PER_METER = 256;
|
|
8926
|
+
var MAX_PIXELS_PER_METER = 2048;
|
|
8927
|
+
var MAX_CANVAS_SIDE = 2048;
|
|
8928
|
+
function resolveCanvasSize(widthMeters, heightMeters, pixelDensity = DEFAULT_PIXELS_PER_METER) {
|
|
8929
|
+
const w = Math.max(0.01, widthMeters);
|
|
8930
|
+
const h = Math.max(0.01, heightMeters);
|
|
8931
|
+
const authored = Math.min(
|
|
8932
|
+
MAX_PIXELS_PER_METER,
|
|
8933
|
+
Math.max(MIN_PIXELS_PER_METER, pixelDensity)
|
|
8934
|
+
);
|
|
8935
|
+
const density = Math.min(authored, MAX_CANVAS_SIDE / Math.max(w, h));
|
|
8936
|
+
const even = /* @__PURE__ */ __name((v) => Math.max(2, 2 * Math.round(v * density / 2)), "even");
|
|
8937
|
+
return { pxWidth: even(w), pxHeight: even(h), density };
|
|
8938
|
+
}
|
|
8939
|
+
__name(resolveCanvasSize, "resolveCanvasSize");
|
|
8940
|
+
function wrapText(measure, text, maxWidthPx) {
|
|
8941
|
+
const lines = [];
|
|
8942
|
+
for (const paragraph of text.split("\n")) {
|
|
8943
|
+
const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
|
|
8944
|
+
if (words.length === 0) {
|
|
8945
|
+
lines.push("");
|
|
8946
|
+
continue;
|
|
8947
|
+
}
|
|
8948
|
+
let line = "";
|
|
8949
|
+
for (const word of words) {
|
|
8950
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
8951
|
+
if (measure(candidate) <= maxWidthPx) {
|
|
8952
|
+
line = candidate;
|
|
8953
|
+
continue;
|
|
8954
|
+
}
|
|
8955
|
+
if (line) {
|
|
8956
|
+
lines.push(line);
|
|
8957
|
+
line = "";
|
|
8958
|
+
}
|
|
8959
|
+
if (measure(word) <= maxWidthPx) {
|
|
8960
|
+
line = word;
|
|
8961
|
+
continue;
|
|
8962
|
+
}
|
|
8963
|
+
let chunk = "";
|
|
8964
|
+
for (const char of word) {
|
|
8965
|
+
const next = chunk + char;
|
|
8966
|
+
if (measure(next) <= maxWidthPx || chunk.length === 0) {
|
|
8967
|
+
chunk = next;
|
|
8968
|
+
} else {
|
|
8969
|
+
lines.push(chunk);
|
|
8970
|
+
chunk = char;
|
|
8971
|
+
}
|
|
8972
|
+
}
|
|
8973
|
+
line = chunk;
|
|
8974
|
+
}
|
|
8975
|
+
lines.push(line);
|
|
8976
|
+
}
|
|
8977
|
+
return lines;
|
|
8978
|
+
}
|
|
8979
|
+
__name(wrapText, "wrapText");
|
|
8980
|
+
var ELLIPSIS = "\u2026";
|
|
8981
|
+
function clampLines(measure, lines, maxLines, maxWidthPx) {
|
|
8982
|
+
if (maxLines <= 0) return [];
|
|
8983
|
+
if (lines.length <= maxLines) return lines;
|
|
8984
|
+
const kept = lines.slice(0, maxLines);
|
|
8985
|
+
let last = kept[maxLines - 1];
|
|
8986
|
+
while (last.length > 0 && measure(last + ELLIPSIS) > maxWidthPx) {
|
|
8987
|
+
last = last.slice(0, -1);
|
|
8988
|
+
}
|
|
8989
|
+
kept[maxLines - 1] = last + ELLIPSIS;
|
|
8990
|
+
return kept;
|
|
8991
|
+
}
|
|
8992
|
+
__name(clampLines, "clampLines");
|
|
8993
|
+
var UI_FONT_STACK = '-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
|
8994
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
8995
|
+
const radius = Math.max(0, Math.min(r, w / 2, h / 2));
|
|
8996
|
+
ctx.beginPath();
|
|
8997
|
+
if (typeof ctx.roundRect === "function") {
|
|
8998
|
+
ctx.roundRect(x, y, w, h, radius);
|
|
8999
|
+
return;
|
|
9000
|
+
}
|
|
9001
|
+
ctx.moveTo(x + radius, y);
|
|
9002
|
+
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
|
9003
|
+
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
|
9004
|
+
ctx.arcTo(x, y + h, x, y, radius);
|
|
9005
|
+
ctx.arcTo(x, y, x + w, y, radius);
|
|
9006
|
+
ctx.closePath();
|
|
9007
|
+
}
|
|
9008
|
+
__name(tracePath, "tracePath");
|
|
9009
|
+
function withOpacity(ctx, color, opacity) {
|
|
9010
|
+
if (opacity >= 1) return color;
|
|
9011
|
+
ctx.save();
|
|
9012
|
+
ctx.fillStyle = color;
|
|
9013
|
+
const parsed = ctx.fillStyle;
|
|
9014
|
+
ctx.restore();
|
|
9015
|
+
if (parsed.startsWith("#") && (parsed.length === 7 || parsed.length === 4)) {
|
|
9016
|
+
const hex = parsed.length === 4 ? `#${parsed[1]}${parsed[1]}${parsed[2]}${parsed[2]}${parsed[3]}${parsed[3]}` : parsed;
|
|
9017
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
9018
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
9019
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
9020
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
9021
|
+
}
|
|
9022
|
+
return parsed;
|
|
9023
|
+
}
|
|
9024
|
+
__name(withOpacity, "withOpacity");
|
|
9025
|
+
function drawChrome(ctx, pxWidth, pxHeight, density, style) {
|
|
9026
|
+
ctx.clearRect(0, 0, pxWidth, pxHeight);
|
|
9027
|
+
const border = Math.max(0, style.borderWidthMeters * density);
|
|
9028
|
+
const radius = style.cornerRadiusMeters * density;
|
|
9029
|
+
tracePath(ctx, 0, 0, pxWidth, pxHeight, radius);
|
|
9030
|
+
ctx.fillStyle = withOpacity(
|
|
9031
|
+
ctx,
|
|
9032
|
+
style.backgroundColor,
|
|
9033
|
+
Math.max(0, Math.min(1, style.backgroundOpacity))
|
|
9034
|
+
);
|
|
9035
|
+
ctx.fill();
|
|
9036
|
+
if (border > 0) {
|
|
9037
|
+
tracePath(
|
|
9038
|
+
ctx,
|
|
9039
|
+
border / 2,
|
|
9040
|
+
border / 2,
|
|
9041
|
+
pxWidth - border,
|
|
9042
|
+
pxHeight - border,
|
|
9043
|
+
Math.max(0, radius - border / 2)
|
|
9044
|
+
);
|
|
9045
|
+
ctx.strokeStyle = style.borderColor;
|
|
9046
|
+
ctx.lineWidth = border;
|
|
9047
|
+
ctx.stroke();
|
|
9048
|
+
}
|
|
9049
|
+
}
|
|
9050
|
+
__name(drawChrome, "drawChrome");
|
|
9051
|
+
function drawTextBlock(ctx, pxWidth, pxHeight, density, text, style) {
|
|
9052
|
+
const fontPx = Math.max(4, style.fontSizeMeters * density);
|
|
9053
|
+
const paddingPx = style.paddingMeters * density;
|
|
9054
|
+
const contentW = Math.max(1, pxWidth - paddingPx * 2);
|
|
9055
|
+
const contentH = Math.max(1, pxHeight - paddingPx * 2);
|
|
9056
|
+
const lineHeightPx = fontPx * style.lineHeight;
|
|
9057
|
+
ctx.font = `${style.fontWeight} ${fontPx}px ${UI_FONT_STACK}`;
|
|
9058
|
+
const measure = /* @__PURE__ */ __name((s) => ctx.measureText(s).width, "measure");
|
|
9059
|
+
const maxLines = Math.max(1, Math.floor(contentH / lineHeightPx));
|
|
9060
|
+
const lines = clampLines(
|
|
9061
|
+
measure,
|
|
9062
|
+
wrapText(measure, text, contentW),
|
|
9063
|
+
maxLines,
|
|
9064
|
+
contentW
|
|
9065
|
+
);
|
|
9066
|
+
const blockH = lines.length * lineHeightPx;
|
|
9067
|
+
const top = style.valign === "top" ? paddingPx : style.valign === "bottom" ? pxHeight - paddingPx - blockH : (pxHeight - blockH) / 2;
|
|
9068
|
+
const anchorX = style.align === "left" ? paddingPx : style.align === "right" ? pxWidth - paddingPx : pxWidth / 2;
|
|
9069
|
+
ctx.save();
|
|
9070
|
+
tracePath(ctx, 0, 0, pxWidth, pxHeight, 0);
|
|
9071
|
+
ctx.clip();
|
|
9072
|
+
ctx.fillStyle = style.color;
|
|
9073
|
+
ctx.textAlign = style.align;
|
|
9074
|
+
ctx.textBaseline = "middle";
|
|
9075
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
9076
|
+
ctx.fillText(lines[i], anchorX, top + (i + 0.5) * lineHeightPx);
|
|
9077
|
+
}
|
|
9078
|
+
ctx.restore();
|
|
9079
|
+
}
|
|
9080
|
+
__name(drawTextBlock, "drawTextBlock");
|
|
9081
|
+
function panelSignature(widthM, heightM, size, style) {
|
|
9082
|
+
return JSON.stringify([
|
|
9083
|
+
widthM,
|
|
9084
|
+
heightM,
|
|
9085
|
+
size.pxWidth,
|
|
9086
|
+
size.pxHeight,
|
|
9087
|
+
style.backgroundColor,
|
|
9088
|
+
style.backgroundOpacity,
|
|
9089
|
+
style.cornerRadiusMeters,
|
|
9090
|
+
style.borderWidthMeters,
|
|
9091
|
+
style.borderColor
|
|
9092
|
+
]);
|
|
9093
|
+
}
|
|
9094
|
+
__name(panelSignature, "panelSignature");
|
|
9095
|
+
function textSignature(widthM, heightM, size, text, style) {
|
|
9096
|
+
return JSON.stringify([
|
|
9097
|
+
widthM,
|
|
9098
|
+
heightM,
|
|
9099
|
+
size.pxWidth,
|
|
9100
|
+
size.pxHeight,
|
|
9101
|
+
text,
|
|
9102
|
+
style.fontSizeMeters,
|
|
9103
|
+
style.fontWeight,
|
|
9104
|
+
style.color,
|
|
9105
|
+
style.align,
|
|
9106
|
+
style.valign,
|
|
9107
|
+
style.lineHeight,
|
|
9108
|
+
style.paddingMeters
|
|
9109
|
+
]);
|
|
9110
|
+
}
|
|
9111
|
+
__name(textSignature, "textSignature");
|
|
9112
|
+
function buttonSignature(widthM, heightM, size, panel, label, text) {
|
|
9113
|
+
return JSON.stringify([
|
|
9114
|
+
panelSignature(widthM, heightM, size, panel),
|
|
9115
|
+
label,
|
|
9116
|
+
text.fontSizeMeters,
|
|
9117
|
+
text.fontWeight,
|
|
9118
|
+
text.color
|
|
9119
|
+
]);
|
|
9120
|
+
}
|
|
9121
|
+
__name(buttonSignature, "buttonSignature");
|
|
9122
|
+
function trackSignature(widthM, heightM, size, panel, accentColor, knobColor) {
|
|
9123
|
+
return JSON.stringify([
|
|
9124
|
+
panelSignature(widthM, heightM, size, panel),
|
|
9125
|
+
accentColor,
|
|
9126
|
+
knobColor
|
|
9127
|
+
]);
|
|
9128
|
+
}
|
|
9129
|
+
__name(trackSignature, "trackSignature");
|
|
9130
|
+
function drawRoundedBar(ctx, pxX, pxY, pxW, pxH, color) {
|
|
9131
|
+
tracePath(ctx, pxX, pxY, pxW, pxH, pxH / 2);
|
|
9132
|
+
ctx.fillStyle = color;
|
|
9133
|
+
ctx.fill();
|
|
9134
|
+
}
|
|
9135
|
+
__name(drawRoundedBar, "drawRoundedBar");
|
|
9136
|
+
|
|
9137
|
+
// core/systems/UIPanelSystem.ts
|
|
9138
|
+
var FACE_RENDER_ORDER = 10;
|
|
9139
|
+
var CHILD_Z = 2e-3;
|
|
9140
|
+
var _UIPanelSystem = class _UIPanelSystem {
|
|
9141
|
+
constructor(options) {
|
|
9142
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
9143
|
+
this.createCanvas = options?.createCanvas ?? (() => document.createElement("canvas"));
|
|
9144
|
+
}
|
|
9145
|
+
update(world, deltaTime = 0) {
|
|
9146
|
+
const live = /* @__PURE__ */ new Set();
|
|
9147
|
+
for (const entityId of world.query(UIPanelComponent, Object3DRef)) {
|
|
9148
|
+
const panel = world.getComponent(entityId, UIPanelComponent);
|
|
9149
|
+
if (!panel) continue;
|
|
9150
|
+
live.add(entityId);
|
|
9151
|
+
this.syncPanel(world, entityId, panel);
|
|
9152
|
+
this.applyFollow(world, entityId, panel);
|
|
9153
|
+
}
|
|
9154
|
+
for (const entityId of world.query(UITextComponent, Object3DRef)) {
|
|
9155
|
+
const text = world.getComponent(entityId, UITextComponent);
|
|
9156
|
+
if (!text) continue;
|
|
9157
|
+
live.add(entityId);
|
|
9158
|
+
this.syncText(world, entityId, text);
|
|
9159
|
+
}
|
|
9160
|
+
for (const entityId of world.query(UIButtonComponent, Object3DRef)) {
|
|
9161
|
+
const button = world.getComponent(entityId, UIButtonComponent);
|
|
9162
|
+
if (!button) continue;
|
|
9163
|
+
live.add(entityId);
|
|
9164
|
+
this.syncButton(world, entityId, button);
|
|
9165
|
+
}
|
|
9166
|
+
for (const entityId of world.query(UIToggleComponent, Object3DRef)) {
|
|
9167
|
+
const toggle = world.getComponent(entityId, UIToggleComponent);
|
|
9168
|
+
if (!toggle) continue;
|
|
9169
|
+
live.add(entityId);
|
|
9170
|
+
this.syncToggle(world, entityId, toggle, deltaTime);
|
|
9171
|
+
}
|
|
9172
|
+
for (const entityId of world.query(UISliderComponent, Object3DRef)) {
|
|
9173
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
9174
|
+
if (!slider) continue;
|
|
9175
|
+
live.add(entityId);
|
|
9176
|
+
this.syncSlider(world, entityId, slider);
|
|
9177
|
+
}
|
|
9178
|
+
for (const [entityId, entry] of this.entries) {
|
|
9179
|
+
if (!live.has(entityId)) {
|
|
9180
|
+
this.destroyEntry(entry);
|
|
9181
|
+
this.entries.delete(entityId);
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
}
|
|
9185
|
+
/** All GPU/canvas resources; call on world teardown. */
|
|
9186
|
+
dispose() {
|
|
9187
|
+
for (const entry of this.entries.values()) this.destroyEntry(entry);
|
|
9188
|
+
this.entries.clear();
|
|
9189
|
+
}
|
|
9190
|
+
// ── Widgets ────────────────────────────────────────────────────────────────
|
|
9191
|
+
syncPanel(world, entityId, panel) {
|
|
9192
|
+
const size = resolveCanvasSize(panel.width, panel.height, panel.pixelDensity);
|
|
9193
|
+
const style = {
|
|
9194
|
+
backgroundColor: panel.backgroundColor,
|
|
9195
|
+
backgroundOpacity: panel.backgroundOpacity,
|
|
9196
|
+
cornerRadiusMeters: panel.cornerRadius,
|
|
9197
|
+
borderWidthMeters: panel.borderWidth,
|
|
9198
|
+
borderColor: panel.borderColor
|
|
9199
|
+
};
|
|
9200
|
+
const signature = panelSignature(panel.width, panel.height, size, style);
|
|
9201
|
+
this.rebuildIfChanged(world, entityId, signature, panel.width, panel.height, size, (ctx) => {
|
|
9202
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, style);
|
|
9203
|
+
});
|
|
9204
|
+
}
|
|
9205
|
+
syncText(world, entityId, text) {
|
|
9206
|
+
const size = resolveCanvasSize(text.width, text.height);
|
|
9207
|
+
const style = {
|
|
9208
|
+
fontSizeMeters: text.fontSize,
|
|
9209
|
+
fontWeight: text.fontWeight,
|
|
9210
|
+
color: text.color,
|
|
9211
|
+
align: text.align,
|
|
9212
|
+
valign: text.valign,
|
|
9213
|
+
lineHeight: text.lineHeight,
|
|
9214
|
+
paddingMeters: text.padding
|
|
9215
|
+
};
|
|
9216
|
+
const signature = textSignature(text.width, text.height, size, text.text, style);
|
|
9217
|
+
this.rebuildIfChanged(world, entityId, signature, text.width, text.height, size, (ctx) => {
|
|
9218
|
+
ctx.clearRect(0, 0, size.pxWidth, size.pxHeight);
|
|
9219
|
+
drawTextBlock(ctx, size.pxWidth, size.pxHeight, size.density, text.text, style);
|
|
9220
|
+
});
|
|
9221
|
+
}
|
|
9222
|
+
syncButton(world, entityId, button) {
|
|
9223
|
+
const size = resolveCanvasSize(button.width, button.height);
|
|
9224
|
+
const chrome = {
|
|
9225
|
+
backgroundColor: button.backgroundColor,
|
|
9226
|
+
backgroundOpacity: button.backgroundOpacity,
|
|
9227
|
+
cornerRadiusMeters: button.cornerRadius,
|
|
9228
|
+
borderWidthMeters: button.borderWidth,
|
|
9229
|
+
borderColor: button.borderColor
|
|
9230
|
+
};
|
|
9231
|
+
const label = {
|
|
9232
|
+
fontSizeMeters: button.fontSize,
|
|
9233
|
+
fontWeight: button.fontWeight,
|
|
9234
|
+
color: button.textColor,
|
|
9235
|
+
align: "center",
|
|
9236
|
+
valign: "middle",
|
|
9237
|
+
lineHeight: 1.2,
|
|
9238
|
+
paddingMeters: 0.01
|
|
9239
|
+
};
|
|
9240
|
+
const signature = buttonSignature(
|
|
9241
|
+
button.width,
|
|
9242
|
+
button.height,
|
|
9243
|
+
size,
|
|
9244
|
+
chrome,
|
|
9245
|
+
button.label,
|
|
9246
|
+
label
|
|
9247
|
+
);
|
|
9248
|
+
this.rebuildIfChanged(world, entityId, signature, button.width, button.height, size, (ctx) => {
|
|
9249
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, chrome);
|
|
9250
|
+
drawTextBlock(ctx, size.pxWidth, size.pxHeight, size.density, button.label, label);
|
|
9251
|
+
});
|
|
9252
|
+
}
|
|
9253
|
+
syncToggle(world, entityId, toggle, deltaTime) {
|
|
9254
|
+
const size = resolveCanvasSize(toggle.width, toggle.height);
|
|
9255
|
+
const chrome = {
|
|
9256
|
+
backgroundColor: toggle.value ? toggle.accentColor : toggle.trackColor,
|
|
9257
|
+
backgroundOpacity: 1,
|
|
9258
|
+
cornerRadiusMeters: toggle.height / 2,
|
|
9259
|
+
borderWidthMeters: 0,
|
|
9260
|
+
borderColor: "#000000"
|
|
9261
|
+
};
|
|
9262
|
+
const signature = JSON.stringify([
|
|
9263
|
+
trackSignature(
|
|
9264
|
+
toggle.width,
|
|
9265
|
+
toggle.height,
|
|
9266
|
+
size,
|
|
9267
|
+
chrome,
|
|
9268
|
+
toggle.accentColor,
|
|
9269
|
+
toggle.knobColor
|
|
9270
|
+
),
|
|
9271
|
+
toggle.value
|
|
9272
|
+
]);
|
|
9273
|
+
const entry = this.rebuildIfChanged(
|
|
9274
|
+
world,
|
|
9275
|
+
entityId,
|
|
9276
|
+
signature,
|
|
9277
|
+
toggle.width,
|
|
9278
|
+
toggle.height,
|
|
9279
|
+
size,
|
|
9280
|
+
(ctx) => {
|
|
9281
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, chrome);
|
|
9282
|
+
}
|
|
9283
|
+
);
|
|
9284
|
+
if (!entry) return;
|
|
9285
|
+
if (!entry.knob) {
|
|
9286
|
+
entry.knob = this.makeDisc(
|
|
9287
|
+
toggle.height * 0.4,
|
|
9288
|
+
toggle.knobColor,
|
|
9289
|
+
entry.face.parent,
|
|
9290
|
+
CHILD_Z
|
|
9291
|
+
);
|
|
9292
|
+
}
|
|
9293
|
+
const inset = toggle.height / 2;
|
|
9294
|
+
const target = toggle.value ? toggle.width / 2 - inset : -(toggle.width / 2 - inset);
|
|
9295
|
+
const knob = entry.knob;
|
|
9296
|
+
const ease = Math.min(1, deltaTime * 14);
|
|
9297
|
+
knob.position.x += (target - knob.position.x) * (deltaTime > 0 ? ease : 1);
|
|
9298
|
+
knob.position.z = CHILD_Z;
|
|
9299
|
+
}
|
|
9300
|
+
syncSlider(world, entityId, slider) {
|
|
9301
|
+
const size = resolveCanvasSize(slider.width, slider.height);
|
|
9302
|
+
const trackThickness = slider.height * 0.32;
|
|
9303
|
+
const knobRadius = slider.height * 0.45;
|
|
9304
|
+
const usable = Math.max(1e-3, slider.width - slider.height);
|
|
9305
|
+
const chrome = {
|
|
9306
|
+
backgroundColor: slider.trackColor,
|
|
9307
|
+
backgroundOpacity: 1,
|
|
9308
|
+
cornerRadiusMeters: trackThickness / 2,
|
|
9309
|
+
borderWidthMeters: 0,
|
|
9310
|
+
borderColor: "#000000"
|
|
9311
|
+
};
|
|
9312
|
+
const signature = trackSignature(
|
|
9313
|
+
slider.width,
|
|
9314
|
+
slider.height,
|
|
9315
|
+
size,
|
|
9316
|
+
chrome,
|
|
9317
|
+
slider.accentColor,
|
|
9318
|
+
slider.knobColor
|
|
9319
|
+
);
|
|
9320
|
+
const entry = this.rebuildIfChanged(
|
|
9321
|
+
world,
|
|
9322
|
+
entityId,
|
|
9323
|
+
signature,
|
|
9324
|
+
slider.width,
|
|
9325
|
+
slider.height,
|
|
9326
|
+
size,
|
|
9327
|
+
(ctx) => {
|
|
9328
|
+
ctx.clearRect(0, 0, size.pxWidth, size.pxHeight);
|
|
9329
|
+
const trackWidthM = usable + trackThickness;
|
|
9330
|
+
drawRoundedBar(
|
|
9331
|
+
ctx,
|
|
9332
|
+
(slider.width - trackWidthM) / 2 * size.density,
|
|
9333
|
+
(slider.height - trackThickness) / 2 * size.density,
|
|
9334
|
+
trackWidthM * size.density,
|
|
9335
|
+
trackThickness * size.density,
|
|
9336
|
+
slider.trackColor
|
|
9337
|
+
);
|
|
9338
|
+
}
|
|
9339
|
+
);
|
|
9340
|
+
if (!entry) return;
|
|
9341
|
+
if (!entry.fill) {
|
|
9342
|
+
entry.fill = this.makeQuad(
|
|
9343
|
+
1,
|
|
9344
|
+
trackThickness,
|
|
9345
|
+
slider.accentColor,
|
|
9346
|
+
entry.face.parent,
|
|
9347
|
+
CHILD_Z / 2
|
|
9348
|
+
);
|
|
9349
|
+
entry.cap = this.makeDisc(
|
|
9350
|
+
trackThickness / 2,
|
|
9351
|
+
slider.accentColor,
|
|
9352
|
+
entry.face.parent,
|
|
9353
|
+
CHILD_Z / 2,
|
|
9354
|
+
Math.PI / 2,
|
|
9355
|
+
Math.PI
|
|
9356
|
+
);
|
|
9357
|
+
entry.cap.position.x = -usable / 2;
|
|
9358
|
+
}
|
|
9359
|
+
if (!entry.knob) {
|
|
9360
|
+
entry.knob = this.makeDisc(
|
|
9361
|
+
knobRadius,
|
|
9362
|
+
slider.knobColor,
|
|
9363
|
+
entry.face.parent,
|
|
9364
|
+
CHILD_Z
|
|
9365
|
+
);
|
|
9366
|
+
}
|
|
9367
|
+
const range = Math.max(1e-6, slider.max - slider.min);
|
|
9368
|
+
const t = Math.min(1, Math.max(0, (slider.value - slider.min) / range));
|
|
9369
|
+
entry.knob.position.x = -usable / 2 + t * usable;
|
|
9370
|
+
entry.knob.position.z = CHILD_Z;
|
|
9371
|
+
entry.fill.scale.x = Math.max(1e-4, t * usable);
|
|
9372
|
+
entry.fill.position.x = -usable / 2 + t * usable / 2;
|
|
9373
|
+
entry.fill.position.z = CHILD_Z / 2;
|
|
9374
|
+
}
|
|
9375
|
+
// ── Machinery ──────────────────────────────────────────────────────────────
|
|
9376
|
+
rebuildIfChanged(world, entityId, signature, widthM, heightM, size, draw) {
|
|
9377
|
+
const root = world.getComponent(entityId, Object3DRef)?.object;
|
|
9378
|
+
if (!root) return null;
|
|
9379
|
+
let entry = this.entries.get(entityId);
|
|
9380
|
+
if (entry && entry.signature === signature) return entry;
|
|
9381
|
+
if (entry) this.disposeMovingParts(entry);
|
|
9382
|
+
if (!entry) {
|
|
9383
|
+
const canvas2 = this.createCanvas();
|
|
9384
|
+
const texture = new THREE19.CanvasTexture(
|
|
9385
|
+
canvas2
|
|
9386
|
+
);
|
|
9387
|
+
this.configureTexture(world, texture);
|
|
9388
|
+
const material = new THREE19.MeshBasicMaterial({
|
|
9389
|
+
map: texture,
|
|
9390
|
+
transparent: true,
|
|
9391
|
+
depthWrite: false,
|
|
9392
|
+
// UI colours are authored absolutes; scene tone mapping would shift
|
|
9393
|
+
// them (the classic "why is my white UI beige" bug).
|
|
9394
|
+
toneMapped: false,
|
|
9395
|
+
side: THREE19.DoubleSide,
|
|
9396
|
+
premultipliedAlpha: true
|
|
9397
|
+
});
|
|
9398
|
+
const face = new THREE19.Mesh(
|
|
9399
|
+
new THREE19.PlaneGeometry(widthM, heightM),
|
|
9400
|
+
material
|
|
9401
|
+
);
|
|
9402
|
+
face.renderOrder = FACE_RENDER_ORDER;
|
|
9403
|
+
root.add(face);
|
|
9404
|
+
entry = {
|
|
9405
|
+
signature: "",
|
|
9406
|
+
face,
|
|
9407
|
+
texture,
|
|
9408
|
+
material,
|
|
9409
|
+
canvas: canvas2,
|
|
9410
|
+
knob: null,
|
|
9411
|
+
fill: null,
|
|
9412
|
+
cap: null
|
|
9413
|
+
};
|
|
9414
|
+
this.entries.set(entityId, entry);
|
|
9415
|
+
}
|
|
9416
|
+
const { canvas } = entry;
|
|
9417
|
+
if (canvas.width !== size.pxWidth || canvas.height !== size.pxHeight) {
|
|
9418
|
+
canvas.width = size.pxWidth;
|
|
9419
|
+
canvas.height = size.pxHeight;
|
|
9420
|
+
}
|
|
9421
|
+
const params = entry.face.geometry.parameters;
|
|
9422
|
+
if (params.width !== widthM || params.height !== heightM) {
|
|
9423
|
+
entry.face.geometry.dispose();
|
|
9424
|
+
entry.face.geometry = new THREE19.PlaneGeometry(widthM, heightM);
|
|
9425
|
+
}
|
|
9426
|
+
const ctx = canvas.getContext("2d");
|
|
9427
|
+
if (ctx) draw(ctx);
|
|
9428
|
+
entry.texture.needsUpdate = true;
|
|
9429
|
+
entry.signature = signature;
|
|
9430
|
+
return entry;
|
|
9431
|
+
}
|
|
9432
|
+
configureTexture(world, texture) {
|
|
9433
|
+
texture.colorSpace = THREE19.SRGBColorSpace;
|
|
9434
|
+
texture.generateMipmaps = true;
|
|
9435
|
+
texture.minFilter = THREE19.LinearMipmapLinearFilter;
|
|
9436
|
+
texture.magFilter = THREE19.LinearFilter;
|
|
9437
|
+
texture.premultiplyAlpha = true;
|
|
9438
|
+
try {
|
|
9439
|
+
const max = world.getRenderer().capabilities?.getMaxAnisotropy?.();
|
|
9440
|
+
if (typeof max === "number") texture.anisotropy = Math.min(8, max);
|
|
9441
|
+
} catch {
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
makeQuad(widthM, heightM, color, parent, z) {
|
|
9445
|
+
return this.addPart(
|
|
9446
|
+
new THREE19.PlaneGeometry(widthM, heightM),
|
|
9447
|
+
color,
|
|
9448
|
+
parent,
|
|
9449
|
+
z
|
|
9450
|
+
);
|
|
9451
|
+
}
|
|
9452
|
+
/** A filled disc (or arc segment, for the fill's rounded cap). */
|
|
9453
|
+
makeDisc(radiusM, color, parent, z, thetaStart = 0, thetaLength = Math.PI * 2) {
|
|
9454
|
+
return this.addPart(
|
|
9455
|
+
new THREE19.CircleGeometry(radiusM, 32, thetaStart, thetaLength),
|
|
9456
|
+
color,
|
|
9457
|
+
parent,
|
|
9458
|
+
z
|
|
9459
|
+
);
|
|
9460
|
+
}
|
|
9461
|
+
addPart(geometry, color, parent, z) {
|
|
9462
|
+
const mesh = new THREE19.Mesh(
|
|
9463
|
+
geometry,
|
|
9464
|
+
new THREE19.MeshBasicMaterial({
|
|
9465
|
+
color,
|
|
9466
|
+
transparent: true,
|
|
9467
|
+
depthWrite: false,
|
|
9468
|
+
toneMapped: false,
|
|
9469
|
+
side: THREE19.DoubleSide
|
|
9470
|
+
})
|
|
9471
|
+
);
|
|
9472
|
+
mesh.renderOrder = FACE_RENDER_ORDER + 1;
|
|
9473
|
+
mesh.position.z = z;
|
|
9474
|
+
mesh.raycast = () => {
|
|
9475
|
+
};
|
|
9476
|
+
parent?.add(mesh);
|
|
9477
|
+
return mesh;
|
|
9478
|
+
}
|
|
9479
|
+
/** Remove + dispose knob/fill/cap so a sync pass rebuilds them fresh. */
|
|
9480
|
+
disposeMovingParts(entry) {
|
|
9481
|
+
for (const key of ["knob", "fill", "cap"]) {
|
|
9482
|
+
const mesh = entry[key];
|
|
9483
|
+
if (!mesh) continue;
|
|
9484
|
+
mesh.removeFromParent();
|
|
9485
|
+
mesh.geometry.dispose();
|
|
9486
|
+
mesh.material.dispose();
|
|
9487
|
+
entry[key] = null;
|
|
9488
|
+
}
|
|
9489
|
+
}
|
|
9490
|
+
/**
|
|
9491
|
+
* Billboard: yaw the ENTITY TRANSFORM toward the camera. Writing the
|
|
9492
|
+
* Object3D directly would be overwritten by `TransformSyncSystem` (which
|
|
9493
|
+
* runs after this system, before render) — the same reason locomotion
|
|
9494
|
+
* writes the rig transform. Skipped in the editor so the gizmo and the
|
|
9495
|
+
* authored rotation stay in charge while authoring.
|
|
9496
|
+
*/
|
|
9497
|
+
applyFollow(world, entityId, panel) {
|
|
9498
|
+
if (panel.follow !== "billboard" || world.isEditorMode()) return;
|
|
9499
|
+
const transform = world.getComponent(entityId, TransformComponent);
|
|
9500
|
+
const object = world.getComponent(entityId, Object3DRef)?.object;
|
|
9501
|
+
if (!transform || !object) return;
|
|
9502
|
+
const camera = world.getCamera();
|
|
9503
|
+
const cam = new THREE19.Vector3();
|
|
9504
|
+
const self = new THREE19.Vector3();
|
|
9505
|
+
camera.getWorldPosition(cam);
|
|
9506
|
+
object.getWorldPosition(self);
|
|
9507
|
+
transform.rotation.y = Math.atan2(cam.x - self.x, cam.z - self.z);
|
|
9508
|
+
}
|
|
9509
|
+
destroyEntry(entry) {
|
|
9510
|
+
this.disposeMovingParts(entry);
|
|
9511
|
+
entry.face.removeFromParent();
|
|
9512
|
+
entry.face.geometry.dispose();
|
|
9513
|
+
entry.face.material.dispose();
|
|
9514
|
+
entry.texture.dispose();
|
|
9515
|
+
}
|
|
9516
|
+
};
|
|
9517
|
+
__name(_UIPanelSystem, "UIPanelSystem");
|
|
9518
|
+
var UIPanelSystem = _UIPanelSystem;
|
|
8200
9519
|
var _XRHitTestSystem = class _XRHitTestSystem {
|
|
8201
9520
|
constructor() {
|
|
8202
9521
|
this.sources = /* @__PURE__ */ new Map();
|
|
@@ -8309,9 +9628,13 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
8309
9628
|
this.anchors = /* @__PURE__ */ new Map();
|
|
8310
9629
|
/** In-flight creations, so one entity is not anchored twice. */
|
|
8311
9630
|
this.pending = /* @__PURE__ */ new Set();
|
|
9631
|
+
/** Per-anchor head-glue gates — see XREnvironmentSystem's TrackedEntry. */
|
|
9632
|
+
this.gates = /* @__PURE__ */ new Map();
|
|
8312
9633
|
this.presenting = false;
|
|
9634
|
+
this.tmpViewerMatrix = new THREE19.Matrix4();
|
|
9635
|
+
this.tmpViewPos = new THREE19.Vector3();
|
|
8313
9636
|
}
|
|
8314
|
-
update(world) {
|
|
9637
|
+
update(world, deltaTime = 0) {
|
|
8315
9638
|
const presenting = world.isXrPresenting();
|
|
8316
9639
|
if (!presenting) {
|
|
8317
9640
|
if (this.presenting) {
|
|
@@ -8331,8 +9654,11 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
8331
9654
|
if (!live.has(entityId)) {
|
|
8332
9655
|
anchor.delete?.();
|
|
8333
9656
|
this.anchors.delete(entityId);
|
|
9657
|
+
this.gates.delete(entityId);
|
|
8334
9658
|
}
|
|
8335
9659
|
}
|
|
9660
|
+
const viewer = frame.getViewerPose?.(referenceSpace);
|
|
9661
|
+
const viewerInverse = viewer ? this.tmpViewerMatrix.fromArray(viewer.transform.matrix).invert() : null;
|
|
8336
9662
|
for (const entityId of entityIds) {
|
|
8337
9663
|
const component = world.getComponent(entityId, XRAnchorComponent);
|
|
8338
9664
|
const transform = world.getComponent(entityId, TransformComponent);
|
|
@@ -8342,6 +9668,7 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
8342
9668
|
if (existing) {
|
|
8343
9669
|
existing.delete?.();
|
|
8344
9670
|
this.anchors.delete(entityId);
|
|
9671
|
+
this.gates.delete(entityId);
|
|
8345
9672
|
}
|
|
8346
9673
|
continue;
|
|
8347
9674
|
}
|
|
@@ -8351,7 +9678,23 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
8351
9678
|
continue;
|
|
8352
9679
|
}
|
|
8353
9680
|
const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
|
|
8354
|
-
if (pose
|
|
9681
|
+
if (pose && pose.emulatedPosition !== true) {
|
|
9682
|
+
let gate = this.gates.get(entityId);
|
|
9683
|
+
if (!gate) {
|
|
9684
|
+
gate = new SurfacePoseGate();
|
|
9685
|
+
this.gates.set(entityId, gate);
|
|
9686
|
+
}
|
|
9687
|
+
const m = pose.transform.matrix;
|
|
9688
|
+
const refPos = { x: m[12], y: m[13], z: m[14] };
|
|
9689
|
+
let viewPos = null;
|
|
9690
|
+
if (viewerInverse) {
|
|
9691
|
+
const v = this.tmpViewPos.set(refPos.x, refPos.y, refPos.z).applyMatrix4(viewerInverse);
|
|
9692
|
+
viewPos = { x: v.x, y: v.y, z: v.z };
|
|
9693
|
+
}
|
|
9694
|
+
if (gate.update(refPos, viewPos, deltaTime)) {
|
|
9695
|
+
decomposeXrPose(pose.transform.matrix, transform);
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
8355
9698
|
}
|
|
8356
9699
|
}
|
|
8357
9700
|
/** Whether this entity's pose is currently being driven by an anchor. */
|
|
@@ -8395,6 +9738,7 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
8395
9738
|
}).finally(() => this.pending.delete(entityId));
|
|
8396
9739
|
}
|
|
8397
9740
|
releaseAll() {
|
|
9741
|
+
this.gates.clear();
|
|
8398
9742
|
for (const anchor of this.anchors.values()) anchor.delete?.();
|
|
8399
9743
|
this.anchors.clear();
|
|
8400
9744
|
this.pending.clear();
|
|
@@ -9640,6 +10984,6 @@ var _XRHoverVisualSystem = class _XRHoverVisualSystem {
|
|
|
9640
10984
|
__name(_XRHoverVisualSystem, "XRHoverVisualSystem");
|
|
9641
10985
|
var XRHoverVisualSystem = _XRHoverVisualSystem;
|
|
9642
10986
|
|
|
9643
|
-
export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CURLED_COS, CameraSyncSystem, DEFAULT_DIRECTION_TOLERANCE, DEFAULT_ENTER_CONFIDENCE, DEFAULT_EXIT_CONFIDENCE, DEFAULT_LOST_GRACE_SECONDS, DEFAULT_MIN_HOLD_SECONDS, EnvironmentSyncSystem, FALLBACK_HAND_SCALE, HYDRATABLE_ENTITY_KINDS, HandPoseTracker, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MATCH_FALLOFF, MAX_PLAUSIBLE_SPEED, MAX_SAMPLE_AGE_SECONDS, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PINCH_FINGERS, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, STRAIGHT_COS, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TeleportAimGate, ThrowVelocityTracker, TransformSyncSystem, VELOCITY_SAMPLES, VRTeleportAreaSystem, VideoSystem, XRAnchorSystem, XRControllerVisualSystem, XREnvironmentSystem, XRHandPoseSystem, XRHitTestSystem, XRHoverVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, XR_FINGERS, XR_HOVER_SHELL_FLAG, angularVelocityFrom, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeFingerCurl, computeHandShape, computeInstanceOverrides, computePalmPose, computePalmWidth, computePinch, computeSpread, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, estimateHandScale, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, matchHandPose, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
|
|
9644
|
-
//# sourceMappingURL=chunk-
|
|
9645
|
-
//# sourceMappingURL=chunk-
|
|
10987
|
+
export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CURLED_COS, CameraSyncSystem, DEFAULT_DIRECTION_TOLERANCE, DEFAULT_ENTER_CONFIDENCE, DEFAULT_EXIT_CONFIDENCE, DEFAULT_LOST_GRACE_SECONDS, DEFAULT_MIN_HOLD_SECONDS, EnvironmentSyncSystem, FALLBACK_HAND_SCALE, HYDRATABLE_ENTITY_KINDS, HandPoseTracker, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MATCH_FALLOFF, MAX_PLAUSIBLE_SPEED, MAX_SAMPLE_AGE_SECONDS, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PINCH_FINGERS, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, STRAIGHT_COS, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TeleportAimGate, ThrowVelocityTracker, TransformSyncSystem, UIInteractionSystem, UIPanelSystem, VELOCITY_SAMPLES, VRTeleportAreaSystem, VideoSystem, XRAnchorSystem, XRControllerVisualSystem, XREnvironmentSystem, XRHandPoseSystem, XRHitTestSystem, XRHoverVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, XR_FINGERS, XR_HOVER_SHELL_FLAG, angularVelocityFrom, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeFingerCurl, computeHandShape, computeInstanceOverrides, computePalmPose, computePalmWidth, computePinch, computeSpread, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, estimateHandScale, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, matchHandPose, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
|
|
10988
|
+
//# sourceMappingURL=chunk-Z6ZMR3CS.js.map
|
|
10989
|
+
//# sourceMappingURL=chunk-Z6ZMR3CS.js.map
|