@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.
- package/GROUNDING.md +1109 -13
- package/dist/{DebuggerUI-BjOMr8q_.js → DebuggerUI-C-ldiozL.js} +1 -1
- package/dist/{DebuggerUI-B6bP6P9r.cjs → DebuggerUI-CWX5EtQv.cjs} +1 -1
- package/dist/FaceMeshRenderer-BEEXZwS6.cjs +158 -0
- package/dist/FaceMeshRenderer-UuH5sTXG.js +1322 -0
- package/dist/FaceRetouch-C7h7x9kd.js +683 -0
- package/dist/FaceRetouch-DqftT26Y.cjs +134 -0
- package/dist/GLTFLoader-C5q63sKO.js +1633 -0
- package/dist/GLTFLoader-Cb_CRwmG.cjs +1 -0
- package/dist/SegmentationOverlay-DQuW3dsc.js +133 -0
- package/dist/SegmentationOverlay-aWVod8Kl.cjs +1 -0
- package/dist/assets/cache.d.ts +99 -0
- package/dist/assets/estimate.d.ts +11 -0
- package/dist/assets/index.d.ts +7 -0
- package/dist/assets/loaders.d.ts +28 -0
- package/dist/assets/react.d.ts +33 -0
- package/dist/calibration/calibration.d.ts +70 -0
- package/dist/calibration/index.d.ts +6 -0
- package/dist/calibration/storage.d.ts +13 -0
- package/dist/calibration/types.d.ts +53 -0
- package/dist/calibration/useXRCalibration.d.ts +46 -0
- package/dist/camera/CameraPermissionGate.d.ts +26 -0
- package/dist/camera/devices.d.ts +26 -0
- package/dist/camera/index.d.ts +8 -0
- package/dist/camera/permission.d.ts +86 -0
- package/dist/camera/useCameraPermission.d.ts +35 -0
- package/dist/capabilities/detect.d.ts +45 -0
- package/dist/capabilities/index.d.ts +5 -0
- package/dist/capabilities/types.d.ts +120 -0
- package/dist/capabilities/useXRCapabilities.d.ts +18 -0
- package/dist/compat/environment.d.ts +5 -0
- package/dist/compat/index.d.ts +4 -0
- package/dist/compat/run.d.ts +18 -0
- package/dist/compat/scenarios.d.ts +10 -0
- package/dist/compat/types.d.ts +101 -0
- package/dist/components/BodyTracker/index.d.ts +9 -0
- package/dist/components/FaceMesh/canonical/CanonicalFaceMeshRenderer.d.ts +33 -3
- package/dist/components/FaceMesh/canonical/parseCanonicalFaceModel.d.ts +12 -0
- package/dist/components/FaceMesh/canonical/shaders.d.ts +1 -1
- package/dist/components/FaceMesh/index.d.ts +1 -1
- package/dist/components/GestureListener/index.d.ts +11 -0
- package/dist/components/HandTracker/index.d.ts +10 -0
- package/dist/components/HolisticTracker/index.d.ts +69 -0
- package/dist/components/HolisticTracker/settings.d.ts +58 -0
- package/dist/components/HolisticTracker/signals.d.ts +49 -0
- package/dist/components/IrisEffect/IrisMaterial.d.ts +73 -0
- package/dist/components/IrisEffect/index.d.ts +51 -0
- package/dist/components/IrisEffect/settings.d.ts +117 -0
- package/dist/components/LipEffect/LipEffect.d.ts +7 -1
- package/dist/components/LipEffect/LipEffectMaterial.d.ts +43 -7
- package/dist/components/LipEffect/LipEffects.d.ts +9 -0
- package/dist/components/LipEffect/defaults.d.ts +27 -4
- package/dist/components/LipEffect/index.d.ts +11 -3
- package/dist/components/LipEffect/settings.d.ts +271 -0
- package/dist/components/LipEffect/types.d.ts +56 -9
- package/dist/components/LipEffect/useLipDesignTextures.d.ts +11 -0
- package/dist/components/LipEffect/uvGuide.d.ts +59 -0
- package/dist/components/PersonCutout/index.d.ts +50 -0
- package/dist/components/PersonCutout/personCutout.d.ts +16 -0
- package/dist/components/ScreenImage/components/ScreenImageBody.d.ts +1 -1
- package/dist/components/ScreenImage/index.d.ts +3 -1
- package/dist/components/ScreenImage/materials/ScreenImageBlendMaterial.d.ts +1 -1
- package/dist/components/ScreenImage/materials/ScreenImageMaskMaterial.d.ts +11 -0
- package/dist/components/ScreenImage/source.d.ts +6 -0
- package/dist/components/ScreenImage/types.d.ts +18 -0
- package/dist/components/SegmentationOverlay/SegmentationOverlay.d.ts +37 -0
- package/dist/components/SegmentationOverlay/SegmentationOverlayMaterial.d.ts +31 -0
- package/dist/components/SegmentationOverlay/index.d.ts +9 -0
- package/dist/components/SegmentationOverlay/maskTexture.d.ts +23 -0
- package/dist/components/SegmentationOverlay/providers.d.ts +72 -0
- package/dist/components/SegmentationOverlay/settings.d.ts +65 -0
- package/dist/components/XRProvider/index.d.ts +16 -2
- package/dist/components/XRViewport/hooks/useViewportExternalFaces.d.ts +1 -1
- package/dist/components/XRViewport/hooks/useViewportMediaSource.d.ts +1 -1
- package/dist/components/XRViewport/hooks/useViewportPipeline.d.ts +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/constants/index.d.ts +4 -1
- package/dist/contexts-CO0sd4JQ.cjs +1 -0
- package/dist/{contexts-ByhzppYl.js → contexts-FzlnnMYD.js} +252 -246
- package/dist/coordinates/feed.d.ts +52 -0
- package/dist/coordinates/index.d.ts +5 -0
- package/dist/coordinates/spaces.d.ts +49 -0
- package/dist/coordinates/viewport.d.ts +22 -0
- package/dist/coordinates/world.d.ts +15 -0
- package/dist/debug-ui/gesture-settings/index.d.ts +14 -0
- package/dist/debug-ui/holistic-tracker-settings/index.d.ts +16 -0
- package/dist/debug-ui/index.d.ts +14 -0
- package/dist/debug-ui/iris-effect-settings/index.d.ts +12 -0
- package/dist/debug-ui/lip-effect-settings/index.d.ts +15 -0
- package/dist/debug-ui/media-source.cjs +1 -1
- package/dist/debug-ui/media-source.js +1 -1
- package/dist/debug-ui/product-hotspots/index.d.ts +24 -0
- package/dist/debug-ui/segmentation-overlay-settings/index.d.ts +12 -0
- package/dist/debug-ui/xr-diagnostics/index.d.ts +25 -0
- package/dist/debug-ui.cjs +3 -3
- package/dist/debug-ui.js +2057 -1687
- package/dist/detector-K8aej4MB.js +59 -0
- package/dist/detector-ijypGtse.cjs +1 -0
- package/dist/entries/core.d.ts +18 -1
- package/dist/entries/face-effects.d.ts +11 -3
- package/dist/entries/low-level.d.ts +22 -3
- package/dist/entries/product.d.ts +11 -0
- package/dist/entries/tracking.d.ts +24 -1
- package/dist/face-effects.cjs +155 -1
- package/dist/face-effects.js +1134 -2197
- package/dist/gestures/definitions.d.ts +85 -0
- package/dist/gestures/detector.d.ts +87 -0
- package/dist/gestures/index.d.ts +8 -0
- package/dist/gestures/measures.d.ts +101 -0
- package/dist/gestures/settings.d.ts +49 -0
- package/dist/{getFaceInfo-C4QpCQIY.js → getFaceInfo-BkZfaQs-.js} +5 -5
- package/dist/{getFaceInfo-fA1bFBub.cjs → getFaceInfo-ahPxQFGu.cjs} +1 -1
- package/dist/hooks/selectors.d.ts +68 -1
- package/dist/hooks/useCalibrationFaceTracker.d.ts +1 -1
- package/dist/hooks/useFaceInfo.d.ts +13 -1
- package/dist/hooks/useFaceInfos.d.ts +1 -1
- package/dist/hooks/useGesture.d.ts +32 -0
- package/dist/hooks/useIrisTracking.d.ts +45 -0
- package/dist/hooks/useXRDiagnostics.d.ts +81 -0
- package/dist/hooks/useXRModel.d.ts +7 -0
- package/dist/hooks/useXRSession/facade.d.ts +42 -2
- package/dist/hooks/useXRSession/index.d.ts +2 -2
- package/dist/hooks/useXRSession/reason.d.ts +3 -1
- package/dist/hooks/useXRSession/registry.d.ts +14 -0
- package/dist/hooks/useXRSession/store.d.ts +52 -3
- package/dist/index--RgGzZ6X.cjs +47 -0
- package/dist/{index-VJ0SJwmG.js → index-CHVbH0hz.js} +4 -4
- package/dist/index-CeVkgsag.cjs +1 -0
- package/dist/index-CnGox6Xr.js +3054 -0
- package/dist/{index-ByoNj0QP.js → index-CsUdQ-pn.js} +113 -111
- package/dist/{index-BjmTe9eM.cjs → index-DKBAh6hV.cjs} +1 -1
- package/dist/index-DsZwTfG0.cjs +1 -0
- package/dist/index-Dw6tj2Mi.js +144 -0
- package/dist/index-OCPjOaeP.cjs +1 -0
- package/dist/{index-DXT3WccE.js → index-s9pF4zkG.js} +1065 -1101
- package/dist/internal-BYza1h8_.cjs +1 -0
- package/dist/{internal-Dmk1cTXq.js → internal-vsfNQycj.js} +15 -16
- package/dist/internal.cjs +1 -1
- package/dist/internal.js +2 -2
- package/dist/iris/compute.d.ts +25 -0
- package/dist/iris/index.d.ts +4 -0
- package/dist/iris/lead.d.ts +19 -0
- package/dist/iris/types.d.ts +87 -0
- package/dist/landmarkMask-CbTFzBYG.js +224 -0
- package/dist/landmarkMask-u7es2k4X.cjs +1 -0
- package/dist/low-level.cjs +1 -1
- package/dist/low-level.js +1085 -634
- package/dist/main.cjs +1 -1
- package/dist/main.js +1519 -465
- package/dist/measures-9dQPdw96.js +154 -0
- package/dist/measures-KfJzpvod.cjs +1 -0
- package/dist/models/Base/index.d.ts +49 -0
- package/dist/models/Body/index.d.ts +1 -1
- package/dist/models/Face/index.d.ts +1 -1
- package/dist/models/Hand/GestureTracking.d.ts +1 -1
- package/dist/models/Hand/HandTracking.d.ts +1 -1
- package/dist/models/Holistic/index.d.ts +49 -0
- package/dist/models/Holistic/result.d.ts +68 -0
- package/dist/models/SemanticSegmentation/index.d.ts +31 -0
- package/dist/models/SemanticSegmentation/presets.d.ts +28 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/product/ProductDimensions.d.ts +16 -0
- package/dist/product/ProductHotspot.d.ts +16 -0
- package/dist/product/ProductViewer.d.ts +9 -0
- package/dist/product/details.d.ts +53 -0
- package/dist/product/index.d.ts +14 -0
- package/dist/product/loadModelViewer.d.ts +4 -0
- package/dist/product/types.d.ts +71 -0
- package/dist/product/useProductAr.d.ts +37 -0
- package/dist/product/useProductDetails.d.ts +60 -0
- package/dist/product.cjs +1 -0
- package/dist/product.js +414 -0
- package/dist/providers-C0Dlhh9Z.js +365 -0
- package/dist/providers-YNwkmNeK.cjs +62 -0
- package/dist/quality/XRQualityController.d.ts +25 -0
- package/dist/quality/controller.d.ts +33 -0
- package/dist/quality/index.d.ts +6 -0
- package/dist/quality/types.d.ts +100 -0
- package/dist/quality/useXRQuality.d.ts +28 -0
- package/dist/{renderGroups-KZk6eRiY.js → renderGroups-CN1cGFUo.js} +1 -1
- package/dist/{renderGroups-80H3DpTl.cjs → renderGroups-O4KmaMl9.cjs} +1 -1
- package/dist/renderPasses-Cg-zqmEz.cjs +21 -0
- package/dist/renderPasses-NnKZVfBy.js +1460 -0
- package/dist/scene-object.cjs +1 -1
- package/dist/scene-object.js +2 -2
- package/dist/screenTransform-BenfCtZt.js +44 -0
- package/dist/screenTransform-BiXdpVf0.cjs +1 -0
- package/dist/segmentation-9PhSvc-R.cjs +1 -0
- package/dist/segmentation-CeOTKemr.js +5 -0
- package/dist/segmentation2d/faceMemory.d.ts +28 -0
- package/dist/segmentation2d/faceRegions.d.ts +24 -0
- package/dist/segmentation2d/index.d.ts +9 -0
- package/dist/segmentation2d/maskUv.d.ts +23 -0
- package/dist/segmentation2d/rasterize.d.ts +14 -0
- package/dist/segmentation2d/types.d.ts +46 -0
- package/dist/selectors-C5G1PMYL.js +195 -0
- package/dist/selectors-Du_PsVnW.cjs +1 -0
- package/dist/{session-CpkiK5YA.cjs → session-BBr0yN8h.cjs} +7 -7
- package/dist/{session-DLFN_VII.js → session-Cy0m7l7-.js} +76 -76
- package/dist/settings-DRR7-snV.cjs +1 -0
- package/dist/settings-aQk845NJ.js +73 -0
- package/dist/subjects/index.d.ts +2 -0
- package/dist/subjects/select.d.ts +62 -0
- package/dist/subjects/types.d.ts +55 -0
- package/dist/tracking-config/apply.d.ts +57 -0
- package/dist/tracking-config/index.d.ts +7 -0
- package/dist/tracking-config/resolve.d.ts +25 -0
- package/dist/tracking-config/storage.d.ts +8 -0
- package/dist/tracking-config/types.d.ts +106 -0
- package/dist/tracking-config/useXRTrackingConfig.d.ts +38 -0
- package/dist/tracking.cjs +1 -1
- package/dist/tracking.js +1056 -383
- package/dist/types/compositor.d.ts +21 -0
- package/dist/types/holistic.d.ts +121 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model.d.ts +22 -2
- package/dist/types/nodes.d.ts +27 -2
- package/dist/types/segmentation.d.ts +19 -0
- package/dist/types/session.d.ts +41 -3
- package/dist/useFaceInfo-Blf7FCu3.js +303 -0
- package/dist/useFaceInfo-C5mO8NQY.cjs +1 -0
- package/dist/useXRCapabilities-B1R2ghy-.cjs +1 -0
- package/dist/useXRCapabilities-C68v__wB.js +30 -0
- package/dist/utils/fns.d.ts +6 -3
- package/dist/utils/integrity.d.ts +4 -1
- package/dist/utils/updateThrottle.d.ts +2 -0
- package/dist/utils/useStableUniforms.d.ts +18 -0
- package/docs/api/adaptive-quality.mdx +104 -0
- package/docs/api/asset-lifecycle.mdx +78 -0
- package/docs/api/camera-permission.mdx +142 -0
- package/docs/api/coordinate-spaces.mdx +165 -0
- package/docs/api/cross-camera.mdx +88 -0
- package/docs/api/diagnostics-calibration.mdx +97 -0
- package/docs/api/face-effects.mdx +6 -0
- package/docs/api/face-mesh.mdx +4 -2
- package/docs/api/gestures.mdx +173 -0
- package/docs/api/holistic-tracker.mdx +159 -0
- package/docs/api/iris-tracking.mdx +242 -0
- package/docs/api/multi-subject.mdx +95 -0
- package/docs/api/person-cutout.mdx +136 -0
- package/docs/api/product-ar.mdx +111 -0
- package/docs/api/product-details.mdx +78 -0
- package/docs/api/screen-space-coordinates.mdx +4 -1
- package/docs/api/segmentation-overlay.mdx +301 -0
- package/docs/api/session-lifecycle.mdx +136 -0
- package/docs/api/trackers.mdx +5 -0
- package/docs/api/tracking-config.mdx +108 -0
- package/docs/docs.json +23 -1
- package/docs/examples/lip-shaders.mdx +224 -0
- package/docs/guides/lip-effect-textures.mdx +292 -0
- package/docs/guides/lip-uv-template.png +0 -0
- package/docs/guides/lip-uv-template.svg +1 -0
- package/package.json +13 -1
- package/dist/FaceMeshRenderer-C5l6VLdO.cjs +0 -155
- package/dist/FaceMeshRenderer-wjAHrJzP.js +0 -1270
- package/dist/FaceRetouch-BkJzoA79.cjs +0 -134
- package/dist/FaceRetouch-DwbKQJAN.js +0 -551
- package/dist/contexts-Dr62It_K.cjs +0 -1
- package/dist/index-9dlAQAhy.js +0 -1462
- package/dist/index-BpEf2BBb.cjs +0 -1
- package/dist/index-CyUUurQT.cjs +0 -1
- package/dist/index-DCiMbxC3.cjs +0 -1
- package/dist/index-DpI-qo5X.cjs +0 -17
- package/dist/index-DyVF4vEW.js +0 -111
- package/dist/internal-Cq-TEQ5Y.cjs +0 -1
- package/dist/renderLayers-lDtL0_43.cjs +0 -1
- package/dist/renderLayers-zrK2pv5O.js +0 -13
- package/dist/renderPasses-BOoBSr8z.cjs +0 -21
- package/dist/renderPasses-BcquyVtu.js +0 -1500
- package/dist/selectors-B8H97CKA.js +0 -59
- package/dist/selectors-CZLKT2Iv.cjs +0 -1
- package/dist/useFaceInfo-BJ8-_MOs.cjs +0 -1
- package/dist/useFaceInfo-Dk426LTu.js +0 -62
- package/dist/useFaceResults-CtetsO9M.js +0 -19
- 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
|
+
};
|
package/dist/utils/fns.d.ts
CHANGED
|
@@ -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: (
|
|
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
|
|
21
|
-
* unprojectToZZeroInto with a pre-allocated out
|
|
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
|
|
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,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.
|