symbiote-ui 0.3.0-alpha.4

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 (322) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +76 -0
  4. package/canvas/AutoLayout.js +731 -0
  5. package/canvas/Breadcrumb/Breadcrumb.css.js +75 -0
  6. package/canvas/Breadcrumb/Breadcrumb.js +96 -0
  7. package/canvas/Breadcrumb/Breadcrumb.tpl.js +7 -0
  8. package/canvas/CanvasConnectionRenderer.js +971 -0
  9. package/canvas/CanvasGraph/CanvasGraph.css.js +29 -0
  10. package/canvas/CanvasGraph/CanvasGraph.js +1697 -0
  11. package/canvas/CanvasGraph/CanvasGraphDrawState.js +280 -0
  12. package/canvas/CanvasGraph/CanvasGraphGeometry.js +194 -0
  13. package/canvas/CanvasViewport.js +550 -0
  14. package/canvas/ConnectionRenderer.js +1283 -0
  15. package/canvas/FlowSimulator.js +326 -0
  16. package/canvas/ForceLayout.js +226 -0
  17. package/canvas/ForceWorker.js +1303 -0
  18. package/canvas/FrameManager.js +223 -0
  19. package/canvas/GraphExplorerShell/GraphExplorerShell.css.js +136 -0
  20. package/canvas/GraphExplorerShell/GraphExplorerShell.js +129 -0
  21. package/canvas/GraphExplorerShell/GraphExplorerShell.tpl.js +12 -0
  22. package/canvas/GraphTabs/GraphTabs.css.js +101 -0
  23. package/canvas/GraphTabs/GraphTabs.js +189 -0
  24. package/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  25. package/canvas/LODManager.js +88 -0
  26. package/canvas/Minimap/Minimap.css.js +73 -0
  27. package/canvas/Minimap/Minimap.js +210 -0
  28. package/canvas/Minimap/Minimap.tpl.js +7 -0
  29. package/canvas/NodeCanvas/NodeCanvas.css.js +398 -0
  30. package/canvas/NodeCanvas/NodeCanvas.js +1499 -0
  31. package/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  32. package/canvas/NodeSearch/NodeSearch.css.js +97 -0
  33. package/canvas/NodeSearch/NodeSearch.js +140 -0
  34. package/canvas/NodeSearch/NodeSearch.tpl.js +25 -0
  35. package/canvas/NodeViewManager.js +748 -0
  36. package/canvas/PcbRouteDiagnostics.js +463 -0
  37. package/canvas/PcbRouter.js +1127 -0
  38. package/canvas/PinExpansion.js +134 -0
  39. package/canvas/PseudoConnection.js +84 -0
  40. package/canvas/SelectionSync.js +163 -0
  41. package/canvas/SubgraphManager.js +203 -0
  42. package/canvas/SubgraphRouter.js +452 -0
  43. package/canvas/ViewportActions.js +473 -0
  44. package/canvas/graph-explorer.js +339 -0
  45. package/canvas/graph-layout.js +148 -0
  46. package/canvas/graph-model.js +68 -0
  47. package/canvas/html-in-canvas.js +202 -0
  48. package/canvas/project-graph-builder.js +440 -0
  49. package/canvas/project-graph-model.js +183 -0
  50. package/chat/ChatComposer/ChatComposer.css.js +652 -0
  51. package/chat/ChatComposer/ChatComposer.js +304 -0
  52. package/chat/ChatList/ChatList.css.js +102 -0
  53. package/chat/ChatList/ChatList.js +99 -0
  54. package/chat/ChatList/ChatList.tpl.js +20 -0
  55. package/chat/ChatListItem/ChatListItem.css.js +117 -0
  56. package/chat/ChatListItem/ChatListItem.js +32 -0
  57. package/chat/ChatListItem/ChatListItem.tpl.js +17 -0
  58. package/chat/ChatMessageItem/ChatMessageItem.css.js +628 -0
  59. package/chat/ChatMessageItem/ChatMessageItem.js +156 -0
  60. package/chat/ChatSidebar/ChatSidebar.css.js +150 -0
  61. package/chat/ChatSidebar/ChatSidebar.js +230 -0
  62. package/chat/ChatSidebar/ChatSidebar.tpl.js +18 -0
  63. package/chat/ChatSidebar/constants.js +11 -0
  64. package/chat/ChatSidebarItem/ChatSidebarItem.css.js +445 -0
  65. package/chat/ChatSidebarItem/ChatSidebarItem.js +304 -0
  66. package/chat/ChatTranscript/ChatTranscript.css.js +90 -0
  67. package/chat/ChatTranscript/ChatTranscript.js +244 -0
  68. package/chat/chat-context.js +123 -0
  69. package/chat/message-model.js +156 -0
  70. package/cli.js +20 -0
  71. package/control/Button/Button.css.js +93 -0
  72. package/control/Button/Button.js +78 -0
  73. package/control/Button/Button.tpl.js +3 -0
  74. package/control/Field/Field.css.js +91 -0
  75. package/control/Field/Field.js +17 -0
  76. package/control/Field/Field.tpl.js +3 -0
  77. package/core/Connection.js +47 -0
  78. package/core/Editor.js +449 -0
  79. package/core/Frame.js +33 -0
  80. package/core/GraphMermaid.js +348 -0
  81. package/core/GraphText.js +228 -0
  82. package/core/Node.js +145 -0
  83. package/core/Portal.js +106 -0
  84. package/core/Socket.js +187 -0
  85. package/core/SubgraphNode.js +121 -0
  86. package/core/base-path.js +55 -0
  87. package/core/dom-utils.js +14 -0
  88. package/core/index.js +18 -0
  89. package/core/local-cache.js +26 -0
  90. package/core/state-sync.js +227 -0
  91. package/custom-elements.json +6380 -0
  92. package/discover.js +240 -0
  93. package/display/Badge/Badge.css.js +44 -0
  94. package/display/Badge/Badge.js +17 -0
  95. package/display/Badge/Badge.tpl.js +3 -0
  96. package/display/Banner/Banner.css.js +61 -0
  97. package/display/Banner/Banner.js +17 -0
  98. package/display/Banner/Banner.tpl.js +3 -0
  99. package/display/CodeBlock/CodeBlock.css.js +194 -0
  100. package/display/CodeBlock/CodeBlock.js +220 -0
  101. package/display/CodeBlock/CodeBlock.tpl.js +11 -0
  102. package/display/DataTable/DataTable.css.js +101 -0
  103. package/display/DataTable/DataTable.js +136 -0
  104. package/display/DataTable/DataTable.tpl.js +13 -0
  105. package/display/EmptyState/EmptyState.css.js +33 -0
  106. package/display/EmptyState/EmptyState.js +17 -0
  107. package/display/EmptyState/EmptyState.tpl.js +3 -0
  108. package/display/EventFeed/EventFeed.css.js +145 -0
  109. package/display/EventFeed/EventFeed.js +64 -0
  110. package/display/EventFeed/EventFeed.tpl.js +14 -0
  111. package/display/EventFeed/EventFeedItem.js +116 -0
  112. package/display/EventFeed/EventFeedItem.tpl.js +22 -0
  113. package/display/LoadingOverlay/LoadingOverlay.css.js +91 -0
  114. package/display/LoadingOverlay/LoadingOverlay.js +48 -0
  115. package/display/LoadingOverlay/LoadingOverlay.tpl.js +12 -0
  116. package/display/Metric/Metric.css.js +60 -0
  117. package/display/Metric/Metric.js +17 -0
  118. package/display/Metric/Metric.tpl.js +6 -0
  119. package/display/OutputGraphPreview/OutputGraphPreview.css.js +122 -0
  120. package/display/OutputGraphPreview/OutputGraphPreview.js +89 -0
  121. package/display/OutputGraphPreview/OutputGraphPreview.tpl.js +13 -0
  122. package/display/OutputListPreview/OutputListPreview.css.js +109 -0
  123. package/display/OutputListPreview/OutputListPreview.js +77 -0
  124. package/display/OutputListPreview/OutputListPreview.tpl.js +13 -0
  125. package/display/SourceEditor/SourceEditor.css.js +39 -0
  126. package/display/SourceEditor/SourceEditor.js +129 -0
  127. package/display/SourceEditor/SourceEditor.tpl.js +10 -0
  128. package/display/SourceViewer/SourceViewer.css.js +80 -0
  129. package/display/SourceViewer/SourceViewer.js +418 -0
  130. package/display/SourceViewer/SourceViewer.tpl.js +17 -0
  131. package/display/StatusRibbon/StatusRibbon.css.js +73 -0
  132. package/display/StatusRibbon/StatusRibbon.js +87 -0
  133. package/display/StatusRibbon/StatusRibbon.tpl.js +7 -0
  134. package/display/event-feed-adapter.js +72 -0
  135. package/display/format-utils.js +29 -0
  136. package/display/highlight.js +659 -0
  137. package/display/icons.js +37 -0
  138. package/display/markdown-formatter.js +60 -0
  139. package/display/network-approval-page.js +487 -0
  140. package/display/output-preview.js +261 -0
  141. package/effects/CellBg/CellBg.css.js +33 -0
  142. package/effects/CellBg/CellBg.js +410 -0
  143. package/effects/CellBg/CellBg.tpl.js +5 -0
  144. package/graph/canvas-adapter.js +223 -0
  145. package/graph/graph-algorithms.js +31 -0
  146. package/graph/index.js +46 -0
  147. package/graph/model.js +176 -0
  148. package/graph/project-graph-build.js +66 -0
  149. package/graph/project-graph-metadata.js +253 -0
  150. package/graph/project-package.js +128 -0
  151. package/graph/project-runtime.js +116 -0
  152. package/graph/project-transaction.js +284 -0
  153. package/graph/skeleton-utils.js +84 -0
  154. package/graph/theme-contract.js +36 -0
  155. package/graph/transaction-parser.js +56 -0
  156. package/icons/MaterialSymbols.js +69 -0
  157. package/icons/material-symbols-outlined-400.ttf +0 -0
  158. package/icons/material-symbols.css +24 -0
  159. package/index.js +95 -0
  160. package/inspector/InspectorPanel/InspectorPanel.css.js +375 -0
  161. package/inspector/InspectorPanel/InspectorPanel.js +368 -0
  162. package/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  163. package/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  164. package/inspector/TemplatePreview/TemplatePreview.js +145 -0
  165. package/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  166. package/interactions/ConnectFlow.js +304 -0
  167. package/interactions/Drag.js +104 -0
  168. package/interactions/Selector.js +133 -0
  169. package/interactions/SnapGrid.js +66 -0
  170. package/interactions/Zoom.js +139 -0
  171. package/layout/ActionZone/ActionZone.css.js +88 -0
  172. package/layout/ActionZone/ActionZone.js +261 -0
  173. package/layout/ActionZone/ActionZone.tpl.js +11 -0
  174. package/layout/CrossLayoutPortalBridge/CrossLayoutPortalBridge.js +255 -0
  175. package/layout/Layout/Layout.css.js +91 -0
  176. package/layout/Layout/Layout.js +637 -0
  177. package/layout/Layout/Layout.tpl.js +27 -0
  178. package/layout/LayoutNode/LayoutNode.css.js +302 -0
  179. package/layout/LayoutNode/LayoutNode.js +509 -0
  180. package/layout/LayoutNode/LayoutNode.tpl.js +39 -0
  181. package/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  182. package/layout/LayoutPreview/LayoutPreview.js +102 -0
  183. package/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  184. package/layout/LayoutRouter/LayoutRouter.js +274 -0
  185. package/layout/LayoutRouter/SectionRegistry.js +135 -0
  186. package/layout/LayoutRouter/routerSync.js +250 -0
  187. package/layout/LayoutSidebar/LayoutSidebar.css.js +411 -0
  188. package/layout/LayoutSidebar/LayoutSidebar.js +368 -0
  189. package/layout/LayoutSidebar/LayoutSidebar.tpl.js +26 -0
  190. package/layout/LayoutSidebar/SidebarSection.css.js +20 -0
  191. package/layout/LayoutSidebar/SidebarSection.js +184 -0
  192. package/layout/LayoutSidebar/SidebarSection.tpl.js +22 -0
  193. package/layout/LayoutTree.js +373 -0
  194. package/layout/PanelMenu/PanelMenu.css.js +43 -0
  195. package/layout/PanelMenu/PanelMenu.js +95 -0
  196. package/layout/PanelMenu/PanelMenu.tpl.js +17 -0
  197. package/layout/ProjectTabs/ProjectTabs.css.js +188 -0
  198. package/layout/ProjectTabs/ProjectTabs.js +77 -0
  199. package/layout/ProjectTabs/ProjectTabs.tpl.js +15 -0
  200. package/layout/index.js +40 -0
  201. package/list/ListDetailShell/ListDetailShell.css.js +128 -0
  202. package/list/ListDetailShell/ListDetailShell.js +72 -0
  203. package/list/ListDetailShell/ListDetailShell.tpl.js +36 -0
  204. package/list/ListItem/ListItem.css.js +111 -0
  205. package/list/ListItem/ListItem.js +66 -0
  206. package/list/ListItem/ListItem.tpl.js +18 -0
  207. package/locale/index.js +503 -0
  208. package/manifest/component-registry.js +2446 -0
  209. package/manifest/graph-schema.js +285 -0
  210. package/manifest/index.js +6 -0
  211. package/manifest/project-schema-catalog.js +246 -0
  212. package/manifest/rule-catalog.js +201 -0
  213. package/manifest/theme-catalog.js +2149 -0
  214. package/manifest/ui-schema-catalog.js +334 -0
  215. package/menu/ContextMenu/ContextMenu.css.js +61 -0
  216. package/menu/ContextMenu/ContextMenu.js +82 -0
  217. package/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  218. package/navigation/QuickOpen/QuickOpen.css.js +92 -0
  219. package/navigation/QuickOpen/QuickOpen.js +185 -0
  220. package/navigation/QuickOpen/QuickOpen.tpl.js +15 -0
  221. package/navigation/quick-open-utils.js +101 -0
  222. package/node/CtrlItem/CtrlItem.css.js +41 -0
  223. package/node/CtrlItem/CtrlItem.js +24 -0
  224. package/node/CtrlItem/CtrlItem.tpl.js +17 -0
  225. package/node/GraphFrame/GraphFrame.css.js +66 -0
  226. package/node/GraphFrame/GraphFrame.js +32 -0
  227. package/node/GraphFrame/GraphFrame.tpl.js +13 -0
  228. package/node/GraphNode/GraphNode.css.js +815 -0
  229. package/node/GraphNode/GraphNode.js +173 -0
  230. package/node/GraphNode/GraphNode.tpl.js +33 -0
  231. package/node/NodeCallout/NodeCallout.css.js +91 -0
  232. package/node/NodeCallout/NodeCallout.js +281 -0
  233. package/node/NodeCallout/NodeCallout.tpl.js +8 -0
  234. package/node/NodeSocket/NodeSocket.css.js +68 -0
  235. package/node/NodeSocket/NodeSocket.js +26 -0
  236. package/node/NodeSocket/NodeSocket.tpl.js +7 -0
  237. package/node/PortItem/PortItem.css.js +93 -0
  238. package/node/PortItem/PortItem.js +87 -0
  239. package/node/PortItem/PortItem.tpl.js +10 -0
  240. package/package.json +165 -0
  241. package/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  242. package/palette/PaletteBrowser/PaletteBrowser.js +152 -0
  243. package/palette/PaletteBrowser/PaletteBrowser.tpl.js +23 -0
  244. package/plugins/History.js +408 -0
  245. package/plugins/Readonly.js +60 -0
  246. package/rules/symbiote-3x.json +170 -0
  247. package/schemas/component-descriptor-v1.json +91 -0
  248. package/schemas/component-descriptor-v2.json +145 -0
  249. package/schemas/graph-model-v1.json +179 -0
  250. package/schemas/graph-v1.json +91 -0
  251. package/schemas/project-package-v1.json +102 -0
  252. package/schemas/project-transaction-v1.json +114 -0
  253. package/schemas/runtime-ui-v1.json +80 -0
  254. package/schemas/theme-rule-block-v1.json +73 -0
  255. package/shapes/CircleShape.js +79 -0
  256. package/shapes/CommentShape.js +35 -0
  257. package/shapes/DiamondShape.js +130 -0
  258. package/shapes/NodeShape.js +79 -0
  259. package/shapes/PillShape.js +91 -0
  260. package/shapes/RectShape.js +84 -0
  261. package/shapes/SVGShape.js +525 -0
  262. package/shapes/index.js +63 -0
  263. package/surface/Card/Card.css.js +57 -0
  264. package/surface/Card/Card.js +17 -0
  265. package/surface/Card/Card.tpl.js +3 -0
  266. package/themes/Palette.js +30 -0
  267. package/themes/Skin.js +113 -0
  268. package/themes/Theme.js +82 -0
  269. package/themes/carbon.js +135 -0
  270. package/themes/dark.js +140 -0
  271. package/themes/default-dark.js +714 -0
  272. package/themes/default-provider.css +635 -0
  273. package/themes/default-provider.js +718 -0
  274. package/themes/ebook.js +136 -0
  275. package/themes/grey.js +137 -0
  276. package/themes/light.js +139 -0
  277. package/themes/neon.js +138 -0
  278. package/themes/pcb.js +273 -0
  279. package/themes/synthwave.js +138 -0
  280. package/tokens/base.json +29 -0
  281. package/tokens/themes/carbon.json +11 -0
  282. package/tokens/themes/dark.json +12 -0
  283. package/tokens/themes/default-dark.json +1543 -0
  284. package/tokens/themes/default-provider.json +1543 -0
  285. package/tokens/themes/ebook.json +11 -0
  286. package/tokens/themes/grey.json +11 -0
  287. package/tokens/themes/light.json +12 -0
  288. package/tokens/themes/neon.json +11 -0
  289. package/tokens/themes/pcb.json +11 -0
  290. package/tokens/themes/synthwave.json +11 -0
  291. package/toolbar/QuickToolbar/QuickToolbar.css.js +152 -0
  292. package/toolbar/QuickToolbar/QuickToolbar.js +529 -0
  293. package/toolbar/QuickToolbar/QuickToolbar.tpl.js +34 -0
  294. package/tree/TreePanel/TreePanel.css.js +112 -0
  295. package/tree/TreePanel/TreePanel.js +147 -0
  296. package/tree/TreePanel/TreePanel.tpl.js +18 -0
  297. package/tree/TreeView/TreeView.css.js +122 -0
  298. package/tree/TreeView/TreeView.js +365 -0
  299. package/tree/TreeView/TreeView.tpl.js +10 -0
  300. package/ui/dialogs.js +221 -0
  301. package/ui/host-adapters.js +114 -0
  302. package/ui/index.js +660 -0
  303. package/ui/locale.js +50 -0
  304. package/ui/overlay-stack.js +89 -0
  305. package/ui/shared-styles.js +26 -0
  306. package/webmcp.js +37 -0
  307. package/xr/deep-graph.js +646 -0
  308. package/xr/emulation.js +198 -0
  309. package/xr/gesture.js +228 -0
  310. package/xr/html-canvas-renderer.js +472 -0
  311. package/xr/index.js +15 -0
  312. package/xr/layout-projection.js +1046 -0
  313. package/xr/panel-frame.js +128 -0
  314. package/xr/panel-host.js +267 -0
  315. package/xr/pointer.js +258 -0
  316. package/xr/scene-controller.js +242 -0
  317. package/xr/spatial-scene.js +212 -0
  318. package/xr/theme-bridge.js +105 -0
  319. package/xr/three-webxr-adapter.js +3439 -0
  320. package/xr/webgl-layer-renderer.js +419 -0
  321. package/xr/webxr.js +679 -0
  322. package/xr/workbench.js +516 -0
@@ -0,0 +1,3439 @@
1
+ import {
2
+ WEBXR_FEATURES,
3
+ createWebXRLaunchGateSummary,
4
+ createXRReadinessSummary,
5
+ redactXRDiagnosticUrl,
6
+ requestWebXRSession,
7
+ } from './webxr.js';
8
+ import {
9
+ createXRHtmlCanvasRenderer,
10
+ createXRPanelTextureSourceSummary,
11
+ } from './html-canvas-renderer.js';
12
+ import { createXRPanelFrame, hitTestXRPanelFrame } from './panel-frame.js';
13
+ import {
14
+ createXRPanelTextureQualitySummary,
15
+ createXRTextureQualityPolicy,
16
+ } from './layout-projection.js';
17
+ import {
18
+ selectPrimaryXRInputSource,
19
+ } from './pointer.js';
20
+ import {
21
+ createXRSceneRootTransform,
22
+ createXRViewerPoseSnapshot,
23
+ } from './spatial-scene.js';
24
+
25
+ export const XR_THREE_WEBXR_ADAPTER = Object.freeze({
26
+ name: 'three-webxr',
27
+ status: 'optional-adapter',
28
+ specifier: 'symbiote-ui/xr',
29
+ description: 'Optional Three.js WebXR adapter for Symbiote XR scenes. The host supplies the THREE module.',
30
+ modes: ['immersive-vr', 'immersive-ar'],
31
+ fallback: 'dom-canvas',
32
+ dependency: {
33
+ name: 'three',
34
+ injection: 'host-supplied',
35
+ required: false,
36
+ },
37
+ capabilities: [
38
+ 'three-webxr-manager',
39
+ 'three-scene-panels',
40
+ 'three-controller-rays',
41
+ 'three-controller-ray-visuals',
42
+ 'three-panel-hit-reticle',
43
+ 'three-panel-frame-hit-target',
44
+ 'three-panel-frame-visuals',
45
+ 'three-frame-target-drag-gate',
46
+ 'three-frame-target-resize-persistence',
47
+ 'three-interaction-state-diagnostics',
48
+ 'three-session-telemetry-snapshot',
49
+ 'three-session-runtime-diagnostics',
50
+ 'three-session-options-diagnostics',
51
+ 'three-session-options-builder',
52
+ 'three-session-health-summary',
53
+ 'three-session-watchdog',
54
+ 'three-diagnostic-payload',
55
+ 'three-diagnostic-timeline',
56
+ 'three-diagnostic-server-summary',
57
+ 'three-troubleshooting-summary',
58
+ 'three-raycaster-controller',
59
+ 'three-ray-plane-panel-drag',
60
+ 'three-drag-response-filter',
61
+ 'three-session-controller',
62
+ 'three-render-host',
63
+ 'three-render-host-diagnostics',
64
+ 'three-render-loop',
65
+ 'three-dom-texture-material-bridge',
66
+ 'three-html-canvas-texture-resolver',
67
+ 'three-texture-quality-policy',
68
+ 'three-dirty-texture-redraw',
69
+ 'three-strict-texture-fail-fast',
70
+ 'three-scene-decoration',
71
+ 'three-camera-resize',
72
+ 'three-controller-select-events',
73
+ 'three-primary-input-source',
74
+ 'three-animation-loop',
75
+ 'three-panel-material-state',
76
+ 'three-session-diagnostics',
77
+ 'symbiote-xr-scene-adapter',
78
+ 'host-supplied-three',
79
+ ],
80
+ });
81
+
82
+ function hasFn(source, name) {
83
+ return typeof source?.[name] === 'function';
84
+ }
85
+
86
+ function assertThree(THREE) {
87
+ let missing = [
88
+ 'Scene',
89
+ 'PerspectiveCamera',
90
+ 'WebGLRenderer',
91
+ 'PlaneGeometry',
92
+ 'Mesh',
93
+ 'MeshStandardMaterial',
94
+ 'Raycaster',
95
+ ].filter((name) => typeof THREE?.[name] !== 'function');
96
+ if (missing.length) {
97
+ return {
98
+ ok: false,
99
+ reason: 'missing-three-api',
100
+ missing,
101
+ };
102
+ }
103
+ return { ok: true, missing: [] };
104
+ }
105
+
106
+ function callSetter(target, method, values = []) {
107
+ if (hasFn(target, method)) {
108
+ target[method](...values);
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+
114
+ function normalizeStringList(value) {
115
+ if (!value) return [];
116
+ try {
117
+ return [...value].map((item) => String(item)).filter(Boolean);
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+
123
+ function normalizeInputSources(inputSources) {
124
+ if (!inputSources) return [];
125
+ try {
126
+ return [...inputSources].map((source) => ({
127
+ handedness: source?.handedness || '',
128
+ targetRayMode: source?.targetRayMode || '',
129
+ profiles: normalizeStringList(source?.profiles),
130
+ }));
131
+ } catch {
132
+ return [];
133
+ }
134
+ }
135
+
136
+ function summarizeTextureSourceQuality(textureSources = []) {
137
+ let sources = Array.isArray(textureSources) ? textureSources : [];
138
+ let statuses = new Map();
139
+ let warnings = [];
140
+ let recommendations = [];
141
+ for (let source of sources) {
142
+ let status = source?.textureQuality?.status || source?.qualityStatus || null;
143
+ if (status) {
144
+ statuses.set(status, (statuses.get(status) || 0) + 1);
145
+ }
146
+ let sourceWarnings = source?.textureQuality?.warnings || source?.qualityWarnings || [];
147
+ for (let warning of sourceWarnings) {
148
+ warnings.push({
149
+ panelId: source?.panelId || null,
150
+ code: String(warning),
151
+ });
152
+ }
153
+ let sourceRecommendations = source?.textureQuality?.recommendations || source?.qualityRecommendations || [];
154
+ for (let recommendation of sourceRecommendations) {
155
+ recommendations.push({
156
+ panelId: source?.panelId || null,
157
+ code: String(recommendation),
158
+ });
159
+ }
160
+ }
161
+ let priority = new Map([
162
+ ['provide-texture-size', 0],
163
+ ['increase-texture-resolution', 1],
164
+ ['increase-texture-density-to-target', 2],
165
+ ['increase-texture-pixel-ratio', 3],
166
+ ['increase-max-texture-size', 4],
167
+ ]);
168
+ let actionMap = new Map();
169
+ for (let recommendation of recommendations) {
170
+ let code = recommendation.code || 'recommendation';
171
+ let action = actionMap.get(code) || {
172
+ code,
173
+ count: 0,
174
+ panelIds: [],
175
+ priority: priority.get(code) ?? 99,
176
+ };
177
+ action.count += 1;
178
+ if (recommendation.panelId && !action.panelIds.includes(recommendation.panelId)) {
179
+ action.panelIds.push(recommendation.panelId);
180
+ }
181
+ actionMap.set(code, action);
182
+ }
183
+ let actions = [...actionMap.values()]
184
+ .sort((first, second) => first.priority - second.priority || second.count - first.count || first.code.localeCompare(second.code))
185
+ .map(({ priority: _priority, ...action }) => action);
186
+ return {
187
+ total: sources.length,
188
+ target: Number(statuses.get('target') || 0),
189
+ readable: Number(statuses.get('readable') || 0),
190
+ low: Number(statuses.get('low') || 0),
191
+ blocked: Number(statuses.get('blocked') || 0),
192
+ warningCount: warnings.length,
193
+ warnings,
194
+ recommendationCount: recommendations.length,
195
+ recommendations,
196
+ primaryRecommendation: actions[0]?.code || null,
197
+ actions,
198
+ };
199
+ }
200
+
201
+ function applyVector(target, values = []) {
202
+ if (!target) return;
203
+ if (!callSetter(target, 'fromArray', [values])) {
204
+ callSetter(target, 'set', values);
205
+ }
206
+ }
207
+
208
+ function applyRotation(target, values = []) {
209
+ if (!target) return;
210
+ if (hasFn(target, 'set')) {
211
+ target.set(...values.map((value) => Number(value || 0) * Math.PI / 180));
212
+ }
213
+ }
214
+
215
+ function readBounds(source, fallback = {}) {
216
+ let rect = source?.getBoundingClientRect?.() || source || {};
217
+ let width = Number(rect.width || fallback.width || 1280);
218
+ let height = Number(rect.height || fallback.height || 720);
219
+ return {
220
+ width: Math.max(1, Math.round(width)),
221
+ height: Math.max(1, Math.round(height)),
222
+ };
223
+ }
224
+
225
+ function clampPixelRatio(value, max = 2) {
226
+ let ratio = Number(value || 1);
227
+ if (!Number.isFinite(ratio) || ratio <= 0) return 1;
228
+ return Math.min(ratio, Number(max || 2));
229
+ }
230
+
231
+ function nowMs(options = {}) {
232
+ if (typeof options.now === 'function') {
233
+ return Number(options.now());
234
+ }
235
+ let value = Number(options.now);
236
+ return Number.isFinite(value) ? value : Date.now();
237
+ }
238
+
239
+ function normalizeRayVisualOptions(options = {}) {
240
+ let length = Number(options.rayLength ?? options.length ?? 3);
241
+ let color = options.rayColor ?? options.color ?? 0x7fd6ff;
242
+ let opacity = Number(options.rayOpacity ?? options.opacity ?? 0.84);
243
+ return {
244
+ enabled: options.enabled !== false,
245
+ length: Number.isFinite(length) ? Math.max(0.2, Math.min(8, length)) : 3,
246
+ color,
247
+ opacity: Number.isFinite(opacity) ? Math.max(0.05, Math.min(1, opacity)) : 0.84,
248
+ };
249
+ }
250
+
251
+ function buildControllerRayVisual(THREE, options = {}) {
252
+ let visual = normalizeRayVisualOptions(options);
253
+ if (!visual.enabled) {
254
+ return { ok: false, reason: 'disabled' };
255
+ }
256
+ let missing = ['BufferGeometry', 'Float32BufferAttribute', 'LineBasicMaterial', 'Line']
257
+ .filter((name) => typeof THREE?.[name] !== 'function');
258
+ if (missing.length) {
259
+ return { ok: false, reason: 'missing-three-ray-visual-api', missing };
260
+ }
261
+ let geometry = new THREE.BufferGeometry();
262
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute([
263
+ 0, 0, 0,
264
+ 0, 0, -visual.length,
265
+ ], 3));
266
+ let material = new THREE.LineBasicMaterial({
267
+ color: visual.color,
268
+ transparent: true,
269
+ opacity: visual.opacity,
270
+ depthTest: false,
271
+ });
272
+ let line = new THREE.Line(geometry, material);
273
+ line.name = 'sn-xr-controller-ray';
274
+ line.userData ||= {};
275
+ line.userData.snControllerRay = true;
276
+ line.renderOrder = Number(options.renderOrder ?? 20);
277
+ return {
278
+ ok: true,
279
+ object: line,
280
+ type: 'controller-ray',
281
+ length: visual.length,
282
+ color: visual.color,
283
+ opacity: visual.opacity,
284
+ };
285
+ }
286
+
287
+ function normalizeHitReticleOptions(options = {}) {
288
+ let innerRadius = Number(options.innerRadius ?? 0.018);
289
+ let outerRadius = Number(options.outerRadius ?? 0.032);
290
+ let color = options.color ?? 0x9ee7ff;
291
+ let opacity = Number(options.opacity ?? 0.92);
292
+ return {
293
+ enabled: options.enabled !== false,
294
+ innerRadius: Number.isFinite(innerRadius) ? Math.max(0.004, Math.min(0.08, innerRadius)) : 0.018,
295
+ outerRadius: Number.isFinite(outerRadius) ? Math.max(0.006, Math.min(0.12, outerRadius)) : 0.032,
296
+ color,
297
+ opacity: Number.isFinite(opacity) ? Math.max(0.05, Math.min(1, opacity)) : 0.92,
298
+ };
299
+ }
300
+
301
+ function buildPanelHitReticleVisual(THREE, options = {}) {
302
+ let visual = normalizeHitReticleOptions(options);
303
+ if (!visual.enabled) {
304
+ return { ok: false, reason: 'disabled' };
305
+ }
306
+ let missing = ['RingGeometry', 'MeshBasicMaterial', 'Mesh']
307
+ .filter((name) => typeof THREE?.[name] !== 'function');
308
+ if (missing.length) {
309
+ return { ok: false, reason: 'missing-three-hit-reticle-api', missing };
310
+ }
311
+ let geometry = new THREE.RingGeometry(visual.innerRadius, visual.outerRadius, Number(options.segments || 32));
312
+ let material = new THREE.MeshBasicMaterial({
313
+ color: visual.color,
314
+ transparent: true,
315
+ opacity: visual.opacity,
316
+ depthTest: false,
317
+ side: THREE.DoubleSide,
318
+ });
319
+ let reticle = new THREE.Mesh(geometry, material);
320
+ reticle.name = 'sn-xr-panel-hit-reticle';
321
+ reticle.visible = false;
322
+ reticle.renderOrder = Number(options.renderOrder ?? 30);
323
+ reticle.userData ||= {};
324
+ reticle.userData.snPanelHitReticle = true;
325
+ return {
326
+ ok: true,
327
+ object: reticle,
328
+ type: 'panel-hit-reticle',
329
+ innerRadius: visual.innerRadius,
330
+ outerRadius: visual.outerRadius,
331
+ color: visual.color,
332
+ opacity: visual.opacity,
333
+ };
334
+ }
335
+
336
+ function updatePanelHitReticleVisual(reticle, hit) {
337
+ let object = reticle?.object || reticle;
338
+ if (!object) return { ok: false, reason: 'missing-hit-reticle' };
339
+ if (!hit?.point || !hit?.object) {
340
+ object.visible = false;
341
+ object.userData ||= {};
342
+ object.userData.panelId = null;
343
+ return { ok: true, visible: false, panelId: null };
344
+ }
345
+ object.visible = true;
346
+ object.userData ||= {};
347
+ object.userData.panelId = hit.object.userData?.panelId || null;
348
+ if (object.position?.copy) object.position.copy(hit.point);
349
+ else applyVector(object.position, [hit.point.x, hit.point.y, hit.point.z]);
350
+ if (object.quaternion?.copy && hit.object.quaternion) object.quaternion.copy(hit.object.quaternion);
351
+ return {
352
+ ok: true,
353
+ visible: true,
354
+ panelId: object.userData.panelId,
355
+ point: vectorData(hit.point),
356
+ distance: Number(hit.distance || 0),
357
+ };
358
+ }
359
+
360
+ function normalizePanelFrameVisualOptions(options = {}) {
361
+ let headerColor = options.headerColor ?? options.color ?? 0x7fd6ff;
362
+ let handleColor = options.handleColor ?? options.color ?? 0x9ee7ff;
363
+ let actionColor = options.actionColor ?? options.color ?? 0xffffff;
364
+ let opacity = Number(options.opacity ?? 0.34);
365
+ let handleOpacity = Number(options.handleOpacity ?? 0.62);
366
+ return {
367
+ enabled: options.enabled !== false,
368
+ headerColor,
369
+ handleColor,
370
+ actionColor,
371
+ opacity: Number.isFinite(opacity) ? Math.max(0.04, Math.min(1, opacity)) : 0.34,
372
+ handleOpacity: Number.isFinite(handleOpacity) ? Math.max(0.04, Math.min(1, handleOpacity)) : 0.62,
373
+ zOffset: Number(options.zOffset ?? 0.006),
374
+ renderOrder: Number(options.renderOrder ?? 28),
375
+ };
376
+ }
377
+
378
+ function frameZoneToPanelRect(zone = {}, size = [0.8, 0.45]) {
379
+ let width = Math.max(0.001, Number(size[0] || 0.8));
380
+ let height = Math.max(0.001, Number(size[1] || 0.45));
381
+ let zoneWidth = Math.max(0.001, Number(zone.width || 0) * width);
382
+ let zoneHeight = Math.max(0.001, Number(zone.height || 0) * height);
383
+ return {
384
+ width: zoneWidth,
385
+ height: zoneHeight,
386
+ x: -width / 2 + Number(zone.x || 0) * width + zoneWidth / 2,
387
+ y: height / 2 - Number(zone.y || 0) * height - zoneHeight / 2,
388
+ };
389
+ }
390
+
391
+ function removePanelFrameVisuals(mesh) {
392
+ let objects = mesh?.userData?.panelFrameVisuals?.objects || [];
393
+ for (let object of objects) {
394
+ if (typeof mesh?.remove === 'function') {
395
+ mesh.remove(object);
396
+ } else if (Array.isArray(mesh?.children)) {
397
+ let index = mesh.children.indexOf(object);
398
+ if (index >= 0) mesh.children.splice(index, 1);
399
+ }
400
+ }
401
+ }
402
+
403
+ function addPanelFrameVisualObject(mesh, object) {
404
+ if (typeof mesh?.add === 'function') {
405
+ mesh.add(object);
406
+ return true;
407
+ }
408
+ if (Array.isArray(mesh?.children)) {
409
+ mesh.children.push(object);
410
+ return true;
411
+ }
412
+ return false;
413
+ }
414
+
415
+ function buildPanelFrameZoneVisual(THREE, zoneName, zone, size, visual, metadata = {}) {
416
+ let rect = frameZoneToPanelRect(zone, size);
417
+ let geometry = new THREE.PlaneGeometry(rect.width, rect.height);
418
+ let material = new THREE.MeshBasicMaterial({
419
+ color: metadata.color ?? visual.handleColor,
420
+ transparent: true,
421
+ opacity: metadata.opacity ?? visual.handleOpacity,
422
+ depthTest: false,
423
+ side: THREE.DoubleSide,
424
+ });
425
+ let object = new THREE.Mesh(geometry, material);
426
+ object.name = `sn-xr-panel-frame-${zoneName}`;
427
+ object.renderOrder = visual.renderOrder;
428
+ object.userData ||= {};
429
+ object.userData.snPanelFrameVisual = true;
430
+ object.userData.zone = metadata.zone || zoneName;
431
+ object.userData.operation = metadata.operation || 'focus';
432
+ object.userData.handle = metadata.handle || null;
433
+ object.userData.action = metadata.action || null;
434
+ object.userData.baseColor = metadata.color ?? visual.handleColor;
435
+ object.userData.baseOpacity = metadata.opacity ?? visual.handleOpacity;
436
+ if (object.position?.set) {
437
+ object.position.set(rect.x, rect.y, visual.zOffset);
438
+ } else {
439
+ object.position = { x: rect.x, y: rect.y, z: visual.zOffset };
440
+ }
441
+ return object;
442
+ }
443
+
444
+ function buildPanelFrameVisuals(THREE, panel, mesh, options = {}) {
445
+ let visual = normalizePanelFrameVisualOptions(options);
446
+ if (!visual.enabled) {
447
+ return { ok: false, reason: 'disabled', objects: [] };
448
+ }
449
+ let missing = ['PlaneGeometry', 'MeshBasicMaterial', 'Mesh']
450
+ .filter((name) => typeof THREE?.[name] !== 'function');
451
+ if (missing.length) {
452
+ return { ok: false, reason: 'missing-three-panel-frame-visual-api', missing, objects: [] };
453
+ }
454
+ if (!mesh) {
455
+ return { ok: false, reason: 'missing-panel-mesh', objects: [] };
456
+ }
457
+
458
+ removePanelFrameVisuals(mesh);
459
+ let frame = createXRPanelFrame(panel || {}, options.frame || {});
460
+ let size = readPanelSize(mesh);
461
+ let objects = [];
462
+ let zones = [];
463
+
464
+ let moveObject = buildPanelFrameZoneVisual(THREE, 'move', frame.zones.move, size, visual, {
465
+ zone: 'move',
466
+ operation: 'move',
467
+ color: visual.headerColor,
468
+ opacity: visual.opacity,
469
+ });
470
+ moveObject.userData.panelId = frame.panelId;
471
+ if (addPanelFrameVisualObject(mesh, moveObject)) {
472
+ objects.push(moveObject);
473
+ zones.push('move');
474
+ }
475
+
476
+ for (let [handle, zone] of Object.entries(frame.zones.resize || {})) {
477
+ let object = buildPanelFrameZoneVisual(THREE, `resize-${handle}`, zone, size, visual, {
478
+ zone: 'resize',
479
+ operation: 'resize',
480
+ handle,
481
+ });
482
+ object.userData.panelId = frame.panelId;
483
+ if (addPanelFrameVisualObject(mesh, object)) {
484
+ objects.push(object);
485
+ zones.push(`resize:${handle}`);
486
+ }
487
+ }
488
+
489
+ for (let [action, zone] of Object.entries(frame.zones.actions || {})) {
490
+ let object = buildPanelFrameZoneVisual(THREE, `action-${action}`, zone, size, visual, {
491
+ zone: 'action',
492
+ operation: 'action',
493
+ action,
494
+ color: visual.actionColor,
495
+ opacity: visual.opacity,
496
+ });
497
+ object.userData.panelId = frame.panelId;
498
+ if (addPanelFrameVisualObject(mesh, object)) {
499
+ objects.push(object);
500
+ zones.push(`action:${action}`);
501
+ }
502
+ }
503
+
504
+ let summary = {
505
+ ok: true,
506
+ type: 'panel-frame-visuals',
507
+ panelId: frame.panelId,
508
+ objectCount: objects.length,
509
+ zones,
510
+ header: true,
511
+ resizeHandles: Object.keys(frame.zones.resize || {}).length,
512
+ actionSlots: Object.keys(frame.zones.actions || {}).length,
513
+ objects,
514
+ };
515
+ mesh.userData ||= {};
516
+ mesh.userData.panelFrameVisuals = summary;
517
+ mesh.userData.updatePanelFrameVisuals = () => buildPanelFrameVisuals(THREE, mesh.userData.panel || panel, mesh, options);
518
+ return summary;
519
+ }
520
+
521
+ function createMaterial(THREE, panel, options = {}) {
522
+ let material = panel.material || {};
523
+ let color = options.colorResolver?.(panel) ||
524
+ material.threeColor ||
525
+ material.backgroundColor ||
526
+ panel.color ||
527
+ 0x243244;
528
+ if (typeof THREE.MeshBasicMaterial === 'function') {
529
+ return new THREE.MeshBasicMaterial({
530
+ color,
531
+ side: THREE.DoubleSide,
532
+ transparent: false,
533
+ opacity: Number(options.opacity ?? 1),
534
+ });
535
+ }
536
+ return new THREE.MeshStandardMaterial({
537
+ color,
538
+ roughness: Number(options.roughness ?? 0.72),
539
+ metalness: Number(options.metalness ?? 0.04),
540
+ side: THREE.DoubleSide,
541
+ transparent: false,
542
+ opacity: Number(options.opacity ?? 1),
543
+ });
544
+ }
545
+
546
+ function applyStrictTextureDiagnosticMaterial(mesh, textureRecord, options = {}) {
547
+ if (!mesh) return;
548
+ mesh.visible = false;
549
+ mesh.userData ||= {};
550
+ mesh.userData.strictTextureHidden = true;
551
+ mesh.userData.strictTextureDiagnostic = false;
552
+ mesh.userData.strictTextureDiagnosticReason = textureRecord?.reason || textureRecord?.stage || 'texture-unavailable';
553
+ let material = mesh.material;
554
+ if (!material) return;
555
+ material.map = null;
556
+ material.transparent = false;
557
+ material.opacity = Number(options.strictTextureDiagnosticOpacity ?? 1);
558
+ if (material.color?.setHex) {
559
+ material.color.setHex(Number(options.strictTextureDiagnosticColor ?? 0x5b6572));
560
+ } else {
561
+ material.color = Number(options.strictTextureDiagnosticColor ?? 0x5b6572);
562
+ }
563
+ if (material.emissive?.setHex) {
564
+ material.emissive.setHex(Number(options.strictTextureDiagnosticEmissive ?? 0x15191f));
565
+ }
566
+ markMaterialUpdated(material);
567
+ }
568
+
569
+ function resolvePanelElement(panel, options = {}) {
570
+ if (typeof options.getPanelElement === 'function') {
571
+ return options.getPanelElement(panel.id, panel);
572
+ }
573
+ if (options.panelElements?.get) {
574
+ return options.panelElements.get(panel.id) || null;
575
+ }
576
+ if (options.panelElements && typeof options.panelElements === 'object') {
577
+ return options.panelElements[panel.id] || null;
578
+ }
579
+ return null;
580
+ }
581
+
582
+ function markMaterialUpdated(material) {
583
+ if (!material) return;
584
+ material.needsUpdate = true;
585
+ }
586
+
587
+ function applyMaterialColor(material, color) {
588
+ if (!material?.color || color == null) return false;
589
+ if (typeof color === 'number' && hasFn(material.color, 'setHex')) {
590
+ material.color.setHex(color);
591
+ markMaterialUpdated(material);
592
+ return true;
593
+ }
594
+ if (typeof color === 'string' && hasFn(material.color, 'setStyle')) {
595
+ material.color.setStyle(color);
596
+ markMaterialUpdated(material);
597
+ return true;
598
+ }
599
+ if (hasFn(material.color, 'set')) {
600
+ material.color.set(color);
601
+ markMaterialUpdated(material);
602
+ return true;
603
+ }
604
+ return false;
605
+ }
606
+
607
+ function colorSummary(color) {
608
+ if (color == null) return null;
609
+ if (typeof color === 'number' || typeof color === 'string') return color;
610
+ if (typeof color.getHexString === 'function') return `#${color.getHexString()}`;
611
+ if (typeof color.getHex === 'function') return color.getHex();
612
+ return null;
613
+ }
614
+
615
+ function textureSummary(texture) {
616
+ if (!texture) return null;
617
+ let image = texture.image || null;
618
+ return {
619
+ name: texture.name || null,
620
+ isTexture: texture.isTexture === true,
621
+ kind: texture.isHTMLTexture ? 'html-texture' : texture.isCanvasTexture ? 'canvas-texture' : texture.isTexture ? 'texture' : 'host-texture',
622
+ width: Number.isFinite(Number(image?.width)) ? Number(image.width) : null,
623
+ height: Number.isFinite(Number(image?.height)) ? Number(image.height) : null,
624
+ colorSpace: texture.colorSpace || texture.encoding || null,
625
+ premultiplyAlpha: texture.premultiplyAlpha == null ? null : Boolean(texture.premultiplyAlpha),
626
+ flipY: texture.flipY == null ? null : Boolean(texture.flipY),
627
+ generateMipmaps: texture.generateMipmaps == null ? null : Boolean(texture.generateMipmaps),
628
+ needsUpdate: texture.needsUpdate == null ? null : Boolean(texture.needsUpdate),
629
+ };
630
+ }
631
+
632
+ function materialSummary(mesh) {
633
+ let material = mesh?.material || null;
634
+ return {
635
+ panelId: mesh?.userData?.panelId || null,
636
+ visible: mesh?.visible !== false,
637
+ transparent: material?.transparent === true,
638
+ opacity: Number.isFinite(Number(material?.opacity)) ? Number(material.opacity) : null,
639
+ mapApplied: Boolean(material?.map),
640
+ mapName: material?.map?.name || null,
641
+ texture: textureSummary(material?.map),
642
+ color: colorSummary(material?.color),
643
+ emissive: colorSummary(material?.emissive),
644
+ side: material?.side == null ? null : String(material.side),
645
+ depthTest: material?.depthTest == null ? null : Boolean(material.depthTest),
646
+ depthWrite: material?.depthWrite == null ? null : Boolean(material.depthWrite),
647
+ renderOrder: Number.isFinite(Number(mesh?.renderOrder)) ? Number(mesh.renderOrder) : 0,
648
+ strictDiagnostic: mesh?.userData?.strictTextureDiagnostic === true,
649
+ strictDiagnosticReason: mesh?.userData?.strictTextureDiagnosticReason || null,
650
+ };
651
+ }
652
+
653
+ function summarizePanelMaterials(meshes = []) {
654
+ let panels = meshes.map(materialSummary);
655
+ return {
656
+ version: 'xr-three-panel-material-diagnostics-v1',
657
+ total: panels.length,
658
+ transparentCount: panels.filter((panel) => panel.transparent).length,
659
+ mappedCount: panels.filter((panel) => panel.mapApplied).length,
660
+ strictDiagnosticCount: panels.filter((panel) => panel.strictDiagnostic).length,
661
+ strictDiagnosticPanelIds: panels.filter((panel) => panel.strictDiagnostic).map((panel) => panel.panelId).filter(Boolean),
662
+ panels,
663
+ };
664
+ }
665
+
666
+ function applyPanelFrameVisualState(mesh, resolved = {}) {
667
+ let objects = mesh?.userData?.panelFrameVisuals?.objects || [];
668
+ let state = resolved.state || 'default';
669
+ let updated = 0;
670
+ let active = state !== 'default';
671
+ for (let object of objects) {
672
+ let material = object?.material || null;
673
+ let color = active ? resolved.color : object?.userData?.baseColor;
674
+ let colorApplied = applyMaterialColor(material, color);
675
+ let opacityApplied = false;
676
+ if (material && 'opacity' in material) {
677
+ let baseOpacity = Number(object?.userData?.baseOpacity ?? material.opacity ?? 0.34);
678
+ material.opacity = active ? Math.min(1, baseOpacity + 0.22) : baseOpacity;
679
+ markMaterialUpdated(material);
680
+ opacityApplied = true;
681
+ }
682
+ object.userData ||= {};
683
+ object.userData.state = state;
684
+ if (colorApplied || opacityApplied) updated += 1;
685
+ }
686
+ return {
687
+ count: objects.length,
688
+ updated,
689
+ state,
690
+ };
691
+ }
692
+
693
+ function resolvePanelStateColor(mesh, state = {}, themeSnapshot = {}) {
694
+ let panelId = mesh?.userData?.panelId || null;
695
+ let material = themeSnapshot.material || {};
696
+ let baseColor = mesh?.userData?.baseColor ||
697
+ mesh?.userData?.panel?.material?.backgroundColor ||
698
+ material.backgroundColor ||
699
+ material.background ||
700
+ null;
701
+ if (panelId && (panelId === state.draggingPanelId || panelId === state.selectedPanelId)) {
702
+ return {
703
+ state: panelId === state.draggingPanelId ? 'dragging' : 'selected',
704
+ color: material.pointerColor || material.pointer || baseColor,
705
+ };
706
+ }
707
+ if (panelId && panelId === state.hoverPanelId) {
708
+ return {
709
+ state: 'hover',
710
+ color: material.borderColor || material.border || baseColor,
711
+ };
712
+ }
713
+ return {
714
+ state: 'default',
715
+ color: baseColor,
716
+ };
717
+ }
718
+
719
+ export function updateXRThreePanelMaterialStates(options = {}) {
720
+ let adapter = options.adapter || null;
721
+ let meshes = options.meshes || adapter?.listPanelMeshes?.() || [];
722
+ let sessionState = options.sessionState || {};
723
+ let themeSnapshot = options.themeSnapshot || {};
724
+ let state = {
725
+ hoverPanelId: sessionState.hover?.panelId || sessionState.hoverPanelId || null,
726
+ selectedPanelId: sessionState.selectedPanelId || null,
727
+ draggingPanelId: sessionState.draggingPanelId || null,
728
+ };
729
+ let panels = [];
730
+
731
+ for (let mesh of meshes) {
732
+ let panelId = mesh?.userData?.panelId || null;
733
+ let resolved = resolvePanelStateColor(mesh, state, themeSnapshot);
734
+ let applied = applyMaterialColor(mesh?.material, resolved.color);
735
+ let frameVisuals = applyPanelFrameVisualState(mesh, resolved);
736
+ panels.push({
737
+ panelId,
738
+ state: resolved.state,
739
+ applied,
740
+ frameVisuals,
741
+ colorType: resolved.color == null ? null : typeof resolved.color,
742
+ });
743
+ }
744
+
745
+ return {
746
+ version: 'xr-three-panel-material-state-v1',
747
+ panelCount: panels.length,
748
+ hoverPanelId: state.hoverPanelId,
749
+ selectedPanelId: state.selectedPanelId,
750
+ draggingPanelId: state.draggingPanelId,
751
+ panels,
752
+ };
753
+ }
754
+
755
+ function classifyTextureBridgeStage(summary, texture, textureReason) {
756
+ if (!summary) return 'texture-source-missing';
757
+ if (texture) return 'three-material-applied';
758
+ if (summary.source !== 'html-in-canvas') return 'html-in-canvas-support';
759
+ if (textureReason === 'texture-resolver-missing') return 'three-texture-resolver';
760
+ if (textureReason === 'texture-resolver-empty') return 'three-texture-upload';
761
+ return 'three-material-pending';
762
+ }
763
+
764
+ function resolveTextureDocument(options = {}) {
765
+ return options.document || options.globalThis?.document || globalThis?.document || null;
766
+ }
767
+
768
+ function createTextureCanvas(documentRef, panel, options = {}) {
769
+ let canvas = options.canvasFactory?.(panel) || documentRef?.createElement?.('canvas') || null;
770
+ if (!canvas) return null;
771
+ resizeTextureCanvas(canvas, panel, options);
772
+ return canvas;
773
+ }
774
+
775
+ function resizeTextureCanvas(canvas, panel, options = {}) {
776
+ let policy = options.qualityPolicy || createXRTextureQualityPolicy(panel, options);
777
+ let width = Number(options.width || policy.texturePixels?.width || panel.previewPixels?.width || 1024);
778
+ let height = Number(options.height || policy.texturePixels?.height || panel.previewPixels?.height || 576);
779
+ canvas.width = Math.max(1, Math.round(width));
780
+ canvas.height = Math.max(1, Math.round(height));
781
+ return canvas;
782
+ }
783
+
784
+ function applyTextureQualityOptions(THREE, texture, options = {}) {
785
+ if (!texture) return null;
786
+ let applied = {
787
+ minFilter: null,
788
+ magFilter: null,
789
+ colorSpace: null,
790
+ mipmaps: null,
791
+ anisotropy: null,
792
+ };
793
+ let minFilter = options.minFilter || THREE?.LinearFilter || null;
794
+ let magFilter = options.magFilter || THREE?.LinearFilter || null;
795
+ if (minFilter != null) {
796
+ texture.minFilter = minFilter;
797
+ applied.minFilter = 'linear';
798
+ }
799
+ if (magFilter != null) {
800
+ texture.magFilter = magFilter;
801
+ applied.magFilter = 'linear';
802
+ }
803
+ if (options.generateMipmaps != null || 'generateMipmaps' in texture) {
804
+ texture.generateMipmaps = options.generateMipmaps === true;
805
+ applied.mipmaps = texture.generateMipmaps;
806
+ }
807
+ let colorSpace = options.colorSpace || THREE?.SRGBColorSpace || null;
808
+ if (colorSpace != null && 'colorSpace' in texture) {
809
+ texture.colorSpace = colorSpace;
810
+ applied.colorSpace = 'srgb';
811
+ } else if (THREE?.sRGBEncoding && 'encoding' in texture) {
812
+ texture.encoding = THREE.sRGBEncoding;
813
+ applied.colorSpace = 'srgb-encoding';
814
+ }
815
+ let anisotropy = Number(options.anisotropy || 0);
816
+ if (Number.isFinite(anisotropy) && anisotropy > 0 && 'anisotropy' in texture) {
817
+ texture.anisotropy = anisotropy;
818
+ applied.anisotropy = anisotropy;
819
+ }
820
+ texture.needsUpdate = true;
821
+ return applied;
822
+ }
823
+
824
+ function createThreeCanvasTexture(THREE, canvas, options = {}) {
825
+ if (!THREE || !canvas) return null;
826
+ let texture = null;
827
+ if (typeof THREE.CanvasTexture === 'function') {
828
+ texture = new THREE.CanvasTexture(canvas);
829
+ } else if (typeof THREE.Texture === 'function') {
830
+ texture = new THREE.Texture(canvas);
831
+ }
832
+ if (!texture) return null;
833
+ texture.name = options.name || 'sn-xr-html-canvas-texture';
834
+ applyTextureQualityOptions(THREE, texture, options);
835
+ if ('isTexture' in texture) texture.isTexture = true;
836
+ return texture;
837
+ }
838
+
839
+ function createThreeHtmlElementTexture(THREE, element, options = {}) {
840
+ if (!THREE || !element || typeof THREE.HTMLTexture !== 'function') return null;
841
+ let texture;
842
+ try {
843
+ texture = new THREE.HTMLTexture(element);
844
+ } catch {
845
+ return null;
846
+ }
847
+ texture.name = options.name || 'sn-xr-html-element-texture';
848
+ applyTextureQualityOptions(THREE, texture, options);
849
+ if ('isTexture' in texture) texture.isTexture = true;
850
+ return texture;
851
+ }
852
+
853
+ function requiresThreeHtmlTexture(input = {}) {
854
+ let support = input.support || {};
855
+ let diagnostics = support.diagnostics || {};
856
+ return diagnostics.textureUploadAvailable === true ||
857
+ support.modes?.webgl === true ||
858
+ support.modes?.webgpu === true;
859
+ }
860
+
861
+ function canUseThreeHtmlTexture(THREE, input = {}) {
862
+ return typeof THREE?.HTMLTexture === 'function' && input.element && requiresThreeHtmlTexture(input);
863
+ }
864
+
865
+ export function createXRThreeTextureCapabilitySummary(THREE, support = {}) {
866
+ let diagnostics = support.diagnostics || {};
867
+ let modes = support.modes || {};
868
+ let textureUploadAvailable = diagnostics.textureUploadAvailable === true ||
869
+ modes.webgl === true ||
870
+ modes.webgpu === true;
871
+ let htmlTextureAvailable = typeof THREE?.HTMLTexture === 'function';
872
+ let htmlTextureUsable = Boolean(htmlTextureAvailable && textureUploadAvailable);
873
+ let threeRevision = THREE?.REVISION == null ? null : String(THREE.REVISION);
874
+ let htmlTextureRequired = textureUploadAvailable;
875
+ let reason = !textureUploadAvailable
876
+ ? 'html-in-canvas-texture-upload-missing'
877
+ : htmlTextureRequired && !htmlTextureAvailable
878
+ ? 'three-html-texture-api-missing'
879
+ : null;
880
+ return {
881
+ version: 'xr-three-texture-capability-v1',
882
+ renderer: 'three',
883
+ threeRevision,
884
+ htmlTextureAvailable,
885
+ htmlTextureUsable,
886
+ htmlTextureRequired,
887
+ textureUploadAvailable,
888
+ modes: {
889
+ webgl: modes.webgl === true,
890
+ webgpu: modes.webgpu === true,
891
+ canvas2d: modes.canvas2d === true,
892
+ },
893
+ ready: htmlTextureUsable,
894
+ reason,
895
+ };
896
+ }
897
+
898
+ function resolveCanvasSource(input = {}) {
899
+ let explicitCanvas = input.canvas || input.prepareResult?.canvas || null;
900
+ if (explicitCanvas) return explicitCanvas;
901
+ let parent = input.element?.parentElement || input.element?.parentNode || null;
902
+ let tagName = String(parent?.tagName || parent?.nodeName || '').toLowerCase();
903
+ return tagName === 'canvas' ? parent : null;
904
+ }
905
+
906
+ export function createXRThreeHtmlCanvasTextureResolver(options = {}) {
907
+ let THREE = options.THREE;
908
+ let documentRef = resolveTextureDocument(options);
909
+ let htmlCanvasRenderer = options.htmlCanvasRenderer || createXRHtmlCanvasRenderer({
910
+ globalThis: options.globalThis,
911
+ mode: options.mode,
912
+ });
913
+ let records = new Map();
914
+ let textures = new Map();
915
+
916
+ function textureDirtyKey(input = {}, canvas, policy) {
917
+ let panel = input.panel || {};
918
+ return String(input.dirtyKey ||
919
+ panel.textureKey ||
920
+ input.element?.dataset?.textureKey ||
921
+ input.element?.dataset?.updatedAt ||
922
+ `${canvas?.width || 0}x${canvas?.height || 0}:${policy.texturePixelRatio}:${policy.redrawMode}`);
923
+ }
924
+
925
+ function resolveTexture(input = {}) {
926
+ let panel = input.panel || {};
927
+ let panelId = panel.id || null;
928
+ if (!panelId) {
929
+ return null;
930
+ }
931
+ if (input.summary?.source !== 'html-in-canvas') {
932
+ records.set(panelId, {
933
+ ok: false,
934
+ panelId,
935
+ reason: input.summary?.reason || 'html-in-canvas-unavailable',
936
+ stage: 'html-in-canvas-support',
937
+ textureApplied: false,
938
+ width: null,
939
+ height: null,
940
+ });
941
+ return null;
942
+ }
943
+ let policy = createXRTextureQualityPolicy(panel, {
944
+ ...options,
945
+ ...(input.textureQuality || {}),
946
+ preferTargetDensity: input.textureQuality?.preferTargetDensity ?? options.preferTargetDensity ?? true,
947
+ });
948
+ if (requiresThreeHtmlTexture(input) && typeof THREE?.HTMLTexture !== 'function') {
949
+ records.set(panelId, {
950
+ ok: false,
951
+ panelId,
952
+ reason: 'three-html-texture-api-missing',
953
+ stage: 'three-html-texture-api',
954
+ textureApplied: false,
955
+ width: panel.contentViewport?.width || panel.texturePixels?.width || null,
956
+ height: panel.contentViewport?.height || panel.texturePixels?.height || null,
957
+ render: null,
958
+ quality: createXRPanelTextureQualitySummary(panel, {
959
+ ...options,
960
+ textureWidth: panel.contentViewport?.width || panel.texturePixels?.width || 0,
961
+ textureHeight: panel.contentViewport?.height || panel.texturePixels?.height || 0,
962
+ texturePixelRatio: policy.texturePixelRatio,
963
+ }),
964
+ redraw: false,
965
+ renderCount: 0,
966
+ redrawCount: 0,
967
+ lastUploadMs: null,
968
+ });
969
+ return null;
970
+ }
971
+ let htmlTextureSupported = canUseThreeHtmlTexture(THREE, input);
972
+ let canvas = resolveCanvasSource(input) || textures.get(panelId)?.canvas || (!htmlTextureSupported ? createTextureCanvas(documentRef, panel, {
973
+ ...options,
974
+ qualityPolicy: policy,
975
+ }) : null);
976
+ if (!canvas && !htmlTextureSupported) {
977
+ records.set(panelId, {
978
+ ok: false,
979
+ panelId,
980
+ reason: 'canvas-target-missing',
981
+ stage: 'canvas-target',
982
+ textureApplied: false,
983
+ width: null,
984
+ height: null,
985
+ });
986
+ return null;
987
+ }
988
+ if (canvas) resizeTextureCanvas(canvas, panel, { ...options, qualityPolicy: policy });
989
+ let dirtyKey = textureDirtyKey(input, canvas, policy);
990
+ let entry = textures.get(panelId);
991
+ let redrawMode = input.redrawMode || policy.redrawMode || options.redrawMode || 'dirty';
992
+ let textureWidth = canvas?.width || panel.contentViewport?.width || panel.texturePixels?.width || 0;
993
+ let textureHeight = canvas?.height || panel.contentViewport?.height || panel.texturePixels?.height || 0;
994
+ let quality = createXRPanelTextureQualitySummary(panel, {
995
+ ...options,
996
+ textureWidth,
997
+ textureHeight,
998
+ texturePixelRatio: policy.texturePixelRatio,
999
+ });
1000
+ if (entry?.texture && entry.dirtyKey === dirtyKey && redrawMode !== 'always') {
1001
+ records.set(panelId, {
1002
+ ok: true,
1003
+ panelId,
1004
+ reason: null,
1005
+ stage: entry.stage === 'three-html-texture-ready'
1006
+ ? 'three-html-texture-reused'
1007
+ : 'three-canvas-texture-reused',
1008
+ textureApplied: true,
1009
+ width: textureWidth,
1010
+ height: textureHeight,
1011
+ render: entry.render || null,
1012
+ quality,
1013
+ redraw: false,
1014
+ renderCount: entry.renderCount || 1,
1015
+ redrawCount: entry.redrawCount || 1,
1016
+ lastUploadMs: entry.lastUploadMs ?? null,
1017
+ });
1018
+ return entry.texture;
1019
+ }
1020
+ let startedAt = nowMs(options);
1021
+ if (htmlTextureSupported) {
1022
+ let texture = entry?.texture || createThreeHtmlElementTexture(THREE, input.element, {
1023
+ name: `sn-xr-panel-${panelId}-html-texture`,
1024
+ ...(options.texture || {}),
1025
+ });
1026
+ let finishedAt = nowMs(options);
1027
+ if (!texture) {
1028
+ records.set(panelId, {
1029
+ ok: false,
1030
+ panelId,
1031
+ reason: 'three-html-texture-api-missing',
1032
+ stage: 'three-html-texture-api',
1033
+ textureApplied: false,
1034
+ width: textureWidth || null,
1035
+ height: textureHeight || null,
1036
+ render: null,
1037
+ quality,
1038
+ redraw: true,
1039
+ renderCount: entry?.renderCount || 0,
1040
+ redrawCount: entry?.redrawCount || 0,
1041
+ lastUploadMs: null,
1042
+ });
1043
+ return null;
1044
+ }
1045
+ let textureOptions = applyTextureQualityOptions(THREE, texture, options.texture || {});
1046
+ let renderCount = (entry?.renderCount || 0) + 1;
1047
+ let redrawCount = (entry?.redrawCount || 0) + 1;
1048
+ let lastUploadMs = Math.max(0, finishedAt - startedAt);
1049
+ textures.set(panelId, {
1050
+ canvas,
1051
+ texture,
1052
+ dirtyKey,
1053
+ render: { rendered: true, mode: 'three-html-texture' },
1054
+ renderCount,
1055
+ redrawCount,
1056
+ lastUploadMs,
1057
+ textureOptions,
1058
+ stage: 'three-html-texture-ready',
1059
+ });
1060
+ records.set(panelId, {
1061
+ ok: true,
1062
+ panelId,
1063
+ reason: null,
1064
+ stage: 'three-html-texture-ready',
1065
+ textureApplied: true,
1066
+ width: textureWidth || null,
1067
+ height: textureHeight || null,
1068
+ render: { rendered: true, mode: 'three-html-texture' },
1069
+ quality,
1070
+ redraw: true,
1071
+ renderCount,
1072
+ redrawCount,
1073
+ lastUploadMs,
1074
+ textureOptions,
1075
+ });
1076
+ return texture;
1077
+ }
1078
+ let renderResult = htmlCanvasRenderer.renderPanelPreview(panelId, canvas, {
1079
+ width: canvas.width,
1080
+ height: canvas.height,
1081
+ ...(options.renderOptions || {}),
1082
+ });
1083
+ let finishedAt = nowMs(options);
1084
+ if (!renderResult?.rendered) {
1085
+ records.set(panelId, {
1086
+ ok: false,
1087
+ panelId,
1088
+ reason: renderResult?.reason || 'html-canvas-preview-render-failed',
1089
+ stage: 'html-canvas-preview',
1090
+ textureApplied: false,
1091
+ width: canvas.width,
1092
+ height: canvas.height,
1093
+ render: renderResult || null,
1094
+ quality,
1095
+ redraw: true,
1096
+ renderCount: entry?.renderCount || 0,
1097
+ redrawCount: entry?.redrawCount || 0,
1098
+ lastUploadMs: null,
1099
+ });
1100
+ return null;
1101
+ }
1102
+ let texture = entry?.texture || createThreeCanvasTexture(THREE, canvas, {
1103
+ name: `sn-xr-panel-${panelId}-texture`,
1104
+ ...(options.texture || {}),
1105
+ });
1106
+ if (!texture) {
1107
+ records.set(panelId, {
1108
+ ok: false,
1109
+ panelId,
1110
+ reason: 'three-texture-api-missing',
1111
+ stage: 'three-texture-api',
1112
+ textureApplied: false,
1113
+ width: canvas.width,
1114
+ height: canvas.height,
1115
+ render: renderResult,
1116
+ quality,
1117
+ redraw: true,
1118
+ renderCount: entry?.renderCount || 0,
1119
+ redrawCount: entry?.redrawCount || 0,
1120
+ lastUploadMs: null,
1121
+ });
1122
+ return null;
1123
+ }
1124
+ let textureOptions = applyTextureQualityOptions(THREE, texture, options.texture || {});
1125
+ let renderCount = (entry?.renderCount || 0) + 1;
1126
+ let redrawCount = (entry?.redrawCount || 0) + 1;
1127
+ let lastUploadMs = Math.max(0, finishedAt - startedAt);
1128
+ textures.set(panelId, {
1129
+ canvas,
1130
+ texture,
1131
+ dirtyKey,
1132
+ render: renderResult,
1133
+ renderCount,
1134
+ redrawCount,
1135
+ lastUploadMs,
1136
+ textureOptions,
1137
+ });
1138
+ records.set(panelId, {
1139
+ ok: true,
1140
+ panelId,
1141
+ reason: null,
1142
+ stage: 'three-canvas-texture-ready',
1143
+ textureApplied: true,
1144
+ width: canvas.width,
1145
+ height: canvas.height,
1146
+ render: renderResult,
1147
+ quality,
1148
+ redraw: true,
1149
+ renderCount,
1150
+ redrawCount,
1151
+ lastUploadMs,
1152
+ textureOptions,
1153
+ });
1154
+ return texture;
1155
+ }
1156
+
1157
+ return {
1158
+ resolve: resolveTexture,
1159
+ getState() {
1160
+ return {
1161
+ version: 'xr-three-html-canvas-texture-resolver-v1',
1162
+ panelCount: records.size,
1163
+ textureCount: textures.size,
1164
+ panelIds: [...records.keys()],
1165
+ records: [...records.values()].map((record) => ({
1166
+ ok: record.ok,
1167
+ panelId: record.panelId,
1168
+ reason: record.reason,
1169
+ stage: record.stage,
1170
+ textureApplied: record.textureApplied,
1171
+ width: record.width,
1172
+ height: record.height,
1173
+ mode: record.render?.mode || null,
1174
+ qualityStatus: record.quality?.status || null,
1175
+ qualityWarnings: record.quality?.warnings || [],
1176
+ qualityRecommendations: record.quality?.recommendations || [],
1177
+ texturePixels: record.quality?.texturePixels || (
1178
+ record.width && record.height ? { width: record.width, height: record.height } : null
1179
+ ),
1180
+ requiredPixels: record.quality?.requiredPixels || null,
1181
+ thresholds: record.quality?.thresholds || null,
1182
+ texturePixelRatio: record.quality?.policy?.texturePixelRatio ?? null,
1183
+ redrawMode: record.quality?.policy?.redrawMode || null,
1184
+ pixelsPerMeter: record.quality?.pixelsPerMeter?.min || null,
1185
+ redraw: record.redraw === true,
1186
+ renderCount: record.renderCount || 0,
1187
+ redrawCount: record.redrawCount || 0,
1188
+ lastUploadMs: record.lastUploadMs ?? null,
1189
+ })),
1190
+ };
1191
+ },
1192
+ dispose() {
1193
+ records.clear();
1194
+ textures.clear();
1195
+ },
1196
+ };
1197
+ }
1198
+
1199
+ function summarizeTextureBridgeSupport(support = {}) {
1200
+ let diagnostics = support.diagnostics || {};
1201
+ return {
1202
+ supported: Boolean(support.supported || diagnostics.supported),
1203
+ preferredMode: support.preferredMode || diagnostics.mode || null,
1204
+ recommendation: diagnostics.recommendation || null,
1205
+ missing: Array.isArray(diagnostics.missing) ? [...diagnostics.missing] : [],
1206
+ blockingMissing: Array.isArray(diagnostics.blockingMissing) ? [...diagnostics.blockingMissing] : [],
1207
+ };
1208
+ }
1209
+
1210
+ export function createXRThreePanelTextureBridge(options = {}) {
1211
+ let htmlCanvasRenderer = options.htmlCanvasRenderer || createXRHtmlCanvasRenderer({
1212
+ globalThis: options.globalThis,
1213
+ mode: options.mode,
1214
+ });
1215
+ let records = new Map();
1216
+
1217
+ function getSupport() {
1218
+ return htmlCanvasRenderer.getSupport();
1219
+ }
1220
+
1221
+ function applyPanelTexture(mesh, panel, applyOptions = {}) {
1222
+ let element = applyOptions.element || resolvePanelElement(panel, { ...options, ...applyOptions });
1223
+ if (!mesh || !panel?.id) {
1224
+ return { ok: false, reason: 'missing-panel-mesh', panelId: panel?.id || null };
1225
+ }
1226
+ if (!element) {
1227
+ let support = getSupport();
1228
+ let supportSummary = summarizeTextureBridgeSupport(support);
1229
+ let summary = createXRPanelTextureSourceSummary(panel, {
1230
+ prepared: false,
1231
+ panelId: panel.id,
1232
+ mode: support.preferredMode || 'unsupported',
1233
+ supported: false,
1234
+ reason: 'panel-element-missing',
1235
+ }, support, {
1236
+ allowMaterialFallback: !(applyOptions.requireTextureUpload ?? options.requireTextureUpload),
1237
+ });
1238
+ let record = {
1239
+ ok: false,
1240
+ panelId: panel.id,
1241
+ reason: 'panel-element-missing',
1242
+ stage: 'panel-element',
1243
+ strictRequired: Boolean(applyOptions.requireTextureUpload ?? options.requireTextureUpload),
1244
+ textureApplied: false,
1245
+ textureKind: null,
1246
+ support: supportSummary,
1247
+ summary,
1248
+ };
1249
+ records.set(panel.id, record);
1250
+ mesh.userData ||= {};
1251
+ mesh.userData.textureSource = summary;
1252
+ mesh.userData.textureBridge = {
1253
+ ok: record.ok,
1254
+ stage: record.stage,
1255
+ strictRequired: record.strictRequired,
1256
+ textureApplied: record.textureApplied,
1257
+ reason: record.reason,
1258
+ };
1259
+ return record;
1260
+ }
1261
+
1262
+ let canvas = applyOptions.canvas || options.canvas || resolveCanvasSource({ element });
1263
+ let prepareResult = htmlCanvasRenderer.preparePanel(element, panel, {
1264
+ mode: applyOptions.mode || options.mode,
1265
+ canvas,
1266
+ });
1267
+ let support = getSupport();
1268
+ let supportSummary = summarizeTextureBridgeSupport(support);
1269
+ let strictRequired = Boolean(applyOptions.requireTextureUpload ?? options.requireTextureUpload);
1270
+ let summary = createXRPanelTextureSourceSummary(panel, prepareResult, support, {
1271
+ allowMaterialFallback: !strictRequired,
1272
+ });
1273
+ let textureQuality = createXRPanelTextureQualitySummary(panel, applyOptions.textureQuality || options.textureQuality || {});
1274
+ let texture = null;
1275
+ let textureReason = null;
1276
+ if (summary.source === 'html-in-canvas' && typeof options.textureResolver === 'function') {
1277
+ texture = options.textureResolver({
1278
+ mesh,
1279
+ panel,
1280
+ element,
1281
+ prepareResult,
1282
+ canvas: prepareResult.canvas || canvas,
1283
+ textureQuality: applyOptions.textureQuality || options.textureQuality || null,
1284
+ support,
1285
+ summary,
1286
+ }) || null;
1287
+ if (!texture) textureReason = 'texture-resolver-empty';
1288
+ } else if (summary.source === 'html-in-canvas') {
1289
+ textureReason = 'texture-resolver-missing';
1290
+ }
1291
+
1292
+ if (texture && mesh.material) {
1293
+ mesh.material.map = texture;
1294
+ if ('color' in mesh.material && typeof mesh.material.color?.setHex === 'function') {
1295
+ mesh.material.color.setHex(0xffffff);
1296
+ }
1297
+ markMaterialUpdated(mesh.material);
1298
+ }
1299
+
1300
+ let stage = classifyTextureBridgeStage(summary, texture, textureReason);
1301
+ let record = {
1302
+ ok: summary.source === 'html-in-canvas' && (!strictRequired || Boolean(texture)),
1303
+ panelId: panel.id,
1304
+ reason: textureReason || summary.reason,
1305
+ stage,
1306
+ strictRequired,
1307
+ textureApplied: Boolean(texture),
1308
+ textureKind: texture?.isTexture ? 'three-texture' : texture ? 'host-texture' : null,
1309
+ support: supportSummary,
1310
+ textureQuality,
1311
+ summary,
1312
+ };
1313
+ records.set(panel.id, record);
1314
+ mesh.userData ||= {};
1315
+ mesh.userData.textureSource = summary;
1316
+ mesh.userData.textureBridge = {
1317
+ ok: record.ok,
1318
+ stage: record.stage,
1319
+ strictRequired: record.strictRequired,
1320
+ textureApplied: record.textureApplied,
1321
+ reason: record.reason,
1322
+ };
1323
+ return record;
1324
+ }
1325
+
1326
+ return {
1327
+ applyPanelTexture,
1328
+ getSupport,
1329
+ dispose() {
1330
+ records.clear();
1331
+ options.textureResolverDispose?.();
1332
+ },
1333
+ getState() {
1334
+ return {
1335
+ version: 'xr-three-panel-texture-bridge-v1',
1336
+ panelCount: records.size,
1337
+ panelIds: [...records.keys()],
1338
+ records: [...records.values()].map((record) => ({
1339
+ ok: record.ok,
1340
+ panelId: record.panelId,
1341
+ reason: record.reason,
1342
+ stage: record.stage,
1343
+ strictRequired: record.strictRequired,
1344
+ textureApplied: record.textureApplied,
1345
+ source: record.summary?.source || null,
1346
+ mode: record.summary?.mode || null,
1347
+ support: record.support || null,
1348
+ textureQuality: record.textureQuality || null,
1349
+ })),
1350
+ };
1351
+ },
1352
+ };
1353
+ }
1354
+
1355
+ function createPanelMesh(THREE, panel, options = {}) {
1356
+ let size = Array.isArray(panel.size) ? panel.size : [0.8, 0.45];
1357
+ let geometry = new THREE.PlaneGeometry(Number(size[0] || 0.8), Number(size[1] || 0.45));
1358
+ let mesh = new THREE.Mesh(geometry, createMaterial(THREE, panel, options));
1359
+ applyVector(mesh.position, Array.isArray(panel.position) ? panel.position : [0, 1.35, -1.8]);
1360
+ applyRotation(mesh.rotation, Array.isArray(panel.rotation) ? panel.rotation : [0, 0, 0]);
1361
+ mesh.userData.panelId = panel.id || null;
1362
+ mesh.userData.panel = panel;
1363
+ mesh.userData.baseSize = [Number(size[0] || 0.8), Number(size[1] || 0.45)];
1364
+ mesh.userData.xrSize = [...mesh.userData.baseSize];
1365
+ mesh.userData.panelFrame = createXRPanelFrame(panel, options.panelFrame || {});
1366
+ mesh.userData.panelFrameVisuals = buildPanelFrameVisuals(THREE, panel, mesh, options.panelFrameVisuals || {});
1367
+ mesh.userData.baseColor = options.colorResolver?.(panel) ||
1368
+ panel.material?.threeColor ||
1369
+ panel.material?.backgroundColor ||
1370
+ panel.color ||
1371
+ null;
1372
+ return mesh;
1373
+ }
1374
+
1375
+ function createRootGroup(THREE, scene, transform) {
1376
+ if (typeof THREE?.Group !== 'function') return null;
1377
+ let group = new THREE.Group();
1378
+ group.name = 'sn-xr-scene-root';
1379
+ group.userData ||= {};
1380
+ group.userData.xrSceneRoot = true;
1381
+ group.userData.xrSceneRootTransform = transform;
1382
+ applyVector(group.position, transform.position);
1383
+ applyRotation(group.rotation, transform.rotation);
1384
+ scene.add?.(group);
1385
+ return group;
1386
+ }
1387
+
1388
+ export function createXRThreePanelSceneAdapter(options = {}) {
1389
+ let THREE = options.THREE;
1390
+ let check = assertThree(THREE);
1391
+ let scene = check.ok ? new THREE.Scene() : null;
1392
+ let panels = new Map();
1393
+ let textureBridge = options.textureBridge || null;
1394
+ let textureRecords = new Map();
1395
+ let rootGroup = null;
1396
+ let rootTransform = null;
1397
+ let activeXRScene = null;
1398
+ let activeSetOptions = {};
1399
+
1400
+ function applyRootTransform(transform) {
1401
+ rootTransform = transform;
1402
+ if (rootGroup) {
1403
+ rootGroup.userData ||= {};
1404
+ rootGroup.userData.xrSceneRootTransform = transform;
1405
+ applyVector(rootGroup.position, transform.position);
1406
+ applyRotation(rootGroup.rotation, transform.rotation);
1407
+ }
1408
+ return rootTransform;
1409
+ }
1410
+
1411
+ function setScene(xrScene, setOptions = {}) {
1412
+ if (!check.ok) {
1413
+ return { ok: false, reason: check.reason, missing: check.missing, panelCount: 0 };
1414
+ }
1415
+ if (rootGroup) {
1416
+ scene.remove?.(rootGroup);
1417
+ } else {
1418
+ for (let mesh of panels.values()) {
1419
+ scene.remove?.(mesh);
1420
+ }
1421
+ }
1422
+ panels.clear();
1423
+ textureRecords.clear();
1424
+ activeXRScene = xrScene || null;
1425
+ activeSetOptions = { ...setOptions };
1426
+ rootTransform = createXRSceneRootTransform(xrScene, {
1427
+ mode: setOptions.mode,
1428
+ referenceSpaceType: setOptions.referenceSpaceType,
1429
+ viewerPose: setOptions.viewerPose,
1430
+ policy: setOptions.placementPolicy || options.placementPolicy,
1431
+ });
1432
+ rootGroup = createRootGroup(THREE, scene, rootTransform);
1433
+ let sceneTarget = rootGroup || scene;
1434
+ let diagnosticPanelIds = [];
1435
+ let hiddenPanelIds = [];
1436
+ let bridge = setOptions.textureBridge || textureBridge;
1437
+ bridge?.dispose?.();
1438
+ for (let panel of xrScene?.panels || []) {
1439
+ let mesh = createPanelMesh(THREE, panel, { ...options, ...setOptions });
1440
+ if (bridge?.applyPanelTexture) {
1441
+ let texture = bridge.applyPanelTexture(mesh, panel, setOptions.textureOptions || {});
1442
+ textureRecords.set(panel.id, texture);
1443
+ let diagnoseStrictFailure = Boolean(setOptions.hideStrictTextureFailures ?? options.hideStrictTextureFailures);
1444
+ if (diagnoseStrictFailure && texture?.strictRequired && !texture.ok) {
1445
+ applyStrictTextureDiagnosticMaterial(mesh, texture, { ...options, ...setOptions });
1446
+ diagnosticPanelIds.push(panel.id);
1447
+ hiddenPanelIds.push(panel.id);
1448
+ }
1449
+ }
1450
+ sceneTarget.add?.(mesh);
1451
+ panels.set(panel.id, mesh);
1452
+ }
1453
+ return {
1454
+ ok: true,
1455
+ scene,
1456
+ rootGroup,
1457
+ rootTransform,
1458
+ panelCount: panels.size,
1459
+ renderedPanelCount: [...panels.values()].filter((mesh) => mesh.visible !== false).length,
1460
+ hiddenPanelCount: hiddenPanelIds.length,
1461
+ hiddenPanelIds,
1462
+ diagnosticPanelCount: diagnosticPanelIds.length,
1463
+ diagnosticPanelIds,
1464
+ panelIds: [...panels.keys()],
1465
+ textureSources: [...textureRecords.values()],
1466
+ };
1467
+ }
1468
+
1469
+ return {
1470
+ setScene,
1471
+ applyViewerPose(viewerPose, poseOptions = {}) {
1472
+ if (!check.ok) return { ok: false, reason: check.reason, missing: check.missing };
1473
+ let snapshot = createXRViewerPoseSnapshot(viewerPose, poseOptions);
1474
+ if (!snapshot.position && !snapshot.rotation) {
1475
+ return { ok: false, reason: 'missing-viewer-pose', snapshot };
1476
+ }
1477
+ let transform = createXRSceneRootTransform(activeXRScene || {}, {
1478
+ ...activeSetOptions,
1479
+ ...poseOptions,
1480
+ viewerPose: snapshot,
1481
+ });
1482
+ applyRootTransform(transform);
1483
+ return {
1484
+ ok: true,
1485
+ version: 'xr-three-viewer-pose-root-transform-v1',
1486
+ snapshot,
1487
+ rootTransform,
1488
+ };
1489
+ },
1490
+ getScene() {
1491
+ return scene;
1492
+ },
1493
+ getPanelMesh(panelId) {
1494
+ return panels.get(panelId) || null;
1495
+ },
1496
+ listPanelMeshes() {
1497
+ return [...panels.values()];
1498
+ },
1499
+ getState() {
1500
+ let meshList = [...panels.values()];
1501
+ let materialDiagnostics = summarizePanelMaterials(meshList);
1502
+ return {
1503
+ ok: check.ok,
1504
+ reason: check.ok ? null : check.reason,
1505
+ missing: check.missing,
1506
+ panelCount: panels.size,
1507
+ rootTransform,
1508
+ renderedPanelCount: meshList.filter((mesh) => mesh.visible !== false).length,
1509
+ hiddenPanelCount: meshList.filter((mesh) => mesh.visible === false).length,
1510
+ hiddenPanelIds: meshList
1511
+ .filter((mesh) => mesh.visible === false)
1512
+ .map((mesh) => mesh.userData?.panelId)
1513
+ .filter(Boolean),
1514
+ diagnosticPanelCount: materialDiagnostics.strictDiagnosticCount,
1515
+ diagnosticPanelIds: materialDiagnostics.strictDiagnosticPanelIds,
1516
+ materialDiagnostics,
1517
+ panelIds: [...panels.keys()],
1518
+ panelFrameVisualCount: meshList.reduce((count, mesh) => (
1519
+ count + Number(mesh.userData?.panelFrameVisuals?.objectCount || 0)
1520
+ ), 0),
1521
+ panelFrameVisuals: [...panels.values()].map((mesh) => ({
1522
+ ok: Boolean(mesh.userData?.panelFrameVisuals?.ok),
1523
+ panelId: mesh.userData?.panelId || null,
1524
+ reason: mesh.userData?.panelFrameVisuals?.reason || null,
1525
+ objectCount: Number(mesh.userData?.panelFrameVisuals?.objectCount || 0),
1526
+ zones: mesh.userData?.panelFrameVisuals?.zones || [],
1527
+ })),
1528
+ textureSources: [...textureRecords.values()].map((record) => ({
1529
+ panelId: record.panelId,
1530
+ ok: record.ok,
1531
+ reason: record.reason,
1532
+ stage: record.stage,
1533
+ strictRequired: record.strictRequired,
1534
+ textureApplied: record.textureApplied,
1535
+ source: record.summary?.source || null,
1536
+ mode: record.summary?.mode || null,
1537
+ textureQuality: record.textureQuality || null,
1538
+ qualityStatus: record.textureQuality?.status || null,
1539
+ qualityWarnings: record.textureQuality?.warnings || [],
1540
+ qualityRecommendations: record.textureQuality?.recommendations || [],
1541
+ texturePixels: record.textureQuality?.texturePixels || null,
1542
+ requiredPixels: record.textureQuality?.requiredPixels || null,
1543
+ pixelsPerMeter: record.textureQuality?.pixelsPerMeter?.min || null,
1544
+ hidden: panels.get(record.panelId)?.visible === false ||
1545
+ panels.get(record.panelId)?.userData?.strictTextureHidden === true,
1546
+ diagnostic: Boolean(panels.get(record.panelId)?.userData?.strictTextureDiagnostic),
1547
+ diagnosticReason: panels.get(record.panelId)?.userData?.strictTextureDiagnosticReason || null,
1548
+ support: record.support || null,
1549
+ })),
1550
+ };
1551
+ },
1552
+ };
1553
+ }
1554
+
1555
+ function setRayFromController(THREE, raycaster, controller) {
1556
+ if (hasFn(raycaster, 'setFromXRController')) {
1557
+ raycaster.setFromXRController(controller);
1558
+ return { ok: true, source: 'setFromXRController' };
1559
+ }
1560
+ if (!hasFn(controller, 'getWorldPosition') || !hasFn(controller, 'getWorldQuaternion')) {
1561
+ return { ok: false, reason: 'missing-controller-transform' };
1562
+ }
1563
+ let origin = controller.getWorldPosition(new THREE.Vector3());
1564
+ let direction = new THREE.Vector3(0, 0, -1).applyQuaternion(controller.getWorldQuaternion(new THREE.Quaternion()));
1565
+ raycaster.set(origin, direction);
1566
+ return { ok: true, source: 'world-transform' };
1567
+ }
1568
+
1569
+ function vectorData(vector) {
1570
+ if (!vector) return null;
1571
+ return {
1572
+ x: Number(vector.x || 0),
1573
+ y: Number(vector.y || 0),
1574
+ z: Number(vector.z || 0),
1575
+ };
1576
+ }
1577
+
1578
+ function framePointFromHit(hit) {
1579
+ if (hit?.uv) {
1580
+ return {
1581
+ x: Number(hit.uv.x || 0),
1582
+ y: 1 - Number(hit.uv.y || 0),
1583
+ };
1584
+ }
1585
+ return { x: 0.5, y: 0.5 };
1586
+ }
1587
+
1588
+ function resolveHitFrameTarget(hit, options = {}) {
1589
+ let mesh = hit?.object || hit;
1590
+ let frame = mesh?.userData?.panelFrame;
1591
+ if (!frame) return null;
1592
+ return hitTestXRPanelFrame(frame, framePointFromHit(hit), options);
1593
+ }
1594
+
1595
+ function isXRFrameDragTarget(frameTarget) {
1596
+ return frameTarget?.operation === 'move' || frameTarget?.operation === 'resize';
1597
+ }
1598
+
1599
+ function distanceBetween(a, b) {
1600
+ if (!a || !b) return 0;
1601
+ let dx = Number(a.x || 0) - Number(b.x || 0);
1602
+ let dy = Number(a.y || 0) - Number(b.y || 0);
1603
+ let dz = Number(a.z || 0) - Number(b.z || 0);
1604
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1605
+ }
1606
+
1607
+ function vectorBetween(a, b) {
1608
+ return {
1609
+ x: Number(a?.x || 0) - Number(b?.x || 0),
1610
+ y: Number(a?.y || 0) - Number(b?.y || 0),
1611
+ z: Number(a?.z || 0) - Number(b?.z || 0),
1612
+ };
1613
+ }
1614
+
1615
+ function vectorLength(vector) {
1616
+ return Math.sqrt(
1617
+ Number(vector?.x || 0) ** 2 +
1618
+ Number(vector?.y || 0) ** 2 +
1619
+ Number(vector?.z || 0) ** 2
1620
+ );
1621
+ }
1622
+
1623
+ function vectorDot(a, b) {
1624
+ return Number(a?.x || 0) * Number(b?.x || 0) +
1625
+ Number(a?.y || 0) * Number(b?.y || 0) +
1626
+ Number(a?.z || 0) * Number(b?.z || 0);
1627
+ }
1628
+
1629
+ function axisVector(THREE, mesh, values) {
1630
+ let vector = THREE?.Vector3 ? new THREE.Vector3(...values) : { x: values[0], y: values[1], z: values[2] };
1631
+ if (mesh?.quaternion && typeof vector.applyQuaternion === 'function') {
1632
+ vector.applyQuaternion(mesh.quaternion);
1633
+ }
1634
+ if (typeof vector.normalize === 'function') {
1635
+ vector.normalize();
1636
+ }
1637
+ return vector;
1638
+ }
1639
+
1640
+ function scaleVector(vector, scalar) {
1641
+ return {
1642
+ x: Number(vector?.x || 0) * scalar,
1643
+ y: Number(vector?.y || 0) * scalar,
1644
+ z: Number(vector?.z || 0) * scalar,
1645
+ };
1646
+ }
1647
+
1648
+ function addVector(target, vector) {
1649
+ if (!target || !vector) return;
1650
+ if (typeof target.add === 'function') {
1651
+ target.add(vector);
1652
+ return;
1653
+ }
1654
+ target.x = Number(target.x || 0) + Number(vector.x || 0);
1655
+ target.y = Number(target.y || 0) + Number(vector.y || 0);
1656
+ target.z = Number(target.z || 0) + Number(vector.z || 0);
1657
+ }
1658
+
1659
+ function readPanelSize(mesh) {
1660
+ let explicit = mesh?.userData?.xrSize || mesh?.userData?.panel?.size;
1661
+ if (Array.isArray(explicit)) {
1662
+ return [
1663
+ Math.max(0.05, Number(explicit[0] || 0.8)),
1664
+ Math.max(0.05, Number(explicit[1] || 0.45)),
1665
+ ];
1666
+ }
1667
+ let parameters = mesh?.geometry?.parameters || {};
1668
+ return [
1669
+ Math.max(0.05, Number(parameters.width || 0.8) * Number(mesh?.scale?.x || 1)),
1670
+ Math.max(0.05, Number(parameters.height || 0.45) * Number(mesh?.scale?.y || 1)),
1671
+ ];
1672
+ }
1673
+
1674
+ function applyPanelSize(mesh, size) {
1675
+ if (!mesh || !Array.isArray(size)) return;
1676
+ let next = [
1677
+ Math.max(0.05, Number(size[0] || 0.8)),
1678
+ Math.max(0.05, Number(size[1] || 0.45)),
1679
+ ];
1680
+ mesh.userData ||= {};
1681
+ mesh.userData.xrSize = next;
1682
+ if (mesh.userData.panel) {
1683
+ mesh.userData.panel = { ...mesh.userData.panel, size: next };
1684
+ }
1685
+ let parameters = mesh.geometry?.parameters || {};
1686
+ let baseWidth = Number(mesh.userData.baseSize?.[0] || parameters.width || next[0]);
1687
+ let baseHeight = Number(mesh.userData.baseSize?.[1] || parameters.height || next[1]);
1688
+ if (mesh.scale && Number.isFinite(baseWidth) && Number.isFinite(baseHeight) && baseWidth > 0 && baseHeight > 0) {
1689
+ if (typeof mesh.scale.set === 'function') {
1690
+ mesh.scale.set(next[0] / baseWidth, next[1] / baseHeight, Number(mesh.scale.z || 1));
1691
+ } else {
1692
+ mesh.scale.x = next[0] / baseWidth;
1693
+ mesh.scale.y = next[1] / baseHeight;
1694
+ mesh.scale.z = Number(mesh.scale.z || 1);
1695
+ }
1696
+ }
1697
+ mesh.userData.updatePanelFrameVisuals?.();
1698
+ }
1699
+
1700
+ function resizePanelFromDrag(THREE, dragState, point, options = {}) {
1701
+ let frameTarget = dragState?.frameTarget || {};
1702
+ if (frameTarget.operation !== 'resize') {
1703
+ return null;
1704
+ }
1705
+ let handle = String(frameTarget.handle || '');
1706
+ let minWidth = Number(options.minWidth ?? options.minPanelWidth ?? 0.24);
1707
+ let minHeight = Number(options.minHeight ?? options.minPanelHeight ?? 0.16);
1708
+ let maxWidth = Number(options.maxWidth ?? options.maxPanelWidth ?? 3.2);
1709
+ let maxHeight = Number(options.maxHeight ?? options.maxPanelHeight ?? 2.4);
1710
+ let startSize = Array.isArray(dragState.startSize) ? dragState.startSize : readPanelSize(dragState.mesh);
1711
+ let delta = vectorBetween(point, dragState.startIntersection || point);
1712
+ let xAxis = axisVector(THREE, dragState.mesh, [1, 0, 0]);
1713
+ let yAxis = axisVector(THREE, dragState.mesh, [0, 1, 0]);
1714
+ let xDelta = vectorDot(delta, xAxis);
1715
+ let yDelta = vectorDot(delta, yAxis);
1716
+ let nextWidth = startSize[0];
1717
+ let nextHeight = startSize[1];
1718
+ let centerShift = { x: 0, y: 0, z: 0 };
1719
+ let east = /east/i.test(handle);
1720
+ let west = /west/i.test(handle);
1721
+ let north = /north/i.test(handle);
1722
+ let south = /south/i.test(handle);
1723
+
1724
+ if (east) {
1725
+ nextWidth += xDelta;
1726
+ centerShift = scaleVector(xAxis, xDelta / 2);
1727
+ }
1728
+ if (west) {
1729
+ nextWidth -= xDelta;
1730
+ centerShift = scaleVector(xAxis, xDelta / 2);
1731
+ }
1732
+ if (north) {
1733
+ nextHeight += yDelta;
1734
+ centerShift = {
1735
+ x: centerShift.x + scaleVector(yAxis, yDelta / 2).x,
1736
+ y: centerShift.y + scaleVector(yAxis, yDelta / 2).y,
1737
+ z: centerShift.z + scaleVector(yAxis, yDelta / 2).z,
1738
+ };
1739
+ }
1740
+ if (south) {
1741
+ nextHeight -= yDelta;
1742
+ centerShift = {
1743
+ x: centerShift.x + scaleVector(yAxis, yDelta / 2).x,
1744
+ y: centerShift.y + scaleVector(yAxis, yDelta / 2).y,
1745
+ z: centerShift.z + scaleVector(yAxis, yDelta / 2).z,
1746
+ };
1747
+ }
1748
+
1749
+ nextWidth = Math.max(minWidth, Math.min(maxWidth, nextWidth));
1750
+ nextHeight = Math.max(minHeight, Math.min(maxHeight, nextHeight));
1751
+ let size = [nextWidth, nextHeight];
1752
+ applyPanelSize(dragState.mesh, size);
1753
+ if (dragState.startPosition?.clone && dragState.mesh?.position?.copy) {
1754
+ dragState.mesh.position.copy(dragState.startPosition.clone());
1755
+ }
1756
+ addVector(dragState.mesh?.position, centerShift);
1757
+ return {
1758
+ operation: 'resize',
1759
+ handle,
1760
+ size,
1761
+ delta: { x: xDelta, y: yDelta },
1762
+ centerShift,
1763
+ };
1764
+ }
1765
+
1766
+ function normalizeDragResponse(options = {}) {
1767
+ let smoothing = Number(options.dragSmoothing ?? options.smoothing ?? 0.72);
1768
+ let deadzone = Number(options.dragDeadzone ?? options.deadzone ?? 0.0015);
1769
+ let maxStep = Number(options.maxDragStep ?? options.maxStep ?? 0.18);
1770
+ return {
1771
+ smoothing: Number.isFinite(smoothing) ? Math.max(0.05, Math.min(1, smoothing)) : 0.72,
1772
+ deadzone: Number.isFinite(deadzone) ? Math.max(0, deadzone) : 0.0015,
1773
+ maxStep: Number.isFinite(maxStep) ? Math.max(0.01, maxStep) : 0.18,
1774
+ };
1775
+ }
1776
+
1777
+ function filteredDragPosition(previousPosition, rawPosition, response) {
1778
+ let rawDelta = vectorBetween(rawPosition, previousPosition);
1779
+ let rawDistance = vectorLength(rawDelta);
1780
+ let next = previousPosition.clone?.() || { ...previousPosition };
1781
+ let clamped = false;
1782
+ let settled = rawDistance <= response.deadzone;
1783
+
1784
+ if (!settled) {
1785
+ let appliedDelta = {
1786
+ x: rawDelta.x * response.smoothing,
1787
+ y: rawDelta.y * response.smoothing,
1788
+ z: rawDelta.z * response.smoothing,
1789
+ };
1790
+ let appliedDistance = vectorLength(appliedDelta);
1791
+ if (appliedDistance > response.maxStep) {
1792
+ let scale = response.maxStep / appliedDistance;
1793
+ appliedDelta.x *= scale;
1794
+ appliedDelta.y *= scale;
1795
+ appliedDelta.z *= scale;
1796
+ clamped = true;
1797
+ }
1798
+ next.x = Number(previousPosition.x || 0) + appliedDelta.x;
1799
+ next.y = Number(previousPosition.y || 0) + appliedDelta.y;
1800
+ next.z = Number(previousPosition.z || 0) + appliedDelta.z;
1801
+ }
1802
+
1803
+ return {
1804
+ position: next,
1805
+ diagnostics: {
1806
+ smoothing: response.smoothing,
1807
+ deadzone: response.deadzone,
1808
+ maxStep: response.maxStep,
1809
+ rawDelta,
1810
+ rawDistance,
1811
+ appliedDelta: vectorBetween(next, previousPosition),
1812
+ appliedDistance: distanceBetween(next, previousPosition),
1813
+ clamped,
1814
+ settled,
1815
+ },
1816
+ };
1817
+ }
1818
+
1819
+ export function createXRThreeControllerRayAdapter(options = {}) {
1820
+ let THREE = options.THREE;
1821
+ let raycaster = options.raycaster || (THREE?.Raycaster ? new THREE.Raycaster() : null);
1822
+ let dragResponse = normalizeDragResponse(options.dragResponse || options);
1823
+ let dragPlane = THREE?.Plane ? new THREE.Plane() : null;
1824
+ let intersection = THREE?.Vector3 ? new THREE.Vector3() : null;
1825
+ let normal = THREE?.Vector3 ? new THREE.Vector3() : null;
1826
+ let cameraPosition = THREE?.Vector3 ? new THREE.Vector3() : null;
1827
+ let dragging = null;
1828
+ let counters = {
1829
+ hits: 0,
1830
+ misses: 0,
1831
+ dragStarts: 0,
1832
+ dragUpdates: 0,
1833
+ dragMisses: 0,
1834
+ };
1835
+ let diagnostics = {
1836
+ version: 'xr-three-controller-diagnostics-v1',
1837
+ raySource: null,
1838
+ lastHit: null,
1839
+ lastMissReason: null,
1840
+ drag: null,
1841
+ };
1842
+
1843
+ function getHits(controller, meshes = []) {
1844
+ if (!THREE || !raycaster) return [];
1845
+ let ray = setRayFromController(THREE, raycaster, controller);
1846
+ diagnostics.raySource = ray.source || null;
1847
+ if (!ray.ok) {
1848
+ counters.misses += 1;
1849
+ diagnostics.lastMissReason = ray.reason || 'ray-unavailable';
1850
+ diagnostics.lastHit = null;
1851
+ return [];
1852
+ }
1853
+ let hits = raycaster.intersectObjects(meshes, false).map((hit) => {
1854
+ let frameTarget = resolveHitFrameTarget(hit, options.panelFrameHitTest || {});
1855
+ if (frameTarget) {
1856
+ hit.frameTarget = frameTarget;
1857
+ hit.object.userData ||= {};
1858
+ hit.object.userData.lastFrameTarget = frameTarget;
1859
+ }
1860
+ return hit;
1861
+ });
1862
+ if (hits.length) {
1863
+ counters.hits += 1;
1864
+ diagnostics.lastMissReason = null;
1865
+ diagnostics.lastHit = {
1866
+ panelId: hits[0]?.object?.userData?.panelId || null,
1867
+ distance: Number(hits[0]?.distance || 0),
1868
+ point: vectorData(hits[0]?.point),
1869
+ frameTarget: hits[0]?.frameTarget || null,
1870
+ };
1871
+ } else {
1872
+ counters.misses += 1;
1873
+ diagnostics.lastMissReason = 'no-panel-hit';
1874
+ diagnostics.lastHit = null;
1875
+ }
1876
+ return hits;
1877
+ }
1878
+
1879
+ function beginDrag(controller, meshOrHit, camera) {
1880
+ let hit = meshOrHit?.object ? meshOrHit : null;
1881
+ let mesh = hit?.object || meshOrHit;
1882
+ if (!THREE || !raycaster || !dragPlane || !intersection || !normal || !mesh) {
1883
+ return { ok: false, reason: 'missing-drag-dependency' };
1884
+ }
1885
+ let ray = setRayFromController(THREE, raycaster, controller);
1886
+ diagnostics.raySource = ray.source || null;
1887
+ if (!ray.ok) {
1888
+ counters.dragMisses += 1;
1889
+ diagnostics.lastMissReason = ray.reason || 'ray-unavailable';
1890
+ return ray;
1891
+ }
1892
+ camera?.getWorldPosition?.(cameraPosition);
1893
+ normal.copy(cameraPosition).sub(mesh.position).normalize();
1894
+ dragPlane.setFromNormalAndCoplanarPoint(normal, mesh.position);
1895
+ if (!raycaster.ray.intersectPlane(dragPlane, intersection)) {
1896
+ counters.dragMisses += 1;
1897
+ diagnostics.lastMissReason = 'ray-plane-miss';
1898
+ return { ok: false, reason: 'ray-plane-miss' };
1899
+ }
1900
+ dragging = {
1901
+ mesh,
1902
+ controller,
1903
+ frameTarget: hit?.frameTarget || mesh.userData?.lastFrameTarget || null,
1904
+ plane: dragPlane.clone(),
1905
+ offset: mesh.position.clone().sub(intersection),
1906
+ rotation: mesh.quaternion?.clone?.() || null,
1907
+ startIntersection: intersection.clone?.() || null,
1908
+ startPosition: mesh.position.clone?.() || null,
1909
+ startSize: readPanelSize(mesh),
1910
+ lastPosition: mesh.position.clone?.() || null,
1911
+ lastRawPosition: mesh.position.clone?.() || null,
1912
+ };
1913
+ counters.dragStarts += 1;
1914
+ diagnostics.lastMissReason = null;
1915
+ diagnostics.drag = {
1916
+ active: true,
1917
+ panelId: mesh.userData?.panelId || null,
1918
+ frameTarget: hit?.frameTarget || mesh.userData?.lastFrameTarget || null,
1919
+ model: 'controller-ray-plane',
1920
+ position: vectorData(mesh.position),
1921
+ rotation: vectorData(mesh.rotation),
1922
+ size: readPanelSize(mesh),
1923
+ planeNormal: vectorData(normal),
1924
+ planePoint: vectorData(mesh.position),
1925
+ intersection: vectorData(intersection),
1926
+ delta: { x: 0, y: 0, z: 0, distance: 0 },
1927
+ response: {
1928
+ smoothing: dragResponse.smoothing,
1929
+ deadzone: dragResponse.deadzone,
1930
+ maxStep: dragResponse.maxStep,
1931
+ },
1932
+ };
1933
+ return {
1934
+ ok: true,
1935
+ panelId: mesh.userData?.panelId || null,
1936
+ frameTarget: hit?.frameTarget || mesh.userData?.lastFrameTarget || null,
1937
+ dragModel: 'controller-ray-plane',
1938
+ };
1939
+ }
1940
+
1941
+ function updateDrag(controller = dragging?.controller) {
1942
+ if (!dragging || !controller || !raycaster || !intersection) return { ok: false, reason: 'not-dragging' };
1943
+ let ray = setRayFromController(THREE, raycaster, controller);
1944
+ diagnostics.raySource = ray.source || null;
1945
+ if (!ray.ok) {
1946
+ counters.dragMisses += 1;
1947
+ diagnostics.lastMissReason = ray.reason || 'ray-unavailable';
1948
+ return ray;
1949
+ }
1950
+ if (!raycaster.ray.intersectPlane(dragging.plane, intersection)) {
1951
+ counters.dragMisses += 1;
1952
+ diagnostics.lastMissReason = 'ray-plane-miss';
1953
+ return { ok: false, reason: 'ray-plane-miss' };
1954
+ }
1955
+ let previousPosition = dragging.mesh.position.clone?.() || dragging.lastPosition;
1956
+ let rawPosition = intersection.clone?.() || new THREE.Vector3(intersection.x, intersection.y, intersection.z);
1957
+ let resize = resizePanelFromDrag(THREE, dragging, rawPosition, options.resize || options);
1958
+ let filtered = null;
1959
+ if (!resize) {
1960
+ rawPosition.add(dragging.offset);
1961
+ filtered = filteredDragPosition(previousPosition, rawPosition, dragResponse);
1962
+ dragging.mesh.position.copy(filtered.position);
1963
+ }
1964
+ if (dragging.rotation && dragging.mesh.quaternion?.copy) {
1965
+ dragging.mesh.quaternion.copy(dragging.rotation);
1966
+ }
1967
+ counters.dragUpdates += 1;
1968
+ diagnostics.lastMissReason = null;
1969
+ diagnostics.drag = {
1970
+ active: true,
1971
+ panelId: dragging.mesh.userData?.panelId || null,
1972
+ frameTarget: dragging.frameTarget || null,
1973
+ model: 'controller-ray-plane',
1974
+ position: vectorData(dragging.mesh.position),
1975
+ rotation: vectorData(dragging.mesh.rotation),
1976
+ size: readPanelSize(dragging.mesh),
1977
+ resize,
1978
+ planeNormal: vectorData(dragging.plane.normal),
1979
+ planePoint: vectorData(dragging.plane.point),
1980
+ intersection: vectorData(intersection),
1981
+ delta: {
1982
+ x: Number(dragging.mesh.position.x || 0) - Number(previousPosition?.x || 0),
1983
+ y: Number(dragging.mesh.position.y || 0) - Number(previousPosition?.y || 0),
1984
+ z: Number(dragging.mesh.position.z || 0) - Number(previousPosition?.z || 0),
1985
+ distance: distanceBetween(dragging.mesh.position, previousPosition),
1986
+ },
1987
+ rawPosition: vectorData(rawPosition),
1988
+ response: resize ? {
1989
+ operation: 'resize',
1990
+ handle: resize.handle,
1991
+ delta: resize.delta,
1992
+ size: resize.size,
1993
+ } : filtered.diagnostics,
1994
+ };
1995
+ dragging.lastPosition = dragging.mesh.position.clone?.() || null;
1996
+ dragging.lastRawPosition = rawPosition.clone?.() || null;
1997
+ return {
1998
+ ok: true,
1999
+ panelId: dragging.mesh.userData?.panelId || null,
2000
+ frameTarget: dragging.frameTarget || null,
2001
+ dragModel: 'controller-ray-plane',
2002
+ };
2003
+ }
2004
+
2005
+ function endDrag() {
2006
+ let panelId = dragging?.mesh?.userData?.panelId || null;
2007
+ let pose = diagnostics.drag
2008
+ ? {
2009
+ position: diagnostics.drag.position || null,
2010
+ rotation: diagnostics.drag.rotation || null,
2011
+ size: diagnostics.drag.size || null,
2012
+ }
2013
+ : null;
2014
+ dragging = null;
2015
+ if (diagnostics.drag) diagnostics.drag = { ...diagnostics.drag, active: false };
2016
+ return { ok: true, panelId, frameTarget: diagnostics.drag?.frameTarget || null, pose };
2017
+ }
2018
+
2019
+ return {
2020
+ getHits,
2021
+ beginDrag,
2022
+ updateDrag,
2023
+ endDrag,
2024
+ getState() {
2025
+ return {
2026
+ dragging: Boolean(dragging),
2027
+ panelId: dragging?.mesh?.userData?.panelId || null,
2028
+ dragModel: 'controller-ray-plane',
2029
+ diagnostics: this.getDiagnostics(),
2030
+ };
2031
+ },
2032
+ getDiagnostics() {
2033
+ return {
2034
+ ...diagnostics,
2035
+ counters: { ...counters },
2036
+ };
2037
+ },
2038
+ };
2039
+ }
2040
+
2041
+ export function createXRThreeWebXRAdapter(options = {}) {
2042
+ let THREE = options.THREE;
2043
+ let check = assertThree(THREE);
2044
+ let sceneAdapter = createXRThreePanelSceneAdapter(options);
2045
+ let rayAdapter = createXRThreeControllerRayAdapter(options);
2046
+ let state = {
2047
+ renderer: null,
2048
+ camera: null,
2049
+ scene: sceneAdapter.getScene(),
2050
+ panelCount: 0,
2051
+ session: null,
2052
+ };
2053
+
2054
+ function createRenderer(rendererOptions = {}) {
2055
+ if (!check.ok) return { ok: false, reason: check.reason, missing: check.missing };
2056
+ let renderer;
2057
+ try {
2058
+ renderer = rendererOptions.renderer || new THREE.WebGLRenderer({
2059
+ antialias: true,
2060
+ alpha: true,
2061
+ ...(rendererOptions.webgl || {}),
2062
+ });
2063
+ } catch (error) {
2064
+ return {
2065
+ ok: false,
2066
+ reason: 'webgl-renderer-create-failed',
2067
+ error: error?.name || 'Error',
2068
+ message: error?.message || '',
2069
+ };
2070
+ }
2071
+ if (renderer.xr) renderer.xr.enabled = true;
2072
+ state.renderer = renderer;
2073
+ return { ok: true, renderer };
2074
+ }
2075
+
2076
+ function createCamera(cameraOptions = {}) {
2077
+ if (!check.ok) return { ok: false, reason: check.reason, missing: check.missing };
2078
+ let camera = cameraOptions.camera || new THREE.PerspectiveCamera(
2079
+ Number(cameraOptions.fov || 70),
2080
+ Number(cameraOptions.aspect || 1),
2081
+ Number(cameraOptions.near || 0.01),
2082
+ Number(cameraOptions.far || 100),
2083
+ );
2084
+ applyVector(camera.position, cameraOptions.position || [0, 1.6, 2]);
2085
+ state.camera = camera;
2086
+ return { ok: true, camera };
2087
+ }
2088
+
2089
+ function setScene(xrScene, setOptions = {}) {
2090
+ let result = sceneAdapter.setScene(xrScene, setOptions);
2091
+ state.scene = sceneAdapter.getScene();
2092
+ state.panelCount = result.panelCount || 0;
2093
+ return result;
2094
+ }
2095
+
2096
+ async function setSession(session, sessionOptions = {}) {
2097
+ if (!state.renderer?.xr?.setSession) {
2098
+ return { ok: false, reason: 'missing-three-webxr-manager' };
2099
+ }
2100
+ if (sessionOptions.referenceSpaceType && hasFn(state.renderer.xr, 'setReferenceSpaceType')) {
2101
+ state.renderer.xr.setReferenceSpaceType(sessionOptions.referenceSpaceType);
2102
+ }
2103
+ await state.renderer.xr.setSession(session);
2104
+ state.session = session;
2105
+ let referenceSpace = state.renderer.xr.getReferenceSpace?.() || sessionOptions.referenceSpace || null;
2106
+ return { ok: true, session, referenceSpace };
2107
+ }
2108
+
2109
+ return {
2110
+ ...XR_THREE_WEBXR_ADAPTER,
2111
+ createRenderer,
2112
+ createCamera,
2113
+ setScene,
2114
+ setSession,
2115
+ applyViewerPose: sceneAdapter.applyViewerPose,
2116
+ getPanelMesh: sceneAdapter.getPanelMesh,
2117
+ listPanelMeshes: sceneAdapter.listPanelMeshes,
2118
+ createControllerRayVisual(controller, visualOptions = {}) {
2119
+ let visual = buildControllerRayVisual(THREE, visualOptions);
2120
+ if (visual.ok && controller?.add) {
2121
+ controller.add(visual.object);
2122
+ }
2123
+ return visual;
2124
+ },
2125
+ createPanelHitReticleVisual(scene, visualOptions = {}) {
2126
+ let visual = buildPanelHitReticleVisual(THREE, visualOptions);
2127
+ if (visual.ok && scene?.add) {
2128
+ scene.add(visual.object);
2129
+ }
2130
+ return visual;
2131
+ },
2132
+ updatePanelHitReticleVisual,
2133
+ controllerRays: rayAdapter,
2134
+ getState() {
2135
+ let sceneState = sceneAdapter.getState();
2136
+ return {
2137
+ ...sceneState,
2138
+ renderer: Boolean(state.renderer),
2139
+ camera: Boolean(state.camera),
2140
+ scene: Boolean(state.scene),
2141
+ panelCount: state.panelCount || sceneState.panelCount,
2142
+ session: Boolean(state.session),
2143
+ controller: rayAdapter.getState(),
2144
+ };
2145
+ },
2146
+ getDiagnostics() {
2147
+ return {
2148
+ ...this.getState(),
2149
+ controller: rayAdapter.getDiagnostics(),
2150
+ };
2151
+ },
2152
+ };
2153
+ }
2154
+
2155
+ function summarizeThreeRenderer(renderer = null) {
2156
+ let canvas = renderer?.domElement || null;
2157
+ let gl = renderer?.getContext?.() || null;
2158
+ let contextAttributes = gl?.getContextAttributes?.() || null;
2159
+ return {
2160
+ version: 'xr-three-renderer-diagnostics-v1',
2161
+ present: Boolean(renderer),
2162
+ xrEnabled: renderer?.xr?.enabled === true,
2163
+ outputColorSpace: renderer?.outputColorSpace || renderer?.outputEncoding || null,
2164
+ canvas: canvas ? {
2165
+ width: Number.isFinite(Number(canvas.width)) ? Number(canvas.width) : null,
2166
+ height: Number.isFinite(Number(canvas.height)) ? Number(canvas.height) : null,
2167
+ clientWidth: Number.isFinite(Number(canvas.clientWidth)) ? Number(canvas.clientWidth) : null,
2168
+ clientHeight: Number.isFinite(Number(canvas.clientHeight)) ? Number(canvas.clientHeight) : null,
2169
+ } : null,
2170
+ contextAttributes: contextAttributes ? {
2171
+ alpha: contextAttributes.alpha == null ? null : Boolean(contextAttributes.alpha),
2172
+ premultipliedAlpha: contextAttributes.premultipliedAlpha == null ? null : Boolean(contextAttributes.premultipliedAlpha),
2173
+ preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer == null ? null : Boolean(contextAttributes.preserveDrawingBuffer),
2174
+ antialias: contextAttributes.antialias == null ? null : Boolean(contextAttributes.antialias),
2175
+ depth: contextAttributes.depth == null ? null : Boolean(contextAttributes.depth),
2176
+ stencil: contextAttributes.stencil == null ? null : Boolean(contextAttributes.stencil),
2177
+ } : null,
2178
+ };
2179
+ }
2180
+
2181
+ export function createXRThreeRenderHost(options = {}) {
2182
+ let THREE = options.THREE;
2183
+ let adapter = options.adapter || createXRThreeWebXRAdapter(options);
2184
+ let renderer = options.renderer || null;
2185
+ let camera = options.camera || null;
2186
+ let scene = null;
2187
+ let diagnostics = {
2188
+ version: 'xr-three-render-host-v1',
2189
+ renderer: false,
2190
+ camera: false,
2191
+ scene: false,
2192
+ decorated: false,
2193
+ loopRunning: false,
2194
+ frames: 0,
2195
+ width: 0,
2196
+ height: 0,
2197
+ aspect: 1,
2198
+ pixelRatio: 1,
2199
+ lastError: null,
2200
+ };
2201
+
2202
+ function decorateScene(targetScene, decorateOptions = {}) {
2203
+ if (!targetScene || targetScene.userData?.snSpatialDecorated) return false;
2204
+ targetScene.userData ||= {};
2205
+ targetScene.userData.snSpatialDecorated = true;
2206
+ if (THREE?.Color) {
2207
+ targetScene.background = new THREE.Color(decorateOptions.background ?? 0x11151d);
2208
+ }
2209
+ if (THREE?.HemisphereLight && hasFn(targetScene, 'add')) {
2210
+ targetScene.add(new THREE.HemisphereLight(
2211
+ decorateOptions.skyColor ?? 0xffffff,
2212
+ decorateOptions.groundColor ?? 0x182030,
2213
+ Number(decorateOptions.intensity ?? 1.2),
2214
+ ));
2215
+ }
2216
+ diagnostics.decorated = true;
2217
+ return true;
2218
+ }
2219
+
2220
+ function ensureRenderer(hostOptions = {}) {
2221
+ if (renderer) return { ok: true, renderer };
2222
+ let result = adapter.createRenderer({
2223
+ webgl: {
2224
+ antialias: true,
2225
+ alpha: true,
2226
+ ...(hostOptions.webgl || options.webgl || {}),
2227
+ },
2228
+ renderer: hostOptions.renderer,
2229
+ });
2230
+ if (!result.ok) {
2231
+ diagnostics.lastError = result.reason || 'renderer-create-failed';
2232
+ return result;
2233
+ }
2234
+ renderer = result.renderer;
2235
+ diagnostics.renderer = true;
2236
+ let className = hostOptions.className || options.className || null;
2237
+ if (className && renderer.domElement) renderer.domElement.className = className;
2238
+ let parent = hostOptions.hostElement || options.hostElement;
2239
+ if (parent && renderer.domElement && renderer.domElement.parentNode !== parent) {
2240
+ parent.append?.(renderer.domElement);
2241
+ }
2242
+ return { ok: true, renderer };
2243
+ }
2244
+
2245
+ function ensureCamera(hostOptions = {}, bounds = {}) {
2246
+ let aspect = Math.max(0.1, Number(bounds.width || 1280) / Math.max(1, Number(bounds.height || 720)));
2247
+ if (!camera) {
2248
+ let result = adapter.createCamera({
2249
+ aspect,
2250
+ position: hostOptions.cameraPosition || options.cameraPosition || [0, 1.6, 2],
2251
+ ...(hostOptions.camera || options.camera || {}),
2252
+ });
2253
+ if (!result.ok) {
2254
+ diagnostics.lastError = result.reason || 'camera-create-failed';
2255
+ return result;
2256
+ }
2257
+ camera = result.camera;
2258
+ } else {
2259
+ camera.aspect = aspect;
2260
+ camera.updateProjectionMatrix?.();
2261
+ }
2262
+ diagnostics.camera = true;
2263
+ diagnostics.aspect = aspect;
2264
+ return { ok: true, camera };
2265
+ }
2266
+
2267
+ function ensureTarget(hostOptions = {}) {
2268
+ let rendererResult = ensureRenderer(hostOptions);
2269
+ if (!rendererResult.ok) return rendererResult;
2270
+
2271
+ let bounds = readBounds(hostOptions.bounds || hostOptions.stageElement || options.stageElement, hostOptions.fallbackBounds);
2272
+ let pixelRatio = clampPixelRatio(hostOptions.pixelRatio ?? options.pixelRatio ?? options.globalThis?.devicePixelRatio ?? 1, hostOptions.maxPixelRatio ?? options.maxPixelRatio);
2273
+ rendererResult.renderer.setPixelRatio?.(pixelRatio);
2274
+ rendererResult.renderer.setSize?.(bounds.width, bounds.height, false);
2275
+
2276
+ let cameraResult = ensureCamera(hostOptions, bounds);
2277
+ if (!cameraResult.ok) return cameraResult;
2278
+
2279
+ let sceneResult = adapter.setScene(hostOptions.scene || options.scene || null, hostOptions.sceneOptions || {});
2280
+ if (!sceneResult.ok) {
2281
+ diagnostics.lastError = sceneResult.reason || 'scene-create-failed';
2282
+ return sceneResult;
2283
+ }
2284
+ scene = sceneResult.scene;
2285
+ decorateScene(scene, hostOptions.decoration || options.decoration || {});
2286
+ diagnostics = {
2287
+ ...diagnostics,
2288
+ renderer: true,
2289
+ camera: true,
2290
+ scene: Boolean(scene),
2291
+ width: bounds.width,
2292
+ height: bounds.height,
2293
+ aspect: Math.max(0.1, bounds.width / Math.max(1, bounds.height)),
2294
+ pixelRatio,
2295
+ lastError: null,
2296
+ };
2297
+ return {
2298
+ ok: true,
2299
+ renderer: rendererResult.renderer,
2300
+ camera: cameraResult.camera,
2301
+ scene,
2302
+ width: bounds.width,
2303
+ height: bounds.height,
2304
+ pixelRatio,
2305
+ };
2306
+ }
2307
+
2308
+ function getDiagnostics() {
2309
+ return {
2310
+ ...diagnostics,
2311
+ rendererDiagnostics: summarizeThreeRenderer(renderer),
2312
+ };
2313
+ }
2314
+
2315
+ function startLoop(loopOptions = {}) {
2316
+ let target = loopOptions.target || {
2317
+ ok: Boolean(renderer && camera && scene),
2318
+ renderer,
2319
+ camera,
2320
+ scene,
2321
+ };
2322
+ if (!target.ok || !target.renderer?.setAnimationLoop) {
2323
+ diagnostics.lastError = target.reason || 'missing-three-animation-loop';
2324
+ return { ok: false, reason: diagnostics.lastError };
2325
+ }
2326
+ target.renderer.setAnimationLoop((time, frame) => {
2327
+ loopOptions.onFrame?.({
2328
+ time,
2329
+ frame,
2330
+ target,
2331
+ renderer: target.renderer,
2332
+ scene: target.scene,
2333
+ camera: target.camera,
2334
+ });
2335
+ if (loopOptions.renderFrame !== false) {
2336
+ target.renderer.render?.(target.scene, target.camera);
2337
+ }
2338
+ diagnostics.frames += 1;
2339
+ });
2340
+ diagnostics.loopRunning = true;
2341
+ diagnostics.lastError = null;
2342
+ return {
2343
+ ok: true,
2344
+ version: 'xr-three-render-loop-v1',
2345
+ renderFrame: loopOptions.renderFrame !== false,
2346
+ };
2347
+ }
2348
+
2349
+ function stopLoop(loopOptions = {}) {
2350
+ let targetRenderer = loopOptions.renderer || renderer;
2351
+ if (!targetRenderer?.setAnimationLoop) {
2352
+ diagnostics.lastError = 'missing-three-animation-loop';
2353
+ return { ok: false, reason: diagnostics.lastError };
2354
+ }
2355
+ targetRenderer.setAnimationLoop(null);
2356
+ diagnostics.loopRunning = false;
2357
+ return { ok: true, version: 'xr-three-render-loop-v1' };
2358
+ }
2359
+
2360
+ return {
2361
+ ensureTarget,
2362
+ resize: ensureTarget,
2363
+ startLoop,
2364
+ stopLoop,
2365
+ getDiagnostics,
2366
+ getState: getDiagnostics,
2367
+ };
2368
+ }
2369
+
2370
+ function summarizeXRRenderState(session = null) {
2371
+ let renderState = session?.renderState || null;
2372
+ let baseLayer = renderState?.baseLayer || null;
2373
+ let layers = Array.isArray(renderState?.layers) ? renderState.layers : [];
2374
+ return {
2375
+ version: 'xr-render-state-diagnostics-v1',
2376
+ baseLayer: baseLayer ? {
2377
+ present: true,
2378
+ framebufferWidth: Number.isFinite(Number(baseLayer.framebufferWidth)) ? Number(baseLayer.framebufferWidth) : null,
2379
+ framebufferHeight: Number.isFinite(Number(baseLayer.framebufferHeight)) ? Number(baseLayer.framebufferHeight) : null,
2380
+ fixedFoveation: Number.isFinite(Number(baseLayer.fixedFoveation)) ? Number(baseLayer.fixedFoveation) : null,
2381
+ } : { present: false },
2382
+ layers: {
2383
+ count: layers.length,
2384
+ present: layers.length > 0,
2385
+ },
2386
+ depthNear: Number.isFinite(Number(renderState?.depthNear)) ? Number(renderState.depthNear) : null,
2387
+ depthFar: Number.isFinite(Number(renderState?.depthFar)) ? Number(renderState.depthFar) : null,
2388
+ inlineVerticalFieldOfView: Number.isFinite(Number(renderState?.inlineVerticalFieldOfView)) ? Number(renderState.inlineVerticalFieldOfView) : null,
2389
+ };
2390
+ }
2391
+
2392
+ function summarizeXRFrameViewports(frame = null, referenceSpace = null, session = null, options = {}) {
2393
+ let baseLayer = session?.renderState?.baseLayer || null;
2394
+ let pose = options.viewerPose || null;
2395
+ if (!baseLayer) {
2396
+ return {
2397
+ version: 'xr-frame-viewport-diagnostics-v1',
2398
+ viewCount: 0,
2399
+ views: [],
2400
+ reason: 'xr-base-layer-missing',
2401
+ };
2402
+ }
2403
+ if (!pose && frame?.getViewerPose && referenceSpace) {
2404
+ pose = frame.getViewerPose(referenceSpace);
2405
+ }
2406
+ let views = Array.isArray(pose?.views) ? pose.views : [];
2407
+ return {
2408
+ version: 'xr-frame-viewport-diagnostics-v1',
2409
+ viewCount: views.length,
2410
+ views: views.slice(0, 4).map((view) => {
2411
+ let viewport = null;
2412
+ try {
2413
+ viewport = baseLayer?.getViewport?.(view) || null;
2414
+ } catch {
2415
+ viewport = null;
2416
+ }
2417
+ return {
2418
+ eye: view.eye || null,
2419
+ viewport: viewport ? {
2420
+ x: Number.isFinite(Number(viewport.x)) ? Number(viewport.x) : null,
2421
+ y: Number.isFinite(Number(viewport.y)) ? Number(viewport.y) : null,
2422
+ width: Number.isFinite(Number(viewport.width)) ? Number(viewport.width) : null,
2423
+ height: Number.isFinite(Number(viewport.height)) ? Number(viewport.height) : null,
2424
+ } : null,
2425
+ projectionMatrix: Boolean(view.projectionMatrix),
2426
+ transform: Boolean(view.transform),
2427
+ };
2428
+ }),
2429
+ };
2430
+ }
2431
+
2432
+ export function createXRThreeSessionController(options = {}) {
2433
+ let target = options.globalThis || globalThis;
2434
+ let adapter = options.adapter || createXRThreeWebXRAdapter(options);
2435
+ let activeSession = null;
2436
+ let activeTarget = null;
2437
+ let controllers = [];
2438
+ let hitReticle = null;
2439
+ let lastHoverPanelId = null;
2440
+ let diagnostics = {
2441
+ version: 'xr-three-session-controller-v1',
2442
+ status: 'idle',
2443
+ mode: null,
2444
+ lastError: null,
2445
+ controllers: 0,
2446
+ controllerRayVisuals: 0,
2447
+ hitReticleVisuals: 0,
2448
+ selectedPanelId: null,
2449
+ draggingPanelId: null,
2450
+ hover: null,
2451
+ interactionEvents: 0,
2452
+ frames: 0,
2453
+ visibilityState: null,
2454
+ environmentBlendMode: null,
2455
+ interactionMode: null,
2456
+ enabledFeatures: [],
2457
+ inputSources: [],
2458
+ primaryInputSource: null,
2459
+ requestedReferenceSpaceType: null,
2460
+ requestedOptionalFeatures: [],
2461
+ requestedRequiredFeatures: [],
2462
+ requestedDomOverlay: false,
2463
+ renderState: null,
2464
+ viewports: null,
2465
+ viewerPoseCaptured: false,
2466
+ viewerPoseCaptureReason: null,
2467
+ viewerPoseRootTransform: null,
2468
+ frameErrors: 0,
2469
+ lastFrameStage: null,
2470
+ lastEvent: null,
2471
+ };
2472
+
2473
+ function emit(event, details = {}) {
2474
+ diagnostics.lastEvent = event;
2475
+ options.onDiagnostic?.(event, {
2476
+ ...details,
2477
+ session: getDiagnostics(),
2478
+ adapter: adapter.getDiagnostics?.() || adapter.getState?.() || null,
2479
+ });
2480
+ }
2481
+
2482
+ function sessionOptionsFor(mode, startOptions = {}) {
2483
+ return createXRThreeSessionOptions(mode, startOptions);
2484
+ }
2485
+
2486
+ function setupControllers(scene, renderer, camera, startOptions = {}) {
2487
+ if (!scene || !renderer?.xr?.getController || controllers.length) return;
2488
+ for (let index = 0; index < 2; index += 1) {
2489
+ let controller = renderer.xr.getController(index);
2490
+ controller.addEventListener?.('selectstart', () => {
2491
+ let hit = adapter.controllerRays.getHits(
2492
+ controller,
2493
+ adapter.listPanelMeshes(),
2494
+ )[0];
2495
+ if (hit) {
2496
+ diagnostics.selectedPanelId = hit.object?.userData?.panelId || null;
2497
+ diagnostics.interactionEvents += 1;
2498
+ if (isXRFrameDragTarget(hit.frameTarget)) {
2499
+ let drag = adapter.controllerRays.beginDrag(controller, hit, camera);
2500
+ if (drag?.ok !== false) {
2501
+ diagnostics.draggingPanelId = diagnostics.selectedPanelId;
2502
+ emit('spatial-three-drag-start', {
2503
+ panelId: diagnostics.draggingPanelId,
2504
+ frameTarget: hit.frameTarget || null,
2505
+ });
2506
+ return;
2507
+ }
2508
+ }
2509
+ emit('spatial-three-select', {
2510
+ panelId: diagnostics.selectedPanelId,
2511
+ frameTarget: hit.frameTarget || null,
2512
+ });
2513
+ }
2514
+ });
2515
+ controller.addEventListener?.('selectend', () => {
2516
+ let wasDragging = adapter.controllerRays.getState?.().dragging === true;
2517
+ let result = wasDragging ? adapter.controllerRays.endDrag() : null;
2518
+ diagnostics.draggingPanelId = null;
2519
+ diagnostics.interactionEvents += 1;
2520
+ emit(wasDragging ? 'spatial-three-drag-end' : 'spatial-three-select-end', {
2521
+ panelId: result?.panelId || diagnostics.selectedPanelId,
2522
+ frameTarget: result?.frameTarget || null,
2523
+ pose: result?.pose || null,
2524
+ });
2525
+ });
2526
+ if (startOptions.controllerRayVisuals !== false) {
2527
+ let visual = adapter.createControllerRayVisual?.(controller, {
2528
+ ...(options.controllerRayVisuals || {}),
2529
+ ...(startOptions.controllerRayVisuals || {}),
2530
+ });
2531
+ if (visual?.ok) diagnostics.controllerRayVisuals += 1;
2532
+ }
2533
+ scene.add?.(controller);
2534
+ controllers.push(controller);
2535
+ }
2536
+ diagnostics.controllers = controllers.length;
2537
+ if (startOptions.panelHitReticle !== false && !hitReticle) {
2538
+ hitReticle = adapter.createPanelHitReticleVisual?.(scene, {
2539
+ ...(options.panelHitReticle || {}),
2540
+ ...(startOptions.panelHitReticle || {}),
2541
+ });
2542
+ if (hitReticle?.ok) diagnostics.hitReticleVisuals = 1;
2543
+ }
2544
+ }
2545
+
2546
+ function updateHover() {
2547
+ if (!controllers.length) return;
2548
+ let hit = null;
2549
+ for (let controller of controllers) {
2550
+ hit = adapter.controllerRays.getHits(controller, adapter.listPanelMeshes())[0] || null;
2551
+ if (hit) break;
2552
+ }
2553
+ let reticle = adapter.updatePanelHitReticleVisual?.(hitReticle, hit) || null;
2554
+ let panelId = hit?.object?.userData?.panelId || null;
2555
+ diagnostics.hover = {
2556
+ panelId,
2557
+ point: vectorData(hit?.point),
2558
+ distance: Number(hit?.distance || 0),
2559
+ reticleVisible: Boolean(reticle?.visible),
2560
+ frameTarget: hit?.frameTarget || null,
2561
+ };
2562
+ if (panelId !== lastHoverPanelId) {
2563
+ lastHoverPanelId = panelId;
2564
+ emit('spatial-three-hover-change', { hover: diagnostics.hover });
2565
+ }
2566
+ }
2567
+
2568
+ function updateDrag() {
2569
+ if (!adapter.controllerRays.getState().dragging) return;
2570
+ let result = adapter.controllerRays.updateDrag();
2571
+ diagnostics.draggingPanelId = adapter.controllerRays.getState().panelId || diagnostics.draggingPanelId;
2572
+ if (!result.ok) {
2573
+ emit('spatial-three-drag-miss', {
2574
+ error: result.reason || 'drag-update-failed',
2575
+ });
2576
+ }
2577
+ }
2578
+
2579
+ function captureViewerPose(frame, referenceSpace, sessionOptions = {}) {
2580
+ if (diagnostics.viewerPoseCaptured) return null;
2581
+ if (!frame?.getViewerPose || !referenceSpace || !adapter.applyViewerPose) {
2582
+ diagnostics.viewerPoseCaptureReason = 'viewer-pose-unavailable';
2583
+ return null;
2584
+ }
2585
+ let viewerPose = frame.getViewerPose(referenceSpace);
2586
+ if (!viewerPose) {
2587
+ diagnostics.viewerPoseCaptureReason = 'viewer-pose-empty';
2588
+ return null;
2589
+ }
2590
+ let result = adapter.applyViewerPose(viewerPose, {
2591
+ mode: diagnostics.mode,
2592
+ referenceSpaceType: sessionOptions.referenceSpaceType || diagnostics.requestedReferenceSpaceType,
2593
+ });
2594
+ let captured = result && typeof result === 'object' ? { ...result, viewerPose } : { ok: false, viewerPose };
2595
+ diagnostics.viewerPoseCaptured = result?.ok === true;
2596
+ diagnostics.viewerPoseCaptureReason = result?.ok ? null : result?.reason || 'viewer-pose-apply-failed';
2597
+ diagnostics.viewerPoseRootTransform = result?.rootTransform || null;
2598
+ emit(result?.ok ? 'spatial-three-viewer-pose-captured' : 'spatial-three-viewer-pose-failed', {
2599
+ result,
2600
+ reason: diagnostics.viewerPoseCaptureReason,
2601
+ });
2602
+ return captured;
2603
+ }
2604
+
2605
+ function cleanupSession() {
2606
+ activeTarget?.renderer?.setAnimationLoop?.(null);
2607
+ activeSession = null;
2608
+ if (adapter.controllerRays.getState?.().dragging === true) {
2609
+ adapter.controllerRays.endDrag();
2610
+ }
2611
+ diagnostics.status = 'idle';
2612
+ emit('spatial-three-session-ended');
2613
+ }
2614
+
2615
+ function updateSessionRuntimeDiagnostics() {
2616
+ if (!activeSession) {
2617
+ diagnostics.visibilityState = null;
2618
+ diagnostics.environmentBlendMode = null;
2619
+ diagnostics.interactionMode = null;
2620
+ diagnostics.enabledFeatures = [];
2621
+ diagnostics.inputSources = [];
2622
+ diagnostics.renderState = null;
2623
+ diagnostics.viewports = null;
2624
+ return;
2625
+ }
2626
+ diagnostics.visibilityState = activeSession.visibilityState || null;
2627
+ diagnostics.environmentBlendMode = activeSession.environmentBlendMode || null;
2628
+ diagnostics.interactionMode = activeSession.interactionMode || null;
2629
+ diagnostics.enabledFeatures = normalizeStringList(activeSession.enabledFeatures);
2630
+ diagnostics.inputSources = normalizeInputSources(activeSession.inputSources);
2631
+ diagnostics.primaryInputSource = selectPrimaryXRInputSource(activeSession.inputSources || [], options.inputSource || {}).selected;
2632
+ diagnostics.renderState = summarizeXRRenderState(activeSession);
2633
+ }
2634
+
2635
+ function captureFrameStage(stage, fn, context = {}) {
2636
+ diagnostics.lastFrameStage = stage;
2637
+ try {
2638
+ return fn();
2639
+ } catch (error) {
2640
+ diagnostics.frameErrors += 1;
2641
+ diagnostics.lastError = error?.name || `${stage}-failed`;
2642
+ emit('spatial-three-frame-error', {
2643
+ ...context,
2644
+ failureStage: stage,
2645
+ error: diagnostics.lastError,
2646
+ message: error?.message || '',
2647
+ });
2648
+ return null;
2649
+ }
2650
+ }
2651
+
2652
+ async function start(mode = 'immersive-vr', startOptions = {}) {
2653
+ activeTarget = startOptions.target || activeTarget;
2654
+ if (!activeTarget?.ok) {
2655
+ diagnostics.lastError = activeTarget?.reason || 'three-webxr-unavailable';
2656
+ emit('spatial-three-session-failed', {
2657
+ attemptId: startOptions.attemptId || null,
2658
+ failureStage: 'target-unavailable',
2659
+ error: diagnostics.lastError,
2660
+ requestedMode: mode,
2661
+ });
2662
+ return { handled: false, ok: false, reason: diagnostics.lastError, failureStage: 'target-unavailable' };
2663
+ }
2664
+ if (!target?.navigator?.xr?.requestSession) {
2665
+ diagnostics.lastError = 'request-session-unavailable';
2666
+ emit('spatial-three-session-failed', {
2667
+ attemptId: startOptions.attemptId || null,
2668
+ failureStage: 'request-session-unavailable',
2669
+ error: diagnostics.lastError,
2670
+ requestedMode: mode,
2671
+ });
2672
+ return { handled: false, ok: false, reason: diagnostics.lastError, failureStage: 'request-session-unavailable' };
2673
+ }
2674
+
2675
+ diagnostics.status = 'starting';
2676
+ diagnostics.mode = mode;
2677
+ diagnostics.lastError = null;
2678
+ let xrOptions = sessionOptionsFor(mode, startOptions);
2679
+ diagnostics.requestedReferenceSpaceType = xrOptions.referenceSpaceType || null;
2680
+ diagnostics.requestedOptionalFeatures = normalizeStringList(xrOptions.optionalFeatures);
2681
+ diagnostics.requestedRequiredFeatures = normalizeStringList(xrOptions.requiredFeatures);
2682
+ diagnostics.requestedDomOverlay = Boolean(xrOptions.domOverlayRoot);
2683
+ diagnostics.viewerPoseCaptured = false;
2684
+ diagnostics.viewerPoseCaptureReason = null;
2685
+ diagnostics.viewerPoseRootTransform = null;
2686
+ diagnostics.frameErrors = 0;
2687
+ diagnostics.lastFrameStage = null;
2688
+ emit('spatial-three-session-start-requested', {
2689
+ attemptId: startOptions.attemptId || null,
2690
+ requestedMode: mode,
2691
+ sessionOptions: {
2692
+ referenceSpaceType: diagnostics.requestedReferenceSpaceType,
2693
+ optionalFeatures: diagnostics.requestedOptionalFeatures,
2694
+ requiredFeatures: diagnostics.requestedRequiredFeatures,
2695
+ domOverlay: diagnostics.requestedDomOverlay,
2696
+ },
2697
+ });
2698
+
2699
+ try {
2700
+ let adapterOptions = {
2701
+ referenceSpaceType: xrOptions.referenceSpaceType,
2702
+ optionalFeatures: xrOptions.optionalFeatures,
2703
+ };
2704
+ let sessionResult = await requestWebXRSession(target, mode, xrOptions);
2705
+ if (!sessionResult.ok) {
2706
+ diagnostics.status = 'failed';
2707
+ diagnostics.lastError = sessionResult.reason || 'three-session-failed';
2708
+ emit('spatial-three-session-failed', {
2709
+ attemptId: startOptions.attemptId || null,
2710
+ failureStage: 'request-session',
2711
+ error: diagnostics.lastError,
2712
+ requestedMode: mode,
2713
+ });
2714
+ return { handled: true, ok: false, reason: diagnostics.lastError, failureStage: 'request-session' };
2715
+ }
2716
+ let setSession = await adapter.setSession(sessionResult.session, adapterOptions);
2717
+ if (!setSession.ok) {
2718
+ await sessionResult.session.end?.();
2719
+ diagnostics.status = 'failed';
2720
+ diagnostics.lastError = setSession.reason || 'three-session-failed';
2721
+ emit('spatial-three-session-failed', {
2722
+ attemptId: startOptions.attemptId || null,
2723
+ failureStage: 'set-session',
2724
+ error: diagnostics.lastError,
2725
+ requestedMode: mode,
2726
+ });
2727
+ return { handled: true, ok: false, reason: diagnostics.lastError, failureStage: 'set-session' };
2728
+ }
2729
+ activeSession = sessionResult.session;
2730
+ diagnostics.status = 'running';
2731
+ updateSessionRuntimeDiagnostics();
2732
+ setupControllers(activeTarget.scene, activeTarget.renderer, activeTarget.camera, startOptions);
2733
+ activeTarget.renderer.setAnimationLoop?.((time, frame) => {
2734
+ diagnostics.frames += 1;
2735
+ let frameContext = {
2736
+ attemptId: startOptions.attemptId || null,
2737
+ frameNumber: diagnostics.frames,
2738
+ mode,
2739
+ };
2740
+ captureFrameStage('runtime-diagnostics', () => updateSessionRuntimeDiagnostics(), frameContext);
2741
+ let capturedPose = captureFrameStage(
2742
+ 'viewer-pose',
2743
+ () => captureViewerPose(frame, setSession.referenceSpace, xrOptions),
2744
+ frameContext,
2745
+ );
2746
+ captureFrameStage('frame-viewports', () => {
2747
+ diagnostics.viewports = summarizeXRFrameViewports(frame, setSession.referenceSpace, activeSession, {
2748
+ viewerPose: capturedPose?.viewerPose || capturedPose?.result?.viewerPose,
2749
+ });
2750
+ }, frameContext);
2751
+ captureFrameStage('hover', () => updateHover(), frameContext);
2752
+ captureFrameStage('drag', () => updateDrag(), frameContext);
2753
+ captureFrameStage('frame-callback', () => {
2754
+ options.onFrame?.({ time, frame, target: activeTarget, session: activeSession });
2755
+ }, frameContext);
2756
+ if (startOptions.renderFrame !== false) {
2757
+ captureFrameStage('render', () => {
2758
+ activeTarget.renderer.render?.(activeTarget.scene, activeTarget.camera);
2759
+ }, frameContext);
2760
+ }
2761
+ });
2762
+ activeSession.addEventListener?.('end', cleanupSession, { once: true });
2763
+ emit('spatial-three-session-started', {
2764
+ attemptId: startOptions.attemptId || null,
2765
+ mode,
2766
+ });
2767
+ return { handled: true, ok: true, session: activeSession, diagnostics: getDiagnostics() };
2768
+ } catch (error) {
2769
+ diagnostics.status = 'failed';
2770
+ diagnostics.lastError = error?.name || 'three-session-failed';
2771
+ emit('spatial-three-session-failed', {
2772
+ attemptId: startOptions.attemptId || null,
2773
+ failureStage: 'exception',
2774
+ error: diagnostics.lastError,
2775
+ message: error?.message || '',
2776
+ requestedMode: mode,
2777
+ });
2778
+ return { handled: true, ok: false, reason: diagnostics.lastError, failureStage: 'exception', message: error?.message || '' };
2779
+ }
2780
+ }
2781
+
2782
+ async function stop() {
2783
+ let session = activeSession;
2784
+ if (!session?.end) {
2785
+ cleanupSession();
2786
+ return false;
2787
+ }
2788
+ await session.end();
2789
+ return true;
2790
+ }
2791
+
2792
+ function getDiagnostics() {
2793
+ updateSessionRuntimeDiagnostics();
2794
+ return {
2795
+ ...diagnostics,
2796
+ active: Boolean(activeSession),
2797
+ adapter: adapter.getDiagnostics?.() || adapter.getState?.() || null,
2798
+ };
2799
+ }
2800
+
2801
+ return {
2802
+ start,
2803
+ stop,
2804
+ getDiagnostics,
2805
+ getState: getDiagnostics,
2806
+ };
2807
+ }
2808
+
2809
+ export function createXRThreeSessionTelemetrySnapshot(diagnostics = {}, options = {}) {
2810
+ let adapter = diagnostics.adapter || {};
2811
+ let controller = adapter.controller || {};
2812
+ let drag = controller.diagnostics?.drag || {};
2813
+ let response = drag.response || null;
2814
+ let hover = diagnostics.hover || null;
2815
+ let textureQuality = summarizeTextureSourceQuality(adapter.textureSources);
2816
+ let now = Number(options.now ?? Date.now());
2817
+ return {
2818
+ version: 'xr-three-session-telemetry-v1',
2819
+ timestamp: Number.isFinite(now) ? now : null,
2820
+ status: diagnostics.status || 'unknown',
2821
+ mode: diagnostics.mode || null,
2822
+ active: Boolean(diagnostics.active),
2823
+ visibilityState: diagnostics.visibilityState || null,
2824
+ environmentBlendMode: diagnostics.environmentBlendMode || null,
2825
+ interactionMode: diagnostics.interactionMode || null,
2826
+ enabledFeatures: normalizeStringList(diagnostics.enabledFeatures),
2827
+ inputSources: normalizeInputSources(diagnostics.inputSources),
2828
+ primaryInputSource: diagnostics.primaryInputSource || null,
2829
+ sessionOptions: {
2830
+ referenceSpaceType: diagnostics.requestedReferenceSpaceType || null,
2831
+ optionalFeatures: normalizeStringList(diagnostics.requestedOptionalFeatures),
2832
+ requiredFeatures: normalizeStringList(diagnostics.requestedRequiredFeatures),
2833
+ domOverlay: Boolean(diagnostics.requestedDomOverlay),
2834
+ },
2835
+ renderState: diagnostics.renderState || null,
2836
+ viewports: diagnostics.viewports || null,
2837
+ frames: Number(diagnostics.frames || 0),
2838
+ frameErrors: Number(diagnostics.frameErrors || 0),
2839
+ lastFrameStage: diagnostics.lastFrameStage || null,
2840
+ controllers: Number(diagnostics.controllers || 0),
2841
+ controllerRayVisuals: Number(diagnostics.controllerRayVisuals || 0),
2842
+ hitReticleVisuals: Number(diagnostics.hitReticleVisuals || 0),
2843
+ selectedPanelId: diagnostics.selectedPanelId || null,
2844
+ draggingPanelId: diagnostics.draggingPanelId || null,
2845
+ hover: hover ? {
2846
+ panelId: hover.panelId || null,
2847
+ point: hover.point || null,
2848
+ distance: Number(hover.distance || 0),
2849
+ reticleVisible: Boolean(hover.reticleVisible),
2850
+ frameTarget: hover.frameTarget || null,
2851
+ } : null,
2852
+ interactionEvents: Number(diagnostics.interactionEvents || 0),
2853
+ lastEvent: diagnostics.lastEvent || null,
2854
+ lastError: diagnostics.lastError || null,
2855
+ panelCount: Number(adapter.panelCount || 0),
2856
+ panelFrameVisuals: Number(adapter.panelFrameVisualCount || 0),
2857
+ materialDiagnostics: adapter.materialDiagnostics || null,
2858
+ textureQuality,
2859
+ drag: {
2860
+ active: Boolean(drag.active),
2861
+ panelId: drag.panelId || null,
2862
+ frameTarget: drag.frameTarget || null,
2863
+ position: drag.position || null,
2864
+ rotation: drag.rotation || null,
2865
+ size: drag.size || null,
2866
+ resize: drag.resize || null,
2867
+ appliedDistance: response?.appliedDistance == null ? null : Number(response.appliedDistance),
2868
+ rawDistance: response?.rawDistance == null ? null : Number(response.rawDistance),
2869
+ smoothing: response?.smoothing == null ? null : Number(response.smoothing),
2870
+ maxStep: response?.maxStep == null ? null : Number(response.maxStep),
2871
+ deadzone: response?.deadzone == null ? null : Number(response.deadzone),
2872
+ clamped: Boolean(response?.clamped),
2873
+ settled: Boolean(response?.settled),
2874
+ },
2875
+ };
2876
+ }
2877
+
2878
+ export function createXRThreeSessionHealthSummary(input = {}, options = {}) {
2879
+ let telemetry = input?.version === 'xr-three-session-telemetry-v1'
2880
+ ? input
2881
+ : createXRThreeSessionTelemetrySnapshot(input, options);
2882
+ let minFrames = Number(options.minFrames ?? 1);
2883
+ let minControllers = Number(options.minControllers ?? 1);
2884
+ let minFps = Number(options.minFps ?? 45);
2885
+ let fps = options.fps == null ? null : Number(options.fps);
2886
+ let issues = [];
2887
+
2888
+ if (telemetry.lastError) {
2889
+ issues.push({ severity: 'blocked', code: 'session-error', value: telemetry.lastError });
2890
+ }
2891
+ if (telemetry.status === 'failed') {
2892
+ issues.push({ severity: 'blocked', code: 'session-failed' });
2893
+ }
2894
+ if (telemetry.status !== 'running') {
2895
+ issues.push({ severity: 'waiting', code: 'session-not-running', value: telemetry.status });
2896
+ }
2897
+ if (telemetry.active && telemetry.frames < minFrames) {
2898
+ issues.push({ severity: 'warning', code: 'no-xr-frames', value: telemetry.frames });
2899
+ }
2900
+ if (telemetry.active && telemetry.panelCount <= 0) {
2901
+ issues.push({ severity: 'blocked', code: 'no-panels' });
2902
+ }
2903
+ if (telemetry.active && telemetry.panelCount > 0 && telemetry.panelFrameVisuals <= 0) {
2904
+ issues.push({ severity: 'warning', code: 'no-panel-frame-visuals' });
2905
+ }
2906
+ if (telemetry.active && telemetry.renderState?.baseLayer?.present === false) {
2907
+ issues.push({ severity: 'blocked', code: 'xr-base-layer-missing' });
2908
+ }
2909
+ if (telemetry.active && telemetry.viewports && Number(telemetry.viewports.viewCount || 0) <= 0) {
2910
+ issues.push({ severity: 'blocked', code: 'xr-viewports-missing' });
2911
+ }
2912
+ if (telemetry.active && Number(telemetry.materialDiagnostics?.strictDiagnosticCount || 0) > 0) {
2913
+ issues.push({
2914
+ severity: 'blocked',
2915
+ code: 'strict-texture-diagnostic-material',
2916
+ value: telemetry.materialDiagnostics.strictDiagnosticCount,
2917
+ });
2918
+ }
2919
+ if (telemetry.active && Number(telemetry.materialDiagnostics?.transparentCount || 0) > 0) {
2920
+ issues.push({
2921
+ severity: 'warning',
2922
+ code: 'panel-material-transparent',
2923
+ value: telemetry.materialDiagnostics.transparentCount,
2924
+ });
2925
+ }
2926
+ if (telemetry.active && telemetry.textureQuality?.blocked > 0) {
2927
+ issues.push({
2928
+ severity: 'blocked',
2929
+ code: 'texture-quality-blocked',
2930
+ value: telemetry.textureQuality.blocked,
2931
+ });
2932
+ }
2933
+ if (telemetry.active && telemetry.textureQuality?.low > 0) {
2934
+ issues.push({
2935
+ severity: 'warning',
2936
+ code: 'texture-quality-low',
2937
+ value: telemetry.textureQuality.low,
2938
+ });
2939
+ }
2940
+ if (telemetry.active && telemetry.textureQuality?.warningCount > 0) {
2941
+ issues.push({
2942
+ severity: 'warning',
2943
+ code: 'texture-quality-warnings',
2944
+ value: telemetry.textureQuality.warningCount,
2945
+ });
2946
+ }
2947
+ if (telemetry.active && telemetry.controllers < minControllers) {
2948
+ issues.push({ severity: 'warning', code: 'no-input-controllers', value: telemetry.controllers });
2949
+ }
2950
+ if (telemetry.active && telemetry.controllerRayVisuals <= 0) {
2951
+ issues.push({ severity: 'warning', code: 'no-controller-ray-visuals' });
2952
+ }
2953
+ if (telemetry.active && telemetry.hitReticleVisuals <= 0) {
2954
+ issues.push({ severity: 'warning', code: 'no-hit-reticle-visual' });
2955
+ }
2956
+ if (telemetry.active && fps != null && Number.isFinite(fps) && fps > 0 && fps < minFps) {
2957
+ issues.push({ severity: 'warning', code: 'low-fps', value: fps });
2958
+ }
2959
+ if (telemetry.active && !telemetry.hover?.panelId) {
2960
+ issues.push({ severity: 'info', code: 'no-panel-hit-yet' });
2961
+ }
2962
+
2963
+ let blocking = issues.filter((issue) => issue.severity === 'blocked');
2964
+ let warnings = issues.filter((issue) => issue.severity === 'warning');
2965
+ let waiting = issues.filter((issue) => issue.severity === 'waiting');
2966
+ let status = 'healthy';
2967
+ if (blocking.length) status = 'blocked';
2968
+ else if (waiting.length) status = 'waiting';
2969
+ else if (warnings.length) status = 'warning';
2970
+
2971
+ return {
2972
+ version: 'xr-three-session-health-v1',
2973
+ status,
2974
+ reason: issues[0]?.code || 'ok',
2975
+ checks: {
2976
+ running: telemetry.status === 'running',
2977
+ active: telemetry.active === true,
2978
+ frames: telemetry.frames,
2979
+ panelCount: telemetry.panelCount,
2980
+ panelFrameVisuals: telemetry.panelFrameVisuals,
2981
+ controllers: telemetry.controllers,
2982
+ controllerRayVisuals: telemetry.controllerRayVisuals,
2983
+ hitReticleVisuals: telemetry.hitReticleVisuals,
2984
+ hoverPanelId: telemetry.hover?.panelId || null,
2985
+ textureQuality: telemetry.textureQuality || null,
2986
+ fps: Number.isFinite(fps) ? fps : null,
2987
+ },
2988
+ issues,
2989
+ };
2990
+ }
2991
+
2992
+ export function createXRThreeInteractionReadinessSummary(input = {}, options = {}) {
2993
+ let telemetry = input?.version === 'xr-three-session-telemetry-v1'
2994
+ ? input
2995
+ : createXRThreeSessionTelemetrySnapshot(input, options);
2996
+ let texture = options.texture || null;
2997
+ let expectedPanelCount = Number(options.expectedPanelCount ?? telemetry.panelCount ?? 0);
2998
+ let expectedFrameVisuals = Number(options.expectedFrameVisuals ?? expectedPanelCount);
2999
+ let requireInteractionEvent = options.requireInteractionEvent === true;
3000
+ let checks = [];
3001
+
3002
+ function add(id, status, details = {}) {
3003
+ checks.push({ id, status, ...details });
3004
+ }
3005
+
3006
+ add('session-active', telemetry.active ? 'ready' : 'waiting', {
3007
+ status: telemetry.status,
3008
+ mode: telemetry.mode,
3009
+ });
3010
+ add('panels-present', telemetry.panelCount > 0 ? 'ready' : telemetry.active ? 'blocked' : 'waiting', {
3011
+ count: telemetry.panelCount,
3012
+ });
3013
+ add('panel-frame-visuals', telemetry.panelFrameVisuals >= expectedFrameVisuals ? 'ready' : telemetry.active ? 'warning' : 'waiting', {
3014
+ count: telemetry.panelFrameVisuals,
3015
+ expected: expectedFrameVisuals,
3016
+ });
3017
+ add('input-sources-present', telemetry.controllers > 0 || telemetry.inputSources.length > 0 ? 'ready' : telemetry.active ? 'warning' : 'waiting', {
3018
+ controllers: telemetry.controllers,
3019
+ inputSources: telemetry.inputSources.length,
3020
+ });
3021
+ add('controller-rays-visible', telemetry.controllerRayVisuals > 0 ? 'ready' : telemetry.active ? 'warning' : 'waiting', {
3022
+ count: telemetry.controllerRayVisuals,
3023
+ });
3024
+ add('hit-reticle-visible', telemetry.hitReticleVisuals > 0 ? 'ready' : telemetry.active ? 'warning' : 'waiting', {
3025
+ count: telemetry.hitReticleVisuals,
3026
+ });
3027
+ add('panel-hit-state', telemetry.hover?.panelId ? 'ready' : telemetry.active ? 'info' : 'waiting', {
3028
+ panelId: telemetry.hover?.panelId || null,
3029
+ frameTarget: telemetry.hover?.frameTarget || null,
3030
+ });
3031
+ add('interaction-events', telemetry.interactionEvents > 0 || !requireInteractionEvent ? 'ready' : telemetry.active ? 'warning' : 'waiting', {
3032
+ count: telemetry.interactionEvents,
3033
+ required: requireInteractionEvent,
3034
+ });
3035
+ add('drag-resize-state', telemetry.drag.active || telemetry.drag.resize || telemetry.drag.frameTarget ? 'ready' : 'waiting', {
3036
+ active: telemetry.drag.active,
3037
+ panelId: telemetry.drag.panelId,
3038
+ frameTarget: telemetry.drag.frameTarget,
3039
+ resize: telemetry.drag.resize,
3040
+ });
3041
+ if (texture) {
3042
+ add('texture-upload-ready', texture.blocked ? 'blocked' : Number(texture.ready || 0) >= Number(texture.total || 0) ? 'ready' : 'warning', {
3043
+ ready: Number(texture.ready || 0),
3044
+ total: Number(texture.total || 0),
3045
+ reason: texture.reason || null,
3046
+ stage: texture.stage || null,
3047
+ });
3048
+ }
3049
+
3050
+ let blocked = checks.filter((check) => check.status === 'blocked');
3051
+ let warnings = checks.filter((check) => check.status === 'warning');
3052
+ let waiting = checks.filter((check) => check.status === 'waiting');
3053
+ let status = blocked.length ? 'blocked' : warnings.length ? 'warning' : waiting.length ? 'waiting' : 'ready';
3054
+
3055
+ return {
3056
+ version: 'xr-three-interaction-readiness-v1',
3057
+ ready: status === 'ready',
3058
+ status,
3059
+ reason: blocked[0]?.id || warnings[0]?.id || waiting[0]?.id || 'ready',
3060
+ checks,
3061
+ issueCodes: checks.filter((check) => check.status !== 'ready').map((check) => check.id),
3062
+ frameTarget: telemetry.hover?.frameTarget || telemetry.drag.frameTarget || null,
3063
+ dragging: telemetry.drag.active ? {
3064
+ panelId: telemetry.drag.panelId,
3065
+ frameTarget: telemetry.drag.frameTarget,
3066
+ resize: telemetry.drag.resize,
3067
+ appliedDistance: telemetry.drag.appliedDistance,
3068
+ clamped: telemetry.drag.clamped,
3069
+ settled: telemetry.drag.settled,
3070
+ } : null,
3071
+ };
3072
+ }
3073
+
3074
+ export function createXRThreeSessionWatchdogSummary(input = {}, options = {}) {
3075
+ let telemetry = input?.version === 'xr-three-session-telemetry-v1'
3076
+ ? input
3077
+ : createXRThreeSessionTelemetrySnapshot(input, options);
3078
+ let frames = Number(telemetry.frames || 0);
3079
+ let thresholdMs = Number(options.thresholdMs ?? 6000);
3080
+ let elapsedMs = options.elapsedMs == null ? null : Number(options.elapsedMs);
3081
+ let eventPrefix = options.eventPrefix || 'xr-three-session';
3082
+ let status = 'ok';
3083
+ let event = null;
3084
+ let reason = 'ok';
3085
+
3086
+ if (telemetry.status === 'starting') {
3087
+ status = 'waiting';
3088
+ event = `${eventPrefix}-still-starting`;
3089
+ reason = 'session-still-starting';
3090
+ } else if (telemetry.status === 'running' && frames <= 0) {
3091
+ status = 'warning';
3092
+ event = `${eventPrefix}-no-frames`;
3093
+ reason = 'session-no-frames';
3094
+ }
3095
+
3096
+ return {
3097
+ version: 'xr-three-session-watchdog-v1',
3098
+ status,
3099
+ event,
3100
+ reason,
3101
+ thresholdMs: Number.isFinite(thresholdMs) ? thresholdMs : 6000,
3102
+ elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : null,
3103
+ eventPrefix,
3104
+ sessionStatus: telemetry.status,
3105
+ active: telemetry.active,
3106
+ frames,
3107
+ mode: telemetry.mode,
3108
+ };
3109
+ }
3110
+
3111
+ export function createXRThreeDiagnosticPayload(options = {}) {
3112
+ let extra = options.extra || options.details || {};
3113
+ let sessionDiagnostics = options.sessionDiagnostics || {};
3114
+ let telemetry = options.telemetry?.version === 'xr-three-session-telemetry-v1'
3115
+ ? options.telemetry
3116
+ : createXRThreeSessionTelemetrySnapshot(sessionDiagnostics, options);
3117
+ let health = options.health?.version === 'xr-three-session-health-v1'
3118
+ ? options.health
3119
+ : createXRThreeSessionHealthSummary(telemetry, { fps: options.fps });
3120
+ let support = options.support || {};
3121
+ let htmlCanvas = options.htmlCanvas || null;
3122
+ let texture = options.texture || null;
3123
+ let sceneQuality = options.sceneQuality || null;
3124
+ let visual = options.visual || null;
3125
+ let visualReadiness = options.visualReadiness || null;
3126
+ let interactionReadiness = options.interactionReadiness || null;
3127
+ let launchGate = options.launchGate || createWebXRLaunchGateSummary(support, {
3128
+ preferredMode: options.preferredMode || null,
3129
+ selectedMode: options.mode || telemetry.mode || null,
3130
+ launch: options.launch || null,
3131
+ texture,
3132
+ userActivation: options.userActivation || null,
3133
+ requireUserActivation: options.requireUserActivation === true,
3134
+ });
3135
+ let readiness = options.readiness || createXRReadinessSummary({
3136
+ launchGate,
3137
+ htmlCanvas,
3138
+ texture,
3139
+ sceneQuality,
3140
+ sessionHealth: health,
3141
+ sessionActive: telemetry.active,
3142
+ mode: options.mode || telemetry.mode || null,
3143
+ });
3144
+
3145
+ return {
3146
+ version: 'xr-three-diagnostic-payload-v1',
3147
+ clientId: options.clientId || null,
3148
+ event: options.event || null,
3149
+ surface: options.surface || extra.surface || null,
3150
+ surfaceKind: options.surfaceKind || options.surface?.surfaceKind || extra.surfaceKind || extra.surface?.surfaceKind || null,
3151
+ entrypoint: options.entrypoint || options.surface?.entrypoint || extra.entrypoint || extra.surface?.entrypoint || null,
3152
+ projectId: options.projectId || options.surface?.projectId || extra.projectId || extra.surface?.projectId || null,
3153
+ targetSection: options.targetSection || options.surface?.targetSection || extra.targetSection || extra.surface?.targetSection || null,
3154
+ panelContentKind: options.panelContentKind || options.surface?.panelContentKind || extra.panelContentKind || extra.surface?.panelContentKind || null,
3155
+ pageUrl: redactXRDiagnosticUrl(options.pageUrl || ''),
3156
+ secureContext: options.secureContext === true,
3157
+ navigatorXr: options.navigatorXr === true,
3158
+ modes: options.modes || support.modes || {},
3159
+ launch: options.launch || null,
3160
+ mode: options.mode || telemetry.mode || null,
3161
+ selectedPanel: sessionDiagnostics.selectedPanelId || telemetry.selectedPanelId || null,
3162
+ hoveredPanel: sessionDiagnostics.hover?.panelId || telemetry.hover?.panelId || null,
3163
+ session: { ...telemetry, health },
3164
+ error: options.error || extra.error || null,
3165
+ details: { ...extra, htmlCanvas, sceneQuality, texture, visual, visualReadiness, interactionReadiness, launchGate, readiness },
3166
+ };
3167
+ }
3168
+
3169
+ function normalizeTimelineValue(value) {
3170
+ if (value == null || value === '') return null;
3171
+ return String(value).replace(/\s+/g, '-').slice(0, 120);
3172
+ }
3173
+
3174
+ function createXRThreeDiagnosticTimelineItem(event = {}) {
3175
+ let fields = [
3176
+ ['status', event.status],
3177
+ ['health', event.health],
3178
+ ['mode', event.mode],
3179
+ ['html', event.htmlCanvasAvailability],
3180
+ ['scene', event.sceneQualityStatus],
3181
+ ['ready', event.readinessStatus],
3182
+ ['visual', event.visualReadinessStatus],
3183
+ ['interaction', event.interactionReadinessStatus],
3184
+ ['textureMode', event.textureMode],
3185
+ ['texture', event.textureStage],
3186
+ ['resolver', event.textureResolverStage],
3187
+ ['gate', event.launchGateReason],
3188
+ ['stage', event.failureStage],
3189
+ ['error', event.error],
3190
+ ]
3191
+ .map(([key, value]) => [key, normalizeTimelineValue(value)])
3192
+ .filter(([, value]) => value);
3193
+ let eventName = normalizeTimelineValue(event.event) || 'event';
3194
+
3195
+ return {
3196
+ event: eventName,
3197
+ receivedAt: event.receivedAt || null,
3198
+ fields: Object.fromEntries(fields),
3199
+ text: [
3200
+ eventName,
3201
+ ...fields.map(([key, value]) => `${key}:${value}`),
3202
+ ].join(' '),
3203
+ };
3204
+ }
3205
+
3206
+ export function createXRThreeSessionOptions(mode = 'immersive-vr', startOptions = {}) {
3207
+ let defaultOptionalFeatures = [
3208
+ ...(startOptions.includeLocalFeature ? [WEBXR_FEATURES.local] : []),
3209
+ WEBXR_FEATURES.localFloor,
3210
+ WEBXR_FEATURES.boundedFloor,
3211
+ WEBXR_FEATURES.domOverlay,
3212
+ ];
3213
+ let explicitOptionalFeatures = normalizeStringList(startOptions.optionalFeatures);
3214
+ let requiredFeatures = [...new Set(normalizeStringList(startOptions.requiredFeatures))];
3215
+ let optionalFeatures = [...new Set(explicitOptionalFeatures.length ? explicitOptionalFeatures : defaultOptionalFeatures)];
3216
+ let referenceSpaceType = startOptions.referenceSpaceType || WEBXR_FEATURES.localFloor;
3217
+ let result = {
3218
+ requiredFeatures,
3219
+ optionalFeatures,
3220
+ referenceSpaceType,
3221
+ };
3222
+ if (startOptions.domOverlayRoot) {
3223
+ result.domOverlayRoot = startOptions.domOverlayRoot;
3224
+ }
3225
+ return result;
3226
+ }
3227
+
3228
+ export function createXRThreeDiagnosticTimelineSummary(events = [], options = {}) {
3229
+ let limit = Math.max(0, Number(options.limit ?? 12));
3230
+ let list = Array.isArray(events) ? events.slice(limit ? -limit : 0) : [];
3231
+ let items = list.map(createXRThreeDiagnosticTimelineItem);
3232
+ return {
3233
+ version: 'xr-three-diagnostic-timeline-v1',
3234
+ count: items.length,
3235
+ latest: items.at(-1) || null,
3236
+ items,
3237
+ text: items.length ? items.map((item) => item.text).join(' -> ') : null,
3238
+ };
3239
+ }
3240
+
3241
+ function findDiagnosticClient(summary = {}, clientId = null) {
3242
+ let clients = Array.isArray(summary.clients) ? summary.clients : [];
3243
+ return clients.find((client) => client.clientId === clientId) || null;
3244
+ }
3245
+
3246
+ function inputSourcesText(inputSources = []) {
3247
+ return Array.isArray(inputSources) && inputSources.length
3248
+ ? inputSources.map((source) => source.targetRayMode || source.handedness || 'input').join(', ')
3249
+ : null;
3250
+ }
3251
+
3252
+ export function createXRThreeDiagnosticServerSummary(summary = null, options = {}) {
3253
+ let currentClient = summary ? findDiagnosticClient(summary, options.clientId) : null;
3254
+ let latestClient = summary?.latestClient || null;
3255
+ let latestImmersiveClient = summary?.latestImmersiveClient || null;
3256
+ let currentSession = currentClient?.session || null;
3257
+ let currentChecks = currentSession?.health?.checks || {};
3258
+ let currentHtmlCanvas = currentClient?.htmlCanvas || summary?.htmlCanvas || null;
3259
+ let currentSceneQuality = currentClient?.sceneQuality || summary?.sceneQuality || null;
3260
+ let currentReadiness = currentClient?.readiness || summary?.readiness || null;
3261
+ let currentVisualReadiness = currentClient?.visualReadiness || summary?.visualReadiness || null;
3262
+ let currentInteractionReadiness = currentClient?.interactionReadiness || summary?.interactionReadiness || null;
3263
+ let currentTexture = currentClient?.texture || null;
3264
+ let currentTextureResolver = currentTexture?.resolverStages?.[0] || null;
3265
+ let currentLaunchGate = currentClient?.launchGate || null;
3266
+ let currentDeepGraph = currentClient?.deepGraph || summary?.deepGraph || null;
3267
+ let currentDeepGraphPreview = currentClient?.deepGraphPreview || summary?.deepGraphPreview || null;
3268
+ let recentEvents = Array.isArray(currentClient?.recentEvents) ? currentClient.recentEvents : [];
3269
+ let currentLastEvent = recentEvents.at(-1) || null;
3270
+
3271
+ return {
3272
+ version: 'xr-three-diagnostic-server-summary-v1',
3273
+ available: Boolean(summary),
3274
+ summaryVersion: summary?.version || null,
3275
+ clientCount: Number(summary?.clientCount || 0),
3276
+ immersiveClientCount: Number(summary?.immersiveClientCount || 0),
3277
+ currentClient,
3278
+ latestClient,
3279
+ latestImmersiveClient,
3280
+ currentSession,
3281
+ currentChecks,
3282
+ currentHtmlCanvas,
3283
+ currentSceneQuality,
3284
+ currentReadiness,
3285
+ currentVisualReadiness,
3286
+ currentInteractionReadiness,
3287
+ currentTexture,
3288
+ currentTextureResolver,
3289
+ currentLaunchGate,
3290
+ currentDeepGraph,
3291
+ currentDeepGraphPreview,
3292
+ currentRunning: Boolean(currentSession?.active || currentSession?.status === 'running'),
3293
+ currentTimeline: createXRThreeDiagnosticTimelineSummary(recentEvents, options.timeline || {}),
3294
+ currentLastEvent,
3295
+ currentLastEventTimeline: createXRThreeDiagnosticTimelineSummary(currentLastEvent ? [currentLastEvent] : []),
3296
+ inputSourcesText: inputSourcesText(currentSession?.inputSources),
3297
+ latestImmersiveHealth: latestImmersiveClient?.session?.health?.status || null,
3298
+ };
3299
+ }
3300
+
3301
+ function issue(code, severity, source, detail = null) {
3302
+ return {
3303
+ code,
3304
+ severity,
3305
+ source,
3306
+ detail,
3307
+ };
3308
+ }
3309
+
3310
+ export function createXRThreeTroubleshootingSummary(diagnostics = null, options = {}) {
3311
+ let server = diagnostics?.version === 'xr-three-diagnostic-server-summary-v1'
3312
+ ? diagnostics
3313
+ : createXRThreeDiagnosticServerSummary(diagnostics, options);
3314
+ let issues = [];
3315
+ let client = server.currentClient || null;
3316
+ let session = server.currentSession || {};
3317
+ let checks = server.currentChecks || {};
3318
+ let texture = server.currentTexture || null;
3319
+ let htmlCanvas = server.currentHtmlCanvas || null;
3320
+ let launchGate = server.currentLaunchGate || null;
3321
+ let readiness = server.currentReadiness || null;
3322
+ let visualReadiness = server.currentVisualReadiness || null;
3323
+ let interactionReadiness = server.currentInteractionReadiness || null;
3324
+ let sceneQuality = server.currentSceneQuality || null;
3325
+
3326
+ if (!server.available) {
3327
+ issues.push(issue('server-diagnostics-unavailable', 'waiting', 'server'));
3328
+ }
3329
+ if (server.available && !client) {
3330
+ issues.push(issue('client-diagnostics-missing', 'waiting', 'server'));
3331
+ }
3332
+ if (client?.stale) {
3333
+ issues.push(issue('client-diagnostics-stale', 'warning', 'server', { ageMs: client.ageMs }));
3334
+ }
3335
+ if (client?.lastError) {
3336
+ issues.push(issue('client-error', 'blocked', 'session', client.lastError));
3337
+ }
3338
+ if (launchGate?.blocked) {
3339
+ issues.push(issue('launch-gate-blocked', 'blocked', 'launch', launchGate.reason || null));
3340
+ }
3341
+ if (readiness?.status === 'blocked') {
3342
+ issues.push(issue('readiness-blocked', 'blocked', 'readiness', readiness.reason || null));
3343
+ }
3344
+ if (visualReadiness && visualReadiness.ready === false) {
3345
+ issues.push(issue('visual-readiness-blocked', visualReadiness.status === 'fail' ? 'blocked' : 'warning', 'visual', visualReadiness.reason || null));
3346
+ }
3347
+ if (interactionReadiness && interactionReadiness.ready === false) {
3348
+ issues.push(issue('interaction-readiness-blocked', interactionReadiness.status === 'blocked' ? 'blocked' : 'warning', 'interaction', interactionReadiness.reason || null));
3349
+ }
3350
+ if (server.currentRunning && Number(session.frames || checks.frames || 0) <= 0) {
3351
+ issues.push(issue('no-xr-frames', 'blocked', 'session'));
3352
+ }
3353
+ if (server.currentRunning && Number(session.panelCount || checks.panelCount || 0) <= 0) {
3354
+ issues.push(issue('no-panels', 'blocked', 'scene'));
3355
+ }
3356
+ if (
3357
+ server.currentRunning &&
3358
+ Number(session.panelCount || checks.panelCount || 0) > 0 &&
3359
+ Number(session.panelFrameVisuals || checks.panelFrameVisuals || 0) <= 0
3360
+ ) {
3361
+ issues.push(issue('panel-frame-visuals-missing', 'warning', 'scene'));
3362
+ }
3363
+ if (server.currentRunning && session.renderState?.baseLayer?.present === false) {
3364
+ issues.push(issue('xr-base-layer-missing', 'blocked', 'renderer'));
3365
+ }
3366
+ if (server.currentRunning && session.viewports && Number(session.viewports.viewCount || 0) <= 0) {
3367
+ issues.push(issue('xr-viewports-missing', 'blocked', 'renderer'));
3368
+ }
3369
+ if (server.currentRunning && Number(session.materialDiagnostics?.strictDiagnosticCount || 0) > 0) {
3370
+ issues.push(issue('strict-texture-diagnostic-material', 'blocked', 'material', {
3371
+ count: Number(session.materialDiagnostics.strictDiagnosticCount || 0),
3372
+ panelIds: session.materialDiagnostics.strictDiagnosticPanelIds || [],
3373
+ }));
3374
+ }
3375
+ if (server.currentRunning && Number(session.materialDiagnostics?.transparentCount || 0) > 0) {
3376
+ issues.push(issue('panel-material-transparent', 'warning', 'material', {
3377
+ count: Number(session.materialDiagnostics.transparentCount || 0),
3378
+ }));
3379
+ }
3380
+ if (texture?.blocked) {
3381
+ issues.push(issue('texture-gate-blocked', 'blocked', 'texture', texture.reason || texture.stage || null));
3382
+ } else if (texture && Number(texture.ready || 0) < Number(texture.total || 0)) {
3383
+ issues.push(issue('texture-not-ready', 'blocked', 'texture', {
3384
+ ready: Number(texture.ready || 0),
3385
+ total: Number(texture.total || 0),
3386
+ stage: texture.stage || null,
3387
+ }));
3388
+ }
3389
+ if (htmlCanvas && htmlCanvas.textureUploadAvailable === false) {
3390
+ issues.push(issue('html-canvas-texture-upload-missing', 'warning', 'html-canvas', htmlCanvas.availability || null));
3391
+ }
3392
+ if (sceneQuality?.status === 'low') {
3393
+ issues.push(issue('scene-quality-low', 'warning', 'scene', {
3394
+ lowQualityCount: Number(sceneQuality.lowQualityCount || 0),
3395
+ total: Number(sceneQuality.total || 0),
3396
+ }));
3397
+ }
3398
+ if (server.currentRunning && Number(session.controllers || checks.controllers || 0) <= 0) {
3399
+ issues.push(issue('input-controllers-missing', 'warning', 'input'));
3400
+ }
3401
+ if (server.currentRunning && Number(session.controllerRayVisuals || checks.controllerRayVisuals || 0) <= 0) {
3402
+ issues.push(issue('controller-rays-missing', 'warning', 'input'));
3403
+ }
3404
+ if (server.currentRunning && Number(session.hitReticleVisuals || checks.hitReticleVisuals || 0) <= 0) {
3405
+ issues.push(issue('hit-reticle-missing', 'warning', 'input'));
3406
+ }
3407
+ if (server.currentRunning && Number(session.interactionEvents || 0) <= 0) {
3408
+ issues.push(issue('interaction-events-missing', 'waiting', 'input'));
3409
+ }
3410
+
3411
+ let status = 'ready';
3412
+ if (issues.some((item) => item.severity === 'blocked')) {
3413
+ status = 'blocked';
3414
+ } else if (issues.some((item) => item.severity === 'warning')) {
3415
+ status = 'warning';
3416
+ } else if (issues.some((item) => item.severity === 'waiting')) {
3417
+ status = 'waiting';
3418
+ } else if (server.currentRunning) {
3419
+ status = 'running';
3420
+ }
3421
+ let primaryIssue = issues[0] || null;
3422
+
3423
+ return {
3424
+ version: 'xr-three-troubleshooting-summary-v1',
3425
+ status,
3426
+ primaryIssue,
3427
+ issues,
3428
+ issueCodes: issues.map((item) => item.code),
3429
+ issueCount: issues.length,
3430
+ serverAvailable: server.available,
3431
+ clientId: client?.clientId || null,
3432
+ currentRunning: server.currentRunning,
3433
+ frameCount: Number(session.frames || checks.frames || 0),
3434
+ panelCount: Number(session.panelCount || checks.panelCount || 0),
3435
+ textureReady: texture ? Number(texture.ready || 0) : null,
3436
+ textureTotal: texture ? Number(texture.total || 0) : null,
3437
+ timelineText: server.currentTimeline?.text || null,
3438
+ };
3439
+ }