@vincentt-xr/sdk 2.0.0-alpha.1 → 2.0.0-alpha.3

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 (355) hide show
  1. package/GROUNDING.md +1614 -21
  2. package/dist/{DebuggerUI-CatYc-RG.cjs → DebuggerUI-B45_Gb9d.cjs} +1 -1
  3. package/dist/{DebuggerUI-gLMKiVwJ.js → DebuggerUI-xp4bg3gH.js} +1 -1
  4. package/dist/FaceMeshRenderer-CALA4k49.js +1195 -0
  5. package/dist/FaceMeshRenderer-D4PFqsb3.cjs +155 -0
  6. package/dist/FaceRetouch-Bq2rtPlV.cjs +134 -0
  7. package/dist/FaceRetouch-DF5UsOsZ.js +551 -0
  8. package/dist/audio/AudioEngine.d.ts +3 -1
  9. package/dist/audio/AudioPlayer.d.ts +29 -0
  10. package/dist/audio/AudioPlayerLogic.d.ts +4 -0
  11. package/dist/audio/index.d.ts +2 -0
  12. package/dist/components/Axes/index.d.ts +6 -0
  13. package/dist/components/FaceMesh/FaceMeshRenderer.d.ts +17 -0
  14. package/dist/components/FaceMesh/canonical/CanonicalFaceMeshRenderer.d.ts +17 -1
  15. package/dist/components/FaceMesh/canonical/FaceMeshMaterial.d.ts +25 -0
  16. package/dist/components/FaceMesh/canonical/assets.d.ts +3 -0
  17. package/dist/components/FaceMesh/canonical/geometryGroups.d.ts +9 -0
  18. package/dist/components/FaceMesh/canonical/parseCanonicalFaceModel.d.ts +2 -0
  19. package/dist/components/FaceMesh/canonical/shaders.d.ts +2 -0
  20. package/dist/components/FaceMesh/deform.d.ts +3 -0
  21. package/dist/components/FaceMesh/index.d.ts +5 -0
  22. package/dist/components/FaceMesh/material/defaults.d.ts +70 -0
  23. package/dist/components/FaceMesh/material/index.d.ts +1 -0
  24. package/dist/components/FaceMesh/material/options.d.ts +5 -1
  25. package/dist/components/FaceMesh/material/support.d.ts +7 -2
  26. package/dist/components/FaceMesh/material/textures.d.ts +4 -0
  27. package/dist/components/FaceMesh/material/types.d.ts +2 -2
  28. package/dist/components/FaceRetouch/Background.d.ts +5 -6
  29. package/dist/components/FaceRetouch/FaceRetouch.d.ts +47 -0
  30. package/dist/components/FaceRetouch/index.d.ts +2 -8
  31. package/dist/components/FaceRetouch/landmarkMask.d.ts +0 -5
  32. package/dist/components/FaceRetouch/useFaceRetouchMask.d.ts +3 -7
  33. package/dist/components/FaceTracker/index.d.ts +5 -3
  34. package/dist/components/FaceWarp3D/FaceWarp3DHead.d.ts +1 -1
  35. package/dist/components/HandJoints2D/index.d.ts +1 -1
  36. package/dist/components/HandTracking2D/HandVisual.d.ts +1 -1
  37. package/dist/components/HandTracking2D/index.d.ts +1 -1
  38. package/dist/components/HandTracking2DController/index.d.ts +1 -1
  39. package/dist/components/HandTracking3D/index.d.ts +1 -1
  40. package/dist/components/LightObject/index.d.ts +20 -0
  41. package/dist/components/LookAt/index.d.ts +23 -0
  42. package/dist/components/MeshRenderer/index.d.ts +59 -0
  43. package/dist/components/PerspectiveCamera/index.d.ts +2 -8
  44. package/dist/components/PinToMesh/index.d.ts +47 -0
  45. package/dist/components/RenderGroupLayer/index.d.ts +6 -0
  46. package/dist/components/SceneObjectRenderer/index.d.ts +16 -0
  47. package/dist/components/Screen2D/ScreenTransformFrame.d.ts +11 -0
  48. package/dist/components/Screen2D/ScreenTransformHelpers.d.ts +19 -0
  49. package/dist/components/Screen2D/ScreenTransformInteraction.d.ts +18 -0
  50. package/dist/components/Screen2D/index.d.ts +5 -0
  51. package/dist/components/ScreenGlass/defaults.d.ts +3 -0
  52. package/dist/components/ScreenGlass/index.d.ts +5 -0
  53. package/dist/components/ScreenGlass/shaders.d.ts +2 -0
  54. package/dist/components/ScreenGlass/types.d.ts +10 -0
  55. package/dist/components/ScreenGradient/defaults.d.ts +5 -0
  56. package/dist/components/ScreenGradient/index.d.ts +5 -0
  57. package/dist/components/ScreenGradient/shaders.d.ts +2 -0
  58. package/dist/components/ScreenGradient/types.d.ts +19 -0
  59. package/dist/components/ScreenImage/ScreenImage.d.ts +4 -0
  60. package/dist/components/ScreenImage/components/DynamicTextureSync.d.ts +9 -0
  61. package/dist/components/ScreenImage/components/ScreenImageBody.d.ts +9 -0
  62. package/dist/components/ScreenImage/components/ScreenImageFrame.d.ts +9 -0
  63. package/dist/components/ScreenImage/components/ScreenImageTransformFrame.d.ts +9 -0
  64. package/dist/components/ScreenImage/fit.d.ts +52 -0
  65. package/dist/components/ScreenImage/index.d.ts +8 -87
  66. package/dist/components/ScreenImage/materials/ScreenImageBlendMaterial.d.ts +9 -0
  67. package/dist/components/ScreenImage/materials/ScreenImageMatcap.d.ts +3 -0
  68. package/dist/components/ScreenImage/source.d.ts +19 -0
  69. package/dist/components/ScreenImage/types.d.ts +37 -0
  70. package/dist/components/ScreenImage/utils/ScreenImageConstants.d.ts +9 -0
  71. package/dist/components/ScreenImage/utils/ScreenImageHelpers.d.ts +4 -0
  72. package/dist/components/ScreenShape/geometry.d.ts +5 -0
  73. package/dist/components/ScreenShape/index.d.ts +5 -0
  74. package/dist/components/ScreenShape/types.d.ts +41 -0
  75. package/dist/components/ScreenSpaceUI/ScreenTransformGuide.d.ts +15 -0
  76. package/dist/components/ScreenSpaceUI/ScreenTransformOverlayGuide.d.ts +9 -0
  77. package/dist/components/ScreenSpaceUI/index.d.ts +2 -0
  78. package/dist/components/ScreenText/components/ScreenTextBody.d.ts +2 -0
  79. package/dist/components/ScreenText/components/ScreenTextTransformFrame.d.ts +9 -0
  80. package/dist/components/ScreenText/constants.d.ts +5 -0
  81. package/dist/components/ScreenText/index.d.ts +6 -20
  82. package/dist/components/ScreenText/types.d.ts +31 -0
  83. package/dist/components/ScreenText/utils/ScreenTextHelpers.d.ts +14 -0
  84. package/dist/components/ScreenText/utils/ScreenTextMappings.d.ts +17 -0
  85. package/dist/components/SequentialRenderPasses/index.d.ts +6 -0
  86. package/dist/components/TextLabel/TextLabel.d.ts +5 -0
  87. package/dist/components/TextLabel/alignment.d.ts +4 -0
  88. package/dist/components/TextLabel/bend.d.ts +9 -0
  89. package/dist/components/TextLabel/canvas/buildTextLabelTexture.d.ts +4 -0
  90. package/dist/components/TextLabel/canvas/canvasTypography.d.ts +13 -0
  91. package/dist/components/TextLabel/canvas/drawBackground.d.ts +21 -0
  92. package/dist/components/TextLabel/canvas/drawEffects.d.ts +34 -0
  93. package/dist/components/TextLabel/canvas/drawText.d.ts +19 -0
  94. package/dist/components/TextLabel/components/TextLabelMesh.d.ts +13 -0
  95. package/dist/components/TextLabel/constants.d.ts +2 -0
  96. package/dist/components/TextLabel/hooks/useTextLabelTexture.d.ts +7 -0
  97. package/dist/components/TextLabel/index.d.ts +2 -161
  98. package/dist/components/TextLabel/layout/measurement.d.ts +8 -0
  99. package/dist/components/TextLabel/layout/resolveTextLayout.d.ts +25 -0
  100. package/dist/components/TextLabel/layout/wrapText.d.ts +6 -0
  101. package/dist/components/TextLabel/types.d.ts +227 -0
  102. package/dist/components/TrackingAnchor/index.d.ts +18 -2
  103. package/dist/components/Transform3D/index.d.ts +30 -0
  104. package/dist/components/XRMediaSurface/index.d.ts +6 -6
  105. package/dist/components/XRViewport/XRViewport.d.ts +11 -0
  106. package/dist/components/XRViewport/components/HiddenMedia.d.ts +7 -0
  107. package/dist/components/XRViewport/components/MediaAssetPicker.d.ts +8 -0
  108. package/dist/components/XRViewport/components/MediaControlStack.d.ts +13 -0
  109. package/dist/components/XRViewport/components/XRViewportLayers.d.ts +30 -0
  110. package/dist/components/XRViewport/constants.d.ts +24 -0
  111. package/dist/components/XRViewport/contexts.d.ts +6 -0
  112. package/dist/components/XRViewport/hooks/useViewportExternalFaces.d.ts +13 -0
  113. package/dist/components/XRViewport/hooks/useViewportMediaSource.d.ts +16 -0
  114. package/dist/components/XRViewport/hooks/useViewportMediaTexture.d.ts +10 -0
  115. package/dist/components/XRViewport/hooks/useViewportPipeline.d.ts +41 -0
  116. package/dist/components/XRViewport/hooks/useViewportRenderState.d.ts +47 -0
  117. package/dist/components/XRViewport/index.d.ts +3 -118
  118. package/dist/components/XRViewport/layout.d.ts +16 -0
  119. package/dist/components/XRViewport/slots.d.ts +16 -0
  120. package/dist/components/XRViewport/types.d.ts +89 -0
  121. package/dist/components/XRViewport/useViewportSize.d.ts +3 -0
  122. package/dist/components/index.d.ts +12 -0
  123. package/dist/contexts-D5TQcwmv.js +660 -0
  124. package/dist/contexts-Dv1SA0bS.cjs +1 -0
  125. package/dist/debug-ui/component-editor/index.d.ts +19 -0
  126. package/dist/debug-ui/face-mesh-renderer-settings/index.d.ts +8 -0
  127. package/dist/debug-ui/face-retouch/index.d.ts +11 -0
  128. package/dist/debug-ui/hand-joints-2d/index.d.ts +1 -1
  129. package/dist/debug-ui/hand-tracking-2d/index.d.ts +1 -1
  130. package/dist/debug-ui/hand-tracking-2d-controller/index.d.ts +1 -1
  131. package/dist/debug-ui/hand-tracking-3d/index.d.ts +1 -1
  132. package/dist/debug-ui/hierarchy-panel/index.d.ts +29 -0
  133. package/dist/debug-ui/index.d.ts +25 -2
  134. package/dist/debug-ui/layer-panel/index.d.ts +3 -2
  135. package/dist/debug-ui/media-source/index.d.ts +2 -0
  136. package/dist/debug-ui/media-source/session.d.ts +12 -0
  137. package/dist/debug-ui/media-source.cjs +1 -1
  138. package/dist/debug-ui/media-source.js +6 -4
  139. package/dist/debug-ui/mesh-renderer-settings/index.d.ts +6 -0
  140. package/dist/debug-ui/object-inspector-panel/index.d.ts +25 -0
  141. package/dist/debug-ui/pin-to-mesh-settings/index.d.ts +10 -0
  142. package/dist/debug-ui/scene-editor/context.d.ts +38 -0
  143. package/dist/debug-ui/scene-editor/hierarchy-panel.d.ts +9 -0
  144. package/dist/debug-ui/scene-editor/index.d.ts +11 -0
  145. package/dist/debug-ui/scene-editor/inspector-panel.d.ts +7 -0
  146. package/dist/debug-ui/scene-editor/store-utils.d.ts +2 -0
  147. package/dist/debug-ui/scene-editor/viewport-toolbar.d.ts +4 -0
  148. package/dist/debug-ui/scene-editor/viewport.d.ts +21 -0
  149. package/dist/debug-ui/screen-glass-settings/index.d.ts +6 -0
  150. package/dist/debug-ui/screen-gradient-settings/GradientStopsEditor.d.ts +5 -0
  151. package/dist/debug-ui/screen-gradient-settings/index.d.ts +6 -0
  152. package/dist/debug-ui/screen-gradient-settings/styles.d.ts +3 -0
  153. package/dist/debug-ui/screen-image-settings/index.d.ts +4 -2
  154. package/dist/debug-ui/screen-shape-settings/index.d.ts +6 -0
  155. package/dist/debug-ui/screen-text-settings/NumberField.d.ts +9 -0
  156. package/dist/debug-ui/screen-text-settings/ScreenTextSettingsPanel.d.ts +3 -0
  157. package/dist/debug-ui/screen-text-settings/controls/NestedSection.d.ts +7 -0
  158. package/dist/debug-ui/screen-text-settings/controls/Section.d.ts +8 -0
  159. package/dist/debug-ui/screen-text-settings/controls/icons.d.ts +4 -0
  160. package/dist/debug-ui/screen-text-settings/index.d.ts +2 -122
  161. package/dist/debug-ui/screen-text-settings/sections/BackgroundSection.d.ts +8 -0
  162. package/dist/debug-ui/screen-text-settings/sections/ContentSection.d.ts +5 -0
  163. package/dist/debug-ui/screen-text-settings/sections/EffectsSection.d.ts +25 -0
  164. package/dist/debug-ui/screen-text-settings/sections/StyleSection.d.ts +5 -0
  165. package/dist/debug-ui/screen-text-settings/sections/TransformSection.d.ts +5 -0
  166. package/dist/debug-ui/screen-text-settings/sections/background/BackgroundFillControls.d.ts +2 -0
  167. package/dist/debug-ui/screen-text-settings/sections/background/BackgroundSizeControls.d.ts +2 -0
  168. package/dist/debug-ui/screen-text-settings/sections/background/BackgroundStrokeControls.d.ts +2 -0
  169. package/dist/debug-ui/screen-text-settings/sections/background/types.d.ts +7 -0
  170. package/dist/debug-ui/screen-text-settings/sections/effects/BendEffectSection.d.ts +4 -0
  171. package/dist/debug-ui/screen-text-settings/sections/effects/EffectChrome.d.ts +21 -0
  172. package/dist/debug-ui/screen-text-settings/sections/effects/GlowEffectSection.d.ts +4 -0
  173. package/dist/debug-ui/screen-text-settings/sections/effects/InnerShadowEffectSection.d.ts +4 -0
  174. package/dist/debug-ui/screen-text-settings/sections/effects/OutlineEffectSection.d.ts +4 -0
  175. package/dist/debug-ui/screen-text-settings/sections/effects/ShadowEffectSection.d.ts +4 -0
  176. package/dist/debug-ui/screen-text-settings/sections/effects/types.d.ts +14 -0
  177. package/dist/debug-ui/screen-text-settings/settings.d.ts +90 -0
  178. package/dist/debug-ui/screen-text-settings/styles.d.ts +26 -0
  179. package/dist/debug-ui/screen-text-settings/transform.d.ts +6 -0
  180. package/dist/debug-ui/screen-text-settings/types.d.ts +9 -0
  181. package/dist/debug-ui/shared/ScreenTransformSection.d.ts +6 -3
  182. package/dist/debug-ui/transform-3d-settings/index.d.ts +11 -0
  183. package/dist/debug-ui.cjs +3 -13
  184. package/dist/debug-ui.js +7735 -4896
  185. package/dist/drawNormalizedCrop-B-7zU-Xw.cjs +1 -0
  186. package/dist/drawNormalizedCrop-CRdIDyl1.js +44 -0
  187. package/dist/entries/all.d.ts +1 -0
  188. package/dist/entries/core.d.ts +53 -6
  189. package/dist/entries/face-effects.d.ts +10 -8
  190. package/dist/entries/internal.d.ts +1 -1
  191. package/dist/entries/low-level.d.ts +21 -4
  192. package/dist/entries/scene-object.d.ts +10 -0
  193. package/dist/entries/tracking.d.ts +25 -2
  194. package/dist/face-action/index.d.ts +34 -0
  195. package/dist/face-attribute-expression/index.d.ts +10 -0
  196. package/dist/face-effects.cjs +1 -342
  197. package/dist/face-effects.js +1391 -2767
  198. package/dist/face-info/blendshapes.d.ts +3 -0
  199. package/dist/face-info/derive.d.ts +8 -0
  200. package/dist/face-info/drawFaceCrop.d.ts +10 -0
  201. package/dist/face-info/getFaceInfo.d.ts +8 -0
  202. package/dist/face-info/index.d.ts +8 -0
  203. package/dist/face-info/landmarks.d.ts +21 -0
  204. package/dist/face-info/types.d.ts +52 -0
  205. package/dist/getFaceInfo-Cjcgcc4w.js +118 -0
  206. package/dist/getFaceInfo-oQcP40Ha.cjs +1 -0
  207. package/dist/{handJoints2d.d.ts → hand/handJoints2d.d.ts} +1 -1
  208. package/dist/{handTracking2d.d.ts → hand/handTracking2d.d.ts} +1 -1
  209. package/dist/{handTracking2dController.d.ts → hand/handTracking2dController.d.ts} +1 -1
  210. package/dist/{handTracking3d.d.ts → hand/handTracking3d.d.ts} +1 -1
  211. package/dist/hooks/faceResults.d.ts +1 -1
  212. package/dist/hooks/useCalibrationFaceTracker.d.ts +1 -1
  213. package/dist/hooks/useFaceAction.d.ts +3 -0
  214. package/dist/hooks/useFaceAttributeExpression.d.ts +16 -0
  215. package/dist/hooks/useFaceDetection.d.ts +18 -0
  216. package/dist/hooks/useFaceInfo.d.ts +7 -0
  217. package/dist/hooks/useFaceInfos.d.ts +15 -0
  218. package/dist/hooks/useFaceResultReader.d.ts +10 -0
  219. package/dist/hooks/useFrameCapture.d.ts +18 -0
  220. package/dist/hooks/useMediaRecorder.d.ts +14 -2
  221. package/dist/hooks/useOwnedTexture.d.ts +6 -0
  222. package/dist/hooks/useTrackerContext.d.ts +2 -2
  223. package/dist/hooks/useXRModel.d.ts +5 -0
  224. package/dist/hooks/useXRSession/facade.d.ts +3 -0
  225. package/dist/hooks/useXRSession/registry.d.ts +9 -0
  226. package/dist/index-Be-S7uSm.cjs +1 -0
  227. package/dist/index-D4bLzBr6.cjs +1 -0
  228. package/dist/index-DdWBnI7A.cjs +17 -0
  229. package/dist/index-DpVIEBHw.js +2068 -0
  230. package/dist/index-DqhqBi7O.cjs +1 -0
  231. package/dist/index-HxT9XEuh.js +111 -0
  232. package/dist/{index-safCs_af.js → index-JygePOwo.js} +565 -508
  233. package/dist/index-N7C6fNah.js +214 -0
  234. package/dist/index-UU5ODHGU.cjs +1 -0
  235. package/dist/{index-BGNbrc4C.js → index-YMUaJU7U.js} +217 -126
  236. package/dist/{internal-Cae1ps15.js → internal-D7NxAsc6.js} +86 -80
  237. package/dist/internal-i2JkM85N.cjs +1 -0
  238. package/dist/internal.cjs +1 -1
  239. package/dist/internal.js +2 -2
  240. package/dist/low-level.cjs +1 -1
  241. package/dist/low-level.js +1138 -86
  242. package/dist/main.cjs +1 -73
  243. package/dist/main.js +545 -1693
  244. package/dist/material/MaterialFill.d.ts +20 -0
  245. package/dist/material/MeshMaterial.d.ts +14 -0
  246. package/dist/material/index.d.ts +2 -0
  247. package/dist/models/Body/index.d.ts +1 -1
  248. package/dist/models/Face/index.d.ts +1 -0
  249. package/dist/models/Face/rotation.d.ts +3 -0
  250. package/dist/render/renderGroups.d.ts +47 -0
  251. package/dist/render/renderPasses.d.ts +18 -0
  252. package/dist/renderGroups-JEXqSACh.js +152 -0
  253. package/dist/renderGroups-uXXsFpvw.cjs +1 -0
  254. package/dist/renderLayers-lDtL0_43.cjs +1 -0
  255. package/dist/renderLayers-zrK2pv5O.js +13 -0
  256. package/dist/renderPasses-CVWt0t8m.cjs +21 -0
  257. package/dist/renderPasses-CdGZeHHL.js +1500 -0
  258. package/dist/scene-object/index.d.ts +146 -0
  259. package/dist/scene-object/runtimeRegistry.d.ts +14 -0
  260. package/dist/scene-object.cjs +1 -0
  261. package/dist/scene-object.js +40 -0
  262. package/dist/{screenImageSettings.d.ts → screen-image/screenImageSettings.d.ts} +9 -2
  263. package/dist/{screenTextSettings.d.ts → screen-text/screenTextSettings.d.ts} +18 -2
  264. package/dist/screen2d/coordinates.d.ts +87 -0
  265. package/dist/screen2d/index.d.ts +3 -0
  266. package/dist/screen2d/screenTransform.d.ts +47 -0
  267. package/dist/screen2d/smoothing.d.ts +5 -0
  268. package/dist/{selectors-BLl2JUWW.cjs → selectors-2QvO5RYY.cjs} +1 -1
  269. package/dist/{selectors-CXd_x-6t.js → selectors-twqYByYZ.js} +6 -6
  270. package/dist/services/MetricsCollector/index.d.ts +7 -1
  271. package/dist/services/MetricsCollector/utils.d.ts +1 -1
  272. package/dist/session-BChR4TgK.cjs +31 -0
  273. package/dist/session-C1StwN0f.js +363 -0
  274. package/dist/texture/animatedTexture.d.ts +63 -0
  275. package/dist/texture/animationMath.d.ts +8 -0
  276. package/dist/texture/gifReader.d.ts +17 -0
  277. package/dist/texture/gifTextureSource.d.ts +16 -0
  278. package/dist/texture/sourceResolver.d.ts +30 -0
  279. package/dist/tracking.cjs +1 -1
  280. package/dist/tracking.js +580 -321
  281. package/dist/types/face.d.ts +1 -13
  282. package/dist/types/index.d.ts +0 -1
  283. package/dist/types/nodes.d.ts +2 -1
  284. package/dist/types/session.d.ts +2 -0
  285. package/dist/types/tracking.d.ts +8 -2
  286. package/dist/types/util-types.d.ts +1 -1
  287. package/dist/useFaceInfo-DOJCGlDG.js +62 -0
  288. package/dist/useFaceInfo-DSUmuzzO.cjs +1 -0
  289. package/dist/useFaceResults-B1BSGUAW.cjs +1 -0
  290. package/dist/useFaceResults-CBmhT5E8.js +19 -0
  291. package/dist/utils/drawNormalizedCrop.d.ts +22 -0
  292. package/dist/utils/integrity.d.ts +4 -25
  293. package/dist/utils/landmarkBounds.d.ts +1 -1
  294. package/dist/utils/logger.d.ts +2 -2
  295. package/docs/MIGRATION_GUIDE.md +589 -0
  296. package/docs/SDK_DOCUMENTATION.md +1155 -0
  297. package/docs/api/audio.mdx +36 -0
  298. package/docs/api/behavior-components.mdx +164 -0
  299. package/docs/api/capture-recording.mdx +103 -0
  300. package/docs/api/debug-ui-scene-editor.mdx +249 -0
  301. package/docs/api/face-actions.mdx +104 -0
  302. package/docs/api/face-attribute-expression.mdx +87 -0
  303. package/docs/api/face-binding-anchors.mdx +53 -0
  304. package/docs/api/face-effects.mdx +19 -0
  305. package/docs/api/face-info.mdx +112 -0
  306. package/docs/api/face-mesh.mdx +98 -0
  307. package/docs/api/face-painting.mdx +33 -0
  308. package/docs/api/face-retouching.mdx +73 -0
  309. package/docs/api/material-render-state.mdx +132 -0
  310. package/docs/api/runtime-3d.mdx +311 -0
  311. package/docs/api/scene-object-store.mdx +213 -0
  312. package/docs/api/screen-image.mdx +65 -0
  313. package/docs/api/screen-shape.mdx +64 -0
  314. package/docs/api/screen-space-coordinates.mdx +41 -0
  315. package/docs/api/screen-text.mdx +39 -0
  316. package/docs/api/texture-sources.mdx +74 -0
  317. package/docs/api/trackers.mdx +46 -0
  318. package/docs/docs.json +70 -0
  319. package/docs/examples/scene-object-recipes.mdx +194 -0
  320. package/docs/guides/harness-debug-ui.mdx +139 -0
  321. package/docs/inspector/hierarchy-panel.mdx +51 -0
  322. package/docs/inspector/object-inspector-panel.mdx +51 -0
  323. package/docs/introduction.mdx +121 -0
  324. package/docs/scene-objects/overview.mdx +127 -0
  325. package/docs/scene-objects/render-groups.mdx +77 -0
  326. package/docs/scene-objects/runtime-rendering.mdx +75 -0
  327. package/docs/texture-api-consumer.md +84 -0
  328. package/package.json +38 -8
  329. package/dist/MediaSourceSwitcher-C-xYg0nQ.js +0 -317
  330. package/dist/MediaSourceSwitcher-Cq3g4IKw.cjs +0 -31
  331. package/dist/components/FaceRetouch/defaults.d.ts +0 -13
  332. package/dist/components/FaceRetouch/retouchRegions.d.ts +0 -23
  333. package/dist/components/FaceRetouch/useFaceRetouchRegions.d.ts +0 -12
  334. package/dist/defaults-BLpcN3pG.cjs +0 -1
  335. package/dist/defaults-LtY4M7dg.js +0 -55
  336. package/dist/index-BXyObVHF.cjs +0 -1
  337. package/dist/index-CY3bUAcW.js +0 -12
  338. package/dist/index-C_Duh13c.cjs +0 -1
  339. package/dist/index-D8fpPQkC.cjs +0 -1
  340. package/dist/index-DxshgBu0.cjs +0 -1
  341. package/dist/index-O-2crZ3M.js +0 -1102
  342. package/dist/internal-BjyhBP8i.cjs +0 -1
  343. package/dist/modules/XRClient/index.d.ts +0 -9
  344. package/dist/modules/index.d.ts +0 -1
  345. package/dist/screenImageSettings-6NRg4cx9.js +0 -39
  346. package/dist/screenImageSettings-y_B4_767.cjs +0 -1
  347. package/dist/services/ApiService.d.ts +0 -12
  348. package/dist/types/client.d.ts +0 -3
  349. package/dist/useFaceResults-cpsSmKKc.js +0 -81
  350. package/dist/useFaceResults-hVUJYSEY.cjs +0 -1
  351. package/dist/useXRModel-3DcUa28n.cjs +0 -1
  352. package/dist/useXRModel-BMJE1hrk.js +0 -28
  353. /package/dist/{layerStack.d.ts → render/layerStack.d.ts} +0 -0
  354. /package/dist/{screenImagePresets.d.ts → screen-image/screenImagePresets.d.ts} +0 -0
  355. /package/dist/{screenTextPresets.d.ts → screen-text/screenTextPresets.d.ts} +0 -0
package/GROUNDING.md CHANGED
@@ -10,7 +10,8 @@ or spelunk `node_modules` for the API.
10
10
  The API is split into task-domain entry points. Import each symbol from the door
11
11
  this reference shows for it; the imports here are literal.
12
12
 
13
- - `@vincentt-xr/sdk` — **core**: providers, scene, screen-space layout, `TextLabel`, `Panel`, `SpriteAnimation`, `VideoBackground`, `AspectRatioContainer`, capture/share (`useMediaRecorder`, `dataURLtoFile`), audio.
13
+ - `@vincentt-xr/sdk` — **core**: providers, scene, screen-space layout, runtime renderers (`Transform3D`, `MeshRenderer`, `SceneObjectRenderer`), `TextLabel`, `Panel`, `SpriteAnimation`, `VideoBackground`, `AspectRatioContainer`, capture/share (`useFrameCapture`, `useMediaRecorder`, `dataURLtoFile`), audio.
14
+ - `@vincentt-xr/sdk/scene-object` — **public authoring state**: scene-object/component types, factories, selectors, and immutable store operations.
14
15
  - `@vincentt-xr/sdk/tracking` — **trackers**: face/hand/gesture/body/segmentation, `TrackingAnchor`, `GestureTrigger`, the bare `FaceMesh`, `useFaceResults`.
15
16
  - `@vincentt-xr/sdk/face-effects` — **face deep**: FaceMesh material config, retouch, canonical mesh, head-binding.
16
17
  - `@vincentt-xr/sdk/low-level` — **escape hatch**: raw model-node reads, selector hooks, custom-tracker plumbing. Reach here only when core + tracking can't express it.
@@ -24,6 +25,50 @@ logic is R3F `useFrame`; per-mount setup is `useEffect`.
24
25
  > patterns) into the project's `GROUNDING.md`. Edit the SDK API here, in the repo
25
26
  > that owns the API.
26
27
 
28
+ ### Internal source layout
29
+
30
+ The public import doors above are stable. Internal files are organized by domain
31
+ so shared behavior stays reusable instead of duplicated in large components:
32
+
33
+ - `lib/components/ScreenImage/` — thin `ScreenImage.tsx` orchestrator plus
34
+ reusable body, frame, transform-frame, material, matcap, shader, source, and
35
+ helper modules.
36
+ - `lib/components/ScreenText/` — thin `ScreenText.tsx` orchestrator plus
37
+ reusable body, transform-frame, constants, mapping, and helper modules.
38
+ - `lib/components/TextLabel/` — low-level canvas text renderer split into
39
+ texture building, layout, style, hooks, and body modules.
40
+ - `lib/components/XRViewport/` — thin viewport orchestrator plus reusable
41
+ provider, stage, overlays, media, control, sizing, slot, and layout modules.
42
+ - `lib/components/Screen2D/` — shared screen-transform interaction/frame helpers
43
+ used by screen-space authoring components.
44
+ - `lib/debug-ui/screen-text-settings/` — panel orchestrator plus reusable
45
+ sections, controls, settings, styles, transform helpers, and types.
46
+ - `lib/render/` — render-group, render-pass, and layer-stack internals.
47
+ - `lib/hand/`, `lib/screen-image/`, and `lib/screen-text/` — domain-level
48
+ primitives, presets, settings, and tests that used to live flat in `lib/`.
49
+
50
+ When adding features, keep public exports at the documented import doors and put
51
+ implementation details in the matching domain folder. Do not reintroduce
52
+ component-local duplicate shader, transform, text-rendering, or settings logic.
53
+
54
+ ### Example asset layout
55
+
56
+ Example-app assets are organized by purpose, not by whichever page first needed
57
+ them:
58
+
59
+ - `example/src/assets/media/images/` — reusable still textures and sample images.
60
+ - `example/src/assets/media/images/people/` — face/body tracking sample stills.
61
+ - `example/src/assets/media/images/screenshots/` — screenshot reference images.
62
+ - `example/src/assets/media/videos/` — sample MP4/WebM sources.
63
+ - `example/src/assets/media/gifs/` — animated GIF source files.
64
+ - `example/src/assets/media/sequences/<demo>/` — ordered image-sequence frames.
65
+ - `example/src/assets/models/` — GLB files used by example 3D scenes.
66
+ - `example/src/assets/face-effects/` — domain-owned face-effect assets.
67
+
68
+ Do not flatten all example assets into the root. Do not add new `utils/` media
69
+ folders. Compression is a separate visual/behavior review because it can change
70
+ tracking fixtures and texture output.
71
+
27
72
  ---
28
73
 
29
74
  ## Screen-space layout (the primary tool)
@@ -94,6 +139,89 @@ authoring prefer `position`, `size`, and `rotation`. `anchors` still exist as
94
139
  the lower-level layout escape hatch. If no source is provided, it renders the
95
140
  SDK-owned square default image.
96
141
 
142
+ ### `<ScreenShape>` — editable vector primitives
143
+
144
+ Use `ScreenShape` for circles, ellipses, rectangles, rounded rectangles, regular
145
+ polygons, stars, and lines. Do not generate raster image assets for these basic
146
+ shapes. Placement and drag editing use the shared `ScreenTransform` settings.
147
+ Circles, regular polygons, and stars preserve equal axes inside non-square
148
+ transforms; ellipses and rectangles fill the transform.
149
+
150
+ ```tsx
151
+ import { ScreenShape, ScreenSpaceUI } from "@vincentt-xr/sdk";
152
+
153
+ <ScreenSpaceUI>
154
+ <ScreenShape
155
+ transform={{ position: { x: 0, y: 0 }, size: { width: 220, height: 220 } }}
156
+ settings={{
157
+ kind: "star",
158
+ fillColor: "#ffd54a",
159
+ strokeColor: "#ffffff",
160
+ strokeWidth: 4,
161
+ sides: 5,
162
+ innerRadiusRatio: 0.45,
163
+ }}
164
+ />
165
+ </ScreenSpaceUI>;
166
+ ```
167
+
168
+ Scene objects pair `ScreenTransform` with `ScreenShape`. Import scene-object
169
+ factories from `@vincentt-xr/sdk/scene-object` and the live
170
+ `ScreenShapeSettingsPanel` from `@vincentt-xr/sdk/debug-ui`.
171
+
172
+ Settings: `kind`; `fillMode`, `fillColor`, `fillOpacity`; `strokeColor`,
173
+ `strokeOpacity`, `strokeWidth`, `dashed`; `cornerRadius` for rounded rectangles;
174
+ `sides` for polygons/stars; `innerRadiusRatio` for stars; and normalized
175
+ `lineStart`/`lineEnd` `{ x, y }` coordinates for lines.
176
+
177
+ Set `fillMode: "gradient"` and provide `gradient: { type, stops, angle, center,
178
+ radius }`. Types are `linear`, `radial`, and `angular` (conic). A gradient accepts
179
+ 2–8 `{ offset, color }` stops. `ScreenGradientMaterial` is a separate reusable
180
+ component exported from core; its settings panel is exported from debug-ui.
181
+
182
+ Set `fillMode: "glass"` and provide `glass: { tint, opacity, frost, highlight }`
183
+ for lightweight translucent glass. `ScreenGlassMaterial` is a separate reusable
184
+ component. It is a stylized overlay material, not backdrop blur/refraction.
185
+
186
+ ### Shared material primitives
187
+
188
+ Import shared material primitives from `@vincentt-xr/sdk` only:
189
+
190
+ ```tsx
191
+ import {
192
+ DEFAULT_MATERIAL_FILL,
193
+ MaterialFill,
194
+ MeshMaterial,
195
+ createTwoStopGradient,
196
+ type MaterialFillSettings,
197
+ type MeshMaterialSettings,
198
+ } from "@vincentt-xr/sdk";
199
+ ```
200
+
201
+ Use `MaterialFill` when a component needs the same solid/gradient/texture fill
202
+ system as `ScreenShape` and FaceMesh masks. `MaterialFillSettings.source` is
203
+ `"solid" | "gradient" | "texture"`; pass `color`, `opacity`, optional
204
+ `textureUrl`, and optional `gradient`.
205
+
206
+ ```tsx
207
+ const fill: MaterialFillSettings = {
208
+ ...DEFAULT_MATERIAL_FILL,
209
+ source: "gradient",
210
+ color: "#38bdf8",
211
+ opacity: 0.8,
212
+ gradient: createTwoStopGradient("#38bdf8"),
213
+ };
214
+
215
+ <mesh>
216
+ <planeGeometry args={[1, 1]} />
217
+ <MaterialFill settings={fill} />
218
+ </mesh>;
219
+ ```
220
+
221
+ Use `MeshMaterial` for serialized mesh material settings shared by
222
+ `MeshRenderer`; do not re-create local `<meshBasicMaterial>` /
223
+ `<meshStandardMaterial>` branches in scene-object components.
224
+
97
225
  ```tsx
98
226
  import { ScreenImage, ScreenSpaceUI } from "@vincentt-xr/sdk";
99
227
 
@@ -121,7 +249,7 @@ import { ScreenImage, ScreenSpaceUI } from "@vincentt-xr/sdk";
121
249
 
122
250
  Props:
123
251
 
124
- - `source`: `{ kind: "default" }`, `{ kind: "image", src }`, or `{ kind: "texture", texture }`.
252
+ - `source`: `{ kind: "default" }`, `{ kind: "image", src }`, `{ kind: "gif", src }`, or `{ kind: "texture", texture }`.
125
253
  - `src`: shorthand for `{ kind: "image", src }`.
126
254
  - `fit`: `"stretch" | "contain" | "cover"`; default `"contain"`.
127
255
  - `coordinateSpace`: `"normalized" | "canvas"`; default `"normalized"`.
@@ -147,9 +275,16 @@ Settings:
147
275
  position, size, rotation, pivot, render order, overlay/depth behavior, and visibility.
148
276
  - `DEFAULT_SCREEN_IMAGE_SETTINGS.image`: source kind, src, fit, opacity, color,
149
277
  flips, transparency, and alpha test.
150
- - `sourceKind` includes future media categories (`video`, `sequence`, `gif`),
151
- but the current component implementation supports `default`, static `image`,
152
- and already-loaded Three `texture`.
278
+ - GIF URLs are decoded inside `ScreenImage` into a canvas texture. The decoder
279
+ preserves the GIF canvas size, frame delays, optimized partial-frame
280
+ compositing, and disposal rules before fitting the texture to the authored
281
+ rectangle. GIF playback streams into one working canvas buffer plus an
282
+ optional restore buffer; it does not retain every decoded full frame. Sources
283
+ over 5 MB, 1,000,000 decoded pixels, or 180 frames are rejected with a warning
284
+ and fall back to the default texture. Keep authored GIFs around 3-5 MB for
285
+ mobile AR.
286
+ - Video and image-sequence providers resolve their media to a live
287
+ `THREE.Texture` and pass it as `kind: "texture"`.
153
288
 
154
289
  Use `<ScreenImage>` instead of hand-authoring `<ScreenTransform>` + `<mesh>`
155
290
  when the object is conceptually a 2D screen image. Use the lower-level mesh path
@@ -168,6 +303,31 @@ including `name`, `kind: "screenImage"`, `transform`, `image`, `material`, and
168
303
  `screenSpaceUI`. The `transform` includes `coordinateSpace` and `canvasSize`.
169
304
  Use that payload as the handoff from human layout tuning back to code.
170
305
 
306
+ ### ScreenImage contract for every caller
307
+
308
+ `ScreenImage` is the canonical screen-image object in SDK and client apps. Any
309
+ debug panel, hierarchy row, editor, or runtime controller that targets one must
310
+ operate on the same mounted object state:
311
+
312
+ - Keep one source of truth for `transform`, source/material settings, and
313
+ visibility. Panel edits must immediately update the rendered object.
314
+ - Always wire `onScreenTransformChange` for inspector or editor usage. It is the
315
+ real drag/place path; do not implement a panel-only shadow transform.
316
+ - Use `coordinateSpace="canvas"` plus `canvasSize` for authoring tools, then
317
+ map canvas values to `position`/`size`; never treat design pixels as world
318
+ units. `fit`/`stretchMode` changes content fitting, not the object's bounds.
319
+ - Source replacement must preserve the object identity when possible, stop and
320
+ dispose the previous owned provider, and keep the current transform/material
321
+ settings. Dynamic video/canvas textures must continue marking fitted clones
322
+ for GPU upload.
323
+ - Respect capability-gated controls: static images expose image/material
324
+ controls only; animated providers expose only controls they support. Loading,
325
+ error, retry, pause, ended, and cleanup state must be visible to diagnostics.
326
+ - `visible`, `overlay`, `renderOrder`, `transparent`, `alphaTest`, blend mode,
327
+ opacity, tint, flips, pivot, rotation, size, and position are all part of the
328
+ live object contract and must remain synchronized from SDK or client entry
329
+ points.
330
+
171
331
  ### `<SpriteAnimation sheet={{ url, cols, rows, frames, fps }}>` — Animated transparent overlay
172
332
 
173
333
  The primitive for an animated transparent overlay (a frame border, a gesture-instruction graphic, an animated sticker). One transparent WebP atlas of `cols`×`rows` cells; the first `frames` play left-to-right, top-to-bottom at `fps`. It is a single static GPU texture with a per-frame UV offset — no video decode — so it works in every native and in-app browser and costs ~zero heat.
@@ -232,9 +392,190 @@ For local tuning, import `FaceWarp3DSettingsPanel` from
232
392
  `@vincentt-xr/sdk/debug-ui`. It is a draggable debug panel and should not
233
393
  be included in the final production scene.
234
394
 
395
+ ### `FaceMeshRenderer` — scene-object live 3D face mesh
396
+
397
+ Use `FaceMeshRenderer` for the standard SDK face-mesh scene-object component.
398
+ It owns face tracking internally through `FaceTracker`, reads live landmarks,
399
+ deforms the canonical face mesh, and renders through normal face-mesh material
400
+ settings. Prefer this over direct `CanonicalFaceMeshRenderer` in new
401
+ scene-object/editor integrations.
402
+
403
+ Import runtime/material symbols from `@vincentt-xr/sdk/face-effects`:
404
+
405
+ ```tsx
406
+ import {
407
+ DEFAULT_FACE_MESH_RENDERER,
408
+ DEFAULT_FACE_MESH_TRANSFORM,
409
+ FACE_MESH_MATERIAL_TYPES,
410
+ FACE_MESH_SIDES,
411
+ FaceMeshRenderer,
412
+ mapFaceMeshRendererSettingsToProps,
413
+ resolveFaceMeshRendererSettings,
414
+ type FaceMeshRendererSettings,
415
+ } from "@vincentt-xr/sdk/face-effects";
416
+ ```
417
+
418
+ Import scene-object defaults and store helpers from
419
+ `@vincentt-xr/sdk/scene-object`:
420
+
421
+ ```tsx
422
+ import {
423
+ createDefaultComponent,
424
+ createDefaultFaceMeshRendererSettings,
425
+ DEFAULT_RENDER_GROUP_IDS,
426
+ type RenderGroupSceneObjectStore,
427
+ } from "@vincentt-xr/sdk/scene-object";
428
+ ```
429
+
430
+ `FaceMeshRendererSettings` is the serialized component settings contract:
431
+ `enabled`, `faceIndex`, `targetFps`, `showLeftEyeGeometry`,
432
+ `showRightEyeGeometry`, `showMouthGeometry`, `showNoseGeometry`,
433
+ `showOverlayMask`, `showMouthMask`, `showLeftEyeMask`, `showRightEyeMask`,
434
+ `showNoseMask`, `externalMeshUrl`, `externalScale`, `materialType`,
435
+ `shaderPreset`, `textureMap`, `alphaMap`, `alphaOpacity`, `side`, `blendMode`,
436
+ `blendOpacity`, `baseColor`, `opacity`, `compositeShading`,
437
+ `maskTextureSource`, `maskSourceUrl`, `maskColor`, `maskOpacity`,
438
+ `faceTextureUrl`, `faceTextureStyle2Url`, `faceAlphaMaskUrl`, and
439
+ `renderOrder`.
440
+
441
+ `maskTextureSource` is `"solid" | "gradient" | "imageUrl" | "mediaUrl"`.
442
+ Solid and gradient masks use `maskColor`; image/media masks use
443
+ `maskSourceUrl`. `maskOpacity` is always `0..1`.
444
+
445
+ `materialType: "shader"` has three shader presets: `"basicShader"`,
446
+ `"originalFace"`, and `"composite"`. Use
447
+ `resolveFaceMeshRendererSettings(current, patch)` when editing settings so
448
+ shader/material/texture state stays synchronized. Selecting composite applies
449
+ the SDK composite defaults (`textureMap: "canonical"`, hard-light blend,
450
+ `blendOpacity: 0.85`, `compositeShading: true`) instead of silently falling
451
+ back to original-face rendering.
452
+
453
+ `externalMeshUrl` is for canonical-compatible OBJ assets that match the
454
+ renderer's live landmark deformation path. Third-party Face Mesh OBJ files are
455
+ reference assets only until the SDK has an explicit topology mapper from those
456
+ vertices to the 468 tracked face landmarks. Do not bundle OBJ/FBX/PNG reference
457
+ files in `lib/` or examples unless runtime code consumes them correctly.
458
+
459
+ Do not add settings that are not runtime-effective. Skull/ear geometry,
460
+ expression multiplier, and UV1 mapping require parser/runtime support before
461
+ they become public SDK settings.
462
+
463
+ Scene-object example:
464
+
465
+ ```tsx
466
+ const objectId = "face-mesh";
467
+ const transform = createDefaultComponent("Transform", objectId);
468
+ transform.settings.position.y = DEFAULT_FACE_MESH_TRANSFORM.position.y;
469
+
470
+ const faceMesh = createDefaultComponent("FaceMeshRenderer", objectId);
471
+ faceMesh.settings = {
472
+ ...createDefaultFaceMeshRendererSettings(),
473
+ materialType: FACE_MESH_MATERIAL_TYPES.wireframe,
474
+ side: FACE_MESH_SIDES.double,
475
+ baseColor: "#00e5ff",
476
+ opacity: 0.82,
477
+ } satisfies FaceMeshRendererSettings;
478
+
479
+ const store: RenderGroupSceneObjectStore = {
480
+ objects: {
481
+ [objectId]: {
482
+ id: objectId,
483
+ name: "Face Mesh",
484
+ enabled: true,
485
+ renderGroupId: DEFAULT_RENDER_GROUP_IDS.scene3d,
486
+ componentIds: [transform.id, faceMesh.id],
487
+ },
488
+ },
489
+ components: {
490
+ [transform.id]: transform,
491
+ [faceMesh.id]: faceMesh,
492
+ },
493
+ renderGroups: {
494
+ [DEFAULT_RENDER_GROUP_IDS.scene3d]: {
495
+ id: DEFAULT_RENDER_GROUP_IDS.scene3d,
496
+ name: "3D Scene",
497
+ domain: "scene3d",
498
+ enabled: true,
499
+ objectIds: [objectId],
500
+ },
501
+ },
502
+ renderGroupOrder: [DEFAULT_RENDER_GROUP_IDS.scene3d],
503
+ };
504
+ ```
505
+
506
+ When `SceneObjectRenderer` sees both `Transform` and `FaceMeshRenderer` on the
507
+ same object, it passes the authored `Transform` into `FaceMeshRenderer`.
508
+ FaceMesh does not use normal world-space `Transform3D` semantics. It applies
509
+ the supported Transform values in MediaPipe landmark space before camera
510
+ projection; it must never wrap the already-projected mesh in a parent
511
+ `Transform3D`, because that causes depth-dependent screen skew.
512
+
513
+ Supported FaceMesh Transform values are position and uniform scale only.
514
+ Rotation and non-uniform scale are ignored by the runtime until the SDK has a
515
+ real object-space face solve. Authored `position.y` is Y-up, but MediaPipe image
516
+ space is Y-down, so the renderer subtracts Y while adding X/Z.
517
+
518
+ This is the SDK's strict FaceMesh contract: FaceInfo/FaceBinding pose first,
519
+ FaceMesh deformation in face landmark space, supported authored Transform in
520
+ that space, then world/camera projection.
521
+ `DEFAULT_FACE_MESH_TRANSFORM` starts from `DEFAULT_TRANSFORM_3D` and sets
522
+ `position.y` to `0.001` as an intentional FaceMesh authoring bias, not as a
523
+ projection correction. Author a Transform with `position.y = 0` to opt out.
524
+
525
+ `mapFaceMeshRendererSettingsToProps(settings)` maps serialized
526
+ `FaceMeshRendererSettings` to low-level `CanonicalFaceMeshRendererProps`.
527
+ Use it only when bridging custom runtime code; ordinary scene-object rendering
528
+ should use `SceneObjectRenderer`.
529
+
530
+ `CanonicalFaceMeshRenderer` remains exported from `@vincentt-xr/sdk/face-effects`
531
+ for backwards compatibility and low-level experiments, but new editor and
532
+ scene-object work should use `FaceMeshRenderer`.
533
+
534
+ ### `FaceRetouch` — legacy landmark skin retouch
535
+
536
+ Use `FaceRetouch` from `@vincentt-xr/sdk/face-effects` for the legacy
537
+ landmark-masked skin retouch shader. The public controls are intentionally
538
+ minimal:
539
+
540
+ ```tsx
541
+ import {
542
+ FaceRetouch,
543
+ patchFaceRetouchControls,
544
+ resolveFaceRetouchControls,
545
+ type FaceRetouchControls,
546
+ } from "@vincentt-xr/sdk/face-effects";
547
+ import { FaceRetouchSettingsPanel } from "@vincentt-xr/sdk/debug-ui";
548
+
549
+ const [controls, setControls] = useState(() => resolveFaceRetouchControls());
550
+
551
+ const updateControls = (next: Partial<FaceRetouchControls>) => {
552
+ setControls((current) => patchFaceRetouchControls(current, next));
553
+ };
554
+
555
+ <FaceRetouch {...controls} />;
556
+ <FaceRetouchSettingsPanel value={controls} onChange={updateControls} />;
557
+ ```
558
+
559
+ `FaceRetouchControls` has only `skinSmoothing`, `skinRealism`,
560
+ `blemishReduction`, and `previewMask`. Do not invent provider, quality,
561
+ redness, teeth, eye, or blur controls for this component.
562
+
563
+ For multiple people, render one `FaceRetouch` per face:
564
+
565
+ ```tsx
566
+ <FaceRetouch {...controls} faceIndex={0} numFaces={2} />
567
+ <FaceRetouch {...controls} faceIndex={1} numFaces={2} />
568
+ ```
569
+
570
+ `numFaces` defaults to at least `faceIndex + 1`. `clipMinX` and `clipMaxX` are
571
+ optional horizontal UV clipping props in `0..1` space, useful for split-screen
572
+ comparison demos.
573
+
235
574
  ### `<FaceTracker>` — Face tracking (468 landmarks)
236
575
 
237
- Detects faces and provides face transform to children via context. Supports `faceIndex` (default 0) and `scaleWithFace`.
576
+ Detects faces and provides face transform to children via context. Supports
577
+ `faceIndex` (default `0`), `scaleWithFace`, and `followFaceRoll` (default
578
+ `true`; set `false` to keep children upright while the head tilts).
238
579
 
239
580
  ```tsx
240
581
  import { FaceTracker, TrackingAnchor } from "@vincentt-xr/sdk/tracking";
@@ -249,6 +590,274 @@ import { FaceTracker, TrackingAnchor } from "@vincentt-xr/sdk/tracking";
249
590
  </FaceTracker>;
250
591
  ```
251
592
 
593
+ ### `FaceInfo` — face-driven effect logic
594
+
595
+ Use `FaceInfo` for head-pose, expression, landmark, bounds, and face-scale logic.
596
+ Do not read raw MediaPipe results or repeat landmark indices unless the facade
597
+ does not expose data required by a research/debugging tool.
598
+
599
+ ```tsx
600
+ import { hasFaceExpression, useFaceInfo } from "@vincentt-xr/sdk/tracking";
601
+
602
+ function FaceLogic() {
603
+ const face = useFaceInfo({ active: true });
604
+ if (!face) return null;
605
+
606
+ const mouthOpen = hasFaceExpression(face, "jawOpen");
607
+ const lookingLeft = face.pose.rotation.y > 0.2;
608
+ return <group visible={mouthOpen} rotation-y={lookingLeft ? 0.2 : 0} />;
609
+ }
610
+ ```
611
+
612
+ | Field | Coordinate contract |
613
+ | --------------- | ---------------------------------------------------------------- |
614
+ | `index` | Current result-array index; not a persistent identity |
615
+ | `landmarks` | MediaPipe normalized tracker-input coordinates, top-left origin |
616
+ | `bounds` | Normalized tracker-input bounds, top-left origin |
617
+ | `pose.position` | Existing face-tracker Three.js world position |
618
+ | `pose.rotation` | Radians: X pitch (+up), Y yaw (+left), Z roll |
619
+ | `pose.scale` | Smoothed uniform sizing heuristic used by face anchors/occluders |
620
+ | `eyeDistance` | Outer-eye distance in normalized tracker-input coordinates |
621
+ | `blendshapes` | MediaPipe category names and scores in `[0, 1]` |
622
+
623
+ `useFaceInfo({ faceIndex })` is the normal single-face React reader.
624
+ `useFaceInfos({ numFaces })` publishes all current faces. Both acquire the shared
625
+ face model through the SDK registry and use the existing throttle/empty-frame
626
+ hold lifecycle; do not mount a separate result polling loop. Call them inside
627
+ `XRProvider` and the R3F Canvas. Options are `active`, `numFaces`, `targetFps`,
628
+ `throttleMs`, and `holdMs`; `useFaceInfo` additionally accepts `faceIndex`.
629
+
630
+ ```tsx
631
+ function MultiFaceLogic() {
632
+ const faces = useFaceInfos({ numFaces: 3 });
633
+ return <group visible={faces.some((face) => hasFaceExpression(face, "jawOpen"))} />;
634
+ }
635
+ ```
636
+
637
+ For `useFrame` code that already has a low-level face node, call
638
+ `getFaceInfo(node, faceIndex, target)`. Create `target` once with
639
+ `createFaceInfoTarget()` and reuse it to avoid per-frame allocations. Use
640
+ `getFaceInfo` and `createFaceInfoTarget` from `@vincentt-xr/sdk/low-level`; they
641
+ are node-in escape hatches, not the normal React API. Use `getFaceBlendshape`
642
+ and `hasFaceExpression`; absent scores resolve to `0`, and the default expression
643
+ threshold is `DEFAULT_FACE_EXPRESSION_THRESHOLD`. Bind the R3F camera once with
644
+ `node.setCamera(camera, faceIndex)` before reading pose; the React hooks do this
645
+ automatically.
646
+
647
+ All supported expression names are typed by `FaceBlendshapeName` and listed in
648
+ the greppable `FACE_BLENDSHAPE_NAMES` constant exported from
649
+ `@vincentt-xr/sdk/tracking`. This includes `eyeBlinkLeft`, `eyeBlinkRight`,
650
+ `eyeWideLeft`, `eyeWideRight`, and `jawOpen`; do not guess string names.
651
+
652
+ Use `normalizedToScreenPixels` with the tracker input/display viewport size to
653
+ display a face landmark in Screen Image/Text/Shape coordinates. The tracker
654
+ input is already the SDK feed; do not apply media cover/mirroring a second time.
655
+ Never mutate or mirror `FaceInfo.landmarks` in place, manually project them,
656
+ infer a persistent face ID, or copy numeric landmark indices/thresholds into
657
+ application code.
658
+
659
+ ```ts
660
+ const nose = normalizedToScreenPixels({
661
+ point: face.landmarks[FACE_LANDMARK_INDICES["face.noseTip"]],
662
+ viewportSize,
663
+ });
664
+ ```
665
+
666
+ Effect House mapping: `FaceInfo.landmarks` replaces `pointsArray`, `bounds`
667
+ replaces `rect`, `pose.rotation` provides pitch/yaw/roll, and `eyeDistance` is
668
+ named the same. Use Face Actions below for the semantic `FaceAction` role.
669
+ Vincentt uses MediaPipe's face topology and top-left origin; it is not a
670
+ 106-point or bottom-left-compatible alias.
671
+
672
+ Run the example route `/face-info` to verify all landmarks, live bounds, nose
673
+ position, pose, eye distance, and `jawOpen` using `ScreenShape`, `ScreenText`,
674
+ and `Axes`.
675
+
676
+ ### `useFaceDetection()` — face presence lifecycle and count
677
+
678
+ Import `useFaceDetection` from `@vincentt-xr/sdk/tracking` when logic needs face
679
+ detected/lost edges, selected-face presence, or the current face count. It wraps
680
+ the shared `useFaceInfos` reader and does not start another model.
681
+
682
+ ```tsx
683
+ const detection = useFaceDetection({ faceIndex: 0, numFaces: 5 });
684
+
685
+ if (detection.justDetected) showEffect();
686
+ if (detection.detected) updateEffect(detection.face);
687
+ if (detection.justLost) hideEffect();
688
+ ```
689
+
690
+ | Visual-scripting term | SDK field |
691
+ | ------------------------------------- | ------------------------------------ |
692
+ | Face Detection Begin / face appeared | `justDetected`, `phase === "begin"` |
693
+ | Face Detection Stay / face present | `detected`, `phase === "stay"` |
694
+ | Face Detection End / face disappeared | `justLost`, `phase === "end"` |
695
+ | Face Detection None / no face | `!detected`, `phase === "none"` |
696
+ | Face Count / number of faces | `faceCount` and `faces.length` |
697
+ | Which Face / selected face | `faceIndex` option and `face` result |
698
+
699
+ The full result is `face`, `faces`, `faceCount`, `detected`, `justDetected`,
700
+ `justLost`, and `phase`. Options are the normal `useFaceInfos` options plus
701
+ `faceIndex`: `active`, `numFaces`, `targetFps`, `throttleMs`, and `holdMs`.
702
+ Default `holdMs: 250` prevents a transient missed frame from firing `end`; use
703
+ `holdMs: 0` only when immediate disappearance is required. Do not compare raw
704
+ result arrays or build a local previous-frame ref. The verified
705
+ `/face-detection` page tests five selectable face slots and all four phases; its
706
+ real-camera lifecycle flow passed on 26 July 2026.
707
+
708
+ ### Reusable normalized and face image crops
709
+
710
+ Import `drawNormalizedCrop` from `@vincentt-xr/sdk` for any normalized image
711
+ region. Import `drawFaceCrop` from `@vincentt-xr/sdk/tracking` when the bounds
712
+ come from `FaceInfo`. Both preserve the requested output pixel aspect ratio,
713
+ clamp at source edges, and draw into a caller-owned canvas without starting a
714
+ tracker or allocating per call.
715
+
716
+ ```ts
717
+ const cropCanvas = document.createElement("canvas");
718
+
719
+ const drawn = drawFaceCrop({
720
+ source: video,
721
+ face,
722
+ canvas: cropCanvas,
723
+ outputSize: { width: 224, height: 224 },
724
+ padding: 0.2,
725
+ });
726
+ ```
727
+
728
+ Bounds use normalized tracker-input/source coordinates with a top-left origin.
729
+ `padding` is the fraction of detected width/height added on each side. The face
730
+ helper defaults to `0.2`; the generic helper defaults to `0`. The functions do
731
+ not mirror, classify, upload, or retain pixels. A `false` result means the
732
+ source dimensions, bounds, output dimensions, or canvas context were unusable.
733
+ Reuse one canvas on frame-driven paths. Face attribute expression should feed
734
+ this crop into its independent image classifier; never derive emotion labels
735
+ from `FaceInfo.blendshapes`.
736
+
737
+ ### Face Actions — semantic face effect logic
738
+
739
+ Import `FACE_ACTION_NAMES`, `DEFAULT_FACE_ACTION_OPTIONS`,
740
+ `getFaceActionValue`, `hasFaceAction`, and `useFaceAction` from
741
+ `@vincentt-xr/sdk/tracking`. They derive from caller-owned `FaceInfo`; they do
742
+ not start a second tracker.
743
+
744
+ ```tsx
745
+ const face = useFaceInfo();
746
+ const blink = useFaceAction(face, "eyeBlink");
747
+
748
+ useEffect(() => {
749
+ if (blink.justStarted) capturePhoto();
750
+ }, [blink.justStarted]);
751
+
752
+ const mouthAmount = face ? getFaceActionValue(face, "mouthOpen") : 0;
753
+ ```
754
+
755
+ | Action | Source and direction | Default start / release / hold |
756
+ | ------------------------------------------------------------- | --------------------------------- | ------------------------------ |
757
+ | `browRaise` | max brow-up blendshape | `0.45 / 0.30 / 50 ms` |
758
+ | `eyeBlink`, `eyeBlinkLeft`, `eyeBlinkRight`, `eyeBlinkEither` | typed eye-blink blendshapes | `0.55 / 0.40 / 50 ms` |
759
+ | `mouthOpen` | `jawOpen` | `0.45 / 0.30 / 50 ms` |
760
+ | `mouthPout` | `mouthPucker` | `0.50 / 0.35 / 50 ms` |
761
+ | `headUp`, `headDown` | positive/negative pitch magnitude | `10° / 6° / 80 ms` |
762
+ | `headTurnLeft`, `headTurnRight` | positive/negative yaw magnitude | `10° / 6° / 80 ms` |
763
+ | `headTiltLeft`, `headTiltRight` | positive/negative roll magnitude | `10° / 6° / 80 ms` |
764
+
765
+ Blendshape values are `[0, 1]`; head values and thresholds are radians. Left
766
+ and up use the SDK-positive pose convention. Directional actions always return
767
+ a non-negative magnitude. Pass `neutralRotation: { ...face.pose.rotation }`
768
+ captured from an explicit user calibration gesture when the resting pose should
769
+ be treated as zero. Without it, head actions use zero rotation.
770
+
771
+ Use `getFaceActionValue` for continuous animation, `hasFaceAction` for a simple
772
+ level check, and `useFaceAction` for `active`, `justStarted`, `justEnded`, and
773
+ `phase: "begin" | "stay" | "end" | "none"`
774
+ with SDK hysteresis and hold timing. Do not guess blendshape strings, flip pose
775
+ signs, or rebuild local debounce/hysteresis logic. For multiple faces, select
776
+ one with `useFaceInfo({ faceIndex })`, or call
777
+ `getStrongestFaceAction(faces, action)` for an "any face" effect, then pass its
778
+ `face` into `useFaceAction`. The strongest selector is one pass and starts no
779
+ additional reader.
780
+
781
+ Effect House-style physical movement maps as follows: left/right/both/either
782
+ blink → `eyeBlinkLeft` / `eyeBlinkRight` / `eyeBlink` / `eyeBlinkEither`;
783
+ mouth open → `mouthOpen`; pout → `mouthPout`; eyebrow movement → `browRaise`.
784
+ Semantic blink left/right is viewer-facing and therefore swaps MediaPipe's raw
785
+ anatomical-side blendshape names. Never swap `FaceInfo.blendshapes` itself.
786
+ Every detected movement emits at least one `stay` frame after `begin` before
787
+ `end`.
788
+ These are observable movements, not image classification. Do not infer
789
+ `happy`, `sad`, `angry`, `fear`, `disgust`, `surprise`, or `neutral` from local
790
+ threshold combinations. Use the separate Face Attribute Expression API below.
791
+
792
+ Run `/face-actions` to inspect every live value, set neutral pose, tune the
793
+ selected action, and repeat the all-actions checklist. All actions and the
794
+ default thresholds were verified with the real-camera page on 26 July 2026.
795
+ Run the verified `/facial-movement-expression` page to test the Effect
796
+ House-compatible physical movement subset, phase edges, fixed-face or
797
+ strongest-face selection, and the image classifier. Its complete real-camera
798
+ flow passed on 26 July 2026.
799
+
800
+ ### Face Attribute Expression — image-based classification
801
+
802
+ Import `useFaceAttributeExpression`, `FACE_ATTRIBUTE_EXPRESSION_NAMES`, and
803
+ their types from `@vincentt-xr/sdk/tracking`. Pass caller-owned `FaceInfo`; the
804
+ hook reuses the shared session input frame, crops that face, and lazy-loads the
805
+ OpenCV MobileFaceNet INT8 ONNX classifier. It does not create another face
806
+ reader or detector.
807
+
808
+ ```tsx
809
+ const face = useFaceInfo();
810
+ const attribute = useFaceAttributeExpression(face, {
811
+ targetFps: 5,
812
+ minConfidence: 0.55,
813
+ });
814
+
815
+ if (attribute.status === "ready") {
816
+ const { expression, confidence, scores } = attribute.result;
817
+ }
818
+ ```
819
+
820
+ The lifecycle is `idle | loading | ready | error`. Ready results contain
821
+ `expression`, `confidence`, and `scores`. `expression` can be `angry`,
822
+ `disgust`, `fear`, `happy`, `neutral`, `sad`,
823
+ `surprise`, or `unknown`. `scores` contains only the seven real model classes;
824
+ there is no fabricated `unknown` score. `unknown` is the low-confidence fallback
825
+ below `minConfidence`. Default `targetFps` is `5`; the model is about 1.36 MB,
826
+ loads outside the main bundle, and is SHA-384 verified. ONNX Runtime's WASM
827
+ worker files load from the jsDelivr package path that matches the active
828
+ `onnxruntime-web` runtime version, not through the consuming app's bundler. The
829
+ runtime files are fetched from jsDelivr without SDK-managed SRI because ONNX
830
+ Runtime resolves its own WASM assets.
831
+
832
+ For best threaded SIMD performance, production pages that run expression
833
+ classification should be cross-origin isolated
834
+ (`Cross-Origin-Opener-Policy: same-origin` and
835
+ `Cross-Origin-Embedder-Policy: require-corp`) so `SharedArrayBuffer` is
836
+ available. Without those headers, ONNX Runtime falls back to single-threaded
837
+ WASM.
838
+
839
+ `modelUrl` overrides must accept float32 NCHW `[1, 3, 112, 112]`, with planar
840
+ RGB normalized to `[-1, 1]`, and return seven logits ordered `angry`, `disgust`,
841
+ `fear`, `happy`, `neutral`, `sad`, `surprise`. Do not assume an arbitrary
842
+ MobileFaceNet ONNX model is compatible. Failed loads are removed from the
843
+ session cache so a remounted consumer can retry.
844
+
845
+ This is a visual-expression estimate, not ground-truth emotion, intent,
846
+ identity, or mental state. Never replace it with blendshape heuristics. Run the
847
+ verified `/facial-movement-expression` page to inspect every live model score.
848
+
849
+ ### `<Axes>` — generic 3D orientation gizmo
850
+
851
+ Use `Axes` to visualize any 3D pose. X is red, Y is green, and Z is blue. Keep
852
+ tracking separate: bind a face, hand, or object pose through standard Three.js
853
+ `position` and `rotation` props.
854
+
855
+ ```tsx
856
+ import { Axes } from "@vincentt-xr/sdk";
857
+
858
+ <Axes size={0.38} position={pose.position} rotation={pose.rotation} />;
859
+ ```
860
+
252
861
  ### `<FaceTracker.Mesh />` / `<FaceMesh>` — Deforming 468-point face mesh
253
862
 
254
863
  For masks, makeup, and full-face decoration that deforms with skin.
@@ -353,6 +962,12 @@ Every tracker takes an `active` prop (default `true`). Set `active={false}` and
353
962
 
354
963
  When several consumers share one model, it runs while **any** of them is active. `active` gates whether the model runs; `perf.targetFps` (below) caps how often it runs while active — use both. `<Segmentation>` also takes `active` (distinct from its `visible` prop, which only gates mask exposure while inference keeps running).
355
964
 
965
+ Session lifecycle preserves that ownership state: `session.stop()` pauses every
966
+ tracker without releasing its MediaPipe resources, and the next `session.start()`
967
+ resumes only models that still have at least one active owner. Static-image
968
+ detection pulses use the same suspension path, so source switches do not
969
+ accidentally reactivate an `active={false}` tracker.
970
+
356
971
  ### `perf` — cap inference rate + camera resolution
357
972
 
358
973
  Set `perf` on `<XRProvider>` to trade tracking fidelity for lower load and heat. This is the single biggest lever if the device runs hot.
@@ -378,6 +993,11 @@ Per-tracker override beats the session default:
378
993
 
379
994
  The base template ships `perf={{ targetFps: 24, camRes: 720 }}` by default — good for both dev laptops and end-user phones. Don't lower it further without a reason; don't remove it to "get full quality" unless you know the target device can take the heat.
380
995
 
996
+ SDK-owned tracker, occluder, and canonical-face assets are SHA-384 verified
997
+ against hashes pinned in the package. A missing hash for
998
+ `https://cdn.vincentt.studio` fails closed; custom consumer-owned asset URLs
999
+ remain best-effort and warn when no pinned hash exists.
1000
+
381
1001
  ---
382
1002
 
383
1003
  ## Reading tracker state
@@ -386,9 +1006,11 @@ The base template ships `perf={{ targetFps: 24, camRes: 720 }}` by default — g
386
1006
 
387
1007
  Wraps children; the child's world position is updated to the tracked landmark each frame. Use for all decoration that simply follows a face/hand/body point.
388
1008
 
389
- Valid targets:
1009
+ Valid targets are centralized in `FACE_TARGETS`; use that exported list for
1010
+ editor dropdowns instead of copying it. Left/right mean the tracked person's
1011
+ perspective.
390
1012
 
391
- - **Face:** `face.forehead`, `face.noseTip`, `face.chin`, `face.leftEyeOuter`, `face.rightEyeOuter`, `face.leftCheek`, `face.rightCheek`, `face.mouthCenter`
1013
+ - **Face binding presets:** `face.center`, `face.forehead`, `face.noseTip`, `face.chin`, `face.leftEye`, `face.rightEye`, `face.leftEyeOuter`, `face.rightEyeOuter`, `face.leftCheek`, `face.rightCheek`, `face.mouthCenter`, `face.leftEarlobe`, `face.rightEarlobe`, `face.custom`
392
1014
  - **Hand:** `hand.wrist`, `hand.thumbTip`, `hand.indexTip`, `hand.middleTip`, `hand.ringTip`, `hand.pinkyTip`, `hand.indexBase`, `hand.middleBase`, `hand.ringBase`, `hand.pinkyBase`
393
1015
  - **Body:** `body.head`, `body.neck`, `body.leftShoulder`, `body.rightShoulder`, `body.leftElbow`, `body.rightElbow`, `body.leftWrist`, `body.rightWrist`, `body.leftHip`, `body.rightHip`, `body.leftKnee`, `body.rightKnee`, `body.leftAnkle`, `body.rightAnkle`
394
1016
 
@@ -403,6 +1025,32 @@ Valid targets:
403
1025
  </FaceTracker>
404
1026
  ```
405
1027
 
1028
+ `face.center` is the general default. Eye targets are composite eye centers;
1029
+ `*EyeOuter` targets are exact outer-corner landmarks. Earlobe targets are the
1030
+ nearest stable MediaPipe face-oval points, not anatomical earlobe detections.
1031
+ All Effect House `FaceBindingAnchorType` names therefore have a Vincentt
1032
+ equivalent, but their numeric canonical-mesh coordinates are not compatible.
1033
+
1034
+ For `face.custom`, pass normalized face-local coordinates. The origin is face
1035
+ center; X runs from person's left `-1` to right `+1`, Y from chin `-1` to
1036
+ forehead `+1`, and positive Z comes toward the camera.
1037
+
1038
+ ```tsx
1039
+ <FaceTracker>
1040
+ <TrackingAnchor target="face.custom" customAnchorPoint={{ x: 0.7, y: 0.2, z: 0 }} smoothing={15}>
1041
+ <mesh />
1042
+ </TrackingAnchor>
1043
+ </FaceTracker>
1044
+ ```
1045
+
1046
+ Scene objects use the same `TrackingAnchor` component settings. A 2D null with
1047
+ `ScreenTransform` + `TrackingAnchor` projects the preset into SDK design pixels,
1048
+ so all child Screen Shapes move as one group. Use `/face-binding-anchors` to
1049
+ test every preset on the editable `rose null`; the inspector target options
1050
+ come directly from `FACE_TARGETS`. Toggle `Follow Face Roll` to rotate the
1051
+ attachment with head tilt. The example hierarchy intentionally shows only the
1052
+ combined root object.
1053
+
406
1054
  **`smoothing` — smooth motion from throttled inference.** Inference is throttled (the `perf` API caps it at 24fps by default) while rendering runs at 60-120fps, so a raw anchor holds then jumps on each new sample — visible stepping under head/hand movement. A plain lerp only hides it at rest; under motion it still steps. Pass `smoothing` (`~15` is a good default) and the anchor runs a **One Euro filter** — the standard adaptive low-pass for interactive tracking: it smooths hard when the landmark moves slowly (kills jitter) and loosens as it speeds up (kills lag), and never extrapolates so it can't overshoot or wobble. Higher `smoothing` is snappier (less lag, slightly more jitter); lower is smoother. It snaps (resets the filter) on first placement and on re-acquire, so it never slides in from a stale position. `0` (default) keeps the raw snap. Rotation is not smoothed. Prefer this over a hand-rolled `useFrame` lerp — a hand-rolled lerp reintroduces the stepping.
407
1055
 
408
1056
  ```tsx
@@ -471,10 +1119,116 @@ import { GestureTracker, GestureTrigger } from "@vincentt-xr/sdk/tracking";
471
1119
 
472
1120
  In a v2-template project this is already mounted in `src/App.tsx` — do NOT add a second one there. Mentioned here because `customBackground` is how you do background replacement: pair it with `<Segmentation type="portrait" />` and pass the segmentation mask texture as `segmentationMask`.
473
1121
 
1122
+ `VideoBackground` is a flat camera-background component: its material keeps
1123
+ `depthTest={false}` and `depthWrite={false}`. Editor-style 3D viewports that
1124
+ treat the camera feed as a real scene plane should render a separate editor
1125
+ video plane instead of changing `VideoBackground` semantics.
1126
+
474
1127
  ---
475
1128
 
476
1129
  ## Mesh primitives and textures
477
1130
 
1131
+ `ScreenImage` keeps fitted clones synchronized for dynamic video and canvas textures.
1132
+
1133
+ ### Texture Types API usage
1134
+
1135
+ `ScreenImage` is the common render target for static and animated texture sources. Use
1136
+ the normal runtime import for rendering:
1137
+
1138
+ ```tsx
1139
+ import { ScreenImage, ScreenSpaceUI } from "@vincentt-xr/sdk";
1140
+
1141
+ <ScreenSpaceUI>
1142
+ <ScreenImage
1143
+ source={{ kind: "image", src: imageUrl }}
1144
+ materialType="unlit"
1145
+ stretchMode="fit"
1146
+ transparent
1147
+ />
1148
+ </ScreenSpaceUI>;
1149
+ ```
1150
+
1151
+ Supported source behavior:
1152
+
1153
+ - Image: static URL texture. No playback controls.
1154
+ - Runtime texture: pass `{ kind: "texture", texture }` when supplying an existing `THREE.Texture`.
1155
+ - Video: use a managed `THREE.VideoTexture`; support play/pause, restart, frame stepping, playback rate, forward, and reverse stepping. Negative `playbackRate` is not used.
1156
+ - GIF: pass `{ kind: "gif", src }` to `ScreenImage`. The SDK decodes frames into
1157
+ a canvas texture, preserves dimensions and per-frame delays, coalesces
1158
+ optimized partial frames, and applies disposal rules before fitting.
1159
+ - Image sequence: draw loaded frames into a canvas texture using FPS and frame state.
1160
+
1161
+ Animated sources support forward, reverse, ping-pong, random, looping, finite loop
1162
+ counts, pause/resume at current frame, restart, frame status, and ended state where
1163
+ the source supports it. Static image sources must not expose animation controls.
1164
+
1165
+ ScreenImage inspector rules:
1166
+
1167
+ - `ScreenImageSettingsPanel` must read/write the same scene-object state used by the rendered `ScreenImage`.
1168
+ - Transform drag/place must be wired through `onScreenTransformChange`.
1169
+ - Use `mapScreenImageSettingsToProps` to convert inspector canvas coordinates into render props.
1170
+ - Do not pass inspector pixel dimensions directly as world-space `ScreenImage` dimensions.
1171
+ - Dynamic canvas/video texture updates must mark fitted texture clones for GPU upload.
1172
+
1173
+ Low-level texture integration imports from `@vincentt-xr/sdk/low-level`:
1174
+
1175
+ ```ts
1176
+ import {
1177
+ BasicAnimatedTextureController,
1178
+ createTextureSourceResolver,
1179
+ gifTextureSourceProvider,
1180
+ getPlaybackFrame,
1181
+ getSequenceFrame,
1182
+ type AnimatedTextureController,
1183
+ type AnimatedTexturePlayMode,
1184
+ type GifTextureSource,
1185
+ type ResolvedTextureSource,
1186
+ type TextureSourceResolver,
1187
+ type TextureSourceProvider,
1188
+ } from "@vincentt-xr/sdk/low-level";
1189
+ ```
1190
+
1191
+ Controller lifecycle is: `play()`, `pause()`, `restart()`, optional `seek()`,
1192
+ `setPlayMode("forward" | "reverse" | "ping-pong" | "random")`,
1193
+ `setLoop(enabled, count)`, `subscribe(listener)`, then `dispose()`. Read
1194
+ `getSnapshot()` for state, frame/time, duration, frame count, loop, and errors;
1195
+ check `capabilities` before rendering a control.
1196
+
1197
+ `gifTextureSourceProvider` is the built-in GIF resolver provider used by
1198
+ `ScreenImage`. It returns a `ResolvedTextureSource` with a
1199
+ `BasicAnimatedTextureController` and streams playback through a bounded canvas
1200
+ buffer instead of retaining all decoded frames. Custom resolver users can compose
1201
+ it directly when they need controller access for pause, restart, forward playback,
1202
+ loop, typed events, and ownership-safe disposal. Because GIF playback is streamed
1203
+ instead of full-frame cached, seeking and random/reverse/ping-pong playback are
1204
+ not advertised capabilities. Loop restart clears the streamed working buffer
1205
+ before frame 0 so optimized partial-frame GIFs do not carry stale pixels across
1206
+ cycles.
1207
+
1208
+ Frame helpers are also available from the same `/low-level` door:
1209
+ `getPlaybackFrame(tick, frameCount, mode, loop, loopCount)` handles forward,
1210
+ reverse, ping-pong, random, loop, and ended math; `getSequenceFrame(elapsedSeconds,
1211
+ fps, frameCount)` maps elapsed time to a bounded sequence index.
1212
+
1213
+ Event compatibility: subscribe with `controller.on(type, listener)` and remove
1214
+ with the returned disposer or `off()`. Supported event types are
1215
+ `KeyFrameInSecond`, `Pause`, `PlayBegin`, `PlayEnd`, `Resume`, and `Stop`.
1216
+ Event snapshots include the current frame/time, playback state, loop settings,
1217
+ and optional duration/frame count. Use capability checks before exposing an
1218
+ event-driven control.
1219
+
1220
+ Consumer example: `docs/texture-api-consumer.md`.
1221
+
1222
+ Browser verification checklist: Chrome/Android (autoplay requires muted media),
1223
+ Safari/iOS (inline video requires `playsInline` and user-gesture fallback), and
1224
+ background tabs (timers may throttle; frame state must remain bounded). Validate
1225
+ source switching, pause-at-current-frame, loop-off ending, reverse/ping-pong,
1226
+ stretch modes, drag/place transforms, and unmount cleanup in each target.
1227
+
1228
+ `ResolvedTextureSource` owns a texture, optional controller, metadata, and
1229
+ ownership-safe `dispose()`. Abort stale loads, stop timers/RAF, remove media
1230
+ listeners, and never dispose externally owned textures.
1231
+
478
1232
  Use R3F intrinsic `<mesh>`, `<group>`, `<planeGeometry>`, `<meshBasicMaterial>`, etc. Load image textures with drei's `useTexture`.
479
1233
 
480
1234
  **Always name your meshes** (`name="myLabel"`). Names enable runtime lookup AND visual-feedback editing by the editor.
@@ -494,6 +1248,292 @@ const tex = useTexture("https://cdn.../badge.webp");
494
1248
 
495
1249
  ---
496
1250
 
1251
+ ## Scene-object runtime and authoring state
1252
+
1253
+ Runtime components import from `@vincentt-xr/sdk`. Scene-object data and store
1254
+ operations import from `@vincentt-xr/sdk/scene-object`. DOM editors import from
1255
+ `@vincentt-xr/sdk/debug-ui`.
1256
+
1257
+ ### `<Transform3D value={settings}>` and `<MeshRenderer value={settings}>`
1258
+
1259
+ `Transform3D` applies position, rotation (degrees), scale, visibility, and optional
1260
+ Drei transform controls to its children. `MeshRenderer` renders a cube, sphere,
1261
+ plane, or first-pass GLTF/GLB asset with basic/standard material settings.
1262
+
1263
+ ```tsx
1264
+ import {
1265
+ DEFAULT_MESH_RENDERER,
1266
+ DEFAULT_TRANSFORM_3D,
1267
+ MeshRenderer,
1268
+ Transform3D,
1269
+ } from "@vincentt-xr/sdk";
1270
+
1271
+ <Transform3D
1272
+ value={{
1273
+ ...DEFAULT_TRANSFORM_3D,
1274
+ position: { x: 0, y: 0, z: -1 },
1275
+ showTransformControls: true,
1276
+ }}
1277
+ onChange={setTransform}
1278
+ >
1279
+ <MeshRenderer
1280
+ value={{
1281
+ ...DEFAULT_MESH_RENDERER,
1282
+ material: { ...DEFAULT_MESH_RENDERER.material, color: "#38bdf8" },
1283
+ }}
1284
+ />
1285
+ </Transform3D>;
1286
+ ```
1287
+
1288
+ `Transform3DProps` is `{ value, onChange?, children }`.
1289
+ `MeshRendererProps` is `{ value, renderOrder? }`. `renderOrder` is applied to the
1290
+ renderable mesh itself. Disabled or invisible settings render nothing.
1291
+
1292
+ `MeshRendererSettings` fields:
1293
+
1294
+ - `enabled`, `visible`: gate rendering.
1295
+ - `mesh`: `"cube" | "sphere" | "plane"` for primitive fallback.
1296
+ - `assetUrl?`: optional GLTF/GLB URL. When present, the asset renders instead of
1297
+ the primitive mesh.
1298
+ - `assetNodeName?`: optional object name inside the loaded GLTF scene. When
1299
+ missing or unresolved, the whole cloned scene renders.
1300
+ - `assetAnimation?`: optional animation clip name to play. Use `"all"` to play
1301
+ every clip; otherwise provide the exact clip name.
1302
+ - `material.type`: `"standard" | "basic"`.
1303
+ - `material.color`, `opacity`, `wireframe`, `roughness`, `metalness`: material
1304
+ overrides. Wireframe belongs to material state, not geometry state.
1305
+ - `material.renderState?`: optional advanced draw behavior:
1306
+ - `depthTest`, `depthWrite`, `colorWrite`: booleans. `colorWrite` is the
1307
+ all-channel write gate.
1308
+ - `colorMask`: `{ r, g, b, a }` per-channel color write gates. This is
1309
+ applied as scoped WebGL color-mask state for each mesh draw and restored
1310
+ after the object renders.
1311
+ - `depthFunc`: `"never" | "less" | "equal" | "lessEqual" | "greater" |
1312
+ "notEqual" | "greaterEqual" | "always"`.
1313
+ - `cullMode`: `"back" | "front" | "off"`. The default `"back"` maps to
1314
+ Three.js `FrontSide`; `"off"` maps to `DoubleSide`.
1315
+ - `blendMode`: `"normal" | "additive" | "subtractive" | "multiply" |
1316
+ "none"`.
1317
+ - `polygonOffset`: `{ enabled, factor, units }`.
1318
+ - `shadowMode`: `"off" | "cast" | "receive" | "castReceive"`.
1319
+
1320
+ Current GLTF support is intentionally first-pass: the renderer clones the loaded
1321
+ scene, preserves authored GLB material colors, and applies opacity, wireframe,
1322
+ render-state fields, and shadow flags to cloned mesh materials. `assetAnimation`
1323
+ plays named clips or all clips while preserving playback when material settings
1324
+ change. Do not rely on it yet for advanced material graph preservation or
1325
+ production asset lifecycle guarantees.
1326
+
1327
+ Render-state defaults preserve current behavior: depth test/write on,
1328
+ `lessEqual` depth function, color write on, back-face culling, normal blending,
1329
+ RGBA color mask fully enabled, and polygon offset disabled. Instance count is not
1330
+ part of `MeshRenderer.material.renderState`; it belongs to instanced mesh
1331
+ rendering rather than material shading state.
1332
+
1333
+ `assetUrl` uses Drei `useGLTF`, so it suspends while loading. `MeshRenderer`
1334
+ wraps asset-backed rendering in `Suspense fallback={null}` so data-driven
1335
+ `SceneObjectRenderer` usage is safe by default. Add your own outer `Suspense`
1336
+ or call `useGLTF.preload(url)` only when you want a visible loading state or
1337
+ early fetch. Primitive meshes do not require Suspense.
1338
+
1339
+ ### Screen Transform data and resolver
1340
+
1341
+ Import `ScreenTransform2DSettings`, `DEFAULT_SCREEN_TRANSFORM_2D`,
1342
+ `DEFAULT_SCREEN_TRANSFORM_ROOT`, `DEFAULT_REFERENCE_PIXELS_PER_UNIT`,
1343
+ `mergeScreenTransform2D`, and `resolveScreenTransform2D` from the core door.
1344
+
1345
+ `ScreenTransform2DSettings` owns enabled/visible state, canvas-pixel position and
1346
+ size, normalized pivot, degree rotation, 2D scale, reference pixels per unit,
1347
+ render order, overlay mode, and guide visibility.
1348
+
1349
+ ```ts
1350
+ import {
1351
+ DEFAULT_SCREEN_TRANSFORM_2D,
1352
+ mergeScreenTransform2D,
1353
+ resolveScreenTransform2D,
1354
+ type ScreenTransform2DSettings,
1355
+ } from "@vincentt-xr/sdk";
1356
+
1357
+ const transform: ScreenTransform2DSettings = mergeScreenTransform2D({
1358
+ position: { x: 0, y: 240 },
1359
+ size: { width: 420, height: 420 },
1360
+ showTransformGuides: true,
1361
+ });
1362
+
1363
+ const resolved = resolveScreenTransform2D({
1364
+ transform,
1365
+ parentSize: { width: 9, height: 16 },
1366
+ });
1367
+ ```
1368
+
1369
+ Use `mergeScreenTransform2D` to fill nested defaults. Use
1370
+ `resolveScreenTransform2D` whenever runtime or editor tooling must convert the
1371
+ authoring model into center, size, rotation, pivot offset, and aspect. Do not copy
1372
+ the default `720x1280` root or `32` reference-pixel constant into another renderer
1373
+ or guide.
1374
+
1375
+ ### Screen/world coordinate utilities
1376
+
1377
+ Import the `*ScreenPixels` coordinate helpers, `screenPointToRay`, and
1378
+ `smoothNumber` / `smoothVector2` / `smoothVector3` from `@vincentt-xr/sdk`. Use
1379
+ these shared helpers instead of rewriting mapping or projection math or assuming
1380
+ design pixels are Three.js world units.
1381
+ SDK screen pixels use the Effect House-style `720x1280` design canvas, center
1382
+ origin, and +Y up.
1383
+
1384
+ ```ts
1385
+ const units = screenPixelsToUnits(120, { parentSize: screenDimensions });
1386
+ const pixels = screenUnitsToPixels(units, { parentSize: screenDimensions });
1387
+
1388
+ const labelPosition = worldToScreenPixels({
1389
+ point: trackedObject.getWorldPosition(new THREE.Vector3()),
1390
+ camera,
1391
+ viewportSize,
1392
+ });
1393
+
1394
+ const placementRay = screenPointToRay({
1395
+ point: { x: tapX, y: tapY },
1396
+ camera,
1397
+ viewportSize,
1398
+ });
1399
+ ```
1400
+
1401
+ - Use pixel/unit conversion for Screen Image, Screen Text, Screen Shape, editor
1402
+ handles, and custom `ScreenSpaceUI` content.
1403
+ - Use `worldToScreenPixels` for labels, prompts, and indicators that follow a
1404
+ face, hand, body joint, or 3D object. Its `depth` is projected NDC depth; hide
1405
+ the result when `depth` is outside `[-1, 1]` because it is outside the camera
1406
+ clip range.
1407
+ - Use `screenPointToRay` for tap selection, hit testing, and 3D placement. Pass
1408
+ design-canvas coordinates, not DOM client pixels.
1409
+ - Use `clientToScreenPixels` / `screenPixelsToClient` for pointer events and DOM
1410
+ overlays; pass the viewport element's `getBoundingClientRect()` result.
1411
+ - Use `normalizedToScreenPixels` / `screenPixelsToNormalized` for normalized
1412
+ viewport coordinates with top-left origin. These values are not automatically
1413
+ camera-crop adjusted.
1414
+ - Use `mediaToScreenPixels` / `screenPixelsToMedia` with the shared
1415
+ `XRMediaLayout` for camera/image pixels. They apply cover/contain offsets,
1416
+ scaling, and optional display mirroring; do not manually repeat that math.
1417
+ - Use `smoothNumber`, `smoothVector2`, or `smoothVector3` for tracked animation.
1418
+ Pass the R3F frame `delta` in seconds and a smoothing rate (larger is faster).
1419
+ Reuse the optional `out` vector in per-frame paths to avoid allocations.
1420
+ - Zero-size viewport/parent inputs return neutral zero coordinates or units so
1421
+ first paint and collapsed layouts do not throw. Invalid custom root sizes and
1422
+ `referencePixelsPerUnit` still throw because they are configuration errors.
1423
+ - Use Three.js `localToWorld`, `worldToLocal`, `Vector3.project/unproject`,
1424
+ `set`, `toArray`, and `fromArray` directly; do not add SDK wrappers for them.
1425
+
1426
+ ### Scene-object store
1427
+
1428
+ ```ts
1429
+ import {
1430
+ addComponentToObject,
1431
+ createDefaultComponent,
1432
+ createSceneObject,
1433
+ deleteSceneObject,
1434
+ getOrderedComponents,
1435
+ moveComponent,
1436
+ removeComponent,
1437
+ type SceneObjectStore,
1438
+ } from "@vincentt-xr/sdk/scene-object";
1439
+
1440
+ const cube = createSceneObject({ name: "Cube" });
1441
+ const transform = createDefaultComponent("Transform", cube.id);
1442
+ const mesh = createDefaultComponent("MeshRenderer", cube.id);
1443
+ cube.componentIds = [transform.id, mesh.id];
1444
+
1445
+ const store: SceneObjectStore = {
1446
+ objects: { [cube.id]: cube },
1447
+ components: { [transform.id]: transform, [mesh.id]: mesh },
1448
+ };
1449
+
1450
+ const ordered = getOrderedComponents(store, cube.id);
1451
+ ```
1452
+
1453
+ `SceneObject` owns identity, display name, enabled state, optional `renderGroupId`,
1454
+ optional hierarchy fields, and ordered `componentIds`. `SceneComponent` owns one
1455
+ typed settings payload and points back to its object. `ComponentRegistry` maps
1456
+ component types to those settings.
1457
+
1458
+ Persisted scenes may still provide the deprecated `layerId`; migration treats it
1459
+ as a fallback when `renderGroupId` is absent. New code should only write
1460
+ `renderGroupId`.
1461
+
1462
+ Nested scene-object fields:
1463
+
1464
+ - `parentObjectId?: SceneObjectId | null` points to the parent object.
1465
+ - `childObjectIds?: SceneObjectId[]` stores ordered child IDs for hierarchy and
1466
+ runtime recursion.
1467
+
1468
+ Nested transforms are evaluated through the runtime object tree. Child objects
1469
+ render inside the parent object, so parent movement affects descendants while
1470
+ each child keeps its own local component settings.
1471
+
1472
+ Store helpers are immutable and preserve extra fields on an app-extended store:
1473
+
1474
+ - `addComponentToObject(store, component)` adds the component and its ID.
1475
+ - `moveComponent(store, objectId, componentId, nextIndex)` reorders one object.
1476
+ - `removeComponent(...)` removes the component and its ID.
1477
+ - `deleteSceneObject(store, objectId)` removes the object and all owned components.
1478
+ - `replaceSceneComponent(store, component)` replaces one component.
1479
+ - `replaceSceneObject(store, object)` replaces one object.
1480
+ - `setSceneObjectVisible(store, objectId, visible)` toggles object `enabled`.
1481
+ - `getSceneObjectChildren(store, objectId)` resolves explicit and implicit child
1482
+ objects in hierarchy order.
1483
+ - `setSceneObjectParent(store, objectId, parentObjectId, nextIndex?)` updates both
1484
+ sides of a parent/child relationship and rejects cycles or mixed transform
1485
+ domains. Parent `Transform` under `Transform`, and `ScreenTransform` under
1486
+ `ScreenTransform`. Existing child values become local-to-parent values.
1487
+ - `setSceneObjectParentPreserveWorldTransform(store, objectId, parentObjectId,
1488
+ nextIndex?)` is the editor seam for drag/drop and parent dropdowns. It updates
1489
+ the hierarchy and recalculates the child's local `Transform` or
1490
+ `ScreenTransform` so the object does not visually jump when nested or
1491
+ un-nested. This is exact for representable transforms. Rotation combined with
1492
+ non-uniform ancestor scale introduces shear, which neither component stores,
1493
+ so preservation in that case is approximate.
1494
+ - `createOrderedComponentsSelector()` creates a memoized ordered selector.
1495
+
1496
+ ### `<SceneObjectRenderer>`
1497
+
1498
+ `SceneObjectRenderer` reads an object plus its ordered components and renders the
1499
+ supported composition. Import the renderer from core and its store type from
1500
+ `/scene-object`.
1501
+
1502
+ ```tsx
1503
+ import { SceneObjectRenderer } from "@vincentt-xr/sdk";
1504
+ import type { SceneComponent, SceneObjectStore } from "@vincentt-xr/sdk/scene-object";
1505
+
1506
+ <SceneObjectRenderer
1507
+ objectId={cube.id}
1508
+ store={store}
1509
+ editorMode
1510
+ onComponentChange={(component: SceneComponent) => replaceComponent(component)}
1511
+ />;
1512
+ ```
1513
+
1514
+ Props are `objectId`, `store`, optional `editorMode`, optional
1515
+ `onComponentChange`, and optional `fallback`. Supported compositions are:
1516
+
1517
+ - `Transform` + `MeshRenderer`
1518
+ - `Transform` + `Light`
1519
+ - `Transform` + `MeshRenderer` with optional `LookAt` and `PinToMesh`
1520
+ - `Transform` only as an invisible 3D null/container
1521
+ - `ScreenTransform` only as an invisible 2D null/container
1522
+ - `ScreenTransform` + `ScreenImage`
1523
+ - `ScreenTransform` + `ScreenText`
1524
+
1525
+ For mesh objects, `SceneObjectRenderer` registers the object id at runtime so
1526
+ behavior components can resolve serialized target ids. Inspector/editor changes
1527
+ must route through `onComponentChange` and replace the selected object's
1528
+ component in the store; do not keep separate page-local copies of transform,
1529
+ material, light, LookAt, or PinToMesh state.
1530
+
1531
+ In editor mode, transform changes return through `onComponentChange`.
1532
+ Unsupported compositions render nothing in production and use `fallback` in
1533
+ editor mode.
1534
+
1535
+ ---
1536
+
497
1537
  ## `<ScreenText>` — Standard screen-space text
498
1538
 
499
1539
  Import from `@vincentt-xr/sdk`. Use this when you want the standard
@@ -545,6 +1585,387 @@ Use `TextLabel` when:
545
1585
  - you want to place text manually in world space or inside a custom transform
546
1586
  - you are building a lower-level composite where another component owns layout
547
1587
 
1588
+ `SceneObjectRenderer` owns the required `ScreenSpaceUI` container when it renders
1589
+ an object composed from `ScreenTransform` + `ScreenText` or `ScreenImage`. Do not
1590
+ wrap those renderer instances in an additional `ScreenSpaceUI`; direct uses of
1591
+ `ScreenText` and `ScreenImage` still require the container shown above.
1592
+
1593
+ Debug panels are independent, selection-driven surfaces. Use `HierarchyPanel` for a
1594
+ render-group-aware scene-object list and render one
1595
+ `ObjectInspectorPanel` for the selected object. Both accept `initialPosition` so the
1596
+ hierarchy and Inspector can be presented side by side instead of overlapping.
1597
+ `ScreenTextSettingsPanel` and `ScreenImageSettingsPanel` already own their complete
1598
+ Inspector UI; pass their optional `inspector` binding when they edit an object from
1599
+ a shared scene store. Do not nest either one inside another `ObjectInspectorPanel`.
1600
+ `ObjectInspectorPanel` is uncontrolled by default, but multi-object editors can pass
1601
+ `position` and `onPositionChange` to preserve one Inspector location while swapping
1602
+ the selected object's panel. `SceneObjectRenderer` forwards screen transform-guide
1603
+ drags through `onComponentChange`; selection policies such as closing active transform
1604
+ controls belong to the parent editor, not to `HierarchyPanel` or the standalone panels.
1605
+
1606
+ ### Reusable XR scene editor debug UI
1607
+
1608
+ For temporary client-app scene setup, prefer the reusable scene editor adapters
1609
+ from `@vincentt-xr/sdk/debug-ui` instead of rewriting viewport, hierarchy, and
1610
+ inspector wiring in each app. These are debug/authoring adapters over the
1611
+ scene-object runtime, not production runtime UI.
1612
+
1613
+ ```tsx
1614
+ import {
1615
+ SceneHierarchyPanel,
1616
+ SceneInspectorPanel,
1617
+ ViewportToolbar,
1618
+ XRSceneEditorProvider,
1619
+ XRSceneViewport,
1620
+ } from "@vincentt-xr/sdk/debug-ui";
1621
+ import {
1622
+ replaceSceneComponent,
1623
+ replaceSceneObject,
1624
+ type RenderGroupSceneObjectStore,
1625
+ } from "@vincentt-xr/sdk/scene-object";
1626
+
1627
+ <XRSceneEditorProvider
1628
+ store={store}
1629
+ selectedObjectId={selectedObjectId}
1630
+ onSelectObject={setSelectedObjectId}
1631
+ onStoreChange={setStore}
1632
+ onComponentChange={(component) =>
1633
+ setStore((current) => replaceSceneComponent(current, component))
1634
+ }
1635
+ onObjectChange={(object) => setStore((current) => replaceSceneObject(current, object))}
1636
+ >
1637
+ <XRSceneViewport />
1638
+ <ViewportToolbar />
1639
+ <SceneHierarchyPanel />
1640
+ <SceneInspectorPanel />
1641
+ </XRSceneEditorProvider>;
1642
+ ```
1643
+
1644
+ All pieces are optional and composable:
1645
+
1646
+ - `XRSceneViewport` renders `XRScene`, an editor-owned camera-texture plane,
1647
+ editor grid, camera, orbit controls, and `SceneObjectRenderer` entries from
1648
+ `createRenderPlan`. The editor video plane consumes the same camera texture as
1649
+ `VideoBackground`, but it is a separate debug-ui plane with `depthTest` and
1650
+ `depthWrite` enabled. Do not add depth props to core `VideoBackground`.
1651
+ - `SceneHierarchyPanel` adapts `HierarchyPanel` to a
1652
+ `RenderGroupSceneObjectStore`.
1653
+ - `SceneInspectorPanel` renders component settings for the selected object and
1654
+ routes edits through `onComponentChange` or `onObjectChange`.
1655
+ - `ViewportToolbar` owns transient viewport controls such as orbit on/off.
1656
+
1657
+ Keep `SceneObjectStore`/`RenderGroupSceneObjectStore` as the only persistent
1658
+ scene data. Selection, orbit state, grid visibility, video-background visibility,
1659
+ and editor camera choices are transient editor session state. Do not keep
1660
+ page-local shadow copies of Transform, MeshRenderer, Light, LookAt, or PinToMesh
1661
+ settings.
1662
+
1663
+ Use these adapters in development/debug surfaces such as `/viewport-editor`
1664
+ or a temporary client-app editor. Omit them for final preview/runtime and render
1665
+ the scene through core runtime components instead.
1666
+
1667
+ For debug pages that need a camera/image/video picker, import
1668
+ `MediaSourceSwitcher` and `useFaceTrackingMediaSourceSession` from
1669
+ `@vincentt-xr/sdk/debug-ui`. `useFaceTrackingMediaSourceSession(activeMediaSource)`
1670
+ applies the selected debug media preset to the current XR session and starts
1671
+ the face-tracking media pipeline used by the debug face examples. The older
1672
+ `useMediaSourceSession(activeMediaSource, { pipeline })` name remains available
1673
+ for custom debug pipelines. Use these only in debug/example UI; production
1674
+ runtime should set the session media source directly.
1675
+
1676
+ ### Debug UI usage in Harness client apps
1677
+
1678
+ #### AI-agent integration contract
1679
+
1680
+ Choose the import door by responsibility. Do not search other doors for an
1681
+ equivalent symbol and do not import SDK source files directly.
1682
+
1683
+ | Agent task | Import door | Symbols |
1684
+ | ----------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------ |
1685
+ | Render managed objects and compose passes | `@vincentt-xr/sdk` | `SceneObjectRenderer`, `SequentialRenderPasses`, `RenderGroupLayer` |
1686
+ | Guard internal render layers | `@vincentt-xr/sdk` | `MAX_RENDER_GROUP_PASSES`, `getRenderGroupLayer`, `EXTERNAL_BACKGROUND_RENDER_LAYER` |
1687
+ | Create and mutate scene-object state | `@vincentt-xr/sdk/scene-object` | `createSceneObject`, `createDefaultComponent`, store helpers |
1688
+ | Materialize and order render groups | `@vincentt-xr/sdk/scene-object` | `migrateToRenderGroups`, `createRenderPlan`, reorder/move helpers |
1689
+ | Render DOM authoring controls | `@vincentt-xr/sdk/debug-ui` | `HierarchyPanel`, `ObjectInspectorPanel`, `ComponentEditor`, settings panels |
1690
+ | Type a bulk hierarchy export | `@vincentt-xr/sdk/debug-ui` | `HierarchyPanelCopyConfig` |
1691
+
1692
+ The integrating client, not the SDK panels, must own all mutable editor state:
1693
+
1694
+ - the complete scene store, including `objects`, `components`, `renderGroups`,
1695
+ and `renderGroupOrder`;
1696
+ - `selectedObjectId` and which selected Inspector is rendered;
1697
+ - component replacement and object/group visibility updates;
1698
+ - reorder and compatibility-checked move operations;
1699
+ - the rule that changing selection closes transform controls and guides belonging
1700
+ to the previously selected object; and
1701
+ - the `HierarchyPanelCopyConfig` value.
1702
+
1703
+ Required invariants:
1704
+
1705
+ - Mount DOM debug panels outside `XRScene`; mount R3F/Three.js renderers and
1706
+ controls inside `XRScene`.
1707
+ - Keep both branches below the same parent state and inside `XRProvider`.
1708
+ - Derive group order from `scene.renderGroupOrder`; never hardcode a canonical
1709
+ order such as background/general/tracking/foreground/post.
1710
+ - Use `SequentialRenderPasses` for guaranteed cross-group composition. Do not
1711
+ replace it with a larger Three.js `renderOrder` value.
1712
+ - Treat camera layers as internal visibility masks, not user-facing layer order.
1713
+ - Keep each managed object in exactly one materialized group and exactly once in
1714
+ that group's `objectIds`.
1715
+ - Do not show transform controls for an unselected object. Initial control state
1716
+ must agree with the initial `selectedObjectId`.
1717
+ - Do not create more than `MAX_RENDER_GROUP_PASSES` managed groups.
1718
+ - Treat Copy Config as a versioned export/debug payload, not as an import or
1719
+ persistence API.
1720
+
1721
+ Use these state transitions:
1722
+
1723
+ | Event | Required parent-state update |
1724
+ | ----------------- | -------------------------------------------------------------------------------- |
1725
+ | Select object | Set `selectedObjectId`; disable all existing 2D guides and 3D transform controls |
1726
+ | Edit component | Replace that component immutably in `scene.components` |
1727
+ | Reorder group | Call `reorderRenderGroup`; keep object lists unchanged |
1728
+ | Reorder object | Call `reorderObjectWithinGroup`; change only the selected group's `objectIds` |
1729
+ | Move object | Validate compatibility, then call `moveObjectToGroup` |
1730
+ | Toggle visibility | Update the object or group; do not encode visibility in selection state |
1731
+ | Render frame | Derive `createRenderPlan(scene)` and active pass layers from current state |
1732
+
1733
+ Failure recovery for generated integrations:
1734
+
1735
+ | Symptom | Likely cause | Required fix | Do not |
1736
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
1737
+ | “R3F hooks can only be used within Canvas” | R3F renderer/control mounted in the DOM branch, or duplicate React/R3F packages | Move the scene component inside `XRScene`; in link mode pin React, React DOM, Three.js, R3F, and Drei to client copies | Move DOM panels into `XRScene` |
1738
+ | Hierarchy order changes but visual composition does not | Only `renderOrder` or array order changed | Recompute the render plan and pass the ordered layers to `SequentialRenderPasses` | Expose camera-layer numbers as visual order |
1739
+ | Wrong object's gizmo remains visible | Selection changed without clearing component control flags | Disable all stale transform controls/guides in the same selection transition | Assume `HierarchyPanel` owns control state |
1740
+ | Screen transform drag stops after one move | An unstable callback caused active pointer listeners to be replaced | Use current SDK components and keep component replacement callbacks wired to parent state | Fork private drag logic into the DOM panel |
1741
+ | Cross-group drop silently fails | Target policy rejects the object's component composition | Call `validateGroupCompatibility` and preserve the original store on `false` | Force incompatible membership |
1742
+ | Group is omitted from the render plan | Its zero-based index is `>= MAX_RENDER_GROUP_PASSES` (`30`), so the 31st group onward is beyond the managed-pass limit | Prevent creation using `MAX_RENDER_GROUP_PASSES` | Reuse/clamp multiple groups onto one layer |
1743
+ | Copied JSON cannot be restored | Copy Config has no public importer | Treat it as export/debug handoff only | Invent an undocumented restore API |
1744
+
1745
+ Harness clients must keep the DOM authoring UI and R3F scene integration in sibling
1746
+ branches under the same parent-owned state:
1747
+
1748
+ ```text
1749
+ XRProvider
1750
+ └─ AspectRatioContainer
1751
+ ├─ XRScene (R3F Canvas)
1752
+ │ ├─ camera, lights, and scene content
1753
+ │ ├─ SequentialRenderPasses
1754
+ │ ├─ SceneObjectRenderer instances
1755
+ │ └─ transform guides and controls
1756
+ ├─ HierarchyPanel
1757
+ └─ selected-object Inspector or standalone settings panel
1758
+ ```
1759
+
1760
+ Mount `HierarchyPanel`, `ObjectInspectorPanel`, `ComponentEditor`,
1761
+ `ScreenTextSettingsPanel`, and `ScreenImageSettingsPanel` outside `XRScene`.
1762
+ They render DOM portals and communicate through props and callbacks. Mount
1763
+ `SceneObjectRenderer`, `SequentialRenderPasses`, `ScreenSpaceUI`, `Transform3D`,
1764
+ and all Three.js content inside `XRScene` because they use R3F Canvas context.
1765
+ Keep both branches inside `XRProvider`.
1766
+
1767
+ The parent Harness component owns:
1768
+
1769
+ - the complete scene-object store;
1770
+ - `selectedObjectId`;
1771
+ - component replacement;
1772
+ - group and object reorder callbacks;
1773
+ - visibility callbacks;
1774
+ - compatibility-checked moves; and
1775
+ - the bulk Copy Config value.
1776
+
1777
+ The data loop is:
1778
+
1779
+ ```text
1780
+ Hierarchy / Inspector callback
1781
+ ↓
1782
+ parent scene store
1783
+ ↓
1784
+ selection + render plan + active pass layers
1785
+ ↓
1786
+ SceneObjectRenderer inside XRScene
1787
+ ↓
1788
+ transform-control change
1789
+ └──────────────→ parent scene store
1790
+ ```
1791
+
1792
+ If React reports that R3F hooks can only be used within Canvas, a scene renderer,
1793
+ render-pass controller, transform guide, or Three.js control has crossed into the
1794
+ DOM panel branch. Do not solve this by moving the panels into `XRScene`; move the
1795
+ R3F-dependent scene component back inside `XRScene` and keep the shared state above
1796
+ both branches.
1797
+
1798
+ The working `test001` Harness client is the canonical external integration. It runs
1799
+ a Cube, Screen Image, and Screen Text together with `HierarchyPanel`,
1800
+ selection-driven Inspectors, component editors, group/object ordering, visibility,
1801
+ transform controls, sequential render passes, and bulk Copy Config. The SDK
1802
+ `/layer-panel-test` page is the matching in-repo fixture.
1803
+
1804
+ For SDK co-development, run `pnpm build:watch` in `vincentt-xr-sdk` and
1805
+ `SDK_LINK=1 pnpm dev` in the Harness client. Use a different `PORT` when the SDK
1806
+ example is already using 5173, for example
1807
+ `PORT=5174 SDK_LINK=1 pnpm dev`. Also validate the client typecheck and linked
1808
+ production build; the in-repo SDK example alone does not validate package exports
1809
+ or Canvas placement in a separate client.
1810
+
1811
+ ### Render groups and hierarchy ordering
1812
+
1813
+ Import render-group types, policies, migration, compatibility, reorder helpers, and
1814
+ `createRenderPlan` from `@vincentt-xr/sdk/scene-object`. Import
1815
+ `SceneObjectRenderer`, `SequentialRenderPasses`, `RenderGroupLayer`,
1816
+ `getRenderGroupLayer`, and `EXTERNAL_BACKGROUND_RENDER_LAYER` from core.
1817
+
1818
+ Use `migrateToRenderGroups(store, objectOrder)` to group a flat scene-object store.
1819
+ It preserves compatible `renderGroupId` assignments, infers missing or incompatible
1820
+ assignments from components, and creates only the groups required by actual scene
1821
+ objects. Their initial order follows the first occurrence in `objectOrder`; there is
1822
+ no fixed render-order list. New
1823
+ objects can call `getDefaultRenderGroupId(components)` after their components are
1824
+ assembled.
1825
+
1826
+ The ownership boundary is:
1827
+
1828
+ | Concern | Owner |
1829
+ | -------------------------------------------------- | -------------------------- |
1830
+ | Object identity, display name, and component order | `SceneObject` |
1831
+ | Object behavior and settings | `SceneComponent` |
1832
+ | Cross-domain composition and group visibility | `RenderGroup` |
1833
+ | Order inside a group | `RenderGroup.objectIds` |
1834
+ | Order between groups | `renderGroupOrder` |
1835
+ | Structural commands | `HierarchyPanel` callbacks |
1836
+ | Property editing and selection policy | Parent editor + Inspector |
1837
+ | Object-to-pass mapping | `createRenderPlan()` |
1838
+ | Final frame composition | `SequentialRenderPasses` |
1839
+
1840
+ The Hierarchy and Inspector emit commands but do not own scene state or rendering.
1841
+ Every managed object must belong to exactly one materialized group, appear exactly
1842
+ once across all group `objectIds` arrays, and have a matching `renderGroupId`.
1843
+ Selection is editor state and never changes render order.
1844
+
1845
+ Use only the immutable helpers to change structure:
1846
+
1847
+ - `reorderRenderGroup(store, groupId, nextIndex)` changes group composition order.
1848
+ - `reorderObjectWithinGroup(store, groupId, objectId, nextIndex)` sorts within a group.
1849
+ - `moveObjectToGroup(...)` moves only when `validateGroupCompatibility(...)` passes.
1850
+ - `createRenderPlan(store)` returns top-first entries and stable 1000-value sorting bands.
1851
+
1852
+ Unknown group/object IDs, incompatible targets, and reorders to the current index
1853
+ are safe no-ops that return the original store. `validateGroupCompatibility` is
1854
+ read-only and returns `false` for missing objects, missing groups, or rejected
1855
+ component compositions.
1856
+
1857
+ Pass each plan entry's `renderOrder` and `renderLayer` to `SceneObjectRenderer`.
1858
+ Mount one `SequentialRenderPasses` with the active plan layers in top-first order.
1859
+ It takes over the R3F loop, preserves color between groups, and clears depth before
1860
+ each later group. Bottom groups therefore composite in front even across Three.js's
1861
+ opaque and transparent queues. `captureFrame()` uses the same grouped renderer.
1862
+
1863
+ These mechanisms are different:
1864
+
1865
+ - `renderGroupOrder` controls cross-group sequential composition. The first group
1866
+ renders first; the last group renders last and appears in front.
1867
+ - `RenderGroup.objectIds` controls order inside one group.
1868
+ - Three.js camera layers are internal visibility masks; their numeric values do not
1869
+ define visual order.
1870
+ - `renderOrder` is a sorting value inside one render call. It cannot by itself
1871
+ impose ordering across opaque and transparent queues.
1872
+
1873
+ ```tsx
1874
+ const plan = createRenderPlan(scene);
1875
+ const passLayers = scene.renderGroupOrder.flatMap((groupId, groupIndex) =>
1876
+ scene.renderGroups[groupId]?.enabled && scene.renderGroups[groupId].objectIds.length
1877
+ ? [getRenderGroupLayer(groupIndex)]
1878
+ : [],
1879
+ );
1880
+
1881
+ <SequentialRenderPasses layers={passLayers} />;
1882
+ {
1883
+ plan.map((entry) => (
1884
+ <SceneObjectRenderer
1885
+ key={entry.objectId}
1886
+ objectId={entry.objectId}
1887
+ store={scene}
1888
+ renderOrder={entry.renderOrder}
1889
+ renderLayer={entry.renderLayer}
1890
+ />
1891
+ ));
1892
+ }
1893
+ ```
1894
+
1895
+ Layer 0 is shared across passes for lights and non-renderable scene infrastructure.
1896
+ All visible managed project content in a grouped scene must be assigned to a group
1897
+ layer. `SequentialRenderPasses` also enables its layers on the R3F interaction
1898
+ raycaster. `RenderGroupLayer` leaves Drei `TransformControls` on shared layer 0,
1899
+ so 2D guides and 3D move/rotate/scale gizmos remain draggable after render-group
1900
+ isolation.
1901
+
1902
+ For legacy flat camera backgrounds, render `VideoBackground` outside managed
1903
+ scene-object content. For the reusable scene editor, use `XRSceneViewport`; it
1904
+ owns a separate depth-writing camera-texture plane for editor-only 3D placement.
1905
+
1906
+ `HierarchyPanel` receives ordered `groups`, scene `objects`, selection callbacks,
1907
+ and separate callbacks for group reorder, within-group reorder, and explicit moves.
1908
+ It owns collapse UI and drop feedback, but the parent store owns selection, group
1909
+ visibility, compatibility validation, the Inspector, and rendering.
1910
+ Empty reserved groups are not materialized in the store or presented in the hierarchy.
1911
+ The panel is expanded by default and can fold to its title button without losing
1912
+ its position, current selection, or per-group collapse state. The normal hierarchy
1913
+ shows human-readable names only; internal domains, IDs, camera layers, and ordering
1914
+ values belong in deliberate debug tooling rather than the primary object list.
1915
+ Pass a `HierarchyPanelCopyConfig` value as `copyConfig` to show one bulk
1916
+ **Copy Config** action. The panel writes that value as formatted JSON and confirms
1917
+ with **Copied Config**. For a complete scene
1918
+ handoff, include `renderGroupOrder`, `renderGroups`, `objects`, and `components`;
1919
+ omit transient editor state such as current selection and panel position. The copied
1920
+ group order therefore contains only groups backed by current scene objects.
1921
+
1922
+ Use this versioned shape for a complete handoff:
1923
+
1924
+ ```ts
1925
+ type SceneConfigV1 = {
1926
+ kind: "scene";
1927
+ version: 1;
1928
+ renderGroupOrder: RenderGroupId[];
1929
+ renderGroups: Record<RenderGroupId, RenderGroup>;
1930
+ objects: Record<SceneObjectId, SceneObject>;
1931
+ components: Record<SceneComponentId, SceneComponent>;
1932
+ };
1933
+ ```
1934
+
1935
+ This is currently an export/debugging contract, not a guaranteed round-trip
1936
+ format: there is no public scene-config importer. Exclude selection, panel
1937
+ position, fold/collapse state, active drag state, feedback messages, and any
1938
+ flat external `VideoBackground`.
1939
+
1940
+ Current compatibility and limits:
1941
+
1942
+ | Domain | Required composition | Status |
1943
+ | ------------------ | -------------------------------------------------- | --------- |
1944
+ | General / 3D Scene | `Transform` + `MeshRenderer` | supported |
1945
+ | 2D Foreground | `ScreenTransform` + `ScreenText` or `ScreenImage` | supported |
1946
+ | Background | editor camera-texture plane or external background | partial |
1947
+ | AR Tracking | tracking scene-object components | reserved |
1948
+ | Post Effects | post-processing scene object | reserved |
1949
+
1950
+ Reserved groups are not materialized or displayed until compatible managed scene
1951
+ objects exist. Each active group adds one scene render, with a maximum of
1952
+ `MAX_RENDER_GROUP_PASSES` (currently 30) managed passes. Import that constant from
1953
+ `@vincentt-xr/sdk` when guarding group creation. `createRenderPlan` omits groups
1954
+ beyond the limit instead of throwing or assigning colliding camera layers.
1955
+ `EXTERNAL_BACKGROUND_RENDER_LAYER` remains temporary infrastructure for legacy
1956
+ external background composition. The reusable scene editor should prefer its
1957
+ own camera-texture plane instead of treating `VideoBackground` as a managed
1958
+ scene object.
1959
+
1960
+ An explicit `renderGroupId` assignment is retained only when its policy accepts the
1961
+ object's components; otherwise grouping uses the inferred compatible domain. Object
1962
+ IDs, component IDs, and component settings remain unchanged.
1963
+
1964
+ The render-group tests protect derived group order, compatible defaults, local
1965
+ object reorder, rejected move no-ops, stable render bands/layers, sequential depth
1966
+ clearing, grouped capture, and interaction across active camera layers. The
1967
+ compiling reference implementation is `/layer-panel-test` in the SDK example.
1968
+
548
1969
  ### Shared debug authoring surface for `ScreenText`
549
1970
 
550
1971
  The SDK also ships a dev-only inspector at `lib/debug-ui/screen-text-settings`
@@ -558,12 +1979,12 @@ including `name`, `kind: "screenText"`, `transform`, `content`, `style`,
558
1979
  `canvasSize`. Use that payload as the handoff from human layout tuning back to
559
1980
  code.
560
1981
 
561
- ### Shared debug layer panel
1982
+ ### Legacy debug layer panel
562
1983
 
563
1984
  The SDK ships a reusable `LayerPanel` in `lib/debug-ui/layer-panel` for
564
- Photoshop-style stack management. It is intended for dev / creator tooling
565
- only and is currently used by the template preview to manage `ScreenImage` and
566
- `ScreenText` layers. Production shipping surfaces should not include the panel.
1985
+ flat-stack compatibility. New grouped editors should use `HierarchyPanel`.
1986
+ Both are intended for dev / creator tooling only. Production shipping surfaces
1987
+ should not include either panel.
567
1988
 
568
1989
  ---
569
1990
 
@@ -663,21 +2084,28 @@ import { AspectRatioContainer } from "@vincentt-xr/sdk";
663
2084
 
664
2085
  Grab a photo of the live render or record the canvas, then save or share it.
665
2086
 
666
- ### `session.captureFrame()` + `dataURLtoFile()` — Single-shot photo
2087
+ ### `useFrameCapture()` + `dataURLtoFile()` — Single-shot photo
667
2088
 
668
- `captureFrame()` forces a fresh render and returns a `data:` URL of the current
669
- frame. `dataURLtoFile(dataUrl, filename)` turns it into a `File` for upload or the
670
- Web Share API.
2089
+ Use `useFrameCapture` for UI/effect logic. It exposes `status`, `capturePhoto`,
2090
+ `clear`, `dataUrl`, and `error`, plus `onCaptureStart`, `onCaptureComplete`, and
2091
+ `onError`. Status is `idle | capturing | completed | error`. The underlying
2092
+ capture forces a fresh render and returns a `data:` URL; `dataURLtoFile` turns
2093
+ it into a `File` for upload or the Web Share API. `capturePhoto()` resolves to
2094
+ the URL on success or `null` after setting `status: "error"` and `error` on
2095
+ failure; callers do not need a rejection handler.
671
2096
 
672
2097
  ```tsx
673
- import { dataURLtoFile } from "@vincentt-xr/sdk";
674
- import { useXRContext } from "@vincentt-xr/sdk/low-level";
2098
+ import { dataURLtoFile, useFrameCapture } from "@vincentt-xr/sdk";
675
2099
 
676
- const { session } = useXRContext();
677
- const dataUrl = session.captureFrame();
678
- const file = dataURLtoFile(dataUrl, "photo.png");
2100
+ const photo = useFrameCapture({
2101
+ onCaptureComplete: (dataUrl) => share(dataURLtoFile(dataUrl, "photo.png")),
2102
+ });
2103
+ await photo.capturePhoto();
679
2104
  ```
680
2105
 
2106
+ For synchronous low-level code, `session.captureFrame()` remains available via
2107
+ `useXRContext` from `@vincentt-xr/sdk/low-level`.
2108
+
681
2109
  ### `useMediaRecorder()` — Record the canvas to a video Blob
682
2110
 
683
2111
  Records a `MediaStream` (from the canvas's `captureStream()`) to a `Blob`. Picks
@@ -694,6 +2122,20 @@ await rec.start(canvas.captureStream(30));
694
2122
  rec.stop(); // -> rec.recordedBlob is the video Blob
695
2123
  ```
696
2124
 
2125
+ Use `rec.status` as the source of truth:
2126
+ `idle | starting | recording | paused | stopping | completed | error`.
2127
+ Controls are `start`, `pause`, `resume`, `stop`, and `clear`. Events are
2128
+ `onRecordingStart`, `onRecordingPause`, `onRecordingResume`,
2129
+ `onRecordingStop`, `onRecordingComplete`, and `onError`. Keep `isRecording`
2130
+ and `isPaused` only as convenience booleans; do not invent parallel state.
2131
+ When `passThroughStream` is configured, call `start()` without an argument;
2132
+ otherwise call `start(stream)` with the canvas capture stream.
2133
+ The verified `/capture-recording` example exercises the complete photo and video
2134
+ lifecycle against the live XR canvas. It displays a recording HUD and elapsed
2135
+ time for `starting`, `recording`, `paused`, `stopping`, and `completed`, then
2136
+ previews the resulting `recordedBlob` in a video player. Photo and recording
2137
+ flows were verified with a real webcam on 26 July 2026.
2138
+
697
2139
  ---
698
2140
 
699
2141
  ## Audio
@@ -753,3 +2195,154 @@ function SoundToggle() {
753
2195
  Audio assets: m4a (AAC). A rare app needing a second, independent mixer can
754
2196
  `new AudioEngine()` and drive it directly; the free functions always target the
755
2197
  one default engine.
2198
+
2199
+ ## 3D behavior components: LookAt and PinToMesh
2200
+
2201
+ `LookAt` and `PinToMesh` are partial-stage XRScene behavior components. They are
2202
+ exported for editor/API prototyping, but they are not yet a fully verified
2203
+ Effect House parity surface.
2204
+
2205
+ `LookAt` rotates its child toward a target object resolved either from a direct
2206
+ `Object3D` prop or from serialized scene-object target ids. Treat it as suitable
2207
+ for current editor experiments, not final production behavior, until point vs
2208
+ direction mode, axis conventions, offset ordering, and missing-target behavior
2209
+ are covered by tests.
2210
+
2211
+ When `SceneObjectRenderer` renders nested objects, child scene objects do not
2212
+ inherit the parent's LookAt reorientation by default. `LookAtSettings` includes
2213
+ `inheritChildren: boolean` for the special case where children should rigidly
2214
+ follow the parent's look-at facing. Defaults from
2215
+ `createDefaultComponent("LookAt", objectId)` set `inheritChildren: false`.
2216
+
2217
+ `PinToMesh` wraps a source object and pins that source wrapper to a target mesh
2218
+ surface. Pass `PinToMeshSettings` with
2219
+ `enabled: true` and a non-null `targetObjectId`; default settings are disabled
2220
+ with no target. The component supports static mesh surface lookup, UV input,
2221
+ orientation, vertex-normal option, offset space, offset position, and offset
2222
+ rotation. Offset position and offset rotation default to `{ x: 0, y: 0, z: 0 }`.
2223
+ When PinToMesh is disabled or the target becomes unavailable, the runtime pin
2224
+ wrapper resets to local position `0,0,0` and identity rotation so the child
2225
+ `Transform3D` remains the authored source of truth.
2226
+
2227
+ Pin wrapper rotation is identity-based for `orientation: "none"` and
2228
+ `orientation: "position"`. `orientation: "positionAndDirection"` aligns the
2229
+ wrapper to the target surface normal before offset rotation is applied. Offset
2230
+ rotation is reapplied from a clean base each frame and must not compound over
2231
+ time.
2232
+
2233
+ Use `PinToMeshSettingsPanel` from `@vincentt-xr/sdk/debug-ui` for editor
2234
+ controls. The panel uses Transform-style text number inputs for UV and offsets:
2235
+ visible zero defaults, decimal validation, 0.1 arrow-step increments, Shift for
2236
+ larger steps, and Alt for finer steps. Serialized scenes should store stable
2237
+ scene-object ids for targets; runtime renderers resolve those ids to registered
2238
+ `Object3D` instances.
2239
+
2240
+ Direct runtime usage:
2241
+
2242
+ ```tsx
2243
+ import { LookAt, PinToMesh, Transform3D, MeshRenderer } from "@vincentt-xr/sdk";
2244
+
2245
+ <PinToMesh
2246
+ target={targetObject}
2247
+ settings={{
2248
+ enabled: true,
2249
+ targetObjectId: "base-plane",
2250
+ orientation: "positionAndDirection",
2251
+ uv: { x: 0.5, y: 0.5 },
2252
+ vertexNormal: true,
2253
+ offsetSpace: "local",
2254
+ offsetPosition: { x: 0, y: 0.25, z: 0 },
2255
+ offsetRotation: { x: 0, y: 0, z: 0 },
2256
+ }}
2257
+ >
2258
+ <Transform3D value={transformSettings}>
2259
+ <LookAt
2260
+ target={lookTargetObject}
2261
+ settings={{
2262
+ enabled: true,
2263
+ targetObjectId: "target-object",
2264
+ mode: "point",
2265
+ offsetRotation: { x: 0, y: 0, z: 0 },
2266
+ }}
2267
+ >
2268
+ <MeshRenderer value={meshSettings} />
2269
+ </LookAt>
2270
+ </Transform3D>
2271
+ </PinToMesh>;
2272
+ ```
2273
+
2274
+ Serialized component defaults:
2275
+
2276
+ - `createDefaultComponent("LookAt", objectId)` returns disabled LookAt settings
2277
+ with `targetObjectId: null`, `mode: "point"`, and zero offset rotation.
2278
+ - `createDefaultComponent("PinToMesh", objectId)` returns disabled PinToMesh
2279
+ settings with `targetObjectId: null`, UV `{ x: 0.5, y: 0.5 }`,
2280
+ `orientation: "none"`, `vertexNormal: false`, `offsetSpace: "local"`, and
2281
+ zero position/rotation offsets.
2282
+
2283
+ Current limitations:
2284
+
2285
+ - UV behavior is not final. Missing UVs currently fall back to target bounds
2286
+ center behavior instead of a visible invalid-target state.
2287
+ - LookAt has a minimal API: `enabled`, `targetObjectId`, `mode`, and
2288
+ `offsetRotation`. Axis remapping and world-up controls are still planned.
2289
+ - Runtime target resolution is scoped by `XRProvider` through an internal
2290
+ runtime-object registry. Direct calls to `registerRuntimeObject` and
2291
+ `resolveRuntimeObject` outside an `XRProvider` use a legacy fallback registry
2292
+ and should be avoided for multi-scene pages.
2293
+
2294
+ ## 3D light scene objects
2295
+
2296
+ `LightObject` renders a serialized light component through `Transform3D`.
2297
+ Import from `@vincentt-xr/sdk`.
2298
+
2299
+ ```tsx
2300
+ import { LightObject } from "@vincentt-xr/sdk";
2301
+
2302
+ <LightObject
2303
+ transform={transformSettings}
2304
+ settings={{
2305
+ enabled: true,
2306
+ type: "directional",
2307
+ color: "#ffffff",
2308
+ intensity: 2.2,
2309
+ castShadow: true,
2310
+ distance: 10,
2311
+ angle: Math.PI / 6,
2312
+ penumbra: 0.2,
2313
+ decay: 2,
2314
+ showHelper: false,
2315
+ }}
2316
+ />;
2317
+ ```
2318
+
2319
+ `LightSettings.type` is `"ambient" | "directional" | "point" | "spot"`.
2320
+ Directional, point, and spot lights support `castShadow`; ambient lights only
2321
+ contribute global light. `showHelper` draws a small non-physical editor marker
2322
+ for the light object and defaults to `false`; enable it only in editor/debug
2323
+ surfaces.
2324
+
2325
+ For an Effect House-style declarative player, use `<AudioPlayer>`. It supports
2326
+ `once`, a finite `loop` count, and `infinite` playback, plus autoplay intent,
2327
+ per-player mute, and a clamped 0–100 volume. Browser autoplay policy still
2328
+ applies: audible autoplay may reject until a user gesture, surfaced through
2329
+ `onError`. The component is headless (it renders a hidden `<audio>`); attach a
2330
+ ref for custom controls.
2331
+
2332
+ ```tsx
2333
+ import { AudioPlayer, type AudioPlayerHandle } from "@vincentt-xr/sdk";
2334
+
2335
+ const player = useRef<AudioPlayerHandle>(null);
2336
+
2337
+ <AudioPlayer
2338
+ ref={player}
2339
+ src={musicUrl}
2340
+ playMode="loop"
2341
+ loopCount={3}
2342
+ autoplay
2343
+ volume={80}
2344
+ onError={(error) => console.warn(error)}
2345
+ />;
2346
+
2347
+ // From a user gesture: player.current?.play(), .pause(), or .stop().
2348
+ ```