@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
package/dist/systems/index.cjs
CHANGED
|
@@ -2187,6 +2187,100 @@ var _XRAnchorComponent = class _XRAnchorComponent extends Component {
|
|
|
2187
2187
|
__name(_XRAnchorComponent, "XRAnchorComponent");
|
|
2188
2188
|
var XRAnchorComponent = _XRAnchorComponent;
|
|
2189
2189
|
|
|
2190
|
+
// core/components/UIPanel.ts
|
|
2191
|
+
var _UIPanelComponent = class _UIPanelComponent extends Component {
|
|
2192
|
+
constructor(options) {
|
|
2193
|
+
super();
|
|
2194
|
+
this.width = options?.width ?? 0.6;
|
|
2195
|
+
this.height = options?.height ?? 0.4;
|
|
2196
|
+
this.backgroundColor = options?.backgroundColor ?? "#1c1c1c";
|
|
2197
|
+
this.backgroundOpacity = options?.backgroundOpacity ?? 0.92;
|
|
2198
|
+
this.cornerRadius = options?.cornerRadius ?? 0.02;
|
|
2199
|
+
this.borderWidth = options?.borderWidth ?? 0;
|
|
2200
|
+
this.borderColor = options?.borderColor ?? "#333333";
|
|
2201
|
+
this.pixelDensity = options?.pixelDensity ?? 1024;
|
|
2202
|
+
this.follow = options?.follow ?? "world";
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
__name(_UIPanelComponent, "UIPanelComponent");
|
|
2206
|
+
var UIPanelComponent = _UIPanelComponent;
|
|
2207
|
+
|
|
2208
|
+
// core/components/UIText.ts
|
|
2209
|
+
var _UITextComponent = class _UITextComponent extends Component {
|
|
2210
|
+
constructor(options) {
|
|
2211
|
+
super();
|
|
2212
|
+
this.text = options?.text ?? "Text";
|
|
2213
|
+
this.width = options?.width ?? 0.5;
|
|
2214
|
+
this.height = options?.height ?? 0.12;
|
|
2215
|
+
this.fontSize = options?.fontSize ?? 0.035;
|
|
2216
|
+
this.fontWeight = options?.fontWeight ?? 500;
|
|
2217
|
+
this.color = options?.color ?? "#ffffff";
|
|
2218
|
+
this.align = options?.align ?? "center";
|
|
2219
|
+
this.valign = options?.valign ?? "middle";
|
|
2220
|
+
this.lineHeight = options?.lineHeight ?? 1.25;
|
|
2221
|
+
this.padding = options?.padding ?? 0.01;
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
__name(_UITextComponent, "UITextComponent");
|
|
2225
|
+
var UITextComponent = _UITextComponent;
|
|
2226
|
+
|
|
2227
|
+
// core/components/UIButton.ts
|
|
2228
|
+
var _UIButtonComponent = class _UIButtonComponent extends Component {
|
|
2229
|
+
constructor(options) {
|
|
2230
|
+
super();
|
|
2231
|
+
this.label = options?.label ?? "Button";
|
|
2232
|
+
this.actionId = options?.actionId ?? "";
|
|
2233
|
+
this.width = options?.width ?? 0.3;
|
|
2234
|
+
this.height = options?.height ?? 0.1;
|
|
2235
|
+
this.fontSize = options?.fontSize ?? 0.03;
|
|
2236
|
+
this.fontWeight = options?.fontWeight ?? 600;
|
|
2237
|
+
this.textColor = options?.textColor ?? "#ffffff";
|
|
2238
|
+
this.backgroundColor = options?.backgroundColor ?? "#2a2a2a";
|
|
2239
|
+
this.backgroundOpacity = options?.backgroundOpacity ?? 1;
|
|
2240
|
+
this.cornerRadius = options?.cornerRadius ?? 0.02;
|
|
2241
|
+
this.borderWidth = options?.borderWidth ?? 2e-3;
|
|
2242
|
+
this.borderColor = options?.borderColor ?? "#3a3a3a";
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
__name(_UIButtonComponent, "UIButtonComponent");
|
|
2246
|
+
var UIButtonComponent = _UIButtonComponent;
|
|
2247
|
+
|
|
2248
|
+
// core/components/UIToggle.ts
|
|
2249
|
+
var _UIToggleComponent = class _UIToggleComponent extends Component {
|
|
2250
|
+
constructor(options) {
|
|
2251
|
+
super();
|
|
2252
|
+
this.value = options?.value ?? false;
|
|
2253
|
+
this.actionId = options?.actionId ?? "";
|
|
2254
|
+
this.width = options?.width ?? 0.14;
|
|
2255
|
+
this.height = options?.height ?? 0.07;
|
|
2256
|
+
this.trackColor = options?.trackColor ?? "#2a2a2a";
|
|
2257
|
+
this.accentColor = options?.accentColor ?? "#ff6b35";
|
|
2258
|
+
this.knobColor = options?.knobColor ?? "#ffffff";
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
__name(_UIToggleComponent, "UIToggleComponent");
|
|
2262
|
+
var UIToggleComponent = _UIToggleComponent;
|
|
2263
|
+
|
|
2264
|
+
// core/components/UISlider.ts
|
|
2265
|
+
var _UISliderComponent = class _UISliderComponent extends Component {
|
|
2266
|
+
constructor(options) {
|
|
2267
|
+
super();
|
|
2268
|
+
this.min = options?.min ?? 0;
|
|
2269
|
+
this.max = options?.max ?? Math.max(1, this.min + 1);
|
|
2270
|
+
if (this.max <= this.min) this.max = this.min + 1;
|
|
2271
|
+
const v = options?.value ?? (this.min + this.max) / 2;
|
|
2272
|
+
this.value = Math.min(this.max, Math.max(this.min, v));
|
|
2273
|
+
this.actionId = options?.actionId ?? "";
|
|
2274
|
+
this.width = options?.width ?? 0.4;
|
|
2275
|
+
this.height = options?.height ?? 0.06;
|
|
2276
|
+
this.trackColor = options?.trackColor ?? "#2a2a2a";
|
|
2277
|
+
this.accentColor = options?.accentColor ?? "#ff6b35";
|
|
2278
|
+
this.knobColor = options?.knobColor ?? "#ffffff";
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
__name(_UISliderComponent, "UISliderComponent");
|
|
2282
|
+
var UISliderComponent = _UISliderComponent;
|
|
2283
|
+
|
|
2190
2284
|
// core/components/XRInteractor.ts
|
|
2191
2285
|
var _XRInteractorComponent = class _XRInteractorComponent extends Component {
|
|
2192
2286
|
constructor(options) {
|
|
@@ -5288,6 +5382,11 @@ function buildComponentValueMaps(snapshot) {
|
|
|
5288
5382
|
"XRHandPoseComponent"
|
|
5289
5383
|
),
|
|
5290
5384
|
xrAnchorValues: readByComponent(snapshot.components, "XRAnchorComponent"),
|
|
5385
|
+
uiPanelValues: readByComponent(snapshot.components, "UIPanelComponent"),
|
|
5386
|
+
uiTextValues: readByComponent(snapshot.components, "UITextComponent"),
|
|
5387
|
+
uiButtonValues: readByComponent(snapshot.components, "UIButtonComponent"),
|
|
5388
|
+
uiToggleValues: readByComponent(snapshot.components, "UIToggleComponent"),
|
|
5389
|
+
uiSliderValues: readByComponent(snapshot.components, "UISliderComponent"),
|
|
5291
5390
|
xrInteractorSideValues: readByComponent(
|
|
5292
5391
|
snapshot.components,
|
|
5293
5392
|
"XRInteractorSideComponent"
|
|
@@ -5609,6 +5708,49 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
|
|
|
5609
5708
|
}
|
|
5610
5709
|
}
|
|
5611
5710
|
}
|
|
5711
|
+
{
|
|
5712
|
+
const restoreUi = /* @__PURE__ */ __name((map, Ctor, existing) => {
|
|
5713
|
+
const source = asRecord3(map.get(sourceEntityId));
|
|
5714
|
+
if (!source) return;
|
|
5715
|
+
let component = existing;
|
|
5716
|
+
if (!component) {
|
|
5717
|
+
component = new Ctor();
|
|
5718
|
+
world.addComponent(targetEntityId, component);
|
|
5719
|
+
}
|
|
5720
|
+
const target = component;
|
|
5721
|
+
for (const [key, value] of Object.entries(source)) {
|
|
5722
|
+
const current = target[key];
|
|
5723
|
+
if ((typeof value === "string" || typeof value === "number" || typeof value === "boolean") && typeof current === typeof value) {
|
|
5724
|
+
target[key] = value;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
}, "restoreUi");
|
|
5728
|
+
restoreUi(
|
|
5729
|
+
values.uiPanelValues,
|
|
5730
|
+
UIPanelComponent,
|
|
5731
|
+
world.getComponent(targetEntityId, UIPanelComponent)
|
|
5732
|
+
);
|
|
5733
|
+
restoreUi(
|
|
5734
|
+
values.uiTextValues,
|
|
5735
|
+
UITextComponent,
|
|
5736
|
+
world.getComponent(targetEntityId, UITextComponent)
|
|
5737
|
+
);
|
|
5738
|
+
restoreUi(
|
|
5739
|
+
values.uiButtonValues,
|
|
5740
|
+
UIButtonComponent,
|
|
5741
|
+
world.getComponent(targetEntityId, UIButtonComponent)
|
|
5742
|
+
);
|
|
5743
|
+
restoreUi(
|
|
5744
|
+
values.uiToggleValues,
|
|
5745
|
+
UIToggleComponent,
|
|
5746
|
+
world.getComponent(targetEntityId, UIToggleComponent)
|
|
5747
|
+
);
|
|
5748
|
+
restoreUi(
|
|
5749
|
+
values.uiSliderValues,
|
|
5750
|
+
UISliderComponent,
|
|
5751
|
+
world.getComponent(targetEntityId, UISliderComponent)
|
|
5752
|
+
);
|
|
5753
|
+
}
|
|
5612
5754
|
{
|
|
5613
5755
|
const sourceInteractor = asRecord3(
|
|
5614
5756
|
values.xrInteractorValues.get(sourceEntityId)
|
|
@@ -7031,6 +7173,23 @@ var _Behaviour = class _Behaviour {
|
|
|
7031
7173
|
/** Called when this entity is released. Optional XR hook. */
|
|
7032
7174
|
onRelease(input) {
|
|
7033
7175
|
}
|
|
7176
|
+
/** A UI button on this entity (or a descendant) was clicked. Clicks fire on
|
|
7177
|
+
* RELEASE while still pointing at the widget — sliding off cancels. */
|
|
7178
|
+
onUiPress(_event) {
|
|
7179
|
+
}
|
|
7180
|
+
/** A pointer began hovering a UI widget on this entity (or a descendant). */
|
|
7181
|
+
onUiHoverStart(_event) {
|
|
7182
|
+
}
|
|
7183
|
+
/** The pointer left the widget. */
|
|
7184
|
+
onUiHoverEnd(_event) {
|
|
7185
|
+
}
|
|
7186
|
+
/** A UI toggle flipped; `event.value` is the new boolean. */
|
|
7187
|
+
onUiToggle(_event) {
|
|
7188
|
+
}
|
|
7189
|
+
/** A UI slider moved; `event.value` is the new number. Fires on real value
|
|
7190
|
+
* changes during the drag and once more on release. */
|
|
7191
|
+
onUiChange(_event) {
|
|
7192
|
+
}
|
|
7034
7193
|
};
|
|
7035
7194
|
__name(_Behaviour, "Behaviour");
|
|
7036
7195
|
/**
|
|
@@ -8561,6 +8720,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
8561
8720
|
this.grabs = /* @__PURE__ */ new Map();
|
|
8562
8721
|
this.hoveredEntityId = null;
|
|
8563
8722
|
/** Per-hand hover, with the hit distance so the ray can stop at the surface. */
|
|
8723
|
+
this.busyContributors = [];
|
|
8564
8724
|
this.hovers = /* @__PURE__ */ new Map();
|
|
8565
8725
|
/** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
|
|
8566
8726
|
this.elapsed = 0;
|
|
@@ -8569,6 +8729,8 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
8569
8729
|
update(world, deltaTime) {
|
|
8570
8730
|
if (!world.isXrPresenting()) {
|
|
8571
8731
|
this.releaseAll(world);
|
|
8732
|
+
this.hovers.clear();
|
|
8733
|
+
this.hoveredEntityId = null;
|
|
8572
8734
|
return;
|
|
8573
8735
|
}
|
|
8574
8736
|
this.elapsed += Math.max(0, deltaTime);
|
|
@@ -9191,13 +9353,30 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
9191
9353
|
*/
|
|
9192
9354
|
isSourceBusy(world, input) {
|
|
9193
9355
|
if (this.grabs.has(input)) return true;
|
|
9356
|
+
for (const contributor of this.busyContributors) {
|
|
9357
|
+
try {
|
|
9358
|
+
if (contributor(world, input)) return true;
|
|
9359
|
+
} catch {
|
|
9360
|
+
}
|
|
9361
|
+
}
|
|
9194
9362
|
const hover = this.hovers.get(input);
|
|
9195
9363
|
if (!hover) return false;
|
|
9196
9364
|
const interactable = world.getComponent(
|
|
9197
9365
|
hover.entityId,
|
|
9198
9366
|
XRInteractableComponent
|
|
9199
9367
|
);
|
|
9200
|
-
return Boolean(
|
|
9368
|
+
return Boolean(
|
|
9369
|
+
interactable?.enabled && (interactable.grabbable || interactable.pointable)
|
|
9370
|
+
);
|
|
9371
|
+
}
|
|
9372
|
+
/** OR-ed into isSourceBusy; see the comment there. */
|
|
9373
|
+
registerBusyContributor(contributor) {
|
|
9374
|
+
this.busyContributors.push(contributor);
|
|
9375
|
+
}
|
|
9376
|
+
/** Hover for a specific input source (handedness-free — Vision Pro's
|
|
9377
|
+
* transient pointers all report handedness "none"). */
|
|
9378
|
+
getHoverForInput(input) {
|
|
9379
|
+
return this.hovers.get(input) ?? null;
|
|
9201
9380
|
}
|
|
9202
9381
|
/** True when THIS hand is holding something. */
|
|
9203
9382
|
isGrabbingWith(handedness) {
|
|
@@ -9348,6 +9527,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
9348
9527
|
const rig = world.getComponent(rigEntityId, XRRigComponent);
|
|
9349
9528
|
const rigTransform = world.getComponent(rigEntityId, TransformComponent);
|
|
9350
9529
|
if (!rig || !rigTransform) return;
|
|
9530
|
+
if (world.isXrPassthrough()) return;
|
|
9351
9531
|
const inputs = this.inputSystem.getInputs();
|
|
9352
9532
|
if (inputs.length === 0) return;
|
|
9353
9533
|
if (rig.snapTurn) {
|
|
@@ -9610,6 +9790,32 @@ var _XRSessionSystem = class _XRSessionSystem {
|
|
|
9610
9790
|
}
|
|
9611
9791
|
if (presenting) {
|
|
9612
9792
|
this.dispatchInputEdges(world);
|
|
9793
|
+
this.muteDepthOcclusionColor(world);
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
/**
|
|
9797
|
+
* Stop three's depth-occlusion pass from writing COLOR.
|
|
9798
|
+
*
|
|
9799
|
+
* With depth sensing granted, three draws a full-screen, head-locked mesh
|
|
9800
|
+
* every frame whose fragment shader assigns ONLY `gl_FragDepth` — that is
|
|
9801
|
+
* the whole occlusion mechanism — but its material never sets
|
|
9802
|
+
* `colorWrite: false`, so the colour output is UNDEFINED per GLSL. Desktop
|
|
9803
|
+
* drivers happen to zero it, which is why this ships unnoticed; Quest's
|
|
9804
|
+
* mobile GPU frequently leaves the last-computed temporary in the register —
|
|
9805
|
+
* the sampled DEPTH value — so the pass paints the depth camera's view over
|
|
9806
|
+
* the screen as faint head-locked contour bands. On device that reads as "a
|
|
9807
|
+
* large wireframe dome that follows my head".
|
|
9808
|
+
*
|
|
9809
|
+
* The mesh is created lazily inside three's render, so this is re-checked
|
|
9810
|
+
* every frame rather than hooked once; the write is idempotent and the
|
|
9811
|
+
* check is two property reads. Depth-only is the mesh's entire purpose —
|
|
9812
|
+
* occlusion is unaffected.
|
|
9813
|
+
*/
|
|
9814
|
+
muteDepthOcclusionColor(world) {
|
|
9815
|
+
const xr = world.getRenderer().xr;
|
|
9816
|
+
const material = xr.getDepthSensingMesh?.()?.material;
|
|
9817
|
+
if (material && material.colorWrite !== false) {
|
|
9818
|
+
material.colorWrite = false;
|
|
9613
9819
|
}
|
|
9614
9820
|
}
|
|
9615
9821
|
handleSessionStart(world) {
|
|
@@ -9763,8 +9969,12 @@ function planeColliderFromPolygon(polygon) {
|
|
|
9763
9969
|
__name(planeColliderFromPolygon, "planeColliderFromPolygon");
|
|
9764
9970
|
function writeXrMeshGeometry(vertices, indices, existing) {
|
|
9765
9971
|
const geometry = existing ?? new THREE26__namespace.BufferGeometry();
|
|
9766
|
-
geometry.setAttribute(
|
|
9767
|
-
|
|
9972
|
+
geometry.setAttribute(
|
|
9973
|
+
"position",
|
|
9974
|
+
new THREE26__namespace.BufferAttribute(vertices.slice(), 3)
|
|
9975
|
+
);
|
|
9976
|
+
geometry.setIndex(new THREE26__namespace.BufferAttribute(indices.slice(), 1));
|
|
9977
|
+
geometry.deleteAttribute("normal");
|
|
9768
9978
|
geometry.computeBoundingSphere();
|
|
9769
9979
|
geometry.computeVertexNormals();
|
|
9770
9980
|
return geometry;
|
|
@@ -9788,20 +9998,151 @@ function decomposeXrPose(matrixElements, target) {
|
|
|
9788
9998
|
}
|
|
9789
9999
|
__name(decomposeXrPose, "decomposeXrPose");
|
|
9790
10000
|
|
|
10001
|
+
// core/xr/poseStability.ts
|
|
10002
|
+
var GLUED_REF_SPEED = 0.12;
|
|
10003
|
+
var STILL_SPEED = 0.05;
|
|
10004
|
+
var GLUED_FRAMES_TO_FREEZE = 3;
|
|
10005
|
+
var STATIC_FRAMES_TO_RECOVER = 10;
|
|
10006
|
+
var SNAP_SPEED = 3;
|
|
10007
|
+
var dist = /* @__PURE__ */ __name((a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z), "dist");
|
|
10008
|
+
var _SurfacePoseGate = class _SurfacePoseGate {
|
|
10009
|
+
constructor() {
|
|
10010
|
+
this.prevRef = null;
|
|
10011
|
+
this.prevView = null;
|
|
10012
|
+
this.gluedStreak = 0;
|
|
10013
|
+
this.staticStreak = 0;
|
|
10014
|
+
this.suspect = false;
|
|
10015
|
+
this.deltas = 0;
|
|
10016
|
+
}
|
|
10017
|
+
/** True while poses are being rejected as head-glued. */
|
|
10018
|
+
get frozen() {
|
|
10019
|
+
return this.suspect;
|
|
10020
|
+
}
|
|
10021
|
+
/**
|
|
10022
|
+
* True once enough motion has been observed to classify the pose at all.
|
|
10023
|
+
* Surface CREATION waits for this: an entity spawned from a glued pose
|
|
10024
|
+
* materializes wherever the head happens to be — a rotated ghost copy of
|
|
10025
|
+
* the room — and freezing it there is barely better than letting it follow.
|
|
10026
|
+
*/
|
|
10027
|
+
get warmedUp() {
|
|
10028
|
+
return this.deltas >= GLUED_FRAMES_TO_FREEZE;
|
|
10029
|
+
}
|
|
10030
|
+
/**
|
|
10031
|
+
* Feed this frame's surface position in reference space and in view
|
|
10032
|
+
* (viewer-relative) space. Returns true when the pose should be APPLIED,
|
|
10033
|
+
* false when it should be frozen out.
|
|
10034
|
+
*
|
|
10035
|
+
* `viewPos` may be null when the viewer pose is unavailable (older
|
|
10036
|
+
* runtimes) — the gate then passes everything through, degrading to the
|
|
10037
|
+
* flag-only behavior.
|
|
10038
|
+
*/
|
|
10039
|
+
update(refPos, viewPos, deltaTime) {
|
|
10040
|
+
if (!viewPos || deltaTime <= 0) {
|
|
10041
|
+
this.prevRef = { ...refPos };
|
|
10042
|
+
this.prevView = viewPos ? { ...viewPos } : null;
|
|
10043
|
+
return !this.suspect;
|
|
10044
|
+
}
|
|
10045
|
+
const prevRef = this.prevRef;
|
|
10046
|
+
const prevView = this.prevView;
|
|
10047
|
+
this.prevRef = { ...refPos };
|
|
10048
|
+
this.prevView = { ...viewPos };
|
|
10049
|
+
if (!prevRef || !prevView) return !this.suspect;
|
|
10050
|
+
this.deltas += 1;
|
|
10051
|
+
const refSpeed = dist(refPos, prevRef) / deltaTime;
|
|
10052
|
+
const viewSpeed = dist(viewPos, prevView) / deltaTime;
|
|
10053
|
+
const glued = refSpeed > GLUED_REF_SPEED && viewSpeed < STILL_SPEED;
|
|
10054
|
+
const worldStatic = refSpeed < STILL_SPEED && viewSpeed > GLUED_REF_SPEED;
|
|
10055
|
+
if (this.suspect) {
|
|
10056
|
+
if (refSpeed > SNAP_SPEED && viewSpeed > SNAP_SPEED) {
|
|
10057
|
+
this.suspect = false;
|
|
10058
|
+
this.gluedStreak = 0;
|
|
10059
|
+
this.staticStreak = 0;
|
|
10060
|
+
return true;
|
|
10061
|
+
}
|
|
10062
|
+
this.staticStreak = worldStatic ? this.staticStreak + 1 : 0;
|
|
10063
|
+
if (this.staticStreak >= STATIC_FRAMES_TO_RECOVER) {
|
|
10064
|
+
this.suspect = false;
|
|
10065
|
+
this.gluedStreak = 0;
|
|
10066
|
+
this.staticStreak = 0;
|
|
10067
|
+
return true;
|
|
10068
|
+
}
|
|
10069
|
+
return false;
|
|
10070
|
+
}
|
|
10071
|
+
this.gluedStreak = glued ? this.gluedStreak + 1 : 0;
|
|
10072
|
+
if (this.gluedStreak >= GLUED_FRAMES_TO_FREEZE) {
|
|
10073
|
+
this.suspect = true;
|
|
10074
|
+
this.staticStreak = 0;
|
|
10075
|
+
return false;
|
|
10076
|
+
}
|
|
10077
|
+
return true;
|
|
10078
|
+
}
|
|
10079
|
+
};
|
|
10080
|
+
__name(_SurfacePoseGate, "SurfacePoseGate");
|
|
10081
|
+
var SurfacePoseGate = _SurfacePoseGate;
|
|
10082
|
+
|
|
9791
10083
|
// core/systems/XREnvironmentSystem.ts
|
|
9792
10084
|
var DEBUG_COLOR = 5088255;
|
|
10085
|
+
function debugColorFor(kind, semanticLabel) {
|
|
10086
|
+
switch (semanticLabel) {
|
|
10087
|
+
case "floor":
|
|
10088
|
+
return 5100666;
|
|
10089
|
+
// green — the surface everything should rest on
|
|
10090
|
+
case "ceiling":
|
|
10091
|
+
return 11693055;
|
|
10092
|
+
// purple
|
|
10093
|
+
case "wall":
|
|
10094
|
+
case "wall face":
|
|
10095
|
+
return DEBUG_COLOR;
|
|
10096
|
+
// blue, the family's base colour
|
|
10097
|
+
case "global mesh":
|
|
10098
|
+
return 9079442;
|
|
10099
|
+
// dim gray — raw scan, not an object
|
|
10100
|
+
default:
|
|
10101
|
+
return semanticLabel ? 16747594 : DEBUG_COLOR;
|
|
10102
|
+
}
|
|
10103
|
+
}
|
|
10104
|
+
__name(debugColorFor, "debugColorFor");
|
|
10105
|
+
var GLOBAL_MESH_DEBUG_OPACITY = 0.35;
|
|
10106
|
+
var DEBUG_RENDER_ORDER = 900;
|
|
9793
10107
|
var ROOM_CAPTURE_DELAY_SECONDS = 3;
|
|
10108
|
+
var SYNTHETIC_FLOOR_EXTENT = 50;
|
|
10109
|
+
var SYNTHETIC_FLOOR_PLANE = {
|
|
10110
|
+
planeSpace: {},
|
|
10111
|
+
polygon: [
|
|
10112
|
+
{ x: -SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: -SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
10113
|
+
{ x: SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: -SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
10114
|
+
{ x: SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: SYNTHETIC_FLOOR_EXTENT / 2 },
|
|
10115
|
+
{ x: -SYNTHETIC_FLOOR_EXTENT / 2, y: 0, z: SYNTHETIC_FLOOR_EXTENT / 2 }
|
|
10116
|
+
],
|
|
10117
|
+
orientation: "horizontal",
|
|
10118
|
+
semanticLabel: "floor",
|
|
10119
|
+
lastChangedTime: 0
|
|
10120
|
+
};
|
|
9794
10121
|
var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
9795
10122
|
constructor() {
|
|
9796
10123
|
/** Keyed by the XRPlane/XRMesh object itself — the runtime's own identity. */
|
|
9797
10124
|
this.planes = /* @__PURE__ */ new Map();
|
|
9798
10125
|
this.meshes = /* @__PURE__ */ new Map();
|
|
10126
|
+
/**
|
|
10127
|
+
* Surfaces observed but not yet SPAWNED — their gate is still proving the
|
|
10128
|
+
* pose is world-anchored. Creating from an unflagged head-glued pose (seen
|
|
10129
|
+
* on-device after a suspend/resume regenerated the scan identities) plants
|
|
10130
|
+
* a rotated ghost copy of the room wherever the head happens to be, which
|
|
10131
|
+
* the apply-gate then freezes there — wrong forever. So admission itself
|
|
10132
|
+
* is gated: a few frames of world-anchored behavior first.
|
|
10133
|
+
*/
|
|
10134
|
+
this.pendingPlanes = /* @__PURE__ */ new Map();
|
|
10135
|
+
this.pendingMeshes = /* @__PURE__ */ new Map();
|
|
9799
10136
|
this.presenting = false;
|
|
9800
10137
|
this.debugVisible = false;
|
|
9801
10138
|
/** Seconds presenting, used to decide the room has genuinely nothing in it. */
|
|
9802
10139
|
this.elapsed = 0;
|
|
9803
10140
|
/** `initiateRoomCapture` may be called only ONCE per session. */
|
|
9804
10141
|
this.roomCaptureRequested = false;
|
|
10142
|
+
/** This frame's inverse viewer matrix, or null when unavailable. */
|
|
10143
|
+
this.viewerInverse = null;
|
|
10144
|
+
this.tmpViewerMatrix = new THREE26__namespace.Matrix4();
|
|
10145
|
+
this.tmpViewPos = new THREE26__namespace.Vector3();
|
|
9805
10146
|
}
|
|
9806
10147
|
update(world, deltaTime = 0) {
|
|
9807
10148
|
const presenting = world.isXrPresenting();
|
|
@@ -9831,16 +10172,23 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9831
10172
|
const frame = xr.getFrame?.();
|
|
9832
10173
|
const referenceSpace = xr.getReferenceSpace();
|
|
9833
10174
|
if (!frame || !referenceSpace) return;
|
|
10175
|
+
if (frame.detectedPlanes === void 0 && frame.detectedMeshes === void 0) {
|
|
10176
|
+
this.ensureSyntheticFloor(world);
|
|
10177
|
+
return;
|
|
10178
|
+
}
|
|
10179
|
+
this.removeSyntheticFloor(world);
|
|
9834
10180
|
const meshesAvailable = Boolean(frame.detectedMeshes);
|
|
9835
10181
|
const useMesh = (mode === "mesh" || mode === "auto") && meshesAvailable;
|
|
9836
10182
|
const usePlanes = mode === "planes" || mode === "auto" && !useMesh;
|
|
10183
|
+
const viewer = frame.getViewerPose?.(referenceSpace);
|
|
10184
|
+
this.viewerInverse = viewer ? this.tmpViewerMatrix.fromArray(viewer.transform.matrix).invert() : null;
|
|
9837
10185
|
if (usePlanes) {
|
|
9838
|
-
this.syncPlanes(world, frame, referenceSpace);
|
|
10186
|
+
this.syncPlanes(world, frame, referenceSpace, deltaTime);
|
|
9839
10187
|
} else if (this.planes.size > 0) {
|
|
9840
10188
|
this.clearMap(world, this.planes);
|
|
9841
10189
|
}
|
|
9842
10190
|
if (useMesh) {
|
|
9843
|
-
this.syncMeshes(world, frame, referenceSpace);
|
|
10191
|
+
this.syncMeshes(world, frame, referenceSpace, deltaTime);
|
|
9844
10192
|
} else if (this.meshes.size > 0) {
|
|
9845
10193
|
this.clearMap(world, this.meshes);
|
|
9846
10194
|
}
|
|
@@ -9877,12 +10225,57 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9877
10225
|
getDetectedCount() {
|
|
9878
10226
|
return { planes: this.planes.size, meshes: this.meshes.size };
|
|
9879
10227
|
}
|
|
10228
|
+
/**
|
|
10229
|
+
* A census of every surface currently spawned: what it is, where it sits and
|
|
10230
|
+
* roughly how big it is. Built for the template's post-session readout, so a
|
|
10231
|
+
* mystery shape on the headset can be looked up by its label instead of
|
|
10232
|
+
* guessed at — a small-radius entry at head height IS the mystery shape, and
|
|
10233
|
+
* a clean list means whatever was seen did not come from this system.
|
|
10234
|
+
*/
|
|
10235
|
+
getDebugSnapshot(world) {
|
|
10236
|
+
const snapshot = [];
|
|
10237
|
+
for (const [plane, entry] of this.planes) {
|
|
10238
|
+
const transform = world.getComponent(entry.entityId, TransformComponent);
|
|
10239
|
+
const collider = planeColliderFromPolygon(plane.polygon);
|
|
10240
|
+
snapshot.push({
|
|
10241
|
+
kind: "plane",
|
|
10242
|
+
label: plane.semanticLabel ?? null,
|
|
10243
|
+
position: transform ? { ...transform.position } : { x: 0, y: 0, z: 0 },
|
|
10244
|
+
radius: collider ? Math.hypot(collider.size.x, collider.size.z) / 2 : 0,
|
|
10245
|
+
emulated: entry.emulated
|
|
10246
|
+
});
|
|
10247
|
+
}
|
|
10248
|
+
for (const [mesh, entry] of this.meshes) {
|
|
10249
|
+
const transform = world.getComponent(entry.entityId, TransformComponent);
|
|
10250
|
+
snapshot.push({
|
|
10251
|
+
kind: "mesh",
|
|
10252
|
+
label: mesh.semanticLabel ?? null,
|
|
10253
|
+
position: transform ? { ...transform.position } : { x: 0, y: 0, z: 0 },
|
|
10254
|
+
radius: entry.geometry?.boundingSphere?.radius ?? 0,
|
|
10255
|
+
emulated: entry.emulated
|
|
10256
|
+
});
|
|
10257
|
+
}
|
|
10258
|
+
return snapshot;
|
|
10259
|
+
}
|
|
10260
|
+
/** Create the no-scan floor once; it lives in the ordinary plane map. */
|
|
10261
|
+
ensureSyntheticFloor(world) {
|
|
10262
|
+
if (this.planes.has(SYNTHETIC_FLOOR_PLANE)) return;
|
|
10263
|
+
const created = this.createPlaneEntity(world, SYNTHETIC_FLOOR_PLANE);
|
|
10264
|
+
if (created) this.planes.set(SYNTHETIC_FLOOR_PLANE, created);
|
|
10265
|
+
}
|
|
10266
|
+
/** Real scene data exists (or arrived) — the stand-in must go. */
|
|
10267
|
+
removeSyntheticFloor(world) {
|
|
10268
|
+
const entry = this.planes.get(SYNTHETIC_FLOOR_PLANE);
|
|
10269
|
+
if (!entry) return;
|
|
10270
|
+
this.destroyEntry(world, entry);
|
|
10271
|
+
this.planes.delete(SYNTHETIC_FLOOR_PLANE);
|
|
10272
|
+
}
|
|
9880
10273
|
findRig(world) {
|
|
9881
10274
|
const ids = world.query(XRRigComponent);
|
|
9882
10275
|
if (ids.length === 0) return null;
|
|
9883
10276
|
return world.getComponent(ids[0], XRRigComponent) ?? null;
|
|
9884
10277
|
}
|
|
9885
|
-
syncPlanes(world, frame, referenceSpace) {
|
|
10278
|
+
syncPlanes(world, frame, referenceSpace, deltaTime) {
|
|
9886
10279
|
const detected = frame.detectedPlanes;
|
|
9887
10280
|
if (!detected) return;
|
|
9888
10281
|
for (const [plane, entry] of this.planes) {
|
|
@@ -9891,14 +10284,22 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9891
10284
|
this.planes.delete(plane);
|
|
9892
10285
|
}
|
|
9893
10286
|
}
|
|
10287
|
+
for (const plane of this.pendingPlanes.keys()) {
|
|
10288
|
+
if (!detected.has(plane)) this.pendingPlanes.delete(plane);
|
|
10289
|
+
}
|
|
9894
10290
|
for (const plane of detected) {
|
|
9895
10291
|
const pose = frame.getPose(plane.planeSpace, referenceSpace);
|
|
9896
10292
|
if (!pose) continue;
|
|
10293
|
+
const emulated = pose.emulatedPosition === true;
|
|
9897
10294
|
let entry = this.planes.get(plane);
|
|
9898
10295
|
const changed = plane.lastChangedTime ?? 0;
|
|
9899
10296
|
if (!entry) {
|
|
10297
|
+
if (emulated) continue;
|
|
10298
|
+
const gate = this.admit(this.pendingPlanes, plane, pose, deltaTime);
|
|
10299
|
+
if (!gate) continue;
|
|
9900
10300
|
const created = this.createPlaneEntity(world, plane);
|
|
9901
10301
|
if (!created) continue;
|
|
10302
|
+
created.gate = gate;
|
|
9902
10303
|
entry = created;
|
|
9903
10304
|
this.planes.set(plane, entry);
|
|
9904
10305
|
} else if (changed !== entry.lastChangedTime) {
|
|
@@ -9909,14 +10310,21 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9909
10310
|
if (collider) {
|
|
9910
10311
|
entry.geometry?.dispose();
|
|
9911
10312
|
entry.geometry = null;
|
|
9912
|
-
this.attachPlaneDebug(
|
|
10313
|
+
this.attachPlaneDebug(
|
|
10314
|
+
world,
|
|
10315
|
+
entry,
|
|
10316
|
+
collider,
|
|
10317
|
+
plane.semanticLabel ?? null
|
|
10318
|
+
);
|
|
9913
10319
|
}
|
|
9914
10320
|
}
|
|
9915
10321
|
}
|
|
9916
|
-
this.
|
|
10322
|
+
const allowed = !emulated && this.gateAllows(entry.gate, pose, deltaTime);
|
|
10323
|
+
entry.emulated = emulated || entry.gate.frozen;
|
|
10324
|
+
if (allowed) this.applyPose(world, entry.entityId, pose);
|
|
9917
10325
|
}
|
|
9918
10326
|
}
|
|
9919
|
-
syncMeshes(world, frame, referenceSpace) {
|
|
10327
|
+
syncMeshes(world, frame, referenceSpace, deltaTime) {
|
|
9920
10328
|
const detected = frame.detectedMeshes;
|
|
9921
10329
|
if (!detected) return;
|
|
9922
10330
|
for (const [mesh, entry] of this.meshes) {
|
|
@@ -9925,22 +10333,67 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9925
10333
|
this.meshes.delete(mesh);
|
|
9926
10334
|
}
|
|
9927
10335
|
}
|
|
10336
|
+
for (const mesh of this.pendingMeshes.keys()) {
|
|
10337
|
+
if (!detected.has(mesh)) this.pendingMeshes.delete(mesh);
|
|
10338
|
+
}
|
|
9928
10339
|
for (const mesh of detected) {
|
|
9929
10340
|
const pose = frame.getPose(mesh.meshSpace, referenceSpace);
|
|
9930
10341
|
if (!pose) continue;
|
|
10342
|
+
const emulated = pose.emulatedPosition === true;
|
|
9931
10343
|
let entry = this.meshes.get(mesh);
|
|
9932
10344
|
const changed = mesh.lastChangedTime ?? 0;
|
|
9933
10345
|
if (!entry) {
|
|
10346
|
+
if (emulated) continue;
|
|
10347
|
+
const gate = this.admit(this.pendingMeshes, mesh, pose, deltaTime);
|
|
10348
|
+
if (!gate) continue;
|
|
9934
10349
|
const created = this.createMeshEntity(world, mesh);
|
|
9935
10350
|
if (!created) continue;
|
|
10351
|
+
created.gate = gate;
|
|
9936
10352
|
entry = created;
|
|
9937
10353
|
this.meshes.set(mesh, entry);
|
|
9938
10354
|
} else if (changed !== entry.lastChangedTime) {
|
|
9939
10355
|
entry.lastChangedTime = changed;
|
|
9940
10356
|
writeXrMeshGeometry(mesh.vertices, mesh.indices, entry.geometry);
|
|
9941
10357
|
}
|
|
9942
|
-
this.
|
|
10358
|
+
const allowed = !emulated && this.gateAllows(entry.gate, pose, deltaTime);
|
|
10359
|
+
entry.emulated = emulated || entry.gate.frozen;
|
|
10360
|
+
if (allowed) this.applyPose(world, entry.entityId, pose);
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
/**
|
|
10364
|
+
* Admission control for a NOT-yet-spawned surface. With a viewer pose
|
|
10365
|
+
* available, the surface must behave world-anchored for a few frames before
|
|
10366
|
+
* an entity is created for it; without one (older runtimes, tests) it is
|
|
10367
|
+
* admitted immediately — the gate can't classify anyway. Returns the
|
|
10368
|
+
* warmed-up gate (carried into the entry so its motion history survives),
|
|
10369
|
+
* or null while the surface must keep waiting.
|
|
10370
|
+
*/
|
|
10371
|
+
admit(pending, key, pose, deltaTime) {
|
|
10372
|
+
if (!this.viewerInverse) return new SurfacePoseGate();
|
|
10373
|
+
let gate = pending.get(key);
|
|
10374
|
+
if (!gate) {
|
|
10375
|
+
gate = new SurfacePoseGate();
|
|
10376
|
+
pending.set(key, gate);
|
|
10377
|
+
}
|
|
10378
|
+
const allowed = this.gateAllows(gate, pose, deltaTime);
|
|
10379
|
+
if (!allowed || !gate.warmedUp) return null;
|
|
10380
|
+
pending.delete(key);
|
|
10381
|
+
return gate;
|
|
10382
|
+
}
|
|
10383
|
+
/**
|
|
10384
|
+
* Run a surface's pose through its head-glue gate: position in reference
|
|
10385
|
+
* space vs position relative to the viewer. Without a viewer pose the gate
|
|
10386
|
+
* passes everything, degrading to the flag-only behavior.
|
|
10387
|
+
*/
|
|
10388
|
+
gateAllows(gate, pose, deltaTime) {
|
|
10389
|
+
const m = pose.transform.matrix;
|
|
10390
|
+
const refPos = { x: m[12], y: m[13], z: m[14] };
|
|
10391
|
+
let viewPos = null;
|
|
10392
|
+
if (this.viewerInverse) {
|
|
10393
|
+
const v = this.tmpViewPos.set(refPos.x, refPos.y, refPos.z).applyMatrix4(this.viewerInverse);
|
|
10394
|
+
viewPos = { x: v.x, y: v.y, z: v.z };
|
|
9943
10395
|
}
|
|
10396
|
+
return gate.update(refPos, viewPos, deltaTime);
|
|
9944
10397
|
}
|
|
9945
10398
|
createPlaneEntity(world, plane) {
|
|
9946
10399
|
const collider = planeColliderFromPolygon(plane.polygon);
|
|
@@ -9959,9 +10412,13 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9959
10412
|
entityId,
|
|
9960
10413
|
lastChangedTime: plane.lastChangedTime ?? 0,
|
|
9961
10414
|
geometry: null,
|
|
9962
|
-
material: null
|
|
10415
|
+
material: null,
|
|
10416
|
+
emulated: false,
|
|
10417
|
+
gate: new SurfacePoseGate()
|
|
9963
10418
|
};
|
|
9964
|
-
if (this.debugVisible)
|
|
10419
|
+
if (this.debugVisible) {
|
|
10420
|
+
this.attachPlaneDebug(world, entry, collider, plane.semanticLabel ?? null);
|
|
10421
|
+
}
|
|
9965
10422
|
return entry;
|
|
9966
10423
|
}
|
|
9967
10424
|
applyPlaneCollider(world, entityId, plane) {
|
|
@@ -9986,15 +10443,36 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
9986
10443
|
createMeshEntity(world, mesh) {
|
|
9987
10444
|
if (!mesh.vertices?.length || !mesh.indices?.length) return null;
|
|
9988
10445
|
const geometry = writeXrMeshGeometry(mesh.vertices, mesh.indices);
|
|
10446
|
+
const label = mesh.semanticLabel ?? null;
|
|
10447
|
+
const isGlobalMesh = label === "global mesh";
|
|
9989
10448
|
const material = new THREE26__namespace.MeshBasicMaterial({
|
|
9990
|
-
color:
|
|
9991
|
-
wireframe: true
|
|
10449
|
+
color: debugColorFor("mesh", label),
|
|
10450
|
+
wireframe: true,
|
|
10451
|
+
// The global scan is a coarse triangle soup of the whole room; at full
|
|
10452
|
+
// strength it reads as phantom objects rather than as context.
|
|
10453
|
+
transparent: isGlobalMesh,
|
|
10454
|
+
opacity: isGlobalMesh ? GLOBAL_MESH_DEBUG_OPACITY : 1,
|
|
10455
|
+
// Depth-TESTED, deliberately. The overlay was X-ray for one release —
|
|
10456
|
+
// introduced on the theory that depth occlusion was carving it into a
|
|
10457
|
+
// head-locked dome — but that theory was falsified (the dome was the
|
|
10458
|
+
// depth mesh's unset colour output, plus emulated poses; both fixed).
|
|
10459
|
+
// X-ray had its own on-device failure: the global scan is DENSE, and a
|
|
10460
|
+
// dense wireframe at 0.35 opacity fuses into a translucent sheet — with
|
|
10461
|
+
// no depth test the walls' sheets drew through everything, stacked in
|
|
10462
|
+
// front of the user's eyes ("planes covering my face"). With the depth
|
|
10463
|
+
// buffer now holding CORRECT real-world depth, normal testing gives the
|
|
10464
|
+
// right picture: the near wall hides the far one. depthWrite stays off
|
|
10465
|
+
// so the overlay never occludes actual content.
|
|
10466
|
+
depthTest: true,
|
|
10467
|
+
depthWrite: false
|
|
9992
10468
|
});
|
|
9993
10469
|
const root = new THREE26__namespace.Group();
|
|
9994
10470
|
const child = new THREE26__namespace.Mesh(geometry, material);
|
|
9995
10471
|
child.visible = this.debugVisible;
|
|
10472
|
+
child.renderOrder = DEBUG_RENDER_ORDER;
|
|
9996
10473
|
root.add(child);
|
|
9997
10474
|
const entityId = world.createEntity();
|
|
10475
|
+
root.userData.entityId = entityId;
|
|
9998
10476
|
world.addComponents(entityId, [
|
|
9999
10477
|
new TransformComponent(),
|
|
10000
10478
|
new Object3DRef(root),
|
|
@@ -10008,29 +10486,38 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
10008
10486
|
entityId,
|
|
10009
10487
|
lastChangedTime: mesh.lastChangedTime ?? 0,
|
|
10010
10488
|
geometry,
|
|
10011
|
-
material
|
|
10489
|
+
material,
|
|
10490
|
+
emulated: false,
|
|
10491
|
+
gate: new SurfacePoseGate()
|
|
10012
10492
|
};
|
|
10013
10493
|
}
|
|
10014
10494
|
/** A thin box so a detected plane can be SEEN, not just collided with. */
|
|
10015
|
-
attachPlaneDebug(world, entry, collider) {
|
|
10016
|
-
const
|
|
10495
|
+
attachPlaneDebug(world, entry, collider, semanticLabel) {
|
|
10496
|
+
const box = new THREE26__namespace.BoxGeometry(
|
|
10017
10497
|
collider.size.x,
|
|
10018
10498
|
collider.size.y,
|
|
10019
10499
|
collider.size.z
|
|
10020
10500
|
);
|
|
10021
|
-
const
|
|
10022
|
-
|
|
10023
|
-
|
|
10501
|
+
const geometry = new THREE26__namespace.EdgesGeometry(box);
|
|
10502
|
+
box.dispose();
|
|
10503
|
+
const material = new THREE26__namespace.LineBasicMaterial({
|
|
10504
|
+
color: debugColorFor("plane", semanticLabel),
|
|
10505
|
+
// Same depth policy as the mesh overlay — see createMeshEntity.
|
|
10506
|
+
depthTest: true,
|
|
10507
|
+
depthWrite: false
|
|
10024
10508
|
});
|
|
10025
10509
|
const root = new THREE26__namespace.Group();
|
|
10026
|
-
const child = new THREE26__namespace.
|
|
10510
|
+
const child = new THREE26__namespace.LineSegments(geometry, material);
|
|
10511
|
+
child.renderOrder = DEBUG_RENDER_ORDER;
|
|
10027
10512
|
child.position.set(collider.offset.x, collider.offset.y, collider.offset.z);
|
|
10028
10513
|
child.visible = this.debugVisible;
|
|
10029
10514
|
root.add(child);
|
|
10030
10515
|
entry.geometry = geometry;
|
|
10031
10516
|
entry.material = material;
|
|
10517
|
+
root.userData.entityId = entry.entityId;
|
|
10032
10518
|
const existing = world.getComponent(entry.entityId, Object3DRef);
|
|
10033
10519
|
if (existing) {
|
|
10520
|
+
existing.object.removeFromParent();
|
|
10034
10521
|
existing.object = root;
|
|
10035
10522
|
} else {
|
|
10036
10523
|
world.addComponent(entry.entityId, new Object3DRef(root));
|
|
@@ -10046,19 +10533,31 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
10046
10533
|
for (const [plane, entry] of this.planes) {
|
|
10047
10534
|
if (entry.geometry) continue;
|
|
10048
10535
|
const collider = planeColliderFromPolygon(plane.polygon);
|
|
10049
|
-
if (collider)
|
|
10536
|
+
if (collider) {
|
|
10537
|
+
this.attachPlaneDebug(
|
|
10538
|
+
world,
|
|
10539
|
+
entry,
|
|
10540
|
+
collider,
|
|
10541
|
+
plane.semanticLabel ?? null
|
|
10542
|
+
);
|
|
10543
|
+
}
|
|
10050
10544
|
}
|
|
10051
10545
|
}
|
|
10052
10546
|
for (const map of [this.planes, this.meshes]) {
|
|
10053
10547
|
for (const entry of map.values()) {
|
|
10054
10548
|
const ref = world.getComponent(entry.entityId, Object3DRef);
|
|
10055
10549
|
ref?.object.traverse((child) => {
|
|
10056
|
-
|
|
10550
|
+
const drawable = child;
|
|
10551
|
+
if (drawable.isMesh || drawable.isLine) {
|
|
10552
|
+
child.visible = this.debugVisible;
|
|
10553
|
+
}
|
|
10057
10554
|
});
|
|
10058
10555
|
}
|
|
10059
10556
|
}
|
|
10060
10557
|
}
|
|
10061
10558
|
destroyEntry(world, entry) {
|
|
10559
|
+
const ref = world.getComponent(entry.entityId, Object3DRef);
|
|
10560
|
+
ref?.object.removeFromParent();
|
|
10062
10561
|
entry.geometry?.dispose();
|
|
10063
10562
|
entry.material?.dispose();
|
|
10064
10563
|
world.destroyEntity(entry.entityId);
|
|
@@ -10070,6 +10569,8 @@ var _XREnvironmentSystem = class _XREnvironmentSystem {
|
|
|
10070
10569
|
clearAll(world) {
|
|
10071
10570
|
this.clearMap(world, this.planes);
|
|
10072
10571
|
this.clearMap(world, this.meshes);
|
|
10572
|
+
this.pendingPlanes.clear();
|
|
10573
|
+
this.pendingMeshes.clear();
|
|
10073
10574
|
}
|
|
10074
10575
|
dispose(world) {
|
|
10075
10576
|
this.clearAll(world);
|
|
@@ -10081,6 +10582,918 @@ function normalizeOrientation(value) {
|
|
|
10081
10582
|
return value === "horizontal" || value === "vertical" ? value : null;
|
|
10082
10583
|
}
|
|
10083
10584
|
__name(normalizeOrientation, "normalizeOrientation");
|
|
10585
|
+
var MOUSE_CLICK_TRAVEL_PX = 6;
|
|
10586
|
+
var PRESS_TINT_MIN_SECONDS = 0.1;
|
|
10587
|
+
var PRESS_TINT = 0.72;
|
|
10588
|
+
var HOVER_TINT = 0.9;
|
|
10589
|
+
var _UIInteractionSystem = class _UIInteractionSystem {
|
|
10590
|
+
/**
|
|
10591
|
+
* @param interaction The XR interaction system, or null in a world with no
|
|
10592
|
+
* XR (flat 3D experiences) — the mouse path still works via
|
|
10593
|
+
* `options.inputSystem`.
|
|
10594
|
+
*/
|
|
10595
|
+
constructor(interaction, options) {
|
|
10596
|
+
this.xrPresses = /* @__PURE__ */ new Map();
|
|
10597
|
+
this.mousePress = null;
|
|
10598
|
+
this.mouseHover = null;
|
|
10599
|
+
/** entityId → seconds of pressed-tint left to display. */
|
|
10600
|
+
this.tintFloors = /* @__PURE__ */ new Map();
|
|
10601
|
+
this.tinted = /* @__PURE__ */ new Set();
|
|
10602
|
+
this.raycaster = new THREE26__namespace.Raycaster();
|
|
10603
|
+
this.interaction = interaction;
|
|
10604
|
+
this.inputSystem = options?.inputSystem ?? null;
|
|
10605
|
+
interaction?.registerBusyContributor(
|
|
10606
|
+
(_world, input) => this.xrPresses.has(input)
|
|
10607
|
+
);
|
|
10608
|
+
}
|
|
10609
|
+
update(world, deltaTime = 0) {
|
|
10610
|
+
if (world.isEditorMode()) return;
|
|
10611
|
+
if (world.isXrPresenting()) {
|
|
10612
|
+
this.updateXr(world);
|
|
10613
|
+
} else {
|
|
10614
|
+
this.updateMouse(world);
|
|
10615
|
+
}
|
|
10616
|
+
this.updateTints(world, deltaTime);
|
|
10617
|
+
}
|
|
10618
|
+
// ── XR path ────────────────────────────────────────────────────────────────
|
|
10619
|
+
updateXr(world) {
|
|
10620
|
+
const interaction = this.interaction;
|
|
10621
|
+
if (!interaction) return;
|
|
10622
|
+
const xrInput = interaction.inputSystem;
|
|
10623
|
+
const inputs = xrInput.getInputs();
|
|
10624
|
+
for (const input of inputs) {
|
|
10625
|
+
const hover = interaction.getHoverForInput(input);
|
|
10626
|
+
const hovered = hover ? this.widgetKind(world, hover.entityId) : null;
|
|
10627
|
+
if (input.selectStart && hover && hovered) {
|
|
10628
|
+
this.xrPresses.set(input, {
|
|
10629
|
+
entityId: hover.entityId,
|
|
10630
|
+
kind: hovered,
|
|
10631
|
+
travelPx: 0
|
|
10632
|
+
});
|
|
10633
|
+
this.beginPressTint(world, hover.entityId);
|
|
10634
|
+
}
|
|
10635
|
+
const press = this.xrPresses.get(input);
|
|
10636
|
+
if (press && press.kind === "slider") {
|
|
10637
|
+
this.dragSlider(world, press.entityId, input);
|
|
10638
|
+
}
|
|
10639
|
+
if (input.selectEnd || !input.connected) {
|
|
10640
|
+
if (press) {
|
|
10641
|
+
this.xrPresses.delete(input);
|
|
10642
|
+
const stillOn = hover?.entityId === press.entityId || // Vision Pro: the transient source is gone from the hover map on
|
|
10643
|
+
// its release frame — confirm against the last latched ray.
|
|
10644
|
+
this.rayHitsWidget(world, press.entityId, input);
|
|
10645
|
+
if (stillOn || press.kind === "slider") {
|
|
10646
|
+
this.fire(world, press, input);
|
|
10647
|
+
}
|
|
10648
|
+
this.endPressTint(press.entityId);
|
|
10649
|
+
}
|
|
10650
|
+
}
|
|
10651
|
+
}
|
|
10652
|
+
for (const [input] of this.xrPresses) {
|
|
10653
|
+
if (!inputs.includes(input)) this.xrPresses.delete(input);
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
// ── Mouse path (flat preview) ──────────────────────────────────────────────
|
|
10657
|
+
updateMouse(world) {
|
|
10658
|
+
const input = this.inputSystem;
|
|
10659
|
+
if (!input) return;
|
|
10660
|
+
const hit = this.raycastMouse(world);
|
|
10661
|
+
const hovered = hit && this.widgetKind(world, hit) ? hit : null;
|
|
10662
|
+
if (hovered !== this.mouseHover) {
|
|
10663
|
+
if (this.mouseHover !== null) {
|
|
10664
|
+
this.setTint(world, this.mouseHover, 1);
|
|
10665
|
+
this.dispatchBubbling(
|
|
10666
|
+
world,
|
|
10667
|
+
this.mouseHover,
|
|
10668
|
+
(b, e) => b.onUiHoverEnd(e)
|
|
10669
|
+
);
|
|
10670
|
+
}
|
|
10671
|
+
if (hovered !== null) {
|
|
10672
|
+
this.setTint(world, hovered, HOVER_TINT);
|
|
10673
|
+
this.dispatchBubbling(world, hovered, (b, e) => b.onUiHoverStart(e));
|
|
10674
|
+
}
|
|
10675
|
+
this.mouseHover = hovered;
|
|
10676
|
+
}
|
|
10677
|
+
if (input.getMouseDown("left") && hovered) {
|
|
10678
|
+
const kind = this.widgetKind(world, hovered);
|
|
10679
|
+
if (kind) {
|
|
10680
|
+
this.mousePress = { entityId: hovered, kind, travelPx: 0 };
|
|
10681
|
+
this.beginPressTint(world, hovered);
|
|
10682
|
+
}
|
|
10683
|
+
}
|
|
10684
|
+
if (this.mousePress) {
|
|
10685
|
+
const delta = input.getMouseDelta();
|
|
10686
|
+
this.mousePress.travelPx += Math.hypot(delta.x, delta.y);
|
|
10687
|
+
if (this.mousePress.kind === "slider") {
|
|
10688
|
+
this.dragSliderMouse(world, this.mousePress.entityId);
|
|
10689
|
+
}
|
|
10690
|
+
}
|
|
10691
|
+
if (input.getMouseUp("left") && this.mousePress) {
|
|
10692
|
+
const press = this.mousePress;
|
|
10693
|
+
this.mousePress = null;
|
|
10694
|
+
const stillOn = hovered === press.entityId;
|
|
10695
|
+
const withinTravel = press.travelPx <= MOUSE_CLICK_TRAVEL_PX;
|
|
10696
|
+
if (press.kind === "slider" || stillOn && withinTravel) {
|
|
10697
|
+
this.fire(world, press, null);
|
|
10698
|
+
}
|
|
10699
|
+
this.endPressTint(press.entityId);
|
|
10700
|
+
}
|
|
10701
|
+
}
|
|
10702
|
+
raycastMouse(world) {
|
|
10703
|
+
const input = this.inputSystem;
|
|
10704
|
+
if (!input) return null;
|
|
10705
|
+
const dom = world.getRenderer().domElement;
|
|
10706
|
+
const rect = dom.getBoundingClientRect();
|
|
10707
|
+
let ndcX = 0;
|
|
10708
|
+
let ndcY = 0;
|
|
10709
|
+
if (input.isMousePointerLocked()) {
|
|
10710
|
+
ndcX = 0;
|
|
10711
|
+
ndcY = 0;
|
|
10712
|
+
} else {
|
|
10713
|
+
const pos = input.getMousePosition();
|
|
10714
|
+
ndcX = (pos.x - rect.left) / Math.max(1, rect.width) * 2 - 1;
|
|
10715
|
+
ndcY = -((pos.y - rect.top) / Math.max(1, rect.height) * 2 - 1);
|
|
10716
|
+
if (ndcX < -1 || ndcX > 1 || ndcY < -1 || ndcY > 1) return null;
|
|
10717
|
+
}
|
|
10718
|
+
this.raycaster.setFromCamera(new THREE26__namespace.Vector2(ndcX, ndcY), world.getCamera());
|
|
10719
|
+
const hits = world.raycast(
|
|
10720
|
+
this.raycaster.ray.origin,
|
|
10721
|
+
this.raycaster.ray.direction
|
|
10722
|
+
);
|
|
10723
|
+
for (const hit of hits) {
|
|
10724
|
+
if (typeof hit.entityId !== "number") continue;
|
|
10725
|
+
if (this.widgetKind(world, hit.entityId)) return hit.entityId;
|
|
10726
|
+
return null;
|
|
10727
|
+
}
|
|
10728
|
+
return null;
|
|
10729
|
+
}
|
|
10730
|
+
// ── Shared machinery ───────────────────────────────────────────────────────
|
|
10731
|
+
widgetKind(world, entityId) {
|
|
10732
|
+
if (world.getComponent(entityId, UIButtonComponent)) return "button";
|
|
10733
|
+
if (world.getComponent(entityId, UIToggleComponent)) return "toggle";
|
|
10734
|
+
if (world.getComponent(entityId, UISliderComponent)) return "slider";
|
|
10735
|
+
return null;
|
|
10736
|
+
}
|
|
10737
|
+
fire(world, press, input) {
|
|
10738
|
+
if (press.kind === "button") {
|
|
10739
|
+
this.dispatchBubbling(world, press.entityId, (b, e) => b.onUiPress(e), input);
|
|
10740
|
+
return;
|
|
10741
|
+
}
|
|
10742
|
+
if (press.kind === "toggle") {
|
|
10743
|
+
const toggle = world.getComponent(press.entityId, UIToggleComponent);
|
|
10744
|
+
if (!toggle) return;
|
|
10745
|
+
toggle.value = !toggle.value;
|
|
10746
|
+
this.dispatchBubbling(
|
|
10747
|
+
world,
|
|
10748
|
+
press.entityId,
|
|
10749
|
+
(b, e) => b.onUiToggle(e),
|
|
10750
|
+
input,
|
|
10751
|
+
toggle.value
|
|
10752
|
+
);
|
|
10753
|
+
return;
|
|
10754
|
+
}
|
|
10755
|
+
const slider = world.getComponent(press.entityId, UISliderComponent);
|
|
10756
|
+
if (!slider) return;
|
|
10757
|
+
this.dispatchBubbling(
|
|
10758
|
+
world,
|
|
10759
|
+
press.entityId,
|
|
10760
|
+
(b, e) => b.onUiChange(e),
|
|
10761
|
+
input,
|
|
10762
|
+
slider.value
|
|
10763
|
+
);
|
|
10764
|
+
}
|
|
10765
|
+
dragSlider(world, entityId, input) {
|
|
10766
|
+
const point = this.rayPointOnWidget(world, entityId, input);
|
|
10767
|
+
if (point) this.applySliderPoint(world, entityId, point, input);
|
|
10768
|
+
}
|
|
10769
|
+
dragSliderMouse(world, entityId) {
|
|
10770
|
+
const object = this.widgetObject(world, entityId);
|
|
10771
|
+
if (!object) return;
|
|
10772
|
+
const hits = this.raycaster.intersectObject(object, true);
|
|
10773
|
+
if (hits.length > 0) this.applySliderPoint(world, entityId, hits[0].point, null);
|
|
10774
|
+
}
|
|
10775
|
+
applySliderPoint(world, entityId, worldPoint, input) {
|
|
10776
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
10777
|
+
const object = this.widgetObject(world, entityId);
|
|
10778
|
+
if (!slider || !object) return;
|
|
10779
|
+
const local = object.worldToLocal(worldPoint.clone());
|
|
10780
|
+
const inset = slider.height / 2;
|
|
10781
|
+
const usable = Math.max(1e-3, slider.width - inset * 2);
|
|
10782
|
+
const t = Math.min(1, Math.max(0, local.x / usable + 0.5));
|
|
10783
|
+
const next = slider.min + t * (slider.max - slider.min);
|
|
10784
|
+
if (Math.abs(next - slider.value) < 1e-4) return;
|
|
10785
|
+
slider.value = next;
|
|
10786
|
+
this.dispatchBubbling(
|
|
10787
|
+
world,
|
|
10788
|
+
entityId,
|
|
10789
|
+
(b, e) => b.onUiChange(e),
|
|
10790
|
+
input,
|
|
10791
|
+
next
|
|
10792
|
+
);
|
|
10793
|
+
}
|
|
10794
|
+
widgetObject(world, entityId) {
|
|
10795
|
+
return world.getComponent(entityId, Object3DRef)?.object;
|
|
10796
|
+
}
|
|
10797
|
+
rayHitsWidget(world, entityId, input) {
|
|
10798
|
+
const object = this.widgetObject(world, entityId);
|
|
10799
|
+
if (!object) return false;
|
|
10800
|
+
this.raycaster.set(
|
|
10801
|
+
input.rayOrigin.clone(),
|
|
10802
|
+
input.rayDirection.clone().normalize()
|
|
10803
|
+
);
|
|
10804
|
+
return this.raycaster.intersectObject(object, true).length > 0;
|
|
10805
|
+
}
|
|
10806
|
+
rayPointOnWidget(world, entityId, input) {
|
|
10807
|
+
const object = this.widgetObject(world, entityId);
|
|
10808
|
+
if (!object) return null;
|
|
10809
|
+
this.raycaster.set(
|
|
10810
|
+
input.rayOrigin.clone(),
|
|
10811
|
+
input.rayDirection.clone().normalize()
|
|
10812
|
+
);
|
|
10813
|
+
const hits = this.raycaster.intersectObject(object, true);
|
|
10814
|
+
return hits.length > 0 ? hits[0].point : null;
|
|
10815
|
+
}
|
|
10816
|
+
/**
|
|
10817
|
+
* Dispatch to the widget's Behaviours, then BUBBLE up the parent chain so a
|
|
10818
|
+
* panel script hears its children. Bubbling (not a world broadcast) keeps
|
|
10819
|
+
* two spawned copies of the same menu prefab independent.
|
|
10820
|
+
*/
|
|
10821
|
+
dispatchBubbling(world, entityId, visit, input = null, value) {
|
|
10822
|
+
const event = {
|
|
10823
|
+
targetEntityId: entityId,
|
|
10824
|
+
actionId: this.actionIdOf(world, entityId),
|
|
10825
|
+
input,
|
|
10826
|
+
...value === void 0 ? {} : { value }
|
|
10827
|
+
};
|
|
10828
|
+
const visited = /* @__PURE__ */ new Set();
|
|
10829
|
+
let current = entityId;
|
|
10830
|
+
for (; ; ) {
|
|
10831
|
+
if (visited.has(current)) break;
|
|
10832
|
+
visited.add(current);
|
|
10833
|
+
const script = world.getComponent(current, ScriptComponent);
|
|
10834
|
+
const instances = script?.scriptInstances;
|
|
10835
|
+
if (instances) {
|
|
10836
|
+
for (const instance of instances.values()) {
|
|
10837
|
+
if (!(instance instanceof Behaviour)) continue;
|
|
10838
|
+
try {
|
|
10839
|
+
visit(instance, event);
|
|
10840
|
+
} catch (error) {
|
|
10841
|
+
console.error("[UIInteractionSystem] hook failed:", error);
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
const parentId = world.getComponent(current, ParentId)?.value;
|
|
10846
|
+
if (typeof parentId !== "number") break;
|
|
10847
|
+
current = parentId;
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
actionIdOf(world, entityId) {
|
|
10851
|
+
const button = world.getComponent(entityId, UIButtonComponent);
|
|
10852
|
+
if (button?.actionId) return button.actionId;
|
|
10853
|
+
const toggle = world.getComponent(entityId, UIToggleComponent);
|
|
10854
|
+
if (toggle?.actionId) return toggle.actionId;
|
|
10855
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
10856
|
+
if (slider?.actionId) return slider.actionId;
|
|
10857
|
+
const name = world.getComponent(entityId, Name)?.value;
|
|
10858
|
+
return typeof name === "string" ? name : String(entityId);
|
|
10859
|
+
}
|
|
10860
|
+
// ── Pressed/hover tint ─────────────────────────────────────────────────────
|
|
10861
|
+
beginPressTint(world, entityId) {
|
|
10862
|
+
this.setTint(world, entityId, PRESS_TINT);
|
|
10863
|
+
this.tintFloors.set(entityId, PRESS_TINT_MIN_SECONDS);
|
|
10864
|
+
}
|
|
10865
|
+
endPressTint(entityId) {
|
|
10866
|
+
if (!this.tintFloors.has(entityId)) {
|
|
10867
|
+
this.tintFloors.set(entityId, 0);
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
updateTints(world, deltaTime) {
|
|
10871
|
+
for (const [entityId, remaining] of this.tintFloors) {
|
|
10872
|
+
const next = remaining - deltaTime;
|
|
10873
|
+
const stillHeld = [...this.xrPresses.values()].some((p) => p.entityId === entityId) || this.mousePress?.entityId === entityId;
|
|
10874
|
+
if (stillHeld) continue;
|
|
10875
|
+
if (next <= 0) {
|
|
10876
|
+
this.tintFloors.delete(entityId);
|
|
10877
|
+
this.setTint(
|
|
10878
|
+
world,
|
|
10879
|
+
entityId,
|
|
10880
|
+
this.mouseHover === entityId ? HOVER_TINT : 1
|
|
10881
|
+
);
|
|
10882
|
+
} else {
|
|
10883
|
+
this.tintFloors.set(entityId, next);
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
setTint(world, entityId, scalar) {
|
|
10888
|
+
const object = this.widgetObject(world, entityId);
|
|
10889
|
+
if (!object) return;
|
|
10890
|
+
if (scalar === 1) this.tinted.delete(entityId);
|
|
10891
|
+
else this.tinted.add(entityId);
|
|
10892
|
+
object.traverse((child) => {
|
|
10893
|
+
const material = child.material;
|
|
10894
|
+
if (material?.color) material.color.setScalar(scalar);
|
|
10895
|
+
});
|
|
10896
|
+
}
|
|
10897
|
+
};
|
|
10898
|
+
__name(_UIInteractionSystem, "UIInteractionSystem");
|
|
10899
|
+
var UIInteractionSystem = _UIInteractionSystem;
|
|
10900
|
+
|
|
10901
|
+
// core/ui/uiCanvas.ts
|
|
10902
|
+
var DEFAULT_PIXELS_PER_METER = 1024;
|
|
10903
|
+
var MIN_PIXELS_PER_METER = 256;
|
|
10904
|
+
var MAX_PIXELS_PER_METER = 2048;
|
|
10905
|
+
var MAX_CANVAS_SIDE = 2048;
|
|
10906
|
+
function resolveCanvasSize(widthMeters, heightMeters, pixelDensity = DEFAULT_PIXELS_PER_METER) {
|
|
10907
|
+
const w = Math.max(0.01, widthMeters);
|
|
10908
|
+
const h = Math.max(0.01, heightMeters);
|
|
10909
|
+
const authored = Math.min(
|
|
10910
|
+
MAX_PIXELS_PER_METER,
|
|
10911
|
+
Math.max(MIN_PIXELS_PER_METER, pixelDensity)
|
|
10912
|
+
);
|
|
10913
|
+
const density = Math.min(authored, MAX_CANVAS_SIDE / Math.max(w, h));
|
|
10914
|
+
const even = /* @__PURE__ */ __name((v) => Math.max(2, 2 * Math.round(v * density / 2)), "even");
|
|
10915
|
+
return { pxWidth: even(w), pxHeight: even(h), density };
|
|
10916
|
+
}
|
|
10917
|
+
__name(resolveCanvasSize, "resolveCanvasSize");
|
|
10918
|
+
function wrapText(measure, text, maxWidthPx) {
|
|
10919
|
+
const lines = [];
|
|
10920
|
+
for (const paragraph of text.split("\n")) {
|
|
10921
|
+
const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
|
|
10922
|
+
if (words.length === 0) {
|
|
10923
|
+
lines.push("");
|
|
10924
|
+
continue;
|
|
10925
|
+
}
|
|
10926
|
+
let line = "";
|
|
10927
|
+
for (const word of words) {
|
|
10928
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
10929
|
+
if (measure(candidate) <= maxWidthPx) {
|
|
10930
|
+
line = candidate;
|
|
10931
|
+
continue;
|
|
10932
|
+
}
|
|
10933
|
+
if (line) {
|
|
10934
|
+
lines.push(line);
|
|
10935
|
+
line = "";
|
|
10936
|
+
}
|
|
10937
|
+
if (measure(word) <= maxWidthPx) {
|
|
10938
|
+
line = word;
|
|
10939
|
+
continue;
|
|
10940
|
+
}
|
|
10941
|
+
let chunk = "";
|
|
10942
|
+
for (const char of word) {
|
|
10943
|
+
const next = chunk + char;
|
|
10944
|
+
if (measure(next) <= maxWidthPx || chunk.length === 0) {
|
|
10945
|
+
chunk = next;
|
|
10946
|
+
} else {
|
|
10947
|
+
lines.push(chunk);
|
|
10948
|
+
chunk = char;
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
line = chunk;
|
|
10952
|
+
}
|
|
10953
|
+
lines.push(line);
|
|
10954
|
+
}
|
|
10955
|
+
return lines;
|
|
10956
|
+
}
|
|
10957
|
+
__name(wrapText, "wrapText");
|
|
10958
|
+
var ELLIPSIS = "\u2026";
|
|
10959
|
+
function clampLines(measure, lines, maxLines, maxWidthPx) {
|
|
10960
|
+
if (maxLines <= 0) return [];
|
|
10961
|
+
if (lines.length <= maxLines) return lines;
|
|
10962
|
+
const kept = lines.slice(0, maxLines);
|
|
10963
|
+
let last = kept[maxLines - 1];
|
|
10964
|
+
while (last.length > 0 && measure(last + ELLIPSIS) > maxWidthPx) {
|
|
10965
|
+
last = last.slice(0, -1);
|
|
10966
|
+
}
|
|
10967
|
+
kept[maxLines - 1] = last + ELLIPSIS;
|
|
10968
|
+
return kept;
|
|
10969
|
+
}
|
|
10970
|
+
__name(clampLines, "clampLines");
|
|
10971
|
+
var UI_FONT_STACK = '-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
|
10972
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
10973
|
+
const radius = Math.max(0, Math.min(r, w / 2, h / 2));
|
|
10974
|
+
ctx.beginPath();
|
|
10975
|
+
if (typeof ctx.roundRect === "function") {
|
|
10976
|
+
ctx.roundRect(x, y, w, h, radius);
|
|
10977
|
+
return;
|
|
10978
|
+
}
|
|
10979
|
+
ctx.moveTo(x + radius, y);
|
|
10980
|
+
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
|
10981
|
+
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
|
10982
|
+
ctx.arcTo(x, y + h, x, y, radius);
|
|
10983
|
+
ctx.arcTo(x, y, x + w, y, radius);
|
|
10984
|
+
ctx.closePath();
|
|
10985
|
+
}
|
|
10986
|
+
__name(tracePath, "tracePath");
|
|
10987
|
+
function withOpacity(ctx, color, opacity) {
|
|
10988
|
+
if (opacity >= 1) return color;
|
|
10989
|
+
ctx.save();
|
|
10990
|
+
ctx.fillStyle = color;
|
|
10991
|
+
const parsed = ctx.fillStyle;
|
|
10992
|
+
ctx.restore();
|
|
10993
|
+
if (parsed.startsWith("#") && (parsed.length === 7 || parsed.length === 4)) {
|
|
10994
|
+
const hex = parsed.length === 4 ? `#${parsed[1]}${parsed[1]}${parsed[2]}${parsed[2]}${parsed[3]}${parsed[3]}` : parsed;
|
|
10995
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
10996
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
10997
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
10998
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
10999
|
+
}
|
|
11000
|
+
return parsed;
|
|
11001
|
+
}
|
|
11002
|
+
__name(withOpacity, "withOpacity");
|
|
11003
|
+
function drawChrome(ctx, pxWidth, pxHeight, density, style) {
|
|
11004
|
+
ctx.clearRect(0, 0, pxWidth, pxHeight);
|
|
11005
|
+
const border = Math.max(0, style.borderWidthMeters * density);
|
|
11006
|
+
const radius = style.cornerRadiusMeters * density;
|
|
11007
|
+
tracePath(ctx, 0, 0, pxWidth, pxHeight, radius);
|
|
11008
|
+
ctx.fillStyle = withOpacity(
|
|
11009
|
+
ctx,
|
|
11010
|
+
style.backgroundColor,
|
|
11011
|
+
Math.max(0, Math.min(1, style.backgroundOpacity))
|
|
11012
|
+
);
|
|
11013
|
+
ctx.fill();
|
|
11014
|
+
if (border > 0) {
|
|
11015
|
+
tracePath(
|
|
11016
|
+
ctx,
|
|
11017
|
+
border / 2,
|
|
11018
|
+
border / 2,
|
|
11019
|
+
pxWidth - border,
|
|
11020
|
+
pxHeight - border,
|
|
11021
|
+
Math.max(0, radius - border / 2)
|
|
11022
|
+
);
|
|
11023
|
+
ctx.strokeStyle = style.borderColor;
|
|
11024
|
+
ctx.lineWidth = border;
|
|
11025
|
+
ctx.stroke();
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
__name(drawChrome, "drawChrome");
|
|
11029
|
+
function drawTextBlock(ctx, pxWidth, pxHeight, density, text, style) {
|
|
11030
|
+
const fontPx = Math.max(4, style.fontSizeMeters * density);
|
|
11031
|
+
const paddingPx = style.paddingMeters * density;
|
|
11032
|
+
const contentW = Math.max(1, pxWidth - paddingPx * 2);
|
|
11033
|
+
const contentH = Math.max(1, pxHeight - paddingPx * 2);
|
|
11034
|
+
const lineHeightPx = fontPx * style.lineHeight;
|
|
11035
|
+
ctx.font = `${style.fontWeight} ${fontPx}px ${UI_FONT_STACK}`;
|
|
11036
|
+
const measure = /* @__PURE__ */ __name((s) => ctx.measureText(s).width, "measure");
|
|
11037
|
+
const maxLines = Math.max(1, Math.floor(contentH / lineHeightPx));
|
|
11038
|
+
const lines = clampLines(
|
|
11039
|
+
measure,
|
|
11040
|
+
wrapText(measure, text, contentW),
|
|
11041
|
+
maxLines,
|
|
11042
|
+
contentW
|
|
11043
|
+
);
|
|
11044
|
+
const blockH = lines.length * lineHeightPx;
|
|
11045
|
+
const top = style.valign === "top" ? paddingPx : style.valign === "bottom" ? pxHeight - paddingPx - blockH : (pxHeight - blockH) / 2;
|
|
11046
|
+
const anchorX = style.align === "left" ? paddingPx : style.align === "right" ? pxWidth - paddingPx : pxWidth / 2;
|
|
11047
|
+
ctx.save();
|
|
11048
|
+
tracePath(ctx, 0, 0, pxWidth, pxHeight, 0);
|
|
11049
|
+
ctx.clip();
|
|
11050
|
+
ctx.fillStyle = style.color;
|
|
11051
|
+
ctx.textAlign = style.align;
|
|
11052
|
+
ctx.textBaseline = "middle";
|
|
11053
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
11054
|
+
ctx.fillText(lines[i], anchorX, top + (i + 0.5) * lineHeightPx);
|
|
11055
|
+
}
|
|
11056
|
+
ctx.restore();
|
|
11057
|
+
}
|
|
11058
|
+
__name(drawTextBlock, "drawTextBlock");
|
|
11059
|
+
function panelSignature(widthM, heightM, size, style) {
|
|
11060
|
+
return JSON.stringify([
|
|
11061
|
+
widthM,
|
|
11062
|
+
heightM,
|
|
11063
|
+
size.pxWidth,
|
|
11064
|
+
size.pxHeight,
|
|
11065
|
+
style.backgroundColor,
|
|
11066
|
+
style.backgroundOpacity,
|
|
11067
|
+
style.cornerRadiusMeters,
|
|
11068
|
+
style.borderWidthMeters,
|
|
11069
|
+
style.borderColor
|
|
11070
|
+
]);
|
|
11071
|
+
}
|
|
11072
|
+
__name(panelSignature, "panelSignature");
|
|
11073
|
+
function textSignature(widthM, heightM, size, text, style) {
|
|
11074
|
+
return JSON.stringify([
|
|
11075
|
+
widthM,
|
|
11076
|
+
heightM,
|
|
11077
|
+
size.pxWidth,
|
|
11078
|
+
size.pxHeight,
|
|
11079
|
+
text,
|
|
11080
|
+
style.fontSizeMeters,
|
|
11081
|
+
style.fontWeight,
|
|
11082
|
+
style.color,
|
|
11083
|
+
style.align,
|
|
11084
|
+
style.valign,
|
|
11085
|
+
style.lineHeight,
|
|
11086
|
+
style.paddingMeters
|
|
11087
|
+
]);
|
|
11088
|
+
}
|
|
11089
|
+
__name(textSignature, "textSignature");
|
|
11090
|
+
function buttonSignature(widthM, heightM, size, panel, label, text) {
|
|
11091
|
+
return JSON.stringify([
|
|
11092
|
+
panelSignature(widthM, heightM, size, panel),
|
|
11093
|
+
label,
|
|
11094
|
+
text.fontSizeMeters,
|
|
11095
|
+
text.fontWeight,
|
|
11096
|
+
text.color
|
|
11097
|
+
]);
|
|
11098
|
+
}
|
|
11099
|
+
__name(buttonSignature, "buttonSignature");
|
|
11100
|
+
function trackSignature(widthM, heightM, size, panel, accentColor, knobColor) {
|
|
11101
|
+
return JSON.stringify([
|
|
11102
|
+
panelSignature(widthM, heightM, size, panel),
|
|
11103
|
+
accentColor,
|
|
11104
|
+
knobColor
|
|
11105
|
+
]);
|
|
11106
|
+
}
|
|
11107
|
+
__name(trackSignature, "trackSignature");
|
|
11108
|
+
function drawRoundedBar(ctx, pxX, pxY, pxW, pxH, color) {
|
|
11109
|
+
tracePath(ctx, pxX, pxY, pxW, pxH, pxH / 2);
|
|
11110
|
+
ctx.fillStyle = color;
|
|
11111
|
+
ctx.fill();
|
|
11112
|
+
}
|
|
11113
|
+
__name(drawRoundedBar, "drawRoundedBar");
|
|
11114
|
+
|
|
11115
|
+
// core/systems/UIPanelSystem.ts
|
|
11116
|
+
var FACE_RENDER_ORDER = 10;
|
|
11117
|
+
var CHILD_Z = 2e-3;
|
|
11118
|
+
var _UIPanelSystem = class _UIPanelSystem {
|
|
11119
|
+
constructor(options) {
|
|
11120
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
11121
|
+
this.createCanvas = options?.createCanvas ?? (() => document.createElement("canvas"));
|
|
11122
|
+
}
|
|
11123
|
+
update(world, deltaTime = 0) {
|
|
11124
|
+
const live = /* @__PURE__ */ new Set();
|
|
11125
|
+
for (const entityId of world.query(UIPanelComponent, Object3DRef)) {
|
|
11126
|
+
const panel = world.getComponent(entityId, UIPanelComponent);
|
|
11127
|
+
if (!panel) continue;
|
|
11128
|
+
live.add(entityId);
|
|
11129
|
+
this.syncPanel(world, entityId, panel);
|
|
11130
|
+
this.applyFollow(world, entityId, panel);
|
|
11131
|
+
}
|
|
11132
|
+
for (const entityId of world.query(UITextComponent, Object3DRef)) {
|
|
11133
|
+
const text = world.getComponent(entityId, UITextComponent);
|
|
11134
|
+
if (!text) continue;
|
|
11135
|
+
live.add(entityId);
|
|
11136
|
+
this.syncText(world, entityId, text);
|
|
11137
|
+
}
|
|
11138
|
+
for (const entityId of world.query(UIButtonComponent, Object3DRef)) {
|
|
11139
|
+
const button = world.getComponent(entityId, UIButtonComponent);
|
|
11140
|
+
if (!button) continue;
|
|
11141
|
+
live.add(entityId);
|
|
11142
|
+
this.syncButton(world, entityId, button);
|
|
11143
|
+
}
|
|
11144
|
+
for (const entityId of world.query(UIToggleComponent, Object3DRef)) {
|
|
11145
|
+
const toggle = world.getComponent(entityId, UIToggleComponent);
|
|
11146
|
+
if (!toggle) continue;
|
|
11147
|
+
live.add(entityId);
|
|
11148
|
+
this.syncToggle(world, entityId, toggle, deltaTime);
|
|
11149
|
+
}
|
|
11150
|
+
for (const entityId of world.query(UISliderComponent, Object3DRef)) {
|
|
11151
|
+
const slider = world.getComponent(entityId, UISliderComponent);
|
|
11152
|
+
if (!slider) continue;
|
|
11153
|
+
live.add(entityId);
|
|
11154
|
+
this.syncSlider(world, entityId, slider);
|
|
11155
|
+
}
|
|
11156
|
+
for (const [entityId, entry] of this.entries) {
|
|
11157
|
+
if (!live.has(entityId)) {
|
|
11158
|
+
this.destroyEntry(entry);
|
|
11159
|
+
this.entries.delete(entityId);
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
/** All GPU/canvas resources; call on world teardown. */
|
|
11164
|
+
dispose() {
|
|
11165
|
+
for (const entry of this.entries.values()) this.destroyEntry(entry);
|
|
11166
|
+
this.entries.clear();
|
|
11167
|
+
}
|
|
11168
|
+
// ── Widgets ────────────────────────────────────────────────────────────────
|
|
11169
|
+
syncPanel(world, entityId, panel) {
|
|
11170
|
+
const size = resolveCanvasSize(panel.width, panel.height, panel.pixelDensity);
|
|
11171
|
+
const style = {
|
|
11172
|
+
backgroundColor: panel.backgroundColor,
|
|
11173
|
+
backgroundOpacity: panel.backgroundOpacity,
|
|
11174
|
+
cornerRadiusMeters: panel.cornerRadius,
|
|
11175
|
+
borderWidthMeters: panel.borderWidth,
|
|
11176
|
+
borderColor: panel.borderColor
|
|
11177
|
+
};
|
|
11178
|
+
const signature = panelSignature(panel.width, panel.height, size, style);
|
|
11179
|
+
this.rebuildIfChanged(world, entityId, signature, panel.width, panel.height, size, (ctx) => {
|
|
11180
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, style);
|
|
11181
|
+
});
|
|
11182
|
+
}
|
|
11183
|
+
syncText(world, entityId, text) {
|
|
11184
|
+
const size = resolveCanvasSize(text.width, text.height);
|
|
11185
|
+
const style = {
|
|
11186
|
+
fontSizeMeters: text.fontSize,
|
|
11187
|
+
fontWeight: text.fontWeight,
|
|
11188
|
+
color: text.color,
|
|
11189
|
+
align: text.align,
|
|
11190
|
+
valign: text.valign,
|
|
11191
|
+
lineHeight: text.lineHeight,
|
|
11192
|
+
paddingMeters: text.padding
|
|
11193
|
+
};
|
|
11194
|
+
const signature = textSignature(text.width, text.height, size, text.text, style);
|
|
11195
|
+
this.rebuildIfChanged(world, entityId, signature, text.width, text.height, size, (ctx) => {
|
|
11196
|
+
ctx.clearRect(0, 0, size.pxWidth, size.pxHeight);
|
|
11197
|
+
drawTextBlock(ctx, size.pxWidth, size.pxHeight, size.density, text.text, style);
|
|
11198
|
+
});
|
|
11199
|
+
}
|
|
11200
|
+
syncButton(world, entityId, button) {
|
|
11201
|
+
const size = resolveCanvasSize(button.width, button.height);
|
|
11202
|
+
const chrome = {
|
|
11203
|
+
backgroundColor: button.backgroundColor,
|
|
11204
|
+
backgroundOpacity: button.backgroundOpacity,
|
|
11205
|
+
cornerRadiusMeters: button.cornerRadius,
|
|
11206
|
+
borderWidthMeters: button.borderWidth,
|
|
11207
|
+
borderColor: button.borderColor
|
|
11208
|
+
};
|
|
11209
|
+
const label = {
|
|
11210
|
+
fontSizeMeters: button.fontSize,
|
|
11211
|
+
fontWeight: button.fontWeight,
|
|
11212
|
+
color: button.textColor,
|
|
11213
|
+
align: "center",
|
|
11214
|
+
valign: "middle",
|
|
11215
|
+
lineHeight: 1.2,
|
|
11216
|
+
paddingMeters: 0.01
|
|
11217
|
+
};
|
|
11218
|
+
const signature = buttonSignature(
|
|
11219
|
+
button.width,
|
|
11220
|
+
button.height,
|
|
11221
|
+
size,
|
|
11222
|
+
chrome,
|
|
11223
|
+
button.label,
|
|
11224
|
+
label
|
|
11225
|
+
);
|
|
11226
|
+
this.rebuildIfChanged(world, entityId, signature, button.width, button.height, size, (ctx) => {
|
|
11227
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, chrome);
|
|
11228
|
+
drawTextBlock(ctx, size.pxWidth, size.pxHeight, size.density, button.label, label);
|
|
11229
|
+
});
|
|
11230
|
+
}
|
|
11231
|
+
syncToggle(world, entityId, toggle, deltaTime) {
|
|
11232
|
+
const size = resolveCanvasSize(toggle.width, toggle.height);
|
|
11233
|
+
const chrome = {
|
|
11234
|
+
backgroundColor: toggle.value ? toggle.accentColor : toggle.trackColor,
|
|
11235
|
+
backgroundOpacity: 1,
|
|
11236
|
+
cornerRadiusMeters: toggle.height / 2,
|
|
11237
|
+
borderWidthMeters: 0,
|
|
11238
|
+
borderColor: "#000000"
|
|
11239
|
+
};
|
|
11240
|
+
const signature = JSON.stringify([
|
|
11241
|
+
trackSignature(
|
|
11242
|
+
toggle.width,
|
|
11243
|
+
toggle.height,
|
|
11244
|
+
size,
|
|
11245
|
+
chrome,
|
|
11246
|
+
toggle.accentColor,
|
|
11247
|
+
toggle.knobColor
|
|
11248
|
+
),
|
|
11249
|
+
toggle.value
|
|
11250
|
+
]);
|
|
11251
|
+
const entry = this.rebuildIfChanged(
|
|
11252
|
+
world,
|
|
11253
|
+
entityId,
|
|
11254
|
+
signature,
|
|
11255
|
+
toggle.width,
|
|
11256
|
+
toggle.height,
|
|
11257
|
+
size,
|
|
11258
|
+
(ctx) => {
|
|
11259
|
+
drawChrome(ctx, size.pxWidth, size.pxHeight, size.density, chrome);
|
|
11260
|
+
}
|
|
11261
|
+
);
|
|
11262
|
+
if (!entry) return;
|
|
11263
|
+
if (!entry.knob) {
|
|
11264
|
+
entry.knob = this.makeDisc(
|
|
11265
|
+
toggle.height * 0.4,
|
|
11266
|
+
toggle.knobColor,
|
|
11267
|
+
entry.face.parent,
|
|
11268
|
+
CHILD_Z
|
|
11269
|
+
);
|
|
11270
|
+
}
|
|
11271
|
+
const inset = toggle.height / 2;
|
|
11272
|
+
const target = toggle.value ? toggle.width / 2 - inset : -(toggle.width / 2 - inset);
|
|
11273
|
+
const knob = entry.knob;
|
|
11274
|
+
const ease = Math.min(1, deltaTime * 14);
|
|
11275
|
+
knob.position.x += (target - knob.position.x) * (deltaTime > 0 ? ease : 1);
|
|
11276
|
+
knob.position.z = CHILD_Z;
|
|
11277
|
+
}
|
|
11278
|
+
syncSlider(world, entityId, slider) {
|
|
11279
|
+
const size = resolveCanvasSize(slider.width, slider.height);
|
|
11280
|
+
const trackThickness = slider.height * 0.32;
|
|
11281
|
+
const knobRadius = slider.height * 0.45;
|
|
11282
|
+
const usable = Math.max(1e-3, slider.width - slider.height);
|
|
11283
|
+
const chrome = {
|
|
11284
|
+
backgroundColor: slider.trackColor,
|
|
11285
|
+
backgroundOpacity: 1,
|
|
11286
|
+
cornerRadiusMeters: trackThickness / 2,
|
|
11287
|
+
borderWidthMeters: 0,
|
|
11288
|
+
borderColor: "#000000"
|
|
11289
|
+
};
|
|
11290
|
+
const signature = trackSignature(
|
|
11291
|
+
slider.width,
|
|
11292
|
+
slider.height,
|
|
11293
|
+
size,
|
|
11294
|
+
chrome,
|
|
11295
|
+
slider.accentColor,
|
|
11296
|
+
slider.knobColor
|
|
11297
|
+
);
|
|
11298
|
+
const entry = this.rebuildIfChanged(
|
|
11299
|
+
world,
|
|
11300
|
+
entityId,
|
|
11301
|
+
signature,
|
|
11302
|
+
slider.width,
|
|
11303
|
+
slider.height,
|
|
11304
|
+
size,
|
|
11305
|
+
(ctx) => {
|
|
11306
|
+
ctx.clearRect(0, 0, size.pxWidth, size.pxHeight);
|
|
11307
|
+
const trackWidthM = usable + trackThickness;
|
|
11308
|
+
drawRoundedBar(
|
|
11309
|
+
ctx,
|
|
11310
|
+
(slider.width - trackWidthM) / 2 * size.density,
|
|
11311
|
+
(slider.height - trackThickness) / 2 * size.density,
|
|
11312
|
+
trackWidthM * size.density,
|
|
11313
|
+
trackThickness * size.density,
|
|
11314
|
+
slider.trackColor
|
|
11315
|
+
);
|
|
11316
|
+
}
|
|
11317
|
+
);
|
|
11318
|
+
if (!entry) return;
|
|
11319
|
+
if (!entry.fill) {
|
|
11320
|
+
entry.fill = this.makeQuad(
|
|
11321
|
+
1,
|
|
11322
|
+
trackThickness,
|
|
11323
|
+
slider.accentColor,
|
|
11324
|
+
entry.face.parent,
|
|
11325
|
+
CHILD_Z / 2
|
|
11326
|
+
);
|
|
11327
|
+
entry.cap = this.makeDisc(
|
|
11328
|
+
trackThickness / 2,
|
|
11329
|
+
slider.accentColor,
|
|
11330
|
+
entry.face.parent,
|
|
11331
|
+
CHILD_Z / 2,
|
|
11332
|
+
Math.PI / 2,
|
|
11333
|
+
Math.PI
|
|
11334
|
+
);
|
|
11335
|
+
entry.cap.position.x = -usable / 2;
|
|
11336
|
+
}
|
|
11337
|
+
if (!entry.knob) {
|
|
11338
|
+
entry.knob = this.makeDisc(
|
|
11339
|
+
knobRadius,
|
|
11340
|
+
slider.knobColor,
|
|
11341
|
+
entry.face.parent,
|
|
11342
|
+
CHILD_Z
|
|
11343
|
+
);
|
|
11344
|
+
}
|
|
11345
|
+
const range = Math.max(1e-6, slider.max - slider.min);
|
|
11346
|
+
const t = Math.min(1, Math.max(0, (slider.value - slider.min) / range));
|
|
11347
|
+
entry.knob.position.x = -usable / 2 + t * usable;
|
|
11348
|
+
entry.knob.position.z = CHILD_Z;
|
|
11349
|
+
entry.fill.scale.x = Math.max(1e-4, t * usable);
|
|
11350
|
+
entry.fill.position.x = -usable / 2 + t * usable / 2;
|
|
11351
|
+
entry.fill.position.z = CHILD_Z / 2;
|
|
11352
|
+
}
|
|
11353
|
+
// ── Machinery ──────────────────────────────────────────────────────────────
|
|
11354
|
+
rebuildIfChanged(world, entityId, signature, widthM, heightM, size, draw) {
|
|
11355
|
+
const root = world.getComponent(entityId, Object3DRef)?.object;
|
|
11356
|
+
if (!root) return null;
|
|
11357
|
+
let entry = this.entries.get(entityId);
|
|
11358
|
+
if (entry && entry.signature === signature) return entry;
|
|
11359
|
+
if (entry) this.disposeMovingParts(entry);
|
|
11360
|
+
if (!entry) {
|
|
11361
|
+
const canvas2 = this.createCanvas();
|
|
11362
|
+
const texture = new THREE26__namespace.CanvasTexture(
|
|
11363
|
+
canvas2
|
|
11364
|
+
);
|
|
11365
|
+
this.configureTexture(world, texture);
|
|
11366
|
+
const material = new THREE26__namespace.MeshBasicMaterial({
|
|
11367
|
+
map: texture,
|
|
11368
|
+
transparent: true,
|
|
11369
|
+
depthWrite: false,
|
|
11370
|
+
// UI colours are authored absolutes; scene tone mapping would shift
|
|
11371
|
+
// them (the classic "why is my white UI beige" bug).
|
|
11372
|
+
toneMapped: false,
|
|
11373
|
+
side: THREE26__namespace.DoubleSide,
|
|
11374
|
+
premultipliedAlpha: true
|
|
11375
|
+
});
|
|
11376
|
+
const face = new THREE26__namespace.Mesh(
|
|
11377
|
+
new THREE26__namespace.PlaneGeometry(widthM, heightM),
|
|
11378
|
+
material
|
|
11379
|
+
);
|
|
11380
|
+
face.renderOrder = FACE_RENDER_ORDER;
|
|
11381
|
+
root.add(face);
|
|
11382
|
+
entry = {
|
|
11383
|
+
signature: "",
|
|
11384
|
+
face,
|
|
11385
|
+
texture,
|
|
11386
|
+
material,
|
|
11387
|
+
canvas: canvas2,
|
|
11388
|
+
knob: null,
|
|
11389
|
+
fill: null,
|
|
11390
|
+
cap: null
|
|
11391
|
+
};
|
|
11392
|
+
this.entries.set(entityId, entry);
|
|
11393
|
+
}
|
|
11394
|
+
const { canvas } = entry;
|
|
11395
|
+
if (canvas.width !== size.pxWidth || canvas.height !== size.pxHeight) {
|
|
11396
|
+
canvas.width = size.pxWidth;
|
|
11397
|
+
canvas.height = size.pxHeight;
|
|
11398
|
+
}
|
|
11399
|
+
const params = entry.face.geometry.parameters;
|
|
11400
|
+
if (params.width !== widthM || params.height !== heightM) {
|
|
11401
|
+
entry.face.geometry.dispose();
|
|
11402
|
+
entry.face.geometry = new THREE26__namespace.PlaneGeometry(widthM, heightM);
|
|
11403
|
+
}
|
|
11404
|
+
const ctx = canvas.getContext("2d");
|
|
11405
|
+
if (ctx) draw(ctx);
|
|
11406
|
+
entry.texture.needsUpdate = true;
|
|
11407
|
+
entry.signature = signature;
|
|
11408
|
+
return entry;
|
|
11409
|
+
}
|
|
11410
|
+
configureTexture(world, texture) {
|
|
11411
|
+
texture.colorSpace = THREE26__namespace.SRGBColorSpace;
|
|
11412
|
+
texture.generateMipmaps = true;
|
|
11413
|
+
texture.minFilter = THREE26__namespace.LinearMipmapLinearFilter;
|
|
11414
|
+
texture.magFilter = THREE26__namespace.LinearFilter;
|
|
11415
|
+
texture.premultiplyAlpha = true;
|
|
11416
|
+
try {
|
|
11417
|
+
const max = world.getRenderer().capabilities?.getMaxAnisotropy?.();
|
|
11418
|
+
if (typeof max === "number") texture.anisotropy = Math.min(8, max);
|
|
11419
|
+
} catch {
|
|
11420
|
+
}
|
|
11421
|
+
}
|
|
11422
|
+
makeQuad(widthM, heightM, color, parent, z) {
|
|
11423
|
+
return this.addPart(
|
|
11424
|
+
new THREE26__namespace.PlaneGeometry(widthM, heightM),
|
|
11425
|
+
color,
|
|
11426
|
+
parent,
|
|
11427
|
+
z
|
|
11428
|
+
);
|
|
11429
|
+
}
|
|
11430
|
+
/** A filled disc (or arc segment, for the fill's rounded cap). */
|
|
11431
|
+
makeDisc(radiusM, color, parent, z, thetaStart = 0, thetaLength = Math.PI * 2) {
|
|
11432
|
+
return this.addPart(
|
|
11433
|
+
new THREE26__namespace.CircleGeometry(radiusM, 32, thetaStart, thetaLength),
|
|
11434
|
+
color,
|
|
11435
|
+
parent,
|
|
11436
|
+
z
|
|
11437
|
+
);
|
|
11438
|
+
}
|
|
11439
|
+
addPart(geometry, color, parent, z) {
|
|
11440
|
+
const mesh = new THREE26__namespace.Mesh(
|
|
11441
|
+
geometry,
|
|
11442
|
+
new THREE26__namespace.MeshBasicMaterial({
|
|
11443
|
+
color,
|
|
11444
|
+
transparent: true,
|
|
11445
|
+
depthWrite: false,
|
|
11446
|
+
toneMapped: false,
|
|
11447
|
+
side: THREE26__namespace.DoubleSide
|
|
11448
|
+
})
|
|
11449
|
+
);
|
|
11450
|
+
mesh.renderOrder = FACE_RENDER_ORDER + 1;
|
|
11451
|
+
mesh.position.z = z;
|
|
11452
|
+
mesh.raycast = () => {
|
|
11453
|
+
};
|
|
11454
|
+
parent?.add(mesh);
|
|
11455
|
+
return mesh;
|
|
11456
|
+
}
|
|
11457
|
+
/** Remove + dispose knob/fill/cap so a sync pass rebuilds them fresh. */
|
|
11458
|
+
disposeMovingParts(entry) {
|
|
11459
|
+
for (const key of ["knob", "fill", "cap"]) {
|
|
11460
|
+
const mesh = entry[key];
|
|
11461
|
+
if (!mesh) continue;
|
|
11462
|
+
mesh.removeFromParent();
|
|
11463
|
+
mesh.geometry.dispose();
|
|
11464
|
+
mesh.material.dispose();
|
|
11465
|
+
entry[key] = null;
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
/**
|
|
11469
|
+
* Billboard: yaw the ENTITY TRANSFORM toward the camera. Writing the
|
|
11470
|
+
* Object3D directly would be overwritten by `TransformSyncSystem` (which
|
|
11471
|
+
* runs after this system, before render) — the same reason locomotion
|
|
11472
|
+
* writes the rig transform. Skipped in the editor so the gizmo and the
|
|
11473
|
+
* authored rotation stay in charge while authoring.
|
|
11474
|
+
*/
|
|
11475
|
+
applyFollow(world, entityId, panel) {
|
|
11476
|
+
if (panel.follow !== "billboard" || world.isEditorMode()) return;
|
|
11477
|
+
const transform = world.getComponent(entityId, TransformComponent);
|
|
11478
|
+
const object = world.getComponent(entityId, Object3DRef)?.object;
|
|
11479
|
+
if (!transform || !object) return;
|
|
11480
|
+
const camera = world.getCamera();
|
|
11481
|
+
const cam = new THREE26__namespace.Vector3();
|
|
11482
|
+
const self = new THREE26__namespace.Vector3();
|
|
11483
|
+
camera.getWorldPosition(cam);
|
|
11484
|
+
object.getWorldPosition(self);
|
|
11485
|
+
transform.rotation.y = Math.atan2(cam.x - self.x, cam.z - self.z);
|
|
11486
|
+
}
|
|
11487
|
+
destroyEntry(entry) {
|
|
11488
|
+
this.disposeMovingParts(entry);
|
|
11489
|
+
entry.face.removeFromParent();
|
|
11490
|
+
entry.face.geometry.dispose();
|
|
11491
|
+
entry.face.material.dispose();
|
|
11492
|
+
entry.texture.dispose();
|
|
11493
|
+
}
|
|
11494
|
+
};
|
|
11495
|
+
__name(_UIPanelSystem, "UIPanelSystem");
|
|
11496
|
+
var UIPanelSystem = _UIPanelSystem;
|
|
10084
11497
|
var _XRHitTestSystem = class _XRHitTestSystem {
|
|
10085
11498
|
constructor() {
|
|
10086
11499
|
this.sources = /* @__PURE__ */ new Map();
|
|
@@ -10193,9 +11606,13 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
10193
11606
|
this.anchors = /* @__PURE__ */ new Map();
|
|
10194
11607
|
/** In-flight creations, so one entity is not anchored twice. */
|
|
10195
11608
|
this.pending = /* @__PURE__ */ new Set();
|
|
11609
|
+
/** Per-anchor head-glue gates — see XREnvironmentSystem's TrackedEntry. */
|
|
11610
|
+
this.gates = /* @__PURE__ */ new Map();
|
|
10196
11611
|
this.presenting = false;
|
|
11612
|
+
this.tmpViewerMatrix = new THREE26__namespace.Matrix4();
|
|
11613
|
+
this.tmpViewPos = new THREE26__namespace.Vector3();
|
|
10197
11614
|
}
|
|
10198
|
-
update(world) {
|
|
11615
|
+
update(world, deltaTime = 0) {
|
|
10199
11616
|
const presenting = world.isXrPresenting();
|
|
10200
11617
|
if (!presenting) {
|
|
10201
11618
|
if (this.presenting) {
|
|
@@ -10215,8 +11632,11 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
10215
11632
|
if (!live.has(entityId)) {
|
|
10216
11633
|
anchor.delete?.();
|
|
10217
11634
|
this.anchors.delete(entityId);
|
|
11635
|
+
this.gates.delete(entityId);
|
|
10218
11636
|
}
|
|
10219
11637
|
}
|
|
11638
|
+
const viewer = frame.getViewerPose?.(referenceSpace);
|
|
11639
|
+
const viewerInverse = viewer ? this.tmpViewerMatrix.fromArray(viewer.transform.matrix).invert() : null;
|
|
10220
11640
|
for (const entityId of entityIds) {
|
|
10221
11641
|
const component = world.getComponent(entityId, XRAnchorComponent);
|
|
10222
11642
|
const transform = world.getComponent(entityId, TransformComponent);
|
|
@@ -10226,6 +11646,7 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
10226
11646
|
if (existing) {
|
|
10227
11647
|
existing.delete?.();
|
|
10228
11648
|
this.anchors.delete(entityId);
|
|
11649
|
+
this.gates.delete(entityId);
|
|
10229
11650
|
}
|
|
10230
11651
|
continue;
|
|
10231
11652
|
}
|
|
@@ -10235,7 +11656,23 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
10235
11656
|
continue;
|
|
10236
11657
|
}
|
|
10237
11658
|
const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
|
|
10238
|
-
if (pose
|
|
11659
|
+
if (pose && pose.emulatedPosition !== true) {
|
|
11660
|
+
let gate = this.gates.get(entityId);
|
|
11661
|
+
if (!gate) {
|
|
11662
|
+
gate = new SurfacePoseGate();
|
|
11663
|
+
this.gates.set(entityId, gate);
|
|
11664
|
+
}
|
|
11665
|
+
const m = pose.transform.matrix;
|
|
11666
|
+
const refPos = { x: m[12], y: m[13], z: m[14] };
|
|
11667
|
+
let viewPos = null;
|
|
11668
|
+
if (viewerInverse) {
|
|
11669
|
+
const v = this.tmpViewPos.set(refPos.x, refPos.y, refPos.z).applyMatrix4(viewerInverse);
|
|
11670
|
+
viewPos = { x: v.x, y: v.y, z: v.z };
|
|
11671
|
+
}
|
|
11672
|
+
if (gate.update(refPos, viewPos, deltaTime)) {
|
|
11673
|
+
decomposeXrPose(pose.transform.matrix, transform);
|
|
11674
|
+
}
|
|
11675
|
+
}
|
|
10239
11676
|
}
|
|
10240
11677
|
}
|
|
10241
11678
|
/** Whether this entity's pose is currently being driven by an anchor. */
|
|
@@ -10279,6 +11716,7 @@ var _XRAnchorSystem = class _XRAnchorSystem {
|
|
|
10279
11716
|
}).finally(() => this.pending.delete(entityId));
|
|
10280
11717
|
}
|
|
10281
11718
|
releaseAll() {
|
|
11719
|
+
this.gates.clear();
|
|
10282
11720
|
for (const anchor of this.anchors.values()) anchor.delete?.();
|
|
10283
11721
|
this.anchors.clear();
|
|
10284
11722
|
this.pending.clear();
|
|
@@ -11543,6 +12981,8 @@ exports.SCRIPT_OBJECT_ROOT_FLAG = SCRIPT_OBJECT_ROOT_FLAG;
|
|
|
11543
12981
|
exports.ScriptBehaviourSystem = ScriptBehaviourSystem;
|
|
11544
12982
|
exports.ScriptObjectSystem = ScriptObjectSystem;
|
|
11545
12983
|
exports.TransformSyncSystem = TransformSyncSystem;
|
|
12984
|
+
exports.UIInteractionSystem = UIInteractionSystem;
|
|
12985
|
+
exports.UIPanelSystem = UIPanelSystem;
|
|
11546
12986
|
exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
|
|
11547
12987
|
exports.VideoSystem = VideoSystem;
|
|
11548
12988
|
exports.XRAnchorSystem = XRAnchorSystem;
|