@vincentt-xr/sdk 2.0.0-alpha.7 → 2.0.0-alpha.8

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 (275) hide show
  1. package/GROUNDING.md +1109 -13
  2. package/dist/{DebuggerUI-BjOMr8q_.js → DebuggerUI-C-ldiozL.js} +1 -1
  3. package/dist/{DebuggerUI-B6bP6P9r.cjs → DebuggerUI-CWX5EtQv.cjs} +1 -1
  4. package/dist/FaceMeshRenderer-BEEXZwS6.cjs +158 -0
  5. package/dist/FaceMeshRenderer-UuH5sTXG.js +1322 -0
  6. package/dist/FaceRetouch-C7h7x9kd.js +683 -0
  7. package/dist/FaceRetouch-DqftT26Y.cjs +134 -0
  8. package/dist/GLTFLoader-C5q63sKO.js +1633 -0
  9. package/dist/GLTFLoader-Cb_CRwmG.cjs +1 -0
  10. package/dist/SegmentationOverlay-DQuW3dsc.js +133 -0
  11. package/dist/SegmentationOverlay-aWVod8Kl.cjs +1 -0
  12. package/dist/assets/cache.d.ts +99 -0
  13. package/dist/assets/estimate.d.ts +11 -0
  14. package/dist/assets/index.d.ts +7 -0
  15. package/dist/assets/loaders.d.ts +28 -0
  16. package/dist/assets/react.d.ts +33 -0
  17. package/dist/calibration/calibration.d.ts +70 -0
  18. package/dist/calibration/index.d.ts +6 -0
  19. package/dist/calibration/storage.d.ts +13 -0
  20. package/dist/calibration/types.d.ts +53 -0
  21. package/dist/calibration/useXRCalibration.d.ts +46 -0
  22. package/dist/camera/CameraPermissionGate.d.ts +26 -0
  23. package/dist/camera/devices.d.ts +26 -0
  24. package/dist/camera/index.d.ts +8 -0
  25. package/dist/camera/permission.d.ts +86 -0
  26. package/dist/camera/useCameraPermission.d.ts +35 -0
  27. package/dist/capabilities/detect.d.ts +45 -0
  28. package/dist/capabilities/index.d.ts +5 -0
  29. package/dist/capabilities/types.d.ts +120 -0
  30. package/dist/capabilities/useXRCapabilities.d.ts +18 -0
  31. package/dist/compat/environment.d.ts +5 -0
  32. package/dist/compat/index.d.ts +4 -0
  33. package/dist/compat/run.d.ts +18 -0
  34. package/dist/compat/scenarios.d.ts +10 -0
  35. package/dist/compat/types.d.ts +101 -0
  36. package/dist/components/BodyTracker/index.d.ts +9 -0
  37. package/dist/components/FaceMesh/canonical/CanonicalFaceMeshRenderer.d.ts +33 -3
  38. package/dist/components/FaceMesh/canonical/parseCanonicalFaceModel.d.ts +12 -0
  39. package/dist/components/FaceMesh/canonical/shaders.d.ts +1 -1
  40. package/dist/components/FaceMesh/index.d.ts +1 -1
  41. package/dist/components/GestureListener/index.d.ts +11 -0
  42. package/dist/components/HandTracker/index.d.ts +10 -0
  43. package/dist/components/HolisticTracker/index.d.ts +69 -0
  44. package/dist/components/HolisticTracker/settings.d.ts +58 -0
  45. package/dist/components/HolisticTracker/signals.d.ts +49 -0
  46. package/dist/components/IrisEffect/IrisMaterial.d.ts +73 -0
  47. package/dist/components/IrisEffect/index.d.ts +51 -0
  48. package/dist/components/IrisEffect/settings.d.ts +117 -0
  49. package/dist/components/LipEffect/LipEffect.d.ts +7 -1
  50. package/dist/components/LipEffect/LipEffectMaterial.d.ts +43 -7
  51. package/dist/components/LipEffect/LipEffects.d.ts +9 -0
  52. package/dist/components/LipEffect/defaults.d.ts +27 -4
  53. package/dist/components/LipEffect/index.d.ts +11 -3
  54. package/dist/components/LipEffect/settings.d.ts +271 -0
  55. package/dist/components/LipEffect/types.d.ts +56 -9
  56. package/dist/components/LipEffect/useLipDesignTextures.d.ts +11 -0
  57. package/dist/components/LipEffect/uvGuide.d.ts +59 -0
  58. package/dist/components/PersonCutout/index.d.ts +50 -0
  59. package/dist/components/PersonCutout/personCutout.d.ts +16 -0
  60. package/dist/components/ScreenImage/components/ScreenImageBody.d.ts +1 -1
  61. package/dist/components/ScreenImage/index.d.ts +3 -1
  62. package/dist/components/ScreenImage/materials/ScreenImageBlendMaterial.d.ts +1 -1
  63. package/dist/components/ScreenImage/materials/ScreenImageMaskMaterial.d.ts +11 -0
  64. package/dist/components/ScreenImage/source.d.ts +6 -0
  65. package/dist/components/ScreenImage/types.d.ts +18 -0
  66. package/dist/components/SegmentationOverlay/SegmentationOverlay.d.ts +37 -0
  67. package/dist/components/SegmentationOverlay/SegmentationOverlayMaterial.d.ts +31 -0
  68. package/dist/components/SegmentationOverlay/index.d.ts +9 -0
  69. package/dist/components/SegmentationOverlay/maskTexture.d.ts +23 -0
  70. package/dist/components/SegmentationOverlay/providers.d.ts +72 -0
  71. package/dist/components/SegmentationOverlay/settings.d.ts +65 -0
  72. package/dist/components/XRProvider/index.d.ts +16 -2
  73. package/dist/components/XRViewport/hooks/useViewportExternalFaces.d.ts +1 -1
  74. package/dist/components/XRViewport/hooks/useViewportMediaSource.d.ts +1 -1
  75. package/dist/components/XRViewport/hooks/useViewportPipeline.d.ts +1 -1
  76. package/dist/components/index.d.ts +3 -0
  77. package/dist/constants/index.d.ts +4 -1
  78. package/dist/contexts-CO0sd4JQ.cjs +1 -0
  79. package/dist/{contexts-ByhzppYl.js → contexts-FzlnnMYD.js} +252 -246
  80. package/dist/coordinates/feed.d.ts +52 -0
  81. package/dist/coordinates/index.d.ts +5 -0
  82. package/dist/coordinates/spaces.d.ts +49 -0
  83. package/dist/coordinates/viewport.d.ts +22 -0
  84. package/dist/coordinates/world.d.ts +15 -0
  85. package/dist/debug-ui/gesture-settings/index.d.ts +14 -0
  86. package/dist/debug-ui/holistic-tracker-settings/index.d.ts +16 -0
  87. package/dist/debug-ui/index.d.ts +14 -0
  88. package/dist/debug-ui/iris-effect-settings/index.d.ts +12 -0
  89. package/dist/debug-ui/lip-effect-settings/index.d.ts +15 -0
  90. package/dist/debug-ui/media-source.cjs +1 -1
  91. package/dist/debug-ui/media-source.js +1 -1
  92. package/dist/debug-ui/product-hotspots/index.d.ts +24 -0
  93. package/dist/debug-ui/segmentation-overlay-settings/index.d.ts +12 -0
  94. package/dist/debug-ui/xr-diagnostics/index.d.ts +25 -0
  95. package/dist/debug-ui.cjs +3 -3
  96. package/dist/debug-ui.js +2057 -1687
  97. package/dist/detector-K8aej4MB.js +59 -0
  98. package/dist/detector-ijypGtse.cjs +1 -0
  99. package/dist/entries/core.d.ts +18 -1
  100. package/dist/entries/face-effects.d.ts +11 -3
  101. package/dist/entries/low-level.d.ts +22 -3
  102. package/dist/entries/product.d.ts +11 -0
  103. package/dist/entries/tracking.d.ts +24 -1
  104. package/dist/face-effects.cjs +155 -1
  105. package/dist/face-effects.js +1134 -2197
  106. package/dist/gestures/definitions.d.ts +85 -0
  107. package/dist/gestures/detector.d.ts +87 -0
  108. package/dist/gestures/index.d.ts +8 -0
  109. package/dist/gestures/measures.d.ts +101 -0
  110. package/dist/gestures/settings.d.ts +49 -0
  111. package/dist/{getFaceInfo-C4QpCQIY.js → getFaceInfo-BkZfaQs-.js} +5 -5
  112. package/dist/{getFaceInfo-fA1bFBub.cjs → getFaceInfo-ahPxQFGu.cjs} +1 -1
  113. package/dist/hooks/selectors.d.ts +68 -1
  114. package/dist/hooks/useCalibrationFaceTracker.d.ts +1 -1
  115. package/dist/hooks/useFaceInfo.d.ts +13 -1
  116. package/dist/hooks/useFaceInfos.d.ts +1 -1
  117. package/dist/hooks/useGesture.d.ts +32 -0
  118. package/dist/hooks/useIrisTracking.d.ts +45 -0
  119. package/dist/hooks/useXRDiagnostics.d.ts +81 -0
  120. package/dist/hooks/useXRModel.d.ts +7 -0
  121. package/dist/hooks/useXRSession/facade.d.ts +42 -2
  122. package/dist/hooks/useXRSession/index.d.ts +2 -2
  123. package/dist/hooks/useXRSession/reason.d.ts +3 -1
  124. package/dist/hooks/useXRSession/registry.d.ts +14 -0
  125. package/dist/hooks/useXRSession/store.d.ts +52 -3
  126. package/dist/index--RgGzZ6X.cjs +47 -0
  127. package/dist/{index-VJ0SJwmG.js → index-CHVbH0hz.js} +4 -4
  128. package/dist/index-CeVkgsag.cjs +1 -0
  129. package/dist/index-CnGox6Xr.js +3054 -0
  130. package/dist/{index-ByoNj0QP.js → index-CsUdQ-pn.js} +113 -111
  131. package/dist/{index-BjmTe9eM.cjs → index-DKBAh6hV.cjs} +1 -1
  132. package/dist/index-DsZwTfG0.cjs +1 -0
  133. package/dist/index-Dw6tj2Mi.js +144 -0
  134. package/dist/index-OCPjOaeP.cjs +1 -0
  135. package/dist/{index-DXT3WccE.js → index-s9pF4zkG.js} +1065 -1101
  136. package/dist/internal-BYza1h8_.cjs +1 -0
  137. package/dist/{internal-Dmk1cTXq.js → internal-vsfNQycj.js} +15 -16
  138. package/dist/internal.cjs +1 -1
  139. package/dist/internal.js +2 -2
  140. package/dist/iris/compute.d.ts +25 -0
  141. package/dist/iris/index.d.ts +4 -0
  142. package/dist/iris/lead.d.ts +19 -0
  143. package/dist/iris/types.d.ts +87 -0
  144. package/dist/landmarkMask-CbTFzBYG.js +224 -0
  145. package/dist/landmarkMask-u7es2k4X.cjs +1 -0
  146. package/dist/low-level.cjs +1 -1
  147. package/dist/low-level.js +1085 -634
  148. package/dist/main.cjs +1 -1
  149. package/dist/main.js +1519 -465
  150. package/dist/measures-9dQPdw96.js +154 -0
  151. package/dist/measures-KfJzpvod.cjs +1 -0
  152. package/dist/models/Base/index.d.ts +49 -0
  153. package/dist/models/Body/index.d.ts +1 -1
  154. package/dist/models/Face/index.d.ts +1 -1
  155. package/dist/models/Hand/GestureTracking.d.ts +1 -1
  156. package/dist/models/Hand/HandTracking.d.ts +1 -1
  157. package/dist/models/Holistic/index.d.ts +49 -0
  158. package/dist/models/Holistic/result.d.ts +68 -0
  159. package/dist/models/SemanticSegmentation/index.d.ts +31 -0
  160. package/dist/models/SemanticSegmentation/presets.d.ts +28 -0
  161. package/dist/models/index.d.ts +2 -0
  162. package/dist/product/ProductDimensions.d.ts +16 -0
  163. package/dist/product/ProductHotspot.d.ts +16 -0
  164. package/dist/product/ProductViewer.d.ts +9 -0
  165. package/dist/product/details.d.ts +53 -0
  166. package/dist/product/index.d.ts +14 -0
  167. package/dist/product/loadModelViewer.d.ts +4 -0
  168. package/dist/product/types.d.ts +71 -0
  169. package/dist/product/useProductAr.d.ts +37 -0
  170. package/dist/product/useProductDetails.d.ts +60 -0
  171. package/dist/product.cjs +1 -0
  172. package/dist/product.js +414 -0
  173. package/dist/providers-C0Dlhh9Z.js +365 -0
  174. package/dist/providers-YNwkmNeK.cjs +62 -0
  175. package/dist/quality/XRQualityController.d.ts +25 -0
  176. package/dist/quality/controller.d.ts +33 -0
  177. package/dist/quality/index.d.ts +6 -0
  178. package/dist/quality/types.d.ts +100 -0
  179. package/dist/quality/useXRQuality.d.ts +28 -0
  180. package/dist/{renderGroups-KZk6eRiY.js → renderGroups-CN1cGFUo.js} +1 -1
  181. package/dist/{renderGroups-80H3DpTl.cjs → renderGroups-O4KmaMl9.cjs} +1 -1
  182. package/dist/renderPasses-Cg-zqmEz.cjs +21 -0
  183. package/dist/renderPasses-NnKZVfBy.js +1460 -0
  184. package/dist/scene-object.cjs +1 -1
  185. package/dist/scene-object.js +2 -2
  186. package/dist/screenTransform-BenfCtZt.js +44 -0
  187. package/dist/screenTransform-BiXdpVf0.cjs +1 -0
  188. package/dist/segmentation-9PhSvc-R.cjs +1 -0
  189. package/dist/segmentation-CeOTKemr.js +5 -0
  190. package/dist/segmentation2d/faceMemory.d.ts +28 -0
  191. package/dist/segmentation2d/faceRegions.d.ts +24 -0
  192. package/dist/segmentation2d/index.d.ts +9 -0
  193. package/dist/segmentation2d/maskUv.d.ts +23 -0
  194. package/dist/segmentation2d/rasterize.d.ts +14 -0
  195. package/dist/segmentation2d/types.d.ts +46 -0
  196. package/dist/selectors-C5G1PMYL.js +195 -0
  197. package/dist/selectors-Du_PsVnW.cjs +1 -0
  198. package/dist/{session-CpkiK5YA.cjs → session-BBr0yN8h.cjs} +7 -7
  199. package/dist/{session-DLFN_VII.js → session-Cy0m7l7-.js} +76 -76
  200. package/dist/settings-DRR7-snV.cjs +1 -0
  201. package/dist/settings-aQk845NJ.js +73 -0
  202. package/dist/subjects/index.d.ts +2 -0
  203. package/dist/subjects/select.d.ts +62 -0
  204. package/dist/subjects/types.d.ts +55 -0
  205. package/dist/tracking-config/apply.d.ts +57 -0
  206. package/dist/tracking-config/index.d.ts +7 -0
  207. package/dist/tracking-config/resolve.d.ts +25 -0
  208. package/dist/tracking-config/storage.d.ts +8 -0
  209. package/dist/tracking-config/types.d.ts +106 -0
  210. package/dist/tracking-config/useXRTrackingConfig.d.ts +38 -0
  211. package/dist/tracking.cjs +1 -1
  212. package/dist/tracking.js +1056 -383
  213. package/dist/types/compositor.d.ts +21 -0
  214. package/dist/types/holistic.d.ts +121 -0
  215. package/dist/types/index.d.ts +1 -0
  216. package/dist/types/model.d.ts +22 -2
  217. package/dist/types/nodes.d.ts +27 -2
  218. package/dist/types/segmentation.d.ts +19 -0
  219. package/dist/types/session.d.ts +41 -3
  220. package/dist/useFaceInfo-Blf7FCu3.js +303 -0
  221. package/dist/useFaceInfo-C5mO8NQY.cjs +1 -0
  222. package/dist/useXRCapabilities-B1R2ghy-.cjs +1 -0
  223. package/dist/useXRCapabilities-C68v__wB.js +30 -0
  224. package/dist/utils/fns.d.ts +6 -3
  225. package/dist/utils/integrity.d.ts +4 -1
  226. package/dist/utils/updateThrottle.d.ts +2 -0
  227. package/dist/utils/useStableUniforms.d.ts +18 -0
  228. package/docs/api/adaptive-quality.mdx +104 -0
  229. package/docs/api/asset-lifecycle.mdx +78 -0
  230. package/docs/api/camera-permission.mdx +142 -0
  231. package/docs/api/coordinate-spaces.mdx +165 -0
  232. package/docs/api/cross-camera.mdx +88 -0
  233. package/docs/api/diagnostics-calibration.mdx +97 -0
  234. package/docs/api/face-effects.mdx +6 -0
  235. package/docs/api/face-mesh.mdx +4 -2
  236. package/docs/api/gestures.mdx +173 -0
  237. package/docs/api/holistic-tracker.mdx +159 -0
  238. package/docs/api/iris-tracking.mdx +242 -0
  239. package/docs/api/multi-subject.mdx +95 -0
  240. package/docs/api/person-cutout.mdx +136 -0
  241. package/docs/api/product-ar.mdx +111 -0
  242. package/docs/api/product-details.mdx +78 -0
  243. package/docs/api/screen-space-coordinates.mdx +4 -1
  244. package/docs/api/segmentation-overlay.mdx +301 -0
  245. package/docs/api/session-lifecycle.mdx +136 -0
  246. package/docs/api/trackers.mdx +5 -0
  247. package/docs/api/tracking-config.mdx +108 -0
  248. package/docs/docs.json +23 -1
  249. package/docs/examples/lip-shaders.mdx +224 -0
  250. package/docs/guides/lip-effect-textures.mdx +292 -0
  251. package/docs/guides/lip-uv-template.png +0 -0
  252. package/docs/guides/lip-uv-template.svg +1 -0
  253. package/package.json +13 -1
  254. package/dist/FaceMeshRenderer-C5l6VLdO.cjs +0 -155
  255. package/dist/FaceMeshRenderer-wjAHrJzP.js +0 -1270
  256. package/dist/FaceRetouch-BkJzoA79.cjs +0 -134
  257. package/dist/FaceRetouch-DwbKQJAN.js +0 -551
  258. package/dist/contexts-Dr62It_K.cjs +0 -1
  259. package/dist/index-9dlAQAhy.js +0 -1462
  260. package/dist/index-BpEf2BBb.cjs +0 -1
  261. package/dist/index-CyUUurQT.cjs +0 -1
  262. package/dist/index-DCiMbxC3.cjs +0 -1
  263. package/dist/index-DpI-qo5X.cjs +0 -17
  264. package/dist/index-DyVF4vEW.js +0 -111
  265. package/dist/internal-Cq-TEQ5Y.cjs +0 -1
  266. package/dist/renderLayers-lDtL0_43.cjs +0 -1
  267. package/dist/renderLayers-zrK2pv5O.js +0 -13
  268. package/dist/renderPasses-BOoBSr8z.cjs +0 -21
  269. package/dist/renderPasses-BcquyVtu.js +0 -1500
  270. package/dist/selectors-B8H97CKA.js +0 -59
  271. package/dist/selectors-CZLKT2Iv.cjs +0 -1
  272. package/dist/useFaceInfo-BJ8-_MOs.cjs +0 -1
  273. package/dist/useFaceInfo-Dk426LTu.js +0 -62
  274. package/dist/useFaceResults-CtetsO9M.js +0 -19
  275. package/dist/useFaceResults-wBJF3zyp.cjs +0 -1
@@ -0,0 +1 @@
1
+ "use strict";const e=require("react"),C=require("./index-CeVkgsag.cjs"),S=(s={})=>{const{enabled:c=!0,timeoutMs:i,queryPermission:l}=s,f=(s.models??[]).join("|"),a=e.useRef(s.models);a.current=s.models;const[m,b]=e.useState(null),[R,n]=e.useState("idle"),[y,o]=e.useState(null),t=e.useRef(0),u=e.useCallback(async()=>{t.current+=1;const d=t.current;n("checking");try{const r=await C.detectXRCapabilities({models:a.current,timeoutMs:i,queryPermission:l});return d!==t.current?null:(b(r),o(null),n("ready"),r)}catch(r){return d!==t.current||(o(r instanceof Error?r:new Error(String(r))),n("error")),null}},[i,l]);return e.useEffect(()=>{if(c)return u(),()=>{t.current+=1}},[c,u,f]),{capabilities:m,status:R,error:y,refresh:u}};exports.useXRCapabilities=S;
@@ -0,0 +1,30 @@
1
+ import { useRef as m, useState as u, useCallback as E, useEffect as R } from "react";
2
+ import { d as g } from "./index-CnGox6Xr.js";
3
+ const k = (t = {}) => {
4
+ const { enabled: c = !0, timeoutMs: o, queryPermission: l } = t, f = (t.models ?? []).join("|"), i = m(t.models);
5
+ i.current = t.models;
6
+ const [b, p] = u(null), [y, n] = u("idle"), [C, a] = u(null), e = m(0), s = E(async () => {
7
+ e.current += 1;
8
+ const d = e.current;
9
+ n("checking");
10
+ try {
11
+ const r = await g({
12
+ models: i.current,
13
+ timeoutMs: o,
14
+ queryPermission: l
15
+ });
16
+ return d !== e.current ? null : (p(r), a(null), n("ready"), r);
17
+ } catch (r) {
18
+ return d !== e.current || (a(r instanceof Error ? r : new Error(String(r))), n("error")), null;
19
+ }
20
+ }, [o, l]);
21
+ return R(() => {
22
+ if (c)
23
+ return s(), () => {
24
+ e.current += 1;
25
+ };
26
+ }, [c, s, f]), { capabilities: b, status: y, error: C, refresh: s };
27
+ };
28
+ export {
29
+ k as u
30
+ };
@@ -14,11 +14,11 @@ export declare const drawImageDataCover: (canvas: HTMLCanvasElement, input: Imag
14
14
  * trackers call this once per landmark per frame (~468× for face), so the
15
15
  * allocating variant below shows up as a real GC pressure source.
16
16
  */
17
- export declare const unprojectToZZeroInto: ({ x, y, z }: NormalizedLandmark, camera: THREE.Camera, out: THREE.Vector3) => THREE.Vector3;
17
+ export declare const unprojectToZZeroInto: (landmark: NormalizedLandmark, camera: THREE.Camera, out: THREE.Vector3) => THREE.Vector3;
18
18
  /**
19
19
  * Allocating wrapper kept for backwards compatibility with any
20
- * consumer-facing API surface. New call sites in the trackers use
21
- * unprojectToZZeroInto with a pre-allocated out vector.
20
+ * consumer-facing API surface. New call sites use `feedToWorld` from
21
+ * `lib/coordinates` (or `unprojectToZZeroInto` with a pre-allocated out).
22
22
  */
23
23
  export declare const unprojectToZZero: (landmark: NormalizedLandmark, camera: THREE.Camera) => THREE.Vector3;
24
24
  export declare const smoothScale: (originalScale: number, newScale: number, alpha?: number) => number;
@@ -37,7 +37,10 @@ export declare const smoothScale: (originalScale: number, newScale: number, alph
37
37
  export declare class AnalysisDownscaler {
38
38
  private readonly maxDimension;
39
39
  private canvas;
40
+ private scale;
40
41
  constructor(maxDimension: number);
42
+ /** Multiply the cap (0.25..1); the adaptive-quality tier sets it. */
43
+ setScale(scale: number): void;
41
44
  /**
42
45
  * Returns a downscaled copy of `source` capped to `maxDimension` on its
43
46
  * longest side (or `source` unchanged if it's already within the cap, or if a
@@ -14,7 +14,10 @@ export declare const INTEGRITY_MANIFEST: Record<string, IntegrityHash>;
14
14
  * Consumer-owned URLs with no manifest entry pass through with a warning.
15
15
  * SDK-owned CDN URLs with no entry fail closed.
16
16
  */
17
- export declare function fetchAndVerify(url: string): Promise<ArrayBuffer>;
17
+ export declare function fetchAndVerify(url: string, init?: {
18
+ signal?: AbortSignal;
19
+ }): Promise<ArrayBuffer>;
20
+ export declare function isSdkOwnedUrl(url: string): boolean;
18
21
  /**
19
22
  * fetchAndVerify variant that returns a Uint8Array view — what MediaPipe's
20
23
  * `modelAssetBuffer` expects.
@@ -0,0 +1,2 @@
1
+ /** True when a per-frame update should be skipped to honour a `maxFps` cap. */
2
+ export declare const shouldSkipUpdate: (elapsed: number, lastUpdate: number, maxFps: number | undefined) => boolean;
@@ -0,0 +1,18 @@
1
+ export type StableUniforms<T extends Record<string, unknown>> = {
2
+ [K in keyof T]: {
3
+ value: T[K];
4
+ };
5
+ };
6
+ /**
7
+ * Build a `ShaderMaterial` uniforms object whose entries never change identity.
8
+ *
9
+ * three.js captures `material.uniforms` once, when the program is compiled,
10
+ * and keeps reading that object on every draw. A uniforms object re-created
11
+ * on a React render (a plain literal, or a `useMemo` with dependencies) is
12
+ * therefore silently ignored after the first frame unless the material is
13
+ * re-mounted. This hook allocates each `{ value }` entry once and copies the
14
+ * latest values into them on every render, so prop changes reach the shader.
15
+ *
16
+ * The set of keys must be the same on every render.
17
+ */
18
+ export declare const useStableUniforms: <T extends Record<string, unknown>>(values: T) => StableUniforms<T>;
@@ -0,0 +1,104 @@
1
+ ---
2
+ title: "Adaptive Quality & Performance Budget"
3
+ description: "An opt-out performance budget that measures frame time, inference time, memory and device signals, then steps tracker fps, analysis resolution, render pixel ratio and camera resolution between declared tiers; bounded, observable, reversible."
4
+ sidebarTitle: "Adaptive Quality"
5
+ icon: "gauge"
6
+ ---
7
+
8
+ Phones differ by an order of magnitude and heat up over a session. Instead
9
+ of hand-tuning `perf.targetFps`, declare quality tiers and a budget; the
10
+ controller moves between the tiers and tells you why.
11
+
12
+ ```tsx
13
+ import { XRProvider, useXRQuality } from "@vincentt-xr/sdk";
14
+
15
+ // On by default. Customise, pin, or turn it off:
16
+ <XRProvider quality={{ budget: { maxFrameMs: 33 }, minTier: "medium" }}>…</XRProvider>;
17
+ <XRProvider quality={{ adaptive: false, initialTier: "medium" }}>…</XRProvider>;
18
+ <XRProvider quality={false}>…</XRProvider>;
19
+
20
+ const { tier, decision, signals, setTier, lock, unlock, reset } = useXRQuality();
21
+ ```
22
+
23
+ ## Tiers and budget (serializable)
24
+
25
+ ```ts
26
+ const DEFAULT_QUALITY_TIERS = [
27
+ { name: "high", targetFps: 30, renderScale: 1, analysisScale: 1, camRes: 960 },
28
+ { name: "medium", targetFps: 24, renderScale: 0.8, analysisScale: 0.85, camRes: 720 },
29
+ { name: "low", targetFps: 15, renderScale: 0.6, analysisScale: 0.7, camRes: 640 },
30
+ ];
31
+ const DEFAULT_PERFORMANCE_BUDGET = {
32
+ maxFrameMs: 1000 / 24,
33
+ maxInferenceMs: 24,
34
+ maxMemoryMb: null,
35
+ sampleMs: 1000,
36
+ downgradeAfter: 2,
37
+ upgradeAfter: 6,
38
+ upgradeHeadroom: 0.75,
39
+ cooldownMs: 4000,
40
+ };
41
+ ```
42
+
43
+ Tiers are ordered best → worst; every knob is optional. `resolveQualityConfig`
44
+ validates a partial config (clamps scales to 0.25..1, drops invalid knobs,
45
+ resolves `initialTier` / `minTier` / `maxTier` by index or name), and the
46
+ result is plain JSON you can persist.
47
+
48
+ ## Knobs and ownership
49
+
50
+ | Knob | Owner | Applied how |
51
+ | --------------- | ------------------------------------------------------------------- | -------------------------------------------------------------- |
52
+ | `targetFps` | session perf config → every tracker without its own `targetFps` | `_setPerf` re-runs the models; immediate |
53
+ | `analysisScale` | session perf config → every model's analysis downscaler | immediate (next inference) |
54
+ | `renderScale` | `<XRScene>` pixel ratio: `min(devicePixelRatio, cap) × renderScale` | immediate; `cap` is the `dpr` ceiling (3 or `canvasProps.dpr`) |
55
+ | `camRes` | session perf config, **only with `rebindCamera: true`** | re-opens the webcam once when the tier changes |
56
+
57
+ A per-tracker `targetFps` prop still beats the tier. The app's own `perf`
58
+ is the base the tiers are merged over, and it is restored when the
59
+ controller unmounts.
60
+
61
+ ## Behaviour
62
+
63
+ - **Sample** every `sampleMs`: mean main-thread frame time (window RAF),
64
+ fps, total tracker inference (`inferenceMsTotal` from diagnostics), feed
65
+ fps, JS heap (`performance.memory` where it exists), `deviceMemory`,
66
+ `hardwareConcurrency`, thermal. Hidden tabs are never judged.
67
+ - **Judge** most severe first: thermal `critical` → memory over
68
+ `maxMemoryMb` → inference over `maxInferenceMs` → frame time over
69
+ `maxFrameMs`.
70
+ - **Step down** after `downgradeAfter` consecutive bad samples, at most once
71
+ per `cooldownMs`, never below `minTier`. At the floor and still over
72
+ budget on frame time, `signals.thermal` becomes `"suspected"` so the app
73
+ can shed content; nothing else changes.
74
+ - **Step up** after `upgradeAfter` consecutive good samples (frame time and
75
+ inference under `budget × upgradeHeadroom`), never above `maxTier`.
76
+ - **Initial tier**: `initialTier` if set, else one step down from the top on
77
+ devices with ≤ 4 GB or ≤ 4 cores.
78
+ - **Manual**: `setTier()` (controller may move again after the cooldown),
79
+ `lock()` / `unlock()`, `reset()`. Every change is a decision with a
80
+ `reason` (`init`, `frame-time`, `inference`, `memory`, `thermal`,
81
+ `recovered`, `manual`, `locked`, `reset`) kept in `history`.
82
+ - **Unsupported signals degrade gracefully**: no `performance.memory` →
83
+ `memoryMb: null` and memory is ignored; no thermal API in browsers →
84
+ `thermal: "unknown"` unless the host passes one to
85
+ `<XRQualityController thermal=…>`.
86
+
87
+ The state is published to the session store (`useXRQualityState()` in
88
+ low-level) and shows in `useXRDiagnostics().quality` and the
89
+ `XRDiagnosticsPanel`. The pure stepping functions (`stepQualityController`,
90
+ `judgeSample`, …) are exported from `/low-level` for tests and custom
91
+ controllers.
92
+
93
+ ## Manual validation
94
+
95
+ Run `/adaptive-quality` (budget tightened: 2 bad samples, 3 s cooldown):
96
+
97
+ | Case | Steps | Expect |
98
+ | ----------- | --------------------------- | ----------------------------------------------------------------- |
99
+ | Baseline | load with a video source | tier `high` (or `medium` on a weak device), `init` |
100
+ | Load | simulated load 60 ms | within ~3 s `medium` (`frame-time`), then `low` |
101
+ | Recover | load 0 | after ~4 good samples back to `medium`, then `high` (`recovered`) |
102
+ | Lock | lock, load 60 | stays put |
103
+ | Manual | pick `low` | tier `low` (`manual`), diagnostics panel shows it |
104
+ | Pixel ratio | pick `low` on a DPR-3 phone | scene renders softer (0.6×) |
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: "Asset Lifecycle & GPU Memory"
3
+ description: "Preload, cache, reference-count, de-duplicate, dispose and budget textures, GLB models and video with the SDK's own three.js loaders; observe usage and eviction."
4
+ sidebarTitle: "Asset Lifecycle"
5
+ icon: "memory"
6
+ ---
7
+
8
+ GPU memory grows when the same texture is loaded twice, when a route change
9
+ leaves a model behind, or when nobody owns disposal. The asset cache gives
10
+ every asset one identity, one load, one owner list and one disposal.
11
+
12
+ ```tsx
13
+ import {
14
+ XRAssetCacheProvider,
15
+ defineGltfAsset,
16
+ defineTextureAsset,
17
+ useXRAsset,
18
+ useXRAssetStats,
19
+ } from "@vincentt-xr/sdk";
20
+
21
+ const HAT = defineGltfAsset("https://cdn.example.com/hat.glb");
22
+ const LOGO = defineTextureAsset("https://cdn.example.com/logo.png");
23
+
24
+ const Hat = () => {
25
+ const { value, status } = useXRAsset(HAT); // shared with every other <Hat>
26
+ return value ? <primitive object={value.scene} /> : null;
27
+ };
28
+
29
+ <XRAssetCacheProvider>
30
+ {" "}
31
+ {/* optional: scopes a cache; releases unreferenced assets on unmount */}
32
+ <Hat />
33
+ <Hat /> {/* second mount: cache hit, same scene object */}
34
+ </XRAssetCacheProvider>;
35
+ ```
36
+
37
+ ## Model
38
+
39
+ - **Definition** `XRAssetDefinition<T> = { key, load(signal), dispose?(value), bytes?(value) }`. `key` is the identity; `defineTextureAsset`, `defineGltfAsset` and `defineVideoAsset` build one over three's loaders (no drei); SDK-CDN URLs go through the verifying fetch (fail closed without a pinned hash), consumer URLs are fetched plainly. Loads honour the abort signal, and a decode or parse that finishes after a release is disposed, not leaked. Write your own for anything else (audio buffers, JSON, fonts).
40
+ - **Acquire / release** `cache.acquire(asset, ownerId)` returns the shared value (in-flight loads are shared too); `cache.release(key, ownerId)` drops that owner. Owners are ids, so a re-render or a StrictMode double mount never double-counts. `useXRAsset` does both for a component.
41
+ - **Unreferenced values stay cached** until the budget needs room, `disposeUnreferenced()` or `disposeAll()`. A value nobody wanted by the time its load finished is disposed at once; releasing during a load aborts it.
42
+ - **Budget** `createAssetCache({ budgetBytes })` (default 256 MiB, `Infinity` disables). Above it, unreferenced values are disposed least-recently-used first; referenced values are never evicted, so `overBudget` can stay true while everything is in use. `setBudget` applies immediately; `evict(bytes)` forces room.
43
+ - **Bytes** come from `bytes(value)`: `estimateTextureBytes` (RGBA × mip chain), `estimateGeometryBytes`, `estimateObjectBytes` (geometries and textures reachable from a scene, each once). They are estimates of resident GPU memory; the browser exposes no exact figure.
44
+ - **Disposal is exactly once**: `dispose(value)` runs once per loaded value, on eviction, `disposeUnreferenced`, `disposeAll` or a too-late load. `disposeObject3D` releases every geometry, material and texture under an object.
45
+ - **Errors** are recorded on the entry (`status: "error"`); the next acquire retries.
46
+
47
+ ## Observability
48
+
49
+ `useXRAssetStats()` (or `cache.getStats()`) reports `assets`,
50
+ `referencedAssets`, `loadingAssets`, `erroredAssets`, `usageBytes`,
51
+ `reclaimableBytes`, `budgetBytes`, `overBudget`, `evictions`, `hits` and
52
+ `misses`; `cache.list()` gives every entry with its status, bytes and owner
53
+ count; `cache.subscribe(listener)` fires on every change.
54
+
55
+ ## Route changes and session shutdown
56
+
57
+ Owners release on unmount, so navigating away drops every reference held by
58
+ the page. `<XRAssetCacheProvider>` then calls `disposeUnreferenced()` as it
59
+ unmounts (`disposeUnreferencedOnUnmount`, default `true`), so a page-scoped
60
+ cache leaves nothing behind; the app-wide default cache (no provider) keeps
61
+ unreferenced values for the next page until the budget says otherwise. For
62
+ a hard shutdown call `cache.disposeAll()`. Tracker-owned resources (the
63
+ MediaPipe models, the camera texture, segmentation masks) are managed by the
64
+ session registry, released once when the provider unmounts; the asset cache
65
+ covers everything the app loads on top.
66
+
67
+ ## Manual validation
68
+
69
+ Run `/asset-lifecycle` (a 24 MB page-scoped cache):
70
+
71
+ | Case | Steps | Expect |
72
+ | ------------ | ----------------------------------- | ------------------------------------------------------------------ |
73
+ | Dedupe | mount texture 1 twice (remount) | `hits` rises, one entry |
74
+ | Release | unmount a texture | entry stays with 0 owners, `reclaimable` grows |
75
+ | Budget | lower the budget slider | unreferenced entries disappear oldest first, `evictions` counts up |
76
+ | Referenced | budget below usage with all mounted | `OVER` shown, nothing evicted |
77
+ | Preload | "preload texture 4" | entry `ready` with 0 owners; mounting it is a hit |
78
+ | Route change | navigate away and back | entries empty on return (provider disposed the unreferenced ones) |
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: "Camera Permission & Privacy"
3
+ description: "Distinguish unknown, requesting, granted, denied, blocked and unavailable camera states; request, retry and recover without remounting; release the camera cleanly; keep every frame on the device until the viewer allows access."
4
+ sidebarTitle: "Camera Permission"
5
+ icon: "camera"
6
+ ---
7
+
8
+ Camera access is the first thing that can go wrong in an AR experience, and
9
+ the browser's `NotAllowedError` hides four different situations. The SDK
10
+ splits them into states a creator can act on, offers the request flow as a
11
+ hook and a gate component, and guarantees no camera frame is processed
12
+ before explicit, usable access.
13
+
14
+ ## States
15
+
16
+ | State | Meaning | Next step |
17
+ | ------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
18
+ | `unknown` | Never asked (the next request prompts), or the browser cannot say. | Show a primer and a request button. |
19
+ | `requesting` | A prompt is open. | Wait. |
20
+ | `granted` | Usable. | Mount the session. |
21
+ | `denied` | This prompt was declined or dismissed; the next request prompts again. | Offer "try again". |
22
+ | `blocked` | Persistently denied in the site settings; a request fails without a prompt. | Explain how to change the setting. |
23
+ | `unavailable` | No camera, insecure context, unsupported browser, or held by another app (`reason`). | Fall back; `reason.recoverable` says if a retry can help. |
24
+
25
+ `denied` and `blocked` are told apart by reading the Permissions API _after_
26
+ the failed request: `denied` there means blocked. `reason` is an
27
+ `XRFallbackReason` (`camera-permission-denied`, `camera-in-use`,
28
+ `no-camera`, `camera-insecure-context`, `camera-unsupported`, `camera-lost`).
29
+
30
+ ## Before the session: the gate
31
+
32
+ ```tsx
33
+ import { CameraPermissionGate, useCameraPermission } from "@vincentt-xr/sdk";
34
+
35
+ // Component form: children render only while access is granted.
36
+ <CameraPermissionGate
37
+ fallback={({ state, reason, canPrompt, request }) => (
38
+ <Primer state={state} reason={reason} onAllow={canPrompt ? () => request() : undefined} />
39
+ )}
40
+ >
41
+ <XRProvider>
42
+ <Experience />
43
+ </XRProvider>
44
+ </CameraPermissionGate>;
45
+
46
+ // Hook form, for your own layout.
47
+ const { state, reason, canPrompt, usable, request, refresh } = useCameraPermission();
48
+ ```
49
+
50
+ Neither needs an `<XRProvider>`. On mount they only _query_ the permission
51
+ (no prompt); `request()` prompts, and must run from a user gesture (a
52
+ button) as browsers require. The probe stream is stopped at once, so the
53
+ camera indicator goes off again and nothing is captured: the call only
54
+ moves the permission. Pass `auto` to prompt on mount. Both follow the
55
+ site-setting changes through the Permissions API, so a viewer who allows
56
+ the camera in the address bar flips the gate without a reload.
57
+
58
+ The built-in fallback of `<CameraPermissionGate>` announces state changes
59
+ with `aria-live="polite"`, keeps the request behind a real `<button>`,
60
+ disables it when a prompt cannot help, and labels the region "Camera access".
61
+ Keep those in a custom fallback: a visible button, `aria-live` on the
62
+ status text, `aria-disabled` when blocked, and the explanation for
63
+ `blocked` next to the button rather than in a toast.
64
+
65
+ ## Inside the session
66
+
67
+ The session reports its own live state through `useXRSessionCameraPermission()`
68
+ (`@vincentt-xr/sdk/low-level`): `requesting` while its webcam
69
+ `getUserMedia` is pending, `granted` once bound, the classified failure
70
+ otherwise, and `blocked` / `unavailable` after a mid-session revoke or a
71
+ lost camera. Stream and canvas sources leave it `unknown`.
72
+
73
+ ```tsx
74
+ const { state, reason } = useXRSessionCameraPermission();
75
+ ```
76
+
77
+ ### Request, retry, recover without remounting
78
+
79
+ ```tsx
80
+ const { session } = useXRContext();
81
+ const retry = async () => {
82
+ await session.setMediaSource({ source: XRMediaSource.WEBCAM }); // prompts again if allowed
83
+ await session.start();
84
+ };
85
+ ```
86
+
87
+ A failed bind leaves the session where it was (`idle` stays idle, a running
88
+ session keeps its old source), so the same provider, models and scene are
89
+ reused. A revoke mid-session stops the session with a `camera-lost` /
90
+ `permission-denied` error; the same two calls recover it.
91
+
92
+ ### Privacy: releasing the camera
93
+
94
+ - `session.stop()` stops every media track: the browser's camera indicator
95
+ goes off and no frame is read. Models stay initialized, so a later
96
+ `setMediaSource` + `start` is fast. Use this for an in-app "privacy mode".
97
+ - A revoke (`permission-denied`) or a lost device (`camera-lost`) releases
98
+ the tracks once and suspends every model automatically.
99
+ - No model runs before `session.start()`, and `start()` needs a bound
100
+ source, so no camera data is processed before explicit access. The gate
101
+ adds the second guarantee: the session is not even mounted until
102
+ `granted`.
103
+ - Frames never leave the device: inference runs in the page, and the only
104
+ network traffic is the one-time model and engine download.
105
+
106
+ ## Choosing the camera
107
+
108
+ `setMediaSource({ source: XRMediaSource.WEBCAM, facingMode: "environment" })`
109
+ opens the rear camera; `"user"` (default) the front one; `deviceId` picks a
110
+ specific camera from `enumerateDevices()`. Binding again with a different
111
+ facing swaps the camera mid-session. The front camera is mirrored
112
+ (`sourceKind` `"webcam"`), the rear camera is drawn straight (`sourceKind`
113
+ `"rear-camera"`), and `mirrored: true | false` overrides that for a camera
114
+ whose facing is unknown. Trackers and the coordinate toolkit follow the
115
+ source kind, so content stays aligned either way. `useXRCameraFacing()`
116
+ (low-level) reports the bound facing. The full cross-camera and orientation
117
+ contract lands with the next sprint item.
118
+
119
+ ## Low-level helpers
120
+
121
+ `queryCameraPermission()`, `requestCameraAccess({ keepStream, constraints })`,
122
+ `subscribeCameraPermission(listener)` and `canPromptForCamera(state)` are
123
+ plain functions (core door) for non-React code; `CAMERA_PERMISSION_MESSAGES`
124
+ holds the default copy per state. `classifyCameraFailure(reason,
125
+ permissionState)` and `mapDomExceptionToReason(err)` live in `/low-level`
126
+ next to `XRErrorReason`, for callers that classify their own
127
+ `getUserMedia` failure.
128
+
129
+ ## Manual validation
130
+
131
+ Run `/camera-permission`:
132
+
133
+ | Case | Steps | Expect |
134
+ | ------------ | ----------------------------------------- | ----------------------------------------------------------------- |
135
+ | First visit | load | gate `unknown`, no prompt until "Allow camera" |
136
+ | Allow | click Allow, accept | `requesting` → `granted`; the experience mounts and binds |
137
+ | Dismiss | click Allow, close the prompt | `denied`, "Try again" enabled |
138
+ | Block | deny with "never" / site setting | `blocked`, button disabled, settings hint shown |
139
+ | Unblock | allow in the address bar | gate flips to `granted` without reload |
140
+ | Revoke | "simulate revoke" (or revoke in settings) | session `blocked` / `unavailable`, lifecycle `stopped` |
141
+ | Recover | "retry camera" | prompt if needed, lifecycle `running` |
142
+ | Privacy mode | "privacy mode (stop camera)" | camera indicator off, lifecycle `stopped`; "retry camera" resumes |
@@ -0,0 +1,165 @@
1
+ ---
2
+ title: "Coordinate Spaces"
3
+ description: "One toolkit for source, feed, tracker, viewport, screen and world coordinates: cover crop, zoom, mirroring, orientation, and documented conversions."
4
+ sidebarTitle: "Coordinate Spaces"
5
+ icon: "arrows-to-dot"
6
+ ---
7
+
8
+ Every tracking consumer meets the same six spaces. Import the conversions from
9
+ `@vincentt-xr/sdk`; each one performs its origin, axis, crop and mirror change
10
+ exactly once, so never chain a manual flip or crop after it.
11
+
12
+ ## The spaces
13
+
14
+ ```
15
+ source (media px) feed (0..1) viewport (CSS px)
16
+ ┌──────────────────┐ ┌────────┐ ┌────────┐
17
+ │ raw video/image │ cover│ │ identity │ │
18
+ │ 1280 × 720 │ crop │ 720 × │ per axis │ 390 × │
19
+ │ never mirrored │──────▶ 1280 │◀────────────────▶│ 844 │
20
+ └──────────────────┘ zoom │ webcam │ │ │
21
+ mirror│ mirror │ └────────┘
22
+ └───┬────┘ │
23
+ │ x·2−1, 1−y·2 │ design canvas
24
+ ▼ ▼
25
+ tracker (−1..1, Y up) screen (720×1280 design px, Y up)
26
+ │
27
+ │ camera ray → z = 0 plane
28
+ ▼
29
+ world (three.js)
30
+ ```
31
+
32
+ | Space | Origin / axes | Who produces it |
33
+ | ------------ | ---------------------------------------------- | -------------------------------------------------------------- |
34
+ | **source** | top-left, Y down, media pixels, never mirrored | the `<video>` / `<img>` the session was given |
35
+ | **feed** | top-left, Y down, 0..1 | the camera texture; every landmark and SDK mask |
36
+ | **tracker** | center, X right, Y up, Z toward the viewer | hand, body, holistic nodes (`TrackerPoint`) |
37
+ | **viewport** | top-left, Y down, CSS pixels | the rendered canvas, pointer events via `clientToScreenPixels` |
38
+ | **screen** | center, Y up, 720×1280 design pixels | `ScreenSpaceUI`, `ScreenImage`, labels |
39
+ | **world** | three.js world; tracked content on z = 0 | `TrackingAnchor`, face mesh, iris quads |
40
+
41
+ The feed is the source **cover-cropped to the render size**, enlarged by
42
+ `perf.cameraZoom` (default 1.2) about the centre, and **mirrored for the
43
+ webcam only**. Image and video sources are drawn straight. Because the feed
44
+ fills the viewport, feed and viewport agree per axis; a landmark at feed
45
+ (0.5, 0.5) is the middle of the canvas on every aspect ratio.
46
+
47
+ ## The feed crop, once
48
+
49
+ `FeedCrop` is the single description of source → feed. The session's blit
50
+ loop draws with it, `computeMaskUvTransform` fits source-space masks with it,
51
+ and the point conversions use it, so what the trackers saw and what you
52
+ compute agree.
53
+
54
+ ```ts
55
+ import { computeFeedCrop, feedToSource, sourceToFeed } from "@vincentt-xr/sdk";
56
+ import { useXRFeedCrop } from "@vincentt-xr/sdk/low-level";
57
+
58
+ // Inside the provider: the live crop (null until media is bound).
59
+ const crop = useXRFeedCrop();
60
+
61
+ // Or build one yourself for an offline pipeline.
62
+ const offline = computeFeedCrop({
63
+ source: { width: 1280, height: 720 },
64
+ feed: { width: 720, height: 1280 },
65
+ zoom: 1.2,
66
+ mirrored: true, // webcam
67
+ });
68
+
69
+ // A landmark back onto the raw frame (for a crop, a server call, an overlay on the <video>).
70
+ const px = feedToSource({ x: 0.62, y: 0.4 }, crop); // { x, y } in source pixels
71
+ // A detection made on the raw frame into landmark space.
72
+ const fp = sourceToFeed({ x: 800, y: 300 }, crop); // { x, y } in 0..1
73
+ ```
74
+
75
+ `feedToSource` can return points outside the source when the feed shows
76
+ less than the frame (zoom > 1 never does; a contain-shaped source does);
77
+ `isFeedPointInSource` tells you.
78
+
79
+ ## Mirroring
80
+
81
+ - Positions never need a flip: landmarks already live in the (possibly
82
+ mirrored) feed, and the feed is what you see.
83
+ - **Direction signs** do. Holistic hand labels and the iris `offset.x` are
84
+ corrected with `isMirroredSourceKind(sourceKind)` so "the person's left"
85
+ means the same thing on every source. Use the same helper, never
86
+ `sourceKind === "webcam"` inline.
87
+ - `mirrorFeedX` flips a feed point's x (for a direction vector, or to compare
88
+ a selfie feed with a straight recording).
89
+
90
+ ## Camera orientation
91
+
92
+ A frame delivered sideways (a native capture, a rotated file) must be
93
+ rotated before it becomes the source. `orientSourcePoint(point, size,
94
+ orientation)` and `orientedSize(size, orientation)` move a point and a frame
95
+ size through a clockwise 0 / 90 / 180 / 270 rotation; apply them before
96
+ `sourceToFeed`. The browser already orients webcam frames, so the session
97
+ does not rotate.
98
+
99
+ ## Conversions
100
+
101
+ | From → to | Helper |
102
+ | ---------------------- | ------------------------------------------------------------------- |
103
+ | source ↔ feed | `sourceToFeed`, `feedToSource` (with a `FeedCrop`) |
104
+ | feed ↔ tracker | `feedToTracker`, `trackerToFeed` |
105
+ | feed ↔ viewport | `feedToViewport`, `viewportToFeed` (viewport size) |
106
+ | feed ↔ screen | `feedToScreenPixels`, `screenPixelsToFeed` |
107
+ | feed / tracker → world | `feedToWorld`, `trackerToWorld` (camera, optional `planeZ`) |
108
+ | world → feed / tracker | `worldToFeed`, `worldToTracker` (`z` is NDC depth) |
109
+ | media layout | `computeFitLayout(source, destination, "cover" \| "contain", zoom)` |
110
+
111
+ The screen-space helpers (`normalizedToScreenPixels`, `worldToScreenPixels`,
112
+ `screenPointToRay`, `mediaToScreenPixels` …) are unchanged and documented in
113
+ [Screen-Space Coordinates](/api/screen-space-coordinates); a feed point is a
114
+ "normalized viewport point" there.
115
+
116
+ ### Worked examples
117
+
118
+ **A hand joint to a DOM overlay**
119
+
120
+ ```ts
121
+ const joint = handNode.hands?.[0].coordinates?.[8]; // tracker space
122
+ const feed = trackerToFeed(joint); // 0..1
123
+ const css = feedToViewport(feed, { width: el.clientWidth, height: el.clientHeight });
124
+ badge.style.transform = `translate(${css.x}px, ${css.y}px)`;
125
+ ```
126
+
127
+ **A face landmark into the 3D scene** (what `TrackingAnchor` does)
128
+
129
+ ```ts
130
+ useFrame(() => {
131
+ const tip = face.landmarks[1]; // feed space, from useFaceInfo
132
+ feedToWorld(tip, camera, mesh.position); // onto the z = 0 tracking plane
133
+ });
134
+ ```
135
+
136
+ **A tap to the raw frame** (send the touched pixel to a server)
137
+
138
+ ```ts
139
+ const screen = clientToScreenPixels({ point: { x: e.clientX, y: e.clientY }, rect });
140
+ const feed = screenPixelsToFeed(screen, viewportSize);
141
+ const sourcePx = feedToSource(feed, useXRFeedCrop()!);
142
+ ```
143
+
144
+ **A mask made on the raw video**
145
+
146
+ ```ts
147
+ computeMaskUvTransform({
148
+ space: "source",
149
+ maskWidth: 1280,
150
+ maskHeight: 720,
151
+ feedWidth,
152
+ feedHeight,
153
+ zoom: 1.2, // the session's perf.cameraZoom
154
+ mirrored: true, // webcam
155
+ });
156
+ ```
157
+
158
+ ## Regression coverage
159
+
160
+ `lib/coordinates/coordinates.test.ts` pins: the cover crop with zoom against
161
+ the blit loop's rectangle for landscape→portrait, portrait→landscape and 4:3
162
+ pairs; webcam mirroring (source left edge → feed right edge, round trip);
163
+ feed/tracker/viewport/screen round trips and agreement with
164
+ `trackerPointToScreenPixels`; feed→world depth independence and plane
165
+ selection; and the mask fit matching the feed crop, zoom included.