@iwsdk/core 0.5.2 → 1.0.0-rc.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.
Files changed (140) hide show
  1. package/README.md +22 -1
  2. package/dist/asset/asset-manager.d.ts.map +1 -1
  3. package/dist/asset/asset-manager.js +3 -2
  4. package/dist/asset/asset-manager.js.map +1 -1
  5. package/dist/camera/camera-system.d.ts +7 -3
  6. package/dist/camera/camera-system.d.ts.map +1 -1
  7. package/dist/camera/camera-system.js +81 -50
  8. package/dist/camera/camera-system.js.map +1 -1
  9. package/dist/camera/camera-utils.d.ts +5 -0
  10. package/dist/camera/camera-utils.d.ts.map +1 -1
  11. package/dist/camera/camera-utils.js +8 -1
  12. package/dist/camera/camera-utils.js.map +1 -1
  13. package/dist/depth/depth-sensing-system.d.ts +3 -0
  14. package/dist/depth/depth-sensing-system.d.ts.map +1 -1
  15. package/dist/depth/depth-sensing-system.js +55 -1
  16. package/dist/depth/depth-sensing-system.js.map +1 -1
  17. package/dist/ecs/world.d.ts +2 -0
  18. package/dist/ecs/world.d.ts.map +1 -1
  19. package/dist/ecs/world.js +4 -0
  20. package/dist/ecs/world.js.map +1 -1
  21. package/dist/grab/distance-grabbable.d.ts +5 -0
  22. package/dist/grab/distance-grabbable.d.ts.map +1 -1
  23. package/dist/grab/distance-grabbable.js +5 -0
  24. package/dist/grab/distance-grabbable.js.map +1 -1
  25. package/dist/grab/handles.d.ts +6 -0
  26. package/dist/grab/handles.d.ts.map +1 -1
  27. package/dist/grab/handles.js +46 -12
  28. package/dist/grab/handles.js.map +1 -1
  29. package/dist/grab/one-hand-grabbable.d.ts +5 -0
  30. package/dist/grab/one-hand-grabbable.d.ts.map +1 -1
  31. package/dist/grab/one-hand-grabbable.js +5 -0
  32. package/dist/grab/one-hand-grabbable.js.map +1 -1
  33. package/dist/grab/two-hands-grabbable.d.ts +5 -0
  34. package/dist/grab/two-hands-grabbable.d.ts.map +1 -1
  35. package/dist/grab/two-hands-grabbable.js +5 -0
  36. package/dist/grab/two-hands-grabbable.js.map +1 -1
  37. package/dist/index.js +7 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/init/index.d.ts +2 -2
  40. package/dist/init/index.d.ts.map +1 -1
  41. package/dist/init/session-grant.d.ts +34 -0
  42. package/dist/init/session-grant.d.ts.map +1 -0
  43. package/dist/init/session-grant.js +89 -0
  44. package/dist/init/session-grant.js.map +1 -0
  45. package/dist/init/world-initializer.d.ts +87 -4
  46. package/dist/init/world-initializer.d.ts.map +1 -1
  47. package/dist/init/world-initializer.js +48 -43
  48. package/dist/init/world-initializer.js.map +1 -1
  49. package/dist/init/xr.d.ts +19 -0
  50. package/dist/init/xr.d.ts.map +1 -1
  51. package/dist/init/xr.js +136 -41
  52. package/dist/init/xr.js.map +1 -1
  53. package/dist/input/canvas-pointer-system.d.ts +2 -0
  54. package/dist/input/canvas-pointer-system.d.ts.map +1 -1
  55. package/dist/input/canvas-pointer-system.js +33 -13
  56. package/dist/input/canvas-pointer-system.js.map +1 -1
  57. package/dist/input/input-system.d.ts.map +1 -1
  58. package/dist/input/input-system.js +5 -2
  59. package/dist/input/input-system.js.map +1 -1
  60. package/dist/layers/xr-cylinder-layer.d.ts +2 -1
  61. package/dist/layers/xr-cylinder-layer.d.ts.map +1 -1
  62. package/dist/layers/xr-cylinder-layer.js +2 -1
  63. package/dist/layers/xr-cylinder-layer.js.map +1 -1
  64. package/dist/layers/xr-layer-init.d.ts +19 -0
  65. package/dist/layers/xr-layer-init.d.ts.map +1 -0
  66. package/dist/layers/xr-layer-init.js +20 -0
  67. package/dist/layers/xr-layer-init.js.map +1 -0
  68. package/dist/layers/xr-layer-system.d.ts +41 -1
  69. package/dist/layers/xr-layer-system.d.ts.map +1 -1
  70. package/dist/layers/xr-layer-system.js +506 -132
  71. package/dist/layers/xr-layer-system.js.map +1 -1
  72. package/dist/layers/xr-layer-transform.d.ts +18 -0
  73. package/dist/layers/xr-layer-transform.d.ts.map +1 -0
  74. package/dist/layers/xr-layer-transform.js +115 -0
  75. package/dist/layers/xr-layer-transform.js.map +1 -0
  76. package/dist/layers/xr-quad-layer.d.ts +2 -1
  77. package/dist/layers/xr-quad-layer.d.ts.map +1 -1
  78. package/dist/layers/xr-quad-layer.js +2 -1
  79. package/dist/layers/xr-quad-layer.js.map +1 -1
  80. package/dist/level/level-component-applier.d.ts +2 -0
  81. package/dist/level/level-component-applier.d.ts.map +1 -1
  82. package/dist/level/level-component-applier.js +24 -5
  83. package/dist/level/level-component-applier.js.map +1 -1
  84. package/dist/mcp/ecs-debug-tools.d.ts +4 -1
  85. package/dist/mcp/ecs-debug-tools.d.ts.map +1 -1
  86. package/dist/mcp/ecs-debug-tools.js +113 -10
  87. package/dist/mcp/ecs-debug-tools.js.map +1 -1
  88. package/dist/mcp/index.d.ts +1 -0
  89. package/dist/mcp/index.d.ts.map +1 -1
  90. package/dist/mcp/mcp-runtime.d.ts +2 -0
  91. package/dist/mcp/mcp-runtime.d.ts.map +1 -1
  92. package/dist/mcp/mcp-runtime.js +41 -0
  93. package/dist/mcp/mcp-runtime.js.map +1 -1
  94. package/dist/mcp/ui-debug-tools.d.ts +56 -0
  95. package/dist/mcp/ui-debug-tools.d.ts.map +1 -0
  96. package/dist/mcp/ui-debug-tools.js +412 -0
  97. package/dist/mcp/ui-debug-tools.js.map +1 -0
  98. package/dist/physics/index.js +1 -1
  99. package/dist/physics/physics-runtime.d.ts +22 -0
  100. package/dist/physics/physics-runtime.d.ts.map +1 -0
  101. package/dist/physics/physics-runtime.js +423 -0
  102. package/dist/physics/physics-runtime.js.map +1 -0
  103. package/dist/physics/physics-system.d.ts +84 -108
  104. package/dist/physics/physics-system.d.ts.map +1 -1
  105. package/dist/physics/physics-system.js +686 -555
  106. package/dist/physics/physics-system.js.map +1 -1
  107. package/dist/physics/physics-transport.d.ts +20 -0
  108. package/dist/physics/physics-transport.d.ts.map +1 -0
  109. package/dist/physics/physics-transport.js +111 -0
  110. package/dist/physics/physics-transport.js.map +1 -0
  111. package/dist/physics/physics-worker-protocol.d.ts +144 -0
  112. package/dist/physics/physics-worker-protocol.d.ts.map +1 -0
  113. package/dist/physics/physics-worker-protocol.js +41 -0
  114. package/dist/physics/physics-worker-protocol.js.map +1 -0
  115. package/dist/physics/physics-worker-utils.d.ts +8 -0
  116. package/dist/physics/physics-worker-utils.d.ts.map +1 -0
  117. package/dist/physics/physics-worker-utils.js +12 -0
  118. package/dist/physics/physics-worker-utils.js.map +1 -0
  119. package/dist/physics/physics-worker.d.ts +8 -0
  120. package/dist/physics/physics-worker.d.ts.map +1 -0
  121. package/dist/physics/physics-worker.js +66 -0
  122. package/dist/physics/physics-worker.js.map +1 -0
  123. package/dist/project/normalize.js +7 -1
  124. package/dist/project/normalize.js.map +1 -1
  125. package/dist/project/types.d.ts +7 -1
  126. package/dist/project/types.d.ts.map +1 -1
  127. package/dist/project/validation.js +36 -2
  128. package/dist/project/validation.js.map +1 -1
  129. package/dist/scene-understanding/scene-understanding-system.d.ts +42 -6
  130. package/dist/scene-understanding/scene-understanding-system.d.ts.map +1 -1
  131. package/dist/scene-understanding/scene-understanding-system.js +391 -106
  132. package/dist/scene-understanding/scene-understanding-system.js.map +1 -1
  133. package/dist/schemas/iwsdk-project.v1.schema.json +20 -1
  134. package/dist/ui/follow.d.ts.map +1 -1
  135. package/dist/ui/follow.js +0 -1
  136. package/dist/ui/follow.js.map +1 -1
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.d.ts.map +1 -1
  139. package/dist/version.js +1 -1
  140. package/package.json +5 -5
@@ -1,9 +1,11 @@
1
1
  import { createSystem } from '../ecs/system.js';
2
2
  import '../runtime/three.js';
3
3
  import { XRCylinderLayer } from './xr-cylinder-layer.js';
4
+ import { createQuadLayerInit } from './xr-layer-init.js';
4
5
  import { XRLayerState } from './xr-layer-state.js';
6
+ import { resolveLayerReferencePose, resolveQuadLayerDimensions, resolveCylinderLayerDimensions, isValidCylinderLayerAngle } from './xr-layer-transform.js';
5
7
  import { XRQuadLayer } from './xr-quad-layer.js';
6
- import { Vector3, Quaternion, Color, PlaneGeometry, CylinderGeometry, MeshBasicMaterial, Mesh, FrontSide, BackSide, CustomBlending, AddEquation, ZeroFactor, SRGBColorSpace, WebGLRenderTarget, DepthTexture, UnsignedByteType, RGBAFormat, UnsignedInt248Type, UnsignedIntType, DepthStencilFormat, DepthFormat } from 'three';
8
+ import { Vector3, Quaternion, Matrix4, Color, MeshBasicMaterial, FrontSide, BackSide, Mesh, CustomBlending, AddEquation, ZeroFactor, SRGBColorSpace, PlaneGeometry, CylinderGeometry, WebGLRenderTarget, DepthTexture, UnsignedByteType, RGBAFormat, UnsignedInt248Type, UnsignedIntType, DepthStencilFormat, DepthFormat } from 'three';
7
9
 
8
10
  /**
9
11
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -13,17 +15,37 @@ import { Vector3, Quaternion, Color, PlaneGeometry, CylinderGeometry, MeshBasicM
13
15
  */
14
16
  const _position = new Vector3();
15
17
  const _quaternion = new Quaternion();
18
+ const _scale = new Vector3();
19
+ const _configuration = new Vector3();
20
+ const _relativeMatrix = new Matrix4();
16
21
  const _clearColor = new Color();
17
22
  function toRigidTransform(pos, quat) {
18
23
  return new XRRigidTransform({ x: pos.x, y: pos.y, z: pos.z }, { x: quat.x, y: quat.y, z: quat.z, w: quat.w });
19
24
  }
25
+ function isObjectTreeVisible(object) {
26
+ for (let current = object; current; current = current.parent) {
27
+ if (!current.visible) {
28
+ return false;
29
+ }
30
+ }
31
+ return true;
32
+ }
33
+ function sameNumber(first, second) {
34
+ return first === second || (Number.isNaN(first) && Number.isNaN(second));
35
+ }
36
+ function sameVector(first, second) {
37
+ return (sameNumber(first.x, second.x) &&
38
+ sameNumber(first.y, second.y) &&
39
+ sameNumber(first.z, second.z));
40
+ }
20
41
  /**
21
42
  * XRLayerSystem manages WebXR quad and cylinder composition layers.
22
43
  *
23
44
  * For each entity with an {@link XRQuadLayer} or {@link XRCylinderLayer}
24
45
  * component, the system:
25
46
  * - Creates a mesh (plane or cylinder) as a child of the entity's object3D
26
- * - Renders layer content each frame via the component's `renderCallback`
47
+ * - Renders layer content via the component's `renderCallback` while its
48
+ * render surface is visible and active
27
49
  * - In XR sessions with layer support, creates native XR composition layers
28
50
  * and switches the mesh to a punch-through material that makes the
29
51
  * projection layer transparent where the content layer appears
@@ -39,6 +61,12 @@ class XRLayerSystem extends createSystem({
39
61
  constructor() {
40
62
  super(...arguments);
41
63
  this.sessionUsesLayers = false;
64
+ this.sessionLayerInitializationPending = false;
65
+ this.unsupportedLayerStates = new Map();
66
+ this.pendingNativeLayerRemovals = new Set();
67
+ this.retiredLayers = [];
68
+ this.layerVisibility = new Map();
69
+ this.layerConfigurations = new Map();
42
70
  }
43
71
  init() {
44
72
  const onSessionStart = () => this.handleSessionStart();
@@ -49,9 +77,25 @@ class XRLayerSystem extends createSystem({
49
77
  // Register the query subscriptions on cleanupFuncs so they're released on
50
78
  // system teardown (matching the xr listeners above).
51
79
  this.cleanupFuncs.push(this.queries.quadLayers.subscribe('qualify', (entity) => this.setupLayer(entity, 'quad')), this.queries.quadLayers.subscribe('disqualify', (entity) => this.teardownLayer(entity)), this.queries.cylinderLayers.subscribe('qualify', (entity) => this.setupLayer(entity, 'cylinder')), this.queries.cylinderLayers.subscribe('disqualify', (entity) => this.teardownLayer(entity)));
80
+ // Query disqualify does not run merely because the system itself is being
81
+ // destroyed. Release every live compositor and GPU resource explicitly.
82
+ this.cleanupFuncs.push(() => {
83
+ for (const entity of [...this.queries.activeLayers.entities]) {
84
+ this.teardownLayer(entity);
85
+ }
86
+ this.disposeRetiredLayers();
87
+ this.unsupportedLayerStates.clear();
88
+ this.pendingNativeLayerRemovals.clear();
89
+ this.layerVisibility.clear();
90
+ this.layerConfigurations.clear();
91
+ });
52
92
  }
53
93
  update() {
54
- if (this.queries.activeLayers.entities.size === 0) {
94
+ if (this.sessionLayerInitializationPending) {
95
+ this.tryInitializeSessionLayers();
96
+ }
97
+ if (this.queries.activeLayers.entities.size === 0 &&
98
+ this.retiredLayers.length === 0) {
55
99
  return;
56
100
  }
57
101
  this.renderLayers();
@@ -66,21 +110,8 @@ class XRLayerSystem extends createSystem({
66
110
  const pixelWidth = entity.getValue(component, 'pixelWidth');
67
111
  const pixelHeight = entity.getValue(component, 'pixelHeight');
68
112
  const stencil = entity.getValue(component, 'stencil');
69
- let geometry;
70
- let side;
71
- if (isQuad) {
72
- const width = entity.getValue(XRQuadLayer, 'width');
73
- const height = entity.getValue(XRQuadLayer, 'height');
74
- geometry = new PlaneGeometry(width, height);
75
- side = FrontSide;
76
- }
77
- else {
78
- const radius = entity.getValue(XRCylinderLayer, 'radius');
79
- const centralAngle = entity.getValue(XRCylinderLayer, 'centralAngle');
80
- const aspectRatio = entity.getValue(XRCylinderLayer, 'aspectRatio');
81
- geometry = new CylinderGeometry(radius, radius, (radius * centralAngle) / aspectRatio, 64, 64, true, Math.PI - centralAngle / 2, centralAngle);
82
- side = BackSide;
83
- }
113
+ const geometry = this.createFallbackGeometry(entity, isQuad);
114
+ const side = isQuad ? FrontSide : BackSide;
84
115
  const renderTarget = this.createRenderTarget(pixelWidth, pixelHeight, stencil);
85
116
  const fallbackMaterial = new MeshBasicMaterial({
86
117
  color: 0xffffff,
@@ -101,57 +132,90 @@ class XRLayerSystem extends createSystem({
101
132
  pixelHeight,
102
133
  stencil,
103
134
  });
135
+ this.readLayerConfiguration(entity, isQuad, _configuration);
136
+ this.layerConfigurations.set(entity.index, _configuration.clone());
104
137
  if (this.sessionUsesLayers) {
105
138
  this.activateNativeLayer(entity);
106
139
  this.updateSessionLayers();
107
140
  }
108
141
  }
109
142
  teardownLayer(entity) {
143
+ var _a, _b;
110
144
  const mesh = XRLayerState.data.mesh[entity.index];
111
145
  if (!mesh) {
112
146
  return;
113
147
  }
114
- const fallbackMaterial = XRLayerState.data.fallbackMaterial[entity.index];
115
- const renderTarget = XRLayerState.data.renderTarget[entity.index];
116
- const xrLayer = XRLayerState.data.xrLayer[entity.index];
117
- if (xrLayer && this.sessionUsesLayers) {
118
- entity.setValue(XRLayerState, 'xrLayer', null);
119
- this.updateSessionLayers();
120
- }
121
- mesh.geometry.dispose();
122
- fallbackMaterial.dispose();
123
- if (mesh.material !== fallbackMaterial) {
124
- mesh.material.dispose();
148
+ const resources = {
149
+ xrLayer: XRLayerState.data.xrLayer[entity.index],
150
+ mesh,
151
+ fallbackMaterial: XRLayerState.data.fallbackMaterial[entity.index],
152
+ renderTarget: XRLayerState.data.renderTarget[entity.index],
153
+ };
154
+ const nativeLayerIsActive = (_b = (_a = this.xrManager.getSession()) === null || _a === void 0 ? void 0 : _a.renderState.layers) === null || _b === void 0 ? void 0 : _b.includes(resources.xrLayer);
155
+ if (resources.xrLayer && this.sessionUsesLayers) {
156
+ this.updateSessionLayers(entity);
125
157
  }
126
- renderTarget.dispose();
127
158
  mesh.removeFromParent();
128
- entity.removeComponent(XRLayerState);
159
+ if (entity.active) {
160
+ entity.removeComponent(XRLayerState);
161
+ }
162
+ this.unsupportedLayerStates.delete(entity.index);
163
+ this.pendingNativeLayerRemovals.delete(entity.index);
164
+ this.layerVisibility.delete(entity.index);
165
+ this.layerConfigurations.delete(entity.index);
166
+ if (resources.xrLayer && this.sessionUsesLayers && nativeLayerIsActive) {
167
+ this.retiredLayers.push(resources);
168
+ }
169
+ else {
170
+ this.disposeLayerResources(resources);
171
+ }
129
172
  }
130
173
  // ---------------------------------------------------------------------------
131
174
  // XR session lifecycle
132
175
  // ---------------------------------------------------------------------------
133
176
  handleSessionStart() {
177
+ this.sessionUsesLayers = false;
178
+ this.sessionLayerInitializationPending = true;
179
+ this.tryInitializeSessionLayers();
180
+ }
181
+ handleSessionEnd() {
182
+ for (const entity of this.queries.activeLayers.entities) {
183
+ this.deactivateNativeLayer(entity);
184
+ }
185
+ this.disposeRetiredLayers();
186
+ this.unsupportedLayerStates.clear();
187
+ this.pendingNativeLayerRemovals.clear();
188
+ this.layerVisibility.clear();
189
+ this.sessionUsesLayers = false;
190
+ this.sessionLayerInitializationPending = false;
191
+ }
192
+ /**
193
+ * Enable composition layers once the projection layer is active.
194
+ *
195
+ * super-three queues its projection-layer render state before dispatching
196
+ * `sessionstart`, so `renderState.layers` can legitimately be empty during
197
+ * that event. Keep rendering the mesh fallback and retry on subsequent
198
+ * frames instead of permanently classifying the session as unsupported.
199
+ */
200
+ tryInitializeSessionLayers() {
134
201
  var _a;
135
202
  const session = this.xrManager.getSession();
136
- const layers = (_a = session === null || session === void 0 ? void 0 : session.renderState) === null || _a === void 0 ? void 0 : _a.layers;
137
- // Need both a layers array AND a projection layer at index 0.
138
- // The IWER emulator exposes an empty layers array which is truthy
139
- // but has no projection layer -- treat that as "no layers support".
140
- if (!(layers === null || layers === void 0 ? void 0 : layers.length)) {
141
- this.sessionUsesLayers = false;
142
- return;
203
+ const binding = this.xrManager.getBinding();
204
+ const renderState = session === null || session === void 0 ? void 0 : session.renderState;
205
+ if (!session || !binding || !renderState || renderState.baseLayer) {
206
+ this.sessionLayerInitializationPending = false;
207
+ return false;
143
208
  }
209
+ if (!((_a = renderState.layers) === null || _a === void 0 ? void 0 : _a[0])) {
210
+ return false;
211
+ }
212
+ this.sessionLayerInitializationPending = false;
144
213
  this.sessionUsesLayers = true;
145
214
  for (const entity of this.queries.activeLayers.entities) {
146
215
  this.activateNativeLayer(entity);
147
216
  }
148
217
  this.updateSessionLayers();
149
- }
150
- handleSessionEnd() {
151
- for (const entity of this.queries.activeLayers.entities) {
152
- this.deactivateNativeLayer(entity);
153
- }
154
- this.sessionUsesLayers = false;
218
+ return true;
155
219
  }
156
220
  // ---------------------------------------------------------------------------
157
221
  // Native layer management
@@ -160,105 +224,208 @@ class XRLayerSystem extends createSystem({
160
224
  const binding = this.xrManager.getBinding();
161
225
  const session = this.xrManager.getSession();
162
226
  if (!binding || !session) {
163
- return;
227
+ return false;
164
228
  }
165
229
  const isQuad = !!XRLayerState.data.isQuad[entity.index];
166
230
  const mesh = XRLayerState.data.mesh[entity.index];
167
231
  const pixelWidth = XRLayerState.data.pixelWidth[entity.index];
168
232
  const pixelHeight = XRLayerState.data.pixelHeight[entity.index];
169
- const renderTarget = XRLayerState.data.renderTarget[entity.index];
170
233
  const refSpace = this.xrManager.getReferenceSpace();
171
- mesh.updateMatrixWorld(true);
172
- mesh.getWorldPosition(_position);
173
- mesh.getWorldQuaternion(_quaternion);
174
- const transform = toRigidTransform(_position, _quaternion);
234
+ if (!refSpace) {
235
+ return false;
236
+ }
237
+ this.readLayerConfiguration(entity, isQuad, _configuration);
238
+ const hasSupportedTransform = resolveLayerReferencePose(mesh, this.player, _position, _quaternion, _scale, _relativeMatrix);
175
239
  let xrLayer;
176
240
  if (isQuad) {
177
- const width = entity.getValue(XRQuadLayer, 'width');
178
- const height = entity.getValue(XRQuadLayer, 'height');
179
- xrLayer = binding.createQuadLayer({
180
- transform,
181
- width: width / 2,
182
- height: height / 2,
241
+ const dimensions = hasSupportedTransform
242
+ ? resolveQuadLayerDimensions(_configuration.x, _configuration.y, _scale)
243
+ : null;
244
+ if (!dimensions) {
245
+ this.rememberUnsupportedLayer(entity, 'quad');
246
+ return false;
247
+ }
248
+ xrLayer = binding.createQuadLayer(createQuadLayerInit({
249
+ transform: toRigidTransform(_position, _quaternion),
250
+ width: dimensions.width,
251
+ height: dimensions.height,
183
252
  space: refSpace,
184
- viewPixelWidth: pixelWidth,
185
- viewPixelHeight: pixelHeight,
186
- clearOnAccess: false,
187
- });
253
+ pixelWidth,
254
+ pixelHeight,
255
+ }));
188
256
  }
189
257
  else {
190
- const radius = entity.getValue(XRCylinderLayer, 'radius');
191
- const centralAngle = entity.getValue(XRCylinderLayer, 'centralAngle');
192
- const aspectRatio = entity.getValue(XRCylinderLayer, 'aspectRatio');
258
+ const dimensions = hasSupportedTransform
259
+ ? resolveCylinderLayerDimensions(_configuration.x, _configuration.y, _scale)
260
+ : null;
261
+ if (!dimensions || !isValidCylinderLayerAngle(_configuration.z)) {
262
+ this.rememberUnsupportedLayer(entity, 'cylinder');
263
+ return false;
264
+ }
193
265
  xrLayer = binding.createCylinderLayer({
194
- transform,
195
- radius,
196
- centralAngle,
197
- aspectRatio,
266
+ transform: toRigidTransform(_position, _quaternion),
267
+ radius: dimensions.radius,
268
+ centralAngle: _configuration.z,
269
+ aspectRatio: dimensions.aspectRatio,
198
270
  space: refSpace,
199
271
  viewPixelWidth: pixelWidth,
200
272
  viewPixelHeight: pixelHeight,
201
273
  clearOnAccess: false,
202
274
  });
203
275
  }
276
+ this.unsupportedLayerStates.delete(entity.index);
204
277
  entity.setValue(XRLayerState, 'xrLayer', xrLayer);
205
- // Switch to punch-through material so the projection layer is
206
- // transparent where this content layer appears.
207
- const side = isQuad ? FrontSide : BackSide;
278
+ entity.setValue(XRLayerState, 'cachedTransform', {
279
+ position: _position.clone(),
280
+ quaternion: _quaternion.clone(),
281
+ scale: _scale.clone(),
282
+ configuration: _configuration.clone(),
283
+ });
284
+ return true;
285
+ }
286
+ deactivateNativeLayer(entity) {
287
+ var _a;
288
+ const mesh = XRLayerState.data.mesh[entity.index];
289
+ const xrLayer = XRLayerState.data.xrLayer[entity.index];
290
+ if (!xrLayer) {
291
+ entity.setValue(XRLayerState, 'cachedTransform', null);
292
+ this.pendingNativeLayerRemovals.delete(entity.index);
293
+ return;
294
+ }
295
+ (_a = xrLayer.destroy) === null || _a === void 0 ? void 0 : _a.call(xrLayer);
296
+ this.useFallbackSurface(entity, mesh);
297
+ entity.setValue(XRLayerState, 'xrLayer', null);
298
+ entity.setValue(XRLayerState, 'cachedTransform', null);
299
+ this.pendingNativeLayerRemovals.delete(entity.index);
300
+ }
301
+ /**
302
+ * Switch from the internal fallback texture only after the compositor layer
303
+ * is active. This avoids punching a transparent hole while
304
+ * updateRenderState() is still pending.
305
+ */
306
+ useNativeSurface(entity, mesh) {
307
+ const fallbackMaterial = XRLayerState.data.fallbackMaterial[entity.index];
308
+ if (mesh.material !== fallbackMaterial) {
309
+ return;
310
+ }
311
+ const side = XRLayerState.data.isQuad[entity.index] ? FrontSide : BackSide;
208
312
  const punchMaterial = new MeshBasicMaterial({ color: 0xffffff, side });
209
313
  punchMaterial.blending = CustomBlending;
210
314
  punchMaterial.blendEquation = AddEquation;
211
315
  punchMaterial.blendSrc = ZeroFactor;
212
316
  punchMaterial.blendDst = ZeroFactor;
213
317
  mesh.material = punchMaterial;
214
- // Mark render target for external-texture usage and set colorSpace so
215
- // that WebGLPrograms can derive the correct output encoding (super-three
216
- // reads renderTarget.texture.colorSpace when isXRRenderTarget is true).
318
+ const xrLayer = XRLayerState.data.xrLayer[entity.index];
319
+ xrLayer.blendTextureSourceAlpha = true;
320
+ const renderTarget = XRLayerState.data.renderTarget[entity.index];
217
321
  renderTarget.isXRRenderTarget = true;
218
322
  renderTarget.texture.colorSpace = SRGBColorSpace;
219
- entity.setValue(XRLayerState, 'cachedTransform', {
220
- position: _position.clone(),
221
- quaternion: _quaternion.clone(),
222
- });
223
323
  }
224
- deactivateNativeLayer(entity) {
225
- const mesh = XRLayerState.data.mesh[entity.index];
324
+ /** Restore a fresh internal target after an external XR texture was bound. */
325
+ useFallbackSurface(entity, mesh) {
226
326
  const fallbackMaterial = XRLayerState.data.fallbackMaterial[entity.index];
227
- const pixelWidth = XRLayerState.data.pixelWidth[entity.index];
228
- const pixelHeight = XRLayerState.data.pixelHeight[entity.index];
229
- const stencil = !!XRLayerState.data.stencil[entity.index];
230
- // Dispose punch-through material
231
- if (mesh.material !== fallbackMaterial) {
232
- mesh.material.dispose();
327
+ if (mesh.material === fallbackMaterial) {
328
+ return;
233
329
  }
234
- // Recreate render target so stale __hasExternalTextures state is cleared
330
+ mesh.material.dispose();
235
331
  const oldRenderTarget = XRLayerState.data.renderTarget[entity.index];
236
332
  oldRenderTarget.dispose();
237
- const newRenderTarget = this.createRenderTarget(pixelWidth, pixelHeight, stencil);
333
+ const newRenderTarget = this.createRenderTarget(XRLayerState.data.pixelWidth[entity.index], XRLayerState.data.pixelHeight[entity.index], !!XRLayerState.data.stencil[entity.index]);
238
334
  entity.setValue(XRLayerState, 'renderTarget', newRenderTarget);
239
- // Restore fallback material with the fresh render target texture
240
335
  fallbackMaterial.map = newRenderTarget.texture;
241
336
  fallbackMaterial.map.offset.y = 1;
242
337
  fallbackMaterial.map.repeat.y = -1;
243
338
  mesh.material = fallbackMaterial;
244
- entity.setValue(XRLayerState, 'xrLayer', null);
245
- entity.setValue(XRLayerState, 'cachedTransform', null);
339
+ }
340
+ disposeLayerResources(resources) {
341
+ var _a, _b;
342
+ (_b = (_a = resources.xrLayer) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
343
+ resources.mesh.geometry.dispose();
344
+ resources.fallbackMaterial.dispose();
345
+ if (resources.mesh.material !== resources.fallbackMaterial) {
346
+ resources.mesh.material.dispose();
347
+ }
348
+ resources.renderTarget.dispose();
349
+ }
350
+ disposeRetiredLayers() {
351
+ for (const resources of this.retiredLayers) {
352
+ this.disposeLayerResources(resources);
353
+ }
354
+ this.retiredLayers.length = 0;
355
+ }
356
+ /**
357
+ * Keep removed layers alive until the queued render state no longer
358
+ * references them. While removal is pending, clear their native textures so
359
+ * the compositor cannot present stale content from the detached entity.
360
+ */
361
+ renderRetiredLayers(renderer) {
362
+ var _a;
363
+ if (this.retiredLayers.length === 0) {
364
+ return;
365
+ }
366
+ const activeLayers = (_a = this.xrManager.getSession()) === null || _a === void 0 ? void 0 : _a.renderState.layers;
367
+ if (!activeLayers) {
368
+ this.disposeRetiredLayers();
369
+ return;
370
+ }
371
+ const retained = [];
372
+ const released = [];
373
+ for (const resources of this.retiredLayers) {
374
+ if (activeLayers.includes(resources.xrLayer)) {
375
+ retained.push(resources);
376
+ }
377
+ else {
378
+ released.push(resources);
379
+ }
380
+ }
381
+ // Commit the classification before disposal or compositor access. If a
382
+ // later getSubImage() throws, already-released entries cannot be disposed
383
+ // again, while every still-active entry remains tracked for retry.
384
+ this.retiredLayers = retained;
385
+ for (const resources of released) {
386
+ this.disposeLayerResources(resources);
387
+ }
388
+ const binding = this.xrManager.getBinding();
389
+ const frame = this.xrFrame;
390
+ for (const resources of retained) {
391
+ if (!binding || !frame) {
392
+ continue;
393
+ }
394
+ resources.xrLayer.blendTextureSourceAlpha = true;
395
+ resources.renderTarget.isXRRenderTarget = true;
396
+ resources.renderTarget.texture.colorSpace = SRGBColorSpace;
397
+ const subImage = binding.getSubImage(resources.xrLayer, frame);
398
+ renderer.setRenderTargetTextures(resources.renderTarget, subImage.colorTexture, undefined);
399
+ renderer.setRenderTarget(resources.renderTarget);
400
+ renderer.clear(true, true, true);
401
+ }
246
402
  }
247
403
  /**
248
404
  * Rebuild the session's render state layers array.
249
405
  * Keeps the projection layer at index 0, then appends content layers.
250
406
  */
251
- updateSessionLayers() {
407
+ updateSessionLayers(excludedEntity) {
252
408
  const session = this.xrManager.getSession();
253
409
  if (!(session === null || session === void 0 ? void 0 : session.renderState.layers)) {
254
410
  return;
255
411
  }
256
412
  // Projection layer is always the first entry
257
413
  const projLayer = session.renderState.layers[0];
258
- const layers = projLayer ? [projLayer] : [];
414
+ if (!projLayer) {
415
+ return;
416
+ }
417
+ const layers = [projLayer];
259
418
  for (const entity of this.queries.activeLayers.entities) {
419
+ if (entity === excludedEntity) {
420
+ continue;
421
+ }
260
422
  const xrLayer = XRLayerState.data.xrLayer[entity.index];
261
- if (xrLayer) {
423
+ const mesh = XRLayerState.data.mesh[entity.index];
424
+ const visible = mesh != null && isObjectTreeVisible(mesh);
425
+ this.layerVisibility.set(entity.index, visible);
426
+ if (xrLayer &&
427
+ visible &&
428
+ !this.pendingNativeLayerRemovals.has(entity.index)) {
262
429
  layers.push(xrLayer);
263
430
  }
264
431
  }
@@ -268,48 +435,98 @@ class XRLayerSystem extends createSystem({
268
435
  // Per-frame rendering
269
436
  // ---------------------------------------------------------------------------
270
437
  renderLayers() {
438
+ var _a, _b;
439
+ if (this.sessionLayerInitializationPending) {
440
+ this.tryInitializeSessionLayers();
441
+ }
271
442
  const renderer = this.renderer;
272
443
  const savedRenderTarget = renderer.getRenderTarget();
273
- // Disable XR so renderer.render() uses the layer's own camera
274
- // instead of the headset-tracked XR cameras.
275
444
  const savedXrEnabled = renderer.xr.enabled;
276
- renderer.xr.enabled = false;
277
- // Save clear color so we can clear layers to transparent
445
+ // Save clear color so we can clear layers to transparent.
278
446
  renderer.getClearColor(_clearColor);
279
447
  const savedClearAlpha = renderer.getClearAlpha();
280
- renderer.setClearColor(0x000000, 0);
281
- for (const entity of this.queries.activeLayers.entities) {
282
- const isQuad = !!XRLayerState.data.isQuad[entity.index];
283
- const component = isQuad ? XRQuadLayer : XRCylinderLayer;
284
- const callback = entity.getValue(component, 'renderCallback');
285
- if (!callback) {
286
- continue;
287
- }
288
- const xrLayer = XRLayerState.data.xrLayer[entity.index];
289
- const mesh = XRLayerState.data.mesh[entity.index];
290
- const renderTarget = XRLayerState.data.renderTarget[entity.index];
291
- if (this.sessionUsesLayers && xrLayer) {
292
- // XR path: sync native layer transform and render to its texture
293
- const binding = this.xrManager.getBinding();
294
- const frame = this.xrFrame;
295
- if (!binding || !frame) {
448
+ try {
449
+ // Disable XR so renderer.render() uses the layer-specific camera instead of
450
+ // the headset-tracked XR cameras.
451
+ renderer.xr.enabled = false;
452
+ renderer.setClearColor(0x000000, 0);
453
+ this.renderRetiredLayers(renderer);
454
+ for (const entity of this.queries.activeLayers.entities) {
455
+ const isQuad = !!XRLayerState.data.isQuad[entity.index];
456
+ const component = isQuad ? XRQuadLayer : XRCylinderLayer;
457
+ let xrLayer = XRLayerState.data.xrLayer[entity.index];
458
+ const mesh = XRLayerState.data.mesh[entity.index];
459
+ this.syncLayerGeometry(entity, mesh, isQuad);
460
+ const visible = isObjectTreeVisible(mesh);
461
+ if (this.layerVisibility.get(entity.index) !== visible) {
462
+ this.layerVisibility.set(entity.index, visible);
463
+ if (this.sessionUsesLayers && xrLayer) {
464
+ this.updateSessionLayers();
465
+ }
466
+ }
467
+ if (this.sessionUsesLayers && xrLayer) {
468
+ // An unsupported live transform queues removal. Keep the old native
469
+ // surface valid until that pending render state becomes active.
470
+ this.syncLayerTransform(xrLayer, mesh, entity);
471
+ }
472
+ if (this.sessionUsesLayers &&
473
+ !xrLayer &&
474
+ this.shouldRetryNativeLayer(entity, mesh, isQuad) &&
475
+ this.activateNativeLayer(entity)) {
476
+ xrLayer = XRLayerState.data.xrLayer[entity.index];
477
+ this.updateSessionLayers();
478
+ }
479
+ let nativeActive = this.sessionUsesLayers &&
480
+ xrLayer != null &&
481
+ ((_b = (_a = this.xrManager.getSession()) === null || _a === void 0 ? void 0 : _a.renderState.layers) === null || _b === void 0 ? void 0 : _b.includes(xrLayer));
482
+ if (xrLayer &&
483
+ this.pendingNativeLayerRemovals.has(entity.index) &&
484
+ !nativeActive) {
485
+ this.deactivateNativeLayer(entity);
486
+ xrLayer = null;
487
+ nativeActive = false;
488
+ }
489
+ if (nativeActive && xrLayer) {
490
+ this.useNativeSurface(entity, mesh);
491
+ }
492
+ else {
493
+ this.useFallbackSurface(entity, mesh);
494
+ }
495
+ const callback = entity.getValue(component, 'renderCallback');
496
+ if (!visible && !nativeActive) {
296
497
  continue;
297
498
  }
298
- this.syncLayerTransform(xrLayer, mesh, entity);
299
- const subImage = binding.getSubImage(xrLayer, frame);
300
- renderer.setRenderTargetTextures(renderTarget, subImage.colorTexture, undefined);
301
- renderer.setRenderTarget(renderTarget);
302
- }
303
- else {
304
- // Fallback path: render to the internal render target
305
- renderer.setRenderTarget(renderTarget);
499
+ const renderTarget = XRLayerState.data.renderTarget[entity.index];
500
+ if (nativeActive && xrLayer) {
501
+ const binding = this.xrManager.getBinding();
502
+ const frame = this.xrFrame;
503
+ if (!binding || !frame) {
504
+ continue;
505
+ }
506
+ const subImage = binding.getSubImage(xrLayer, frame);
507
+ renderer.setRenderTargetTextures(renderTarget, subImage.colorTexture, undefined);
508
+ renderer.setRenderTarget(renderTarget);
509
+ if (!visible) {
510
+ // A hidden layer remains in the current render state until the next
511
+ // XR frame. Clear it now so that pending removal cannot expose the
512
+ // previous frame's stale content.
513
+ renderer.clear(true, true, true);
514
+ continue;
515
+ }
516
+ }
517
+ else {
518
+ renderer.setRenderTarget(renderTarget);
519
+ }
520
+ callback === null || callback === void 0 ? void 0 : callback();
306
521
  }
307
- callback();
308
522
  }
309
- // Restore renderer state
310
- renderer.setClearColor(_clearColor, savedClearAlpha);
311
- renderer.xr.enabled = savedXrEnabled;
312
- renderer.setRenderTarget(savedRenderTarget);
523
+ finally {
524
+ // Never leak layer-rendering state into the application render, even
525
+ // when a binding or user render callback throws.
526
+ renderer.setClearColor(_clearColor, savedClearAlpha);
527
+ renderer.xr.enabled = savedXrEnabled;
528
+ renderer.setRenderTarget(savedRenderTarget);
529
+ }
313
530
  }
314
531
  // ---------------------------------------------------------------------------
315
532
  // Helpers
@@ -319,25 +536,182 @@ class XRLayerSystem extends createSystem({
319
536
  * avoiding per-frame XRRigidTransform allocations for static layers.
320
537
  */
321
538
  syncLayerTransform(xrLayer, mesh, entity) {
322
- mesh.updateMatrixWorld(true);
323
- mesh.getWorldPosition(_position);
324
- mesh.getWorldQuaternion(_quaternion);
539
+ const hasSupportedTransform = resolveLayerReferencePose(mesh, this.player, _position, _quaternion, _scale, _relativeMatrix);
540
+ const isQuad = !!XRLayerState.data.isQuad[entity.index];
541
+ this.readLayerConfiguration(entity, isQuad, _configuration);
325
542
  const cached = XRLayerState.data.cachedTransform[entity.index];
326
- if ((cached === null || cached === void 0 ? void 0 : cached.position.equals(_position)) &&
327
- cached.quaternion.equals(_quaternion)) {
328
- return;
543
+ if (hasSupportedTransform &&
544
+ (cached === null || cached === void 0 ? void 0 : cached.position.equals(_position)) &&
545
+ cached.quaternion.equals(_quaternion) &&
546
+ cached.scale.equals(_scale) &&
547
+ sameVector(cached.configuration, _configuration)) {
548
+ this.restorePendingNativeLayer(entity);
549
+ return true;
329
550
  }
551
+ if (isQuad) {
552
+ const dimensions = hasSupportedTransform
553
+ ? resolveQuadLayerDimensions(_configuration.x, _configuration.y, _scale)
554
+ : null;
555
+ if (!dimensions) {
556
+ this.rememberUnsupportedLayer(entity, 'quad');
557
+ this.requestNativeLayerRemoval(entity);
558
+ return false;
559
+ }
560
+ xrLayer.width = dimensions.width;
561
+ xrLayer.height = dimensions.height;
562
+ }
563
+ else {
564
+ const dimensions = hasSupportedTransform
565
+ ? resolveCylinderLayerDimensions(_configuration.x, _configuration.y, _scale)
566
+ : null;
567
+ if (!dimensions || !isValidCylinderLayerAngle(_configuration.z)) {
568
+ this.rememberUnsupportedLayer(entity, 'cylinder');
569
+ this.requestNativeLayerRemoval(entity);
570
+ return false;
571
+ }
572
+ xrLayer.radius = dimensions.radius;
573
+ xrLayer.aspectRatio = dimensions.aspectRatio;
574
+ xrLayer.centralAngle = _configuration.z;
575
+ }
576
+ this.restorePendingNativeLayer(entity);
330
577
  xrLayer.transform = toRigidTransform(_position, _quaternion);
331
578
  if (cached) {
332
579
  cached.position.copy(_position);
333
580
  cached.quaternion.copy(_quaternion);
581
+ cached.scale.copy(_scale);
582
+ cached.configuration.copy(_configuration);
334
583
  }
335
584
  else {
336
585
  entity.setValue(XRLayerState, 'cachedTransform', {
337
586
  position: _position.clone(),
338
587
  quaternion: _quaternion.clone(),
588
+ scale: _scale.clone(),
589
+ configuration: _configuration.clone(),
339
590
  });
340
591
  }
592
+ return true;
593
+ }
594
+ requestNativeLayerRemoval(entity) {
595
+ if (this.pendingNativeLayerRemovals.has(entity.index)) {
596
+ return;
597
+ }
598
+ this.pendingNativeLayerRemovals.add(entity.index);
599
+ this.updateSessionLayers();
600
+ }
601
+ restorePendingNativeLayer(entity) {
602
+ this.unsupportedLayerStates.delete(entity.index);
603
+ if (!this.pendingNativeLayerRemovals.delete(entity.index)) {
604
+ return;
605
+ }
606
+ this.updateSessionLayers();
607
+ }
608
+ readLayerConfiguration(entity, isQuad, target) {
609
+ if (isQuad) {
610
+ target.set(entity.getValue(XRQuadLayer, 'width'), entity.getValue(XRQuadLayer, 'height'), 0);
611
+ return;
612
+ }
613
+ target.set(entity.getValue(XRCylinderLayer, 'radius'), entity.getValue(XRCylinderLayer, 'aspectRatio'), entity.getValue(XRCylinderLayer, 'centralAngle'));
614
+ }
615
+ syncLayerGeometry(entity, mesh, isQuad) {
616
+ this.readLayerConfiguration(entity, isQuad, _configuration);
617
+ const cached = this.layerConfigurations.get(entity.index);
618
+ if (cached && sameVector(cached, _configuration)) {
619
+ return;
620
+ }
621
+ this.layerConfigurations.set(entity.index, _configuration.clone());
622
+ // The fallback must reflect every live configuration, including values
623
+ // that a native composition layer cannot represent. Collapse non-finite
624
+ // dimensions to a safe degenerate geometry instead of leaving the last
625
+ // valid (and therefore misleading) shape on screen.
626
+ const geometry = this.createFallbackGeometry(entity, isQuad);
627
+ const oldGeometry = mesh.geometry;
628
+ mesh.geometry = geometry;
629
+ oldGeometry.dispose();
630
+ }
631
+ createFallbackGeometry(entity, isQuad) {
632
+ this.readLayerConfiguration(entity, isQuad, _configuration);
633
+ if (isQuad) {
634
+ const width = Number.isFinite(_configuration.x) ? _configuration.x : 0;
635
+ const height = Number.isFinite(_configuration.y) ? _configuration.y : 0;
636
+ return new PlaneGeometry(width, height);
637
+ }
638
+ const radius = Number.isFinite(_configuration.x) ? _configuration.x : 0;
639
+ const centralAngle = Number.isFinite(_configuration.z)
640
+ ? _configuration.z
641
+ : 0;
642
+ const rawHeight = (_configuration.x * _configuration.z) / _configuration.y;
643
+ const height = Number.isFinite(rawHeight) ? rawHeight : 0;
644
+ return new CylinderGeometry(radius, radius, height, 64, 64, true, Math.PI - centralAngle / 2, centralAngle);
645
+ }
646
+ shouldRetryNativeLayer(entity, mesh, isQuad) {
647
+ const unsupported = this.unsupportedLayerStates.get(entity.index);
648
+ if (!unsupported) {
649
+ return true;
650
+ }
651
+ const hasSupportedTransform = resolveLayerReferencePose(mesh, this.player, _position, _quaternion, _scale, _relativeMatrix);
652
+ this.readLayerConfiguration(entity, isQuad, _configuration);
653
+ const hasSupportedConfiguration = isQuad
654
+ ? resolveQuadLayerDimensions(_configuration.x, _configuration.y, _scale) != null
655
+ : resolveCylinderLayerDimensions(_configuration.x, _configuration.y, _scale) != null && isValidCylinderLayerAngle(_configuration.z);
656
+ // Approximate cache equality suppresses repeated work while a layer stays
657
+ // unsupported. Never let that tolerance hide a boundary crossing back to
658
+ // a representable native layer.
659
+ if (hasSupportedTransform && hasSupportedConfiguration) {
660
+ return true;
661
+ }
662
+ return (!sameVector(unsupported.configuration, _configuration) ||
663
+ this.hasLayerShapeChanged(unsupported.relativeMatrix, _relativeMatrix));
664
+ }
665
+ hasLayerShapeChanged(previous, current) {
666
+ const previousElements = previous.elements;
667
+ const currentElements = current.elements;
668
+ const previousFinite = previousElements.every(Number.isFinite);
669
+ const currentFinite = currentElements.every(Number.isFinite);
670
+ if (!previousFinite || !currentFinite) {
671
+ return previousElements.some((value, index) => !sameNumber(value, currentElements[index]));
672
+ }
673
+ const gram = (elements) => {
674
+ const xx = elements[0] ** 2 + elements[1] ** 2 + elements[2] ** 2;
675
+ const yy = elements[4] ** 2 + elements[5] ** 2 + elements[6] ** 2;
676
+ const zz = elements[8] ** 2 + elements[9] ** 2 + elements[10] ** 2;
677
+ const normalizeDot = (dot, first, second) => {
678
+ const denominator = Math.sqrt(first * second);
679
+ return denominator === 0 ? dot : dot / denominator;
680
+ };
681
+ return [
682
+ xx,
683
+ yy,
684
+ zz,
685
+ normalizeDot(elements[0] * elements[4] +
686
+ elements[1] * elements[5] +
687
+ elements[2] * elements[6], xx, yy),
688
+ normalizeDot(elements[0] * elements[8] +
689
+ elements[1] * elements[9] +
690
+ elements[2] * elements[10], xx, zz),
691
+ normalizeDot(elements[4] * elements[8] +
692
+ elements[5] * elements[9] +
693
+ elements[6] * elements[10], yy, zz),
694
+ ];
695
+ };
696
+ const previousGram = gram(previousElements);
697
+ const currentGram = gram(currentElements);
698
+ const nearlyEqual = (a, b, index) => {
699
+ const tolerance = index < 3
700
+ ? 1e-10 * Math.max(Number.EPSILON, Math.abs(a), Math.abs(b))
701
+ : 1e-10;
702
+ return Math.abs(a - b) <= tolerance;
703
+ };
704
+ return (previousGram.some((value, index) => !nearlyEqual(value, currentGram[index], index)) ||
705
+ Math.sign(previous.determinant()) !== Math.sign(current.determinant()));
706
+ }
707
+ rememberUnsupportedLayer(entity, type) {
708
+ if (!this.unsupportedLayerStates.has(entity.index)) {
709
+ console.warn(`[XRLayerSystem] ${type} layer ${entity.index} has unsupported dimensions or a non-finite, reflected, or sheared transform; using projection fallback.`);
710
+ }
711
+ this.unsupportedLayerStates.set(entity.index, {
712
+ relativeMatrix: _relativeMatrix.clone(),
713
+ configuration: _configuration.clone(),
714
+ });
341
715
  }
342
716
  createRenderTarget(width, height, stencil) {
343
717
  return new WebGLRenderTarget(width, height, {