talking-head-studio 0.4.1 → 0.4.2
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/dist/TalkingHeadVisualization.js +4 -4
- package/dist/editor/AvatarCanvas.d.ts +3 -14
- package/dist/editor/AvatarCanvas.js +5 -4
- package/dist/editor/AvatarEditor.d.ts +1 -0
- package/dist/editor/AvatarEditor.js +6 -0
- package/dist/editor/AvatarEditor.native.d.ts +4 -0
- package/dist/editor/AvatarEditor.native.js +93 -0
- package/dist/editor/boneSnap.d.ts +25 -0
- package/dist/editor/boneSnap.js +93 -0
- package/dist/editor/index.d.ts +4 -0
- package/dist/editor/index.js +13 -1
- package/dist/editor/studioTheme.d.ts +86 -0
- package/dist/editor/studioTheme.js +89 -0
- package/dist/editor/types.d.ts +18 -4
- package/dist/utils/avatarUtils.d.ts +2 -3
- package/dist/utils/avatarUtils.js +5 -6
- package/dist/wardrobe/wardrobeStore.d.ts +1 -1
- package/dist/wgpu/WgpuAvatar.d.ts +3 -0
- package/dist/wgpu/WgpuAvatar.js +54 -86
- package/dist/wgpu/morphTables.js +1 -1
- package/dist/wgpu/useAuthedModelUri.d.ts +11 -0
- package/dist/wgpu/{useAuthedFilamentUri.js → useAuthedModelUri.js} +9 -9
- package/package.json +2 -1
- package/dist/wgpu/useAuthedFilamentUri.d.ts +0 -11
|
@@ -46,10 +46,10 @@ function getLoadingLabel(stage) {
|
|
|
46
46
|
*/
|
|
47
47
|
exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl, authToken, cameraView = 'head', cameraDistance = 0.2, accessories, mood: initialMood = 'neutral', aspect, focalLength, visemeSchedule, onVisemeScheduleApplied, vendorBaseUrl }, ref) => {
|
|
48
48
|
const avatarRef = (0, react_1.useRef)(null);
|
|
49
|
-
// On native,
|
|
49
|
+
// On native, WgpuAvatar ref is wired via callback ref — store it here so
|
|
50
50
|
// scheduleVisemes / sendAmplitude can route to it.
|
|
51
51
|
const wgpuRef = (0, react_1.useRef)(null);
|
|
52
|
-
// Unified accessor —
|
|
52
|
+
// Unified accessor — WgpuAvatar on native, WebView on web
|
|
53
53
|
const activeAvatar = (0, react_1.useCallback)(() => (wgpuRef.current ?? avatarRef.current), []);
|
|
54
54
|
// Fallback local GLB data URI — resolved once on mount
|
|
55
55
|
const [fallbackUrl, setFallbackUrl] = (0, react_1.useState)(_fallbackDataUri);
|
|
@@ -114,7 +114,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
114
114
|
if (lastScheduledVisemeKeyRef.current === scheduleKey)
|
|
115
115
|
return;
|
|
116
116
|
const av = activeAvatar();
|
|
117
|
-
//
|
|
117
|
+
// WgpuAvatar buffers pending schedules internally — no ready gate needed.
|
|
118
118
|
// WebView (avatarRef) still needs the ready gate.
|
|
119
119
|
if (!av || (!wgpuRef.current && !isAvatarReady)) {
|
|
120
120
|
pendingVisemeScheduleRef.current = schedule;
|
|
@@ -181,7 +181,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
181
181
|
});
|
|
182
182
|
lastScheduledVisemeKeyRef.current = scheduleKey;
|
|
183
183
|
}, [isAvatarReady, onVisemeScheduleApplied, visemeSchedule, activeAvatar]);
|
|
184
|
-
// On native use
|
|
184
|
+
// On native use WgpuAvatar — direct morph writes, no WebView bridge.
|
|
185
185
|
if (react_native_1.Platform.OS !== 'web') {
|
|
186
186
|
return ((0, jsx_runtime_1.jsx)(WgpuAvatar_1.WgpuAvatar, { focalLength: focalLength, ref: (fr) => {
|
|
187
187
|
wgpuRef.current = fr;
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { AssetPlacement, EquippedAsset } from './types';
|
|
4
|
-
interface AvatarCanvasProps {
|
|
5
|
-
avatarUrl: string;
|
|
6
|
-
appearance?: AvatarAppearance;
|
|
7
|
-
equipped?: EquippedAsset[];
|
|
8
|
-
placements?: Record<string, AssetPlacement>;
|
|
9
|
-
editingAssetId?: string | null;
|
|
10
|
-
onPlacementChange?: (assetId: string, placement: AssetPlacement) => void;
|
|
1
|
+
import type { AvatarEditorProps } from './types';
|
|
2
|
+
export declare function AvatarCanvas({ avatarUrl, equipped, placements, editingAssetId, activeAssetId, onPlacementChange, onSceneRef, style, className, }: AvatarEditorProps & {
|
|
11
3
|
onSceneRef?: (scene: any) => void;
|
|
12
|
-
style?: React.CSSProperties;
|
|
13
4
|
className?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare function AvatarCanvas({ avatarUrl, equipped, placements, editingAssetId, onPlacementChange, onSceneRef, style, className, }: AvatarCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
-
export {};
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -18,7 +18,9 @@ function SceneRefCapture({ onSceneRef }) {
|
|
|
18
18
|
}, [scene, onSceneRef]);
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
function AvatarCanvas({ avatarUrl, equipped = [], placements = {}, editingAssetId = null, onPlacementChange, onSceneRef, style, className, }) {
|
|
21
|
+
function AvatarCanvas({ avatarUrl, equipped = [], placements = {}, editingAssetId = null, activeAssetId = null, onPlacementChange, onSceneRef, style, className, }) {
|
|
22
|
+
// Support both prop names — web consumers use editingAssetId, native uses activeAssetId
|
|
23
|
+
const editingId = editingAssetId ?? activeAssetId;
|
|
22
24
|
const [skeleton, setSkeleton] = (0, react_1.useState)(null);
|
|
23
25
|
const [avatarScene, setAvatarScene] = (0, react_1.useState)(null);
|
|
24
26
|
const [cameraTarget, setCameraTarget] = (0, react_1.useState)([0, 1, 0]);
|
|
@@ -43,12 +45,11 @@ function AvatarCanvas({ avatarUrl, equipped = [], placements = {}, editingAssetI
|
|
|
43
45
|
]
|
|
44
46
|
.filter(Boolean)
|
|
45
47
|
.join(' ');
|
|
46
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: wrapperClass, style: style, children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 opacity-[0.04] pointer-events-none", style: { backgroundImage: 'linear-gradient(rgba(255,255,255,0.3) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.3) 1px,transparent 1px)', backgroundSize: '60px 60px' } }), avatarUrl ? ((0, jsx_runtime_1.jsx)(AvatarCanvasErrorBoundary_1.AvatarCanvasErrorBoundary, { children: (0, jsx_runtime_1.jsx)(fiber_1.Canvas, { ref: canvasRef, camera: { position: cameraPosition, fov: 45 }, style: { width: '100%', height: '100%' }, children: (0, jsx_runtime_1.jsxs)(react_1.Suspense, { fallback: null, children: [onSceneRef && (0, jsx_runtime_1.jsx)(SceneRefCapture, { onSceneRef: onSceneRef }), (0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.6 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [5, 5, 5], intensity: 0.8 }), (0, jsx_runtime_1.jsx)(drei_1.Environment, { preset: "studio" }), (0, jsx_runtime_1.jsx)(drei_1.OrbitControls, { target: cameraTarget, minDistance: 1, maxDistance: 10, enablePan: false, makeDefault: true, enabled: !
|
|
48
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: wrapperClass, style: style, children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 opacity-[0.04] pointer-events-none", style: { backgroundImage: 'linear-gradient(rgba(255,255,255,0.3) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.3) 1px,transparent 1px)', backgroundSize: '60px 60px' } }), avatarUrl ? ((0, jsx_runtime_1.jsx)(AvatarCanvasErrorBoundary_1.AvatarCanvasErrorBoundary, { children: (0, jsx_runtime_1.jsx)(fiber_1.Canvas, { ref: canvasRef, camera: { position: cameraPosition, fov: 45 }, style: { width: '100%', height: '100%' }, children: (0, jsx_runtime_1.jsxs)(react_1.Suspense, { fallback: null, children: [onSceneRef && (0, jsx_runtime_1.jsx)(SceneRefCapture, { onSceneRef: onSceneRef }), (0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.6 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [5, 5, 5], intensity: 0.8 }), (0, jsx_runtime_1.jsx)(drei_1.Environment, { preset: "studio" }), (0, jsx_runtime_1.jsx)(drei_1.OrbitControls, { target: cameraTarget, minDistance: 1, maxDistance: 10, enablePan: false, makeDefault: true, enabled: !editingId }), (0, jsx_runtime_1.jsx)(AvatarModel_1.AvatarModel, { url: avatarUrl, scale: 1, onSkeletonReady: handleSkeletonReady, onBoundsReady: handleBoundsReady }), equippedItems.map((asset) => {
|
|
47
49
|
if (!asset)
|
|
48
50
|
return null;
|
|
49
51
|
const llmPlacement = placements[asset.id];
|
|
50
|
-
const isEditing =
|
|
51
|
-
// If a placement is provided, always render as rigid attachment
|
|
52
|
+
const isEditing = editingId === asset.id;
|
|
52
53
|
if (llmPlacement) {
|
|
53
54
|
const bone = llmPlacement.bone ?? asset.attach_bone;
|
|
54
55
|
if (!bone)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AvatarCanvas as AvatarEditor } from './AvatarCanvas';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AvatarEditor = void 0;
|
|
4
|
+
// Web: delegate to AvatarCanvas (existing web implementation).
|
|
5
|
+
var AvatarCanvas_1 = require("./AvatarCanvas");
|
|
6
|
+
Object.defineProperty(exports, "AvatarEditor", { enumerable: true, get: function () { return AvatarCanvas_1.AvatarCanvas; } });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AvatarEditorProps } from './types';
|
|
2
|
+
export type { AvatarEditorProps };
|
|
3
|
+
export declare function AvatarEditor({ avatarUrl, activeAssetId, onPlacementChange, style, }: AvatarEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export default AvatarEditor;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.AvatarEditor = void 0;
|
|
27
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
28
|
+
/**
|
|
29
|
+
* Native avatar placement editor.
|
|
30
|
+
* Uses WgpuAvatar (R3F + expo-gl) as the render surface and Three.js raycasting
|
|
31
|
+
* for gesture-driven accessory placement. Replaces the old FilamentEditor.
|
|
32
|
+
*/
|
|
33
|
+
const react_1 = require("react");
|
|
34
|
+
const react_native_1 = require("react-native");
|
|
35
|
+
const THREE = __importStar(require("three"));
|
|
36
|
+
const wgpu_1 = require("../wgpu");
|
|
37
|
+
const boneSnap_1 = require("./boneSnap");
|
|
38
|
+
function tapToNDC(evt, layout) {
|
|
39
|
+
const { locationX, locationY } = evt.nativeEvent;
|
|
40
|
+
return new THREE.Vector2((locationX / layout.width) * 2 - 1, -((locationY / layout.height) * 2 - 1));
|
|
41
|
+
}
|
|
42
|
+
function AvatarEditor({ avatarUrl, activeAssetId = null, onPlacementChange, style, }) {
|
|
43
|
+
const cameraRef = (0, react_1.useRef)(null);
|
|
44
|
+
const layoutRef = (0, react_1.useRef)({ width: 1, height: 1 });
|
|
45
|
+
const [avatarScene, setAvatarScene] = (0, react_1.useState)(null);
|
|
46
|
+
const handleSceneReady = (0, react_1.useCallback)((scene, camera) => {
|
|
47
|
+
cameraRef.current = camera;
|
|
48
|
+
let avatarRoot = scene;
|
|
49
|
+
scene.traverse((obj) => {
|
|
50
|
+
if (obj instanceof THREE.SkinnedMesh && avatarRoot === scene) {
|
|
51
|
+
avatarRoot = obj.parent ?? obj;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
setAvatarScene(avatarRoot);
|
|
55
|
+
}, []);
|
|
56
|
+
const handleTap = (0, react_1.useCallback)((evt) => {
|
|
57
|
+
if (!activeAssetId || !cameraRef.current || !avatarScene)
|
|
58
|
+
return;
|
|
59
|
+
const ndc = tapToNDC(evt, layoutRef.current);
|
|
60
|
+
const raycaster = new THREE.Raycaster();
|
|
61
|
+
raycaster.setFromCamera(ndc, cameraRef.current);
|
|
62
|
+
const meshes = [];
|
|
63
|
+
avatarScene.traverse((obj) => {
|
|
64
|
+
if (obj instanceof THREE.Mesh || obj instanceof THREE.SkinnedMesh) {
|
|
65
|
+
meshes.push(obj);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const hits = raycaster.intersectObjects(meshes, false);
|
|
69
|
+
if (hits.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
const hitPoint = hits[0].point;
|
|
72
|
+
const snap = (0, boneSnap_1.findNearestBone)(avatarScene, hitPoint);
|
|
73
|
+
if (!snap)
|
|
74
|
+
return;
|
|
75
|
+
const offsetPos = [
|
|
76
|
+
hitPoint.x - snap.position.x,
|
|
77
|
+
hitPoint.y - snap.position.y,
|
|
78
|
+
hitPoint.z - snap.position.z,
|
|
79
|
+
];
|
|
80
|
+
onPlacementChange?.(activeAssetId, {
|
|
81
|
+
bone: snap.bone,
|
|
82
|
+
position: offsetPos,
|
|
83
|
+
rotation: [0, 0, 0],
|
|
84
|
+
scale: 1,
|
|
85
|
+
});
|
|
86
|
+
}, [activeAssetId, avatarScene, onPlacementChange]);
|
|
87
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, style], onLayout: (e) => { layoutRef.current = e.nativeEvent.layout; }, children: [(0, jsx_runtime_1.jsx)(wgpu_1.WgpuAvatar, { avatarUrl: avatarUrl, style: react_native_1.StyleSheet.absoluteFill, onSceneReady: handleSceneReady }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: react_native_1.StyleSheet.absoluteFill, onTouchEnd: handleTap, pointerEvents: activeAssetId ? 'auto' : 'none' })] }));
|
|
88
|
+
}
|
|
89
|
+
exports.AvatarEditor = AvatarEditor;
|
|
90
|
+
exports.default = AvatarEditor;
|
|
91
|
+
const styles = react_native_1.StyleSheet.create({
|
|
92
|
+
container: { flex: 1, overflow: 'hidden' },
|
|
93
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
export declare const HUMANOID_BONES: readonly ["Hips", "Spine", "Spine1", "Spine2", "Neck", "Head", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase"];
|
|
3
|
+
export type HumanoidBone = (typeof HUMANOID_BONES)[number];
|
|
4
|
+
export interface BoneSnapResult {
|
|
5
|
+
bone: string;
|
|
6
|
+
position: THREE.Vector3;
|
|
7
|
+
distance: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Given an avatar scene and a world-space position, finds the nearest skeleton
|
|
11
|
+
* bone whose name matches a known humanoid bone name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findNearestBone(avatarScene: THREE.Object3D, worldPos: THREE.Vector3, maxDistance?: number): BoneSnapResult | null;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the world-space position of a named bone in the avatar scene.
|
|
16
|
+
* Returns the scene origin if the bone is not found.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getWorldPositionForBone(avatarScene: THREE.Object3D, boneName: string): THREE.Vector3;
|
|
19
|
+
export declare const getWorldPositionForPlacement: typeof getWorldPositionForBone;
|
|
20
|
+
export declare function snapPlacementToNearestBone(avatarScene: THREE.Object3D, worldPos: THREE.Vector3): BoneSnapResult | null;
|
|
21
|
+
/**
|
|
22
|
+
* Hook that returns a snap function. Fires haptic feedback when a new bone
|
|
23
|
+
* is snapped to.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useBoneSnap(avatarScene: THREE.Object3D | null): (worldPos: THREE.Vector3) => BoneSnapResult | null;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.useBoneSnap = exports.snapPlacementToNearestBone = exports.getWorldPositionForPlacement = exports.getWorldPositionForBone = exports.findNearestBone = exports.HUMANOID_BONES = void 0;
|
|
27
|
+
const react_1 = require("react");
|
|
28
|
+
const Haptics = __importStar(require("expo-haptics"));
|
|
29
|
+
const THREE = __importStar(require("three"));
|
|
30
|
+
exports.HUMANOID_BONES = [
|
|
31
|
+
'Hips', 'Spine', 'Spine1', 'Spine2', 'Neck', 'Head',
|
|
32
|
+
'LeftShoulder', 'LeftArm', 'LeftForeArm', 'LeftHand',
|
|
33
|
+
'RightShoulder', 'RightArm', 'RightForeArm', 'RightHand',
|
|
34
|
+
'LeftUpLeg', 'LeftLeg', 'LeftFoot', 'LeftToeBase',
|
|
35
|
+
'RightUpLeg', 'RightLeg', 'RightFoot', 'RightToeBase',
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Given an avatar scene and a world-space position, finds the nearest skeleton
|
|
39
|
+
* bone whose name matches a known humanoid bone name.
|
|
40
|
+
*/
|
|
41
|
+
function findNearestBone(avatarScene, worldPos, maxDistance = 0.35) {
|
|
42
|
+
let best = null;
|
|
43
|
+
const boneWorldPos = new THREE.Vector3();
|
|
44
|
+
avatarScene.traverse((obj) => {
|
|
45
|
+
if (!(obj instanceof THREE.Bone))
|
|
46
|
+
return;
|
|
47
|
+
if (!exports.HUMANOID_BONES.includes(obj.name))
|
|
48
|
+
return;
|
|
49
|
+
obj.getWorldPosition(boneWorldPos);
|
|
50
|
+
const dist = worldPos.distanceTo(boneWorldPos);
|
|
51
|
+
if (dist < maxDistance && (!best || dist < best.distance)) {
|
|
52
|
+
best = { bone: obj.name, position: boneWorldPos.clone(), distance: dist };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
exports.findNearestBone = findNearestBone;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the world-space position of a named bone in the avatar scene.
|
|
60
|
+
* Returns the scene origin if the bone is not found.
|
|
61
|
+
*/
|
|
62
|
+
function getWorldPositionForBone(avatarScene, boneName) {
|
|
63
|
+
const result = new THREE.Vector3();
|
|
64
|
+
avatarScene.traverse((obj) => {
|
|
65
|
+
if (obj instanceof THREE.Bone && obj.name === boneName) {
|
|
66
|
+
obj.getWorldPosition(result);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
exports.getWorldPositionForBone = getWorldPositionForBone;
|
|
72
|
+
// Legacy alias kept for call-site compatibility
|
|
73
|
+
exports.getWorldPositionForPlacement = getWorldPositionForBone;
|
|
74
|
+
function snapPlacementToNearestBone(avatarScene, worldPos) {
|
|
75
|
+
return findNearestBone(avatarScene, worldPos);
|
|
76
|
+
}
|
|
77
|
+
exports.snapPlacementToNearestBone = snapPlacementToNearestBone;
|
|
78
|
+
/**
|
|
79
|
+
* Hook that returns a snap function. Fires haptic feedback when a new bone
|
|
80
|
+
* is snapped to.
|
|
81
|
+
*/
|
|
82
|
+
function useBoneSnap(avatarScene) {
|
|
83
|
+
return (0, react_1.useCallback)((worldPos) => {
|
|
84
|
+
if (!avatarScene)
|
|
85
|
+
return null;
|
|
86
|
+
const result = findNearestBone(avatarScene, worldPos);
|
|
87
|
+
if (result) {
|
|
88
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => null);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}, [avatarScene]);
|
|
92
|
+
}
|
|
93
|
+
exports.useBoneSnap = useBoneSnap;
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { AvatarCanvas } from './AvatarCanvas';
|
|
2
|
+
export { AvatarEditor } from './AvatarEditor';
|
|
2
3
|
export { AvatarModel } from './AvatarModel';
|
|
3
4
|
export { RigidAccessory } from './RigidAccessory';
|
|
4
5
|
export { SkinnedClothing } from './SkinnedClothing';
|
|
6
|
+
export { studioTheme, withAlpha } from './studioTheme';
|
|
7
|
+
export { useBoneSnap, findNearestBone, getWorldPositionForBone, getWorldPositionForPlacement, snapPlacementToNearestBone, HUMANOID_BONES } from './boneSnap';
|
|
5
8
|
export type { AvatarEditorProps, AssetPlacement, EquippedAsset } from './types';
|
|
9
|
+
export type { BoneSnapResult, HumanoidBone } from './boneSnap';
|
package/dist/editor/index.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SkinnedClothing = exports.RigidAccessory = exports.AvatarModel = exports.AvatarCanvas = void 0;
|
|
3
|
+
exports.HUMANOID_BONES = exports.snapPlacementToNearestBone = exports.getWorldPositionForPlacement = exports.getWorldPositionForBone = exports.findNearestBone = exports.useBoneSnap = exports.withAlpha = exports.studioTheme = exports.SkinnedClothing = exports.RigidAccessory = exports.AvatarModel = exports.AvatarEditor = exports.AvatarCanvas = void 0;
|
|
4
4
|
var AvatarCanvas_1 = require("./AvatarCanvas");
|
|
5
5
|
Object.defineProperty(exports, "AvatarCanvas", { enumerable: true, get: function () { return AvatarCanvas_1.AvatarCanvas; } });
|
|
6
|
+
var AvatarEditor_1 = require("./AvatarEditor");
|
|
7
|
+
Object.defineProperty(exports, "AvatarEditor", { enumerable: true, get: function () { return AvatarEditor_1.AvatarEditor; } });
|
|
6
8
|
var AvatarModel_1 = require("./AvatarModel");
|
|
7
9
|
Object.defineProperty(exports, "AvatarModel", { enumerable: true, get: function () { return AvatarModel_1.AvatarModel; } });
|
|
8
10
|
var RigidAccessory_1 = require("./RigidAccessory");
|
|
9
11
|
Object.defineProperty(exports, "RigidAccessory", { enumerable: true, get: function () { return RigidAccessory_1.RigidAccessory; } });
|
|
10
12
|
var SkinnedClothing_1 = require("./SkinnedClothing");
|
|
11
13
|
Object.defineProperty(exports, "SkinnedClothing", { enumerable: true, get: function () { return SkinnedClothing_1.SkinnedClothing; } });
|
|
14
|
+
var studioTheme_1 = require("./studioTheme");
|
|
15
|
+
Object.defineProperty(exports, "studioTheme", { enumerable: true, get: function () { return studioTheme_1.studioTheme; } });
|
|
16
|
+
Object.defineProperty(exports, "withAlpha", { enumerable: true, get: function () { return studioTheme_1.withAlpha; } });
|
|
17
|
+
var boneSnap_1 = require("./boneSnap");
|
|
18
|
+
Object.defineProperty(exports, "useBoneSnap", { enumerable: true, get: function () { return boneSnap_1.useBoneSnap; } });
|
|
19
|
+
Object.defineProperty(exports, "findNearestBone", { enumerable: true, get: function () { return boneSnap_1.findNearestBone; } });
|
|
20
|
+
Object.defineProperty(exports, "getWorldPositionForBone", { enumerable: true, get: function () { return boneSnap_1.getWorldPositionForBone; } });
|
|
21
|
+
Object.defineProperty(exports, "getWorldPositionForPlacement", { enumerable: true, get: function () { return boneSnap_1.getWorldPositionForPlacement; } });
|
|
22
|
+
Object.defineProperty(exports, "snapPlacementToNearestBone", { enumerable: true, get: function () { return boneSnap_1.snapPlacementToNearestBone; } });
|
|
23
|
+
Object.defineProperty(exports, "HUMANOID_BONES", { enumerable: true, get: function () { return boneSnap_1.HUMANOID_BONES; } });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export declare const studioTheme: {
|
|
2
|
+
readonly colors: {
|
|
3
|
+
readonly background: "#111315";
|
|
4
|
+
readonly backgroundAlt: "#191d20";
|
|
5
|
+
readonly surface: "#1c2125";
|
|
6
|
+
readonly surfaceRaised: "#252b30";
|
|
7
|
+
readonly surfaceSoft: "#2b3238";
|
|
8
|
+
readonly border: "#394249";
|
|
9
|
+
readonly borderStrong: "#53606a";
|
|
10
|
+
readonly textPrimary: "#f6efe4";
|
|
11
|
+
readonly textSecondary: "#cabfaa";
|
|
12
|
+
readonly textMuted: "#8b847a";
|
|
13
|
+
readonly accent: "#ff7a59";
|
|
14
|
+
readonly accentStrong: "#ff9c72";
|
|
15
|
+
readonly accentMuted: "rgba(255, 122, 89, 0.16)";
|
|
16
|
+
readonly success: "#69c08a";
|
|
17
|
+
readonly danger: "#f07a7a";
|
|
18
|
+
readonly shadow: "rgba(0, 0, 0, 0.32)";
|
|
19
|
+
readonly haze: "rgba(255, 255, 255, 0.05)";
|
|
20
|
+
};
|
|
21
|
+
readonly spacing: {
|
|
22
|
+
readonly xs: 6;
|
|
23
|
+
readonly sm: 10;
|
|
24
|
+
readonly md: 16;
|
|
25
|
+
readonly lg: 22;
|
|
26
|
+
readonly xl: 28;
|
|
27
|
+
readonly xxl: 36;
|
|
28
|
+
};
|
|
29
|
+
readonly radius: {
|
|
30
|
+
readonly sm: 12;
|
|
31
|
+
readonly md: 18;
|
|
32
|
+
readonly lg: 24;
|
|
33
|
+
readonly pill: 999;
|
|
34
|
+
};
|
|
35
|
+
readonly shadow: {
|
|
36
|
+
readonly glow: {
|
|
37
|
+
readonly shadowColor: "#000000";
|
|
38
|
+
readonly shadowOffset: {
|
|
39
|
+
readonly width: 0;
|
|
40
|
+
readonly height: 10;
|
|
41
|
+
};
|
|
42
|
+
readonly shadowOpacity: 0.24;
|
|
43
|
+
readonly shadowRadius: 24;
|
|
44
|
+
readonly elevation: 10;
|
|
45
|
+
};
|
|
46
|
+
readonly card: {
|
|
47
|
+
readonly shadowColor: "#000000";
|
|
48
|
+
readonly shadowOffset: {
|
|
49
|
+
readonly width: 0;
|
|
50
|
+
readonly height: 8;
|
|
51
|
+
};
|
|
52
|
+
readonly shadowOpacity: 0.16;
|
|
53
|
+
readonly shadowRadius: 18;
|
|
54
|
+
readonly elevation: 6;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
readonly type: {
|
|
58
|
+
readonly eyebrow: {
|
|
59
|
+
readonly fontSize: 12;
|
|
60
|
+
readonly fontWeight: "700";
|
|
61
|
+
readonly letterSpacing: 1.6;
|
|
62
|
+
readonly textTransform: "uppercase";
|
|
63
|
+
};
|
|
64
|
+
readonly sectionTitle: {
|
|
65
|
+
readonly fontSize: 24;
|
|
66
|
+
readonly fontWeight: "700";
|
|
67
|
+
readonly letterSpacing: 0.2;
|
|
68
|
+
};
|
|
69
|
+
readonly body: {
|
|
70
|
+
readonly fontSize: 15;
|
|
71
|
+
readonly lineHeight: 22;
|
|
72
|
+
readonly fontWeight: "500";
|
|
73
|
+
};
|
|
74
|
+
readonly bodySmall: {
|
|
75
|
+
readonly fontSize: 13;
|
|
76
|
+
readonly lineHeight: 18;
|
|
77
|
+
readonly fontWeight: "500";
|
|
78
|
+
};
|
|
79
|
+
readonly button: {
|
|
80
|
+
readonly fontSize: 15;
|
|
81
|
+
readonly fontWeight: "700";
|
|
82
|
+
readonly letterSpacing: 0.2;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
export declare function withAlpha(hexOrRgb: string, alphaHex: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withAlpha = exports.studioTheme = void 0;
|
|
4
|
+
exports.studioTheme = {
|
|
5
|
+
colors: {
|
|
6
|
+
background: '#111315',
|
|
7
|
+
backgroundAlt: '#191d20',
|
|
8
|
+
surface: '#1c2125',
|
|
9
|
+
surfaceRaised: '#252b30',
|
|
10
|
+
surfaceSoft: '#2b3238',
|
|
11
|
+
border: '#394249',
|
|
12
|
+
borderStrong: '#53606a',
|
|
13
|
+
textPrimary: '#f6efe4',
|
|
14
|
+
textSecondary: '#cabfaa',
|
|
15
|
+
textMuted: '#8b847a',
|
|
16
|
+
accent: '#ff7a59',
|
|
17
|
+
accentStrong: '#ff9c72',
|
|
18
|
+
accentMuted: 'rgba(255, 122, 89, 0.16)',
|
|
19
|
+
success: '#69c08a',
|
|
20
|
+
danger: '#f07a7a',
|
|
21
|
+
shadow: 'rgba(0, 0, 0, 0.32)',
|
|
22
|
+
haze: 'rgba(255, 255, 255, 0.05)',
|
|
23
|
+
},
|
|
24
|
+
spacing: {
|
|
25
|
+
xs: 6,
|
|
26
|
+
sm: 10,
|
|
27
|
+
md: 16,
|
|
28
|
+
lg: 22,
|
|
29
|
+
xl: 28,
|
|
30
|
+
xxl: 36,
|
|
31
|
+
},
|
|
32
|
+
radius: {
|
|
33
|
+
sm: 12,
|
|
34
|
+
md: 18,
|
|
35
|
+
lg: 24,
|
|
36
|
+
pill: 999,
|
|
37
|
+
},
|
|
38
|
+
shadow: {
|
|
39
|
+
glow: {
|
|
40
|
+
shadowColor: '#000000',
|
|
41
|
+
shadowOffset: { width: 0, height: 10 },
|
|
42
|
+
shadowOpacity: 0.24,
|
|
43
|
+
shadowRadius: 24,
|
|
44
|
+
elevation: 10,
|
|
45
|
+
},
|
|
46
|
+
card: {
|
|
47
|
+
shadowColor: '#000000',
|
|
48
|
+
shadowOffset: { width: 0, height: 8 },
|
|
49
|
+
shadowOpacity: 0.16,
|
|
50
|
+
shadowRadius: 18,
|
|
51
|
+
elevation: 6,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
type: {
|
|
55
|
+
eyebrow: {
|
|
56
|
+
fontSize: 12,
|
|
57
|
+
fontWeight: '700',
|
|
58
|
+
letterSpacing: 1.6,
|
|
59
|
+
textTransform: 'uppercase',
|
|
60
|
+
},
|
|
61
|
+
sectionTitle: {
|
|
62
|
+
fontSize: 24,
|
|
63
|
+
fontWeight: '700',
|
|
64
|
+
letterSpacing: 0.2,
|
|
65
|
+
},
|
|
66
|
+
body: {
|
|
67
|
+
fontSize: 15,
|
|
68
|
+
lineHeight: 22,
|
|
69
|
+
fontWeight: '500',
|
|
70
|
+
},
|
|
71
|
+
bodySmall: {
|
|
72
|
+
fontSize: 13,
|
|
73
|
+
lineHeight: 18,
|
|
74
|
+
fontWeight: '500',
|
|
75
|
+
},
|
|
76
|
+
button: {
|
|
77
|
+
fontSize: 15,
|
|
78
|
+
fontWeight: '700',
|
|
79
|
+
letterSpacing: 0.2,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function withAlpha(hexOrRgb, alphaHex) {
|
|
84
|
+
if (hexOrRgb.startsWith('#')) {
|
|
85
|
+
return `${hexOrRgb}${alphaHex}`;
|
|
86
|
+
}
|
|
87
|
+
return hexOrRgb;
|
|
88
|
+
}
|
|
89
|
+
exports.withAlpha = withAlpha;
|
package/dist/editor/types.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AvatarAppearance } from '../appearance';
|
|
2
|
-
import type React from 'react';
|
|
3
2
|
export interface AssetPlacement {
|
|
4
3
|
bone: string;
|
|
5
4
|
position: [number, number, number];
|
|
@@ -15,13 +14,28 @@ export interface EquippedAsset {
|
|
|
15
14
|
offset_position?: [number, number, number];
|
|
16
15
|
offset_rotation?: [number, number, number];
|
|
17
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Shared props understood by both AvatarCanvas (web) and AvatarEditor (native).
|
|
19
|
+
* Web-only fields (className, appearance) are optional and ignored on native.
|
|
20
|
+
* Native-only fields (activeAssetId, onActiveAssetChange, onDone, skinColor)
|
|
21
|
+
* are optional and ignored on web.
|
|
22
|
+
*
|
|
23
|
+
* `style` is typed as `any` to avoid importing react-native into a file
|
|
24
|
+
* compiled by plain tsc. Call-sites should cast to the appropriate type.
|
|
25
|
+
*/
|
|
18
26
|
export interface AvatarEditorProps {
|
|
19
27
|
avatarUrl: string;
|
|
20
|
-
appearance?: AvatarAppearance;
|
|
21
28
|
equipped?: EquippedAsset[];
|
|
22
29
|
placements?: Record<string, AssetPlacement>;
|
|
23
|
-
editingAssetId?: string | null;
|
|
24
30
|
onPlacementChange?: (assetId: string, placement: AssetPlacement) => void;
|
|
31
|
+
/** @web which asset is currently being placed */
|
|
32
|
+
editingAssetId?: string | null;
|
|
33
|
+
appearance?: AvatarAppearance;
|
|
25
34
|
className?: string;
|
|
26
|
-
style?:
|
|
35
|
+
style?: any;
|
|
36
|
+
/** @native which asset is currently being placed (maps to editingAssetId on web) */
|
|
37
|
+
activeAssetId?: string | null;
|
|
38
|
+
onActiveAssetChange?: (id: string | null) => void;
|
|
39
|
+
onDone?: () => void;
|
|
40
|
+
skinColor?: string;
|
|
27
41
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves a local Expo asset module into a file:// URI for use with
|
|
3
|
-
*
|
|
4
|
-
* file:// directly on both iOS and Android.
|
|
3
|
+
* WgpuAvatar model loading.
|
|
5
4
|
*/
|
|
6
|
-
export declare function
|
|
5
|
+
export declare function resolveLocalAssetUri(module: unknown): Promise<string | null>;
|
|
7
6
|
/**
|
|
8
7
|
* Resolves a local Expo asset module (from require()) into a usable URL string.
|
|
9
8
|
* On web, returns an absolute URL.
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveLocalAssetUrl = exports.
|
|
3
|
+
exports.resolveLocalAssetUrl = exports.resolveLocalAssetUri = void 0;
|
|
4
4
|
const expo_asset_1 = require("expo-asset");
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
6
|
const expo_file_system_1 = require("expo-file-system");
|
|
7
7
|
/**
|
|
8
8
|
* Resolves a local Expo asset module into a file:// URI for use with
|
|
9
|
-
*
|
|
10
|
-
* file:// directly on both iOS and Android.
|
|
9
|
+
* WgpuAvatar model loading.
|
|
11
10
|
*/
|
|
12
|
-
async function
|
|
11
|
+
async function resolveLocalAssetUri(module) {
|
|
13
12
|
try {
|
|
14
13
|
const asset = expo_asset_1.Asset.fromModule(module);
|
|
15
14
|
await asset.downloadAsync();
|
|
@@ -17,11 +16,11 @@ async function resolveFilamentAssetUri(module) {
|
|
|
17
16
|
return uri ?? null;
|
|
18
17
|
}
|
|
19
18
|
catch (e) {
|
|
20
|
-
console.error('[AssetUtils] Failed to resolve
|
|
19
|
+
console.error('[AssetUtils] Failed to resolve asset:', e);
|
|
21
20
|
return null;
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
exports.
|
|
23
|
+
exports.resolveLocalAssetUri = resolveLocalAssetUri;
|
|
25
24
|
/**
|
|
26
25
|
* Resolves a local Expo asset module (from require()) into a usable URL string.
|
|
27
26
|
* On web, returns an absolute URL.
|
|
@@ -11,7 +11,7 @@ export interface WardrobeState {
|
|
|
11
11
|
equipped: Record<string, WearableAsset>;
|
|
12
12
|
/** assetId -> placement transform */
|
|
13
13
|
placements: Record<string, AssetPlacement>;
|
|
14
|
-
/** asset currently being positioned (
|
|
14
|
+
/** asset currently being positioned (native mode) */
|
|
15
15
|
activeAssetId: string | null;
|
|
16
16
|
/** current gizmo operation mode */
|
|
17
17
|
gizmoMode: GizmoMode;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
16
|
+
import * as THREE from 'three';
|
|
16
17
|
import type { TalkingHeadMood, TalkingHeadAccessory, TalkingHeadVisemeSchedule } from '../index';
|
|
17
18
|
export interface WgpuAvatarRef {
|
|
18
19
|
setMood: (mood: TalkingHeadMood) => void;
|
|
@@ -31,6 +32,8 @@ interface WgpuAvatarProps {
|
|
|
31
32
|
accessories?: TalkingHeadAccessory[];
|
|
32
33
|
onReady?: () => void;
|
|
33
34
|
onError?: (message: string) => void;
|
|
35
|
+
/** Called once the R3F scene and camera are available (for editor raycasting). */
|
|
36
|
+
onSceneReady?: (scene: THREE.Scene, camera: THREE.Camera) => void;
|
|
34
37
|
}
|
|
35
38
|
export declare const WgpuAvatar: React.ForwardRefExoticComponent<WgpuAvatarProps & React.RefAttributes<WgpuAvatarRef>>;
|
|
36
39
|
export {};
|
package/dist/wgpu/WgpuAvatar.js
CHANGED
|
@@ -44,13 +44,25 @@ const THREE = __importStar(require("three"));
|
|
|
44
44
|
const native_1 = require("@react-three/fiber/native");
|
|
45
45
|
const native_2 = require("@react-three/drei/native");
|
|
46
46
|
const morphTables_1 = require("./morphTables");
|
|
47
|
-
const
|
|
47
|
+
const useAuthedModelUri_1 = require("./useAuthedModelUri");
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
// Camera defaults — match FilamentAvatar constants
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
const CAMERA_POSITION = [0, 1.52, 0.85];
|
|
52
52
|
const CAMERA_TARGET = new THREE.Vector3(0, 1.52, 0);
|
|
53
|
-
const CAMERA_FOV_FULL = 38;
|
|
53
|
+
const CAMERA_FOV_FULL = 38;
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Scene/camera ref capture — runs inside Canvas so it can access R3F context
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function SceneCapture({ onSceneReady }) {
|
|
58
|
+
const { scene, camera } = (0, native_1.useThree)();
|
|
59
|
+
(0, react_1.useEffect)(() => {
|
|
60
|
+
onSceneReady(scene, camera);
|
|
61
|
+
// Only fire once on mount — scene/camera identity is stable
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, []);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
54
66
|
function buildMorphIndex(mesh) {
|
|
55
67
|
const map = new Map();
|
|
56
68
|
const dict = mesh.morphTargetDictionary;
|
|
@@ -58,7 +70,7 @@ function buildMorphIndex(mesh) {
|
|
|
58
70
|
return map;
|
|
59
71
|
for (const [name, idx] of Object.entries(dict)) {
|
|
60
72
|
map.set(name.toLowerCase(), idx);
|
|
61
|
-
map.set(name, idx);
|
|
73
|
+
map.set(name, idx);
|
|
62
74
|
}
|
|
63
75
|
return map;
|
|
64
76
|
}
|
|
@@ -70,13 +82,12 @@ function resolveIndex(morphIndex, aliases) {
|
|
|
70
82
|
}
|
|
71
83
|
return undefined;
|
|
72
84
|
}
|
|
73
|
-
function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
85
|
+
function AvatarScene({ uri, morphStateRef, fov, onReady, onError, onSceneReady }) {
|
|
74
86
|
const { camera } = (0, native_1.useThree)();
|
|
75
87
|
const gltf = (0, native_2.useGLTF)(uri);
|
|
76
88
|
const headMeshRef = (0, react_1.useRef)(null);
|
|
77
89
|
const morphIndexRef = (0, react_1.useRef)(new Map());
|
|
78
90
|
const readyFiredRef = (0, react_1.useRef)(false);
|
|
79
|
-
// Set up camera on mount
|
|
80
91
|
(0, react_1.useEffect)(() => {
|
|
81
92
|
if (!(camera instanceof THREE.PerspectiveCamera))
|
|
82
93
|
return;
|
|
@@ -85,7 +96,6 @@ function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
|
85
96
|
camera.lookAt(CAMERA_TARGET);
|
|
86
97
|
camera.updateProjectionMatrix();
|
|
87
98
|
}, [camera, fov]);
|
|
88
|
-
// Find the head/face mesh with morph targets after GLTF loads
|
|
89
99
|
(0, react_1.useEffect)(() => {
|
|
90
100
|
if (!gltf?.scene)
|
|
91
101
|
return;
|
|
@@ -111,58 +121,43 @@ function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
|
111
121
|
onReady();
|
|
112
122
|
}
|
|
113
123
|
}, [gltf, onReady, onError]);
|
|
114
|
-
// Per-frame morph weight application
|
|
115
124
|
(0, native_1.useFrame)((_, delta) => {
|
|
116
125
|
const mesh = headMeshRef.current;
|
|
117
126
|
if (!mesh?.morphTargetInfluences)
|
|
118
127
|
return;
|
|
119
128
|
const state = morphStateRef.current;
|
|
120
|
-
const alpha = Math.min(1, state.alpha * delta * 60);
|
|
121
|
-
// Merge mood base + viseme target
|
|
129
|
+
const alpha = Math.min(1, state.alpha * delta * 60);
|
|
122
130
|
const combined = { ...state.moodBase };
|
|
123
131
|
for (const [name, w] of Object.entries(state.visemeTarget)) {
|
|
124
132
|
combined[name] = Math.max(combined[name] ?? 0, w);
|
|
125
133
|
}
|
|
126
|
-
|
|
127
|
-
const allNames = new Set([
|
|
128
|
-
...Object.keys(state.current),
|
|
129
|
-
...Object.keys(combined),
|
|
130
|
-
]);
|
|
134
|
+
const allNames = new Set([...Object.keys(state.current), ...Object.keys(combined)]);
|
|
131
135
|
for (const name of allNames) {
|
|
132
136
|
const target = combined[name] ?? 0;
|
|
133
137
|
const cur = state.current[name] ?? 0;
|
|
134
138
|
const next = cur + (target - cur) * alpha;
|
|
135
139
|
state.current[name] = next;
|
|
136
140
|
const idx = resolveIndex(morphIndexRef.current, [name]);
|
|
137
|
-
if (idx !== undefined)
|
|
141
|
+
if (idx !== undefined)
|
|
138
142
|
mesh.morphTargetInfluences[idx] = next;
|
|
139
|
-
}
|
|
140
143
|
}
|
|
141
|
-
// Decay viseme layer
|
|
142
144
|
for (const name of Object.keys(state.visemeTarget)) {
|
|
143
|
-
state.visemeTarget[name] *= Math.pow(0.1, delta);
|
|
144
|
-
if (state.visemeTarget[name] < 0.001)
|
|
145
|
+
state.visemeTarget[name] *= Math.pow(0.1, delta);
|
|
146
|
+
if (state.visemeTarget[name] < 0.001)
|
|
145
147
|
delete state.visemeTarget[name];
|
|
146
|
-
}
|
|
147
148
|
}
|
|
148
149
|
});
|
|
149
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.6 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [1, 3, 2], intensity: 1.2, castShadow: false }), (0, jsx_runtime_1.jsx)("primitive", { object: gltf.scene })] }));
|
|
150
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [onSceneReady && (0, jsx_runtime_1.jsx)(SceneCapture, { onSceneReady: onSceneReady }), (0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.6 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [1, 3, 2], intensity: 1.2, castShadow: false }), (0, jsx_runtime_1.jsx)("primitive", { object: gltf.scene })] }));
|
|
150
151
|
}
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
|
-
// Error boundary
|
|
153
|
+
// Error boundary
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
class GltfErrorBoundary extends react_1.default.Component {
|
|
155
|
-
constructor(props) {
|
|
156
|
-
super(props);
|
|
157
|
-
this.state = { hasError: false };
|
|
158
|
-
}
|
|
156
|
+
constructor(props) { super(props); this.state = { hasError: false }; }
|
|
159
157
|
static getDerivedStateFromError() { return { hasError: true }; }
|
|
160
158
|
componentDidCatch(err) { this.props.onError(err.message); }
|
|
161
|
-
render() {
|
|
162
|
-
|
|
163
|
-
return null;
|
|
164
|
-
return this.props.children;
|
|
165
|
-
}
|
|
159
|
+
render() { if (this.state.hasError)
|
|
160
|
+
return null; return this.props.children; }
|
|
166
161
|
}
|
|
167
162
|
// ---------------------------------------------------------------------------
|
|
168
163
|
// Viseme schedule player
|
|
@@ -172,41 +167,30 @@ function applyVisemeCue(morphState, visemeKey) {
|
|
|
172
167
|
if (!aliases)
|
|
173
168
|
return;
|
|
174
169
|
const w = morphTables_1.VISEME_WEIGHTS[visemeKey] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
|
|
175
|
-
for (const alias of aliases)
|
|
170
|
+
for (const alias of aliases)
|
|
176
171
|
morphState.visemeTarget[alias] = w;
|
|
177
|
-
}
|
|
178
172
|
}
|
|
179
173
|
// ---------------------------------------------------------------------------
|
|
180
174
|
// Main exported component
|
|
181
175
|
// ---------------------------------------------------------------------------
|
|
182
|
-
exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, mood = 'neutral', onReady, onError, }, ref) => {
|
|
183
|
-
// Resolve authenticated file URI (same logic as FilamentAvatar)
|
|
176
|
+
exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, mood = 'neutral', onReady, onError, onSceneReady }, ref) => {
|
|
184
177
|
const remoteUrl = avatarUrl && (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://'))
|
|
185
|
-
? avatarUrl
|
|
186
|
-
|
|
187
|
-
const fileResult = (0, useAuthedFilamentUri_1.useAuthedFilamentUri)(remoteUrl);
|
|
188
|
-
// Lock-in URI on first resolve — never change mid-session
|
|
178
|
+
? avatarUrl : null;
|
|
179
|
+
const fileResult = (0, useAuthedModelUri_1.useAuthedModelUri)(remoteUrl);
|
|
189
180
|
const lockedUriRef = (0, react_1.useRef)(null);
|
|
190
181
|
const currentUri = fileResult?.uri ?? null;
|
|
191
|
-
if (lockedUriRef.current === null && currentUri)
|
|
182
|
+
if (lockedUriRef.current === null && currentUri)
|
|
192
183
|
lockedUriRef.current = currentUri;
|
|
193
|
-
}
|
|
194
184
|
const localUri = lockedUriRef.current;
|
|
195
185
|
const fov = focalLength
|
|
196
186
|
? Math.round(2 * Math.atan(21.634 / focalLength) * (180 / Math.PI))
|
|
197
187
|
: CAMERA_FOV_FULL;
|
|
198
|
-
// Shared morph state — mutated directly in useFrame, never causes re-renders
|
|
199
188
|
const morphStateRef = (0, react_1.useRef)({
|
|
200
|
-
current: {},
|
|
201
|
-
visemeTarget: {},
|
|
202
|
-
moodBase: {},
|
|
203
|
-
alpha: 0.18,
|
|
189
|
+
current: {}, visemeTarget: {}, moodBase: {}, alpha: 0.18,
|
|
204
190
|
});
|
|
205
|
-
// Update mood baseline when mood prop changes
|
|
206
191
|
(0, react_1.useEffect)(() => {
|
|
207
192
|
morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[mood] ?? {}) };
|
|
208
193
|
}, [mood]);
|
|
209
|
-
// Pending viseme schedule
|
|
210
194
|
const scheduleRef = (0, react_1.useRef)(null);
|
|
211
195
|
const scheduleTimersRef = (0, react_1.useRef)([]);
|
|
212
196
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
@@ -215,21 +199,6 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
215
199
|
clearTimeout(t);
|
|
216
200
|
scheduleTimersRef.current = [];
|
|
217
201
|
}, []);
|
|
218
|
-
const handleReady = (0, react_1.useCallback)(() => {
|
|
219
|
-
setIsReady(true);
|
|
220
|
-
onReady?.();
|
|
221
|
-
// Flush any pending schedule
|
|
222
|
-
if (scheduleRef.current) {
|
|
223
|
-
const s = scheduleRef.current;
|
|
224
|
-
scheduleRef.current = null;
|
|
225
|
-
applySchedule(s);
|
|
226
|
-
}
|
|
227
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
228
|
-
}, [onReady]);
|
|
229
|
-
const handleError = (0, react_1.useCallback)((msg) => {
|
|
230
|
-
console.warn('[WgpuAvatar] GLTF error:', msg);
|
|
231
|
-
onError?.(msg);
|
|
232
|
-
}, [onError]);
|
|
233
202
|
const applySchedule = (0, react_1.useCallback)((schedule) => {
|
|
234
203
|
clearScheduleTimers();
|
|
235
204
|
const now = Date.now();
|
|
@@ -238,39 +207,41 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
238
207
|
for (const cue of schedule.cues) {
|
|
239
208
|
const delay = cue.startMs - offset;
|
|
240
209
|
if (delay < -200)
|
|
241
|
-
continue;
|
|
242
|
-
const
|
|
243
|
-
const visemeKey = morphTables_1.RHUBARB_TO_VISEME[rhubarbKey] ?? rhubarbKey;
|
|
210
|
+
continue;
|
|
211
|
+
const visemeKey = morphTables_1.RHUBARB_TO_VISEME[cue.viseme] ?? cue.viseme;
|
|
244
212
|
const t = setTimeout(() => {
|
|
245
213
|
applyVisemeCue(morphStateRef.current, visemeKey);
|
|
246
214
|
}, Math.max(0, delay));
|
|
247
215
|
scheduleTimersRef.current.push(t);
|
|
248
216
|
}
|
|
249
217
|
}, [clearScheduleTimers]);
|
|
250
|
-
|
|
218
|
+
const handleReady = (0, react_1.useCallback)(() => {
|
|
219
|
+
setIsReady(true);
|
|
220
|
+
onReady?.();
|
|
221
|
+
if (scheduleRef.current) {
|
|
222
|
+
const s = scheduleRef.current;
|
|
223
|
+
scheduleRef.current = null;
|
|
224
|
+
applySchedule(s);
|
|
225
|
+
}
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [onReady]);
|
|
228
|
+
const handleError = (0, react_1.useCallback)((msg) => {
|
|
229
|
+
console.warn('[WgpuAvatar] GLTF error:', msg);
|
|
230
|
+
onError?.(msg);
|
|
231
|
+
}, [onError]);
|
|
251
232
|
(0, react_1.useEffect)(() => () => clearScheduleTimers(), [clearScheduleTimers]);
|
|
252
233
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
253
|
-
setMood: (m) => {
|
|
254
|
-
|
|
255
|
-
},
|
|
256
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
257
|
-
sendAmplitude: (_amplitude) => {
|
|
258
|
-
// Optional: could drive jaw open with amplitude
|
|
259
|
-
// morphStateRef.current.visemeTarget['jawOpen'] = _amplitude * 0.3;
|
|
260
|
-
},
|
|
234
|
+
setMood: (m) => { morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[m] ?? {}) }; },
|
|
235
|
+
sendAmplitude: (_amplitude) => { },
|
|
261
236
|
sendViseme: (viseme, weight) => {
|
|
262
237
|
const aliases = morphTables_1.VISEME_MORPH_ALIASES[viseme];
|
|
263
238
|
if (!aliases)
|
|
264
239
|
return;
|
|
265
240
|
const w = weight ?? morphTables_1.VISEME_WEIGHTS[viseme] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
|
|
266
|
-
for (const alias of aliases)
|
|
241
|
+
for (const alias of aliases)
|
|
267
242
|
morphStateRef.current.visemeTarget[alias] = w;
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
clearVisemes: () => {
|
|
271
|
-
clearScheduleTimers();
|
|
272
|
-
morphStateRef.current.visemeTarget = {};
|
|
273
243
|
},
|
|
244
|
+
clearVisemes: () => { clearScheduleTimers(); morphStateRef.current.visemeTarget = {}; },
|
|
274
245
|
scheduleVisemes: (schedule) => {
|
|
275
246
|
if (!isReady) {
|
|
276
247
|
scheduleRef.current = schedule;
|
|
@@ -279,12 +250,9 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
279
250
|
applySchedule(schedule);
|
|
280
251
|
},
|
|
281
252
|
}), [isReady, applySchedule, clearScheduleTimers]);
|
|
282
|
-
if (!localUri)
|
|
253
|
+
if (!localUri)
|
|
283
254
|
return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.placeholder, style] });
|
|
284
|
-
}
|
|
285
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(native_1.Canvas, { style: react_native_1.StyleSheet.absoluteFill, camera: { fov, position: CAMERA_POSITION, near: 0.01, far: 100 }, gl: { antialias: true, alpha: false }, onCreated: ({ gl }) => {
|
|
286
|
-
gl.setClearColor(new THREE.Color('#1a1a2e'));
|
|
287
|
-
}, children: (0, jsx_runtime_1.jsx)(GltfErrorBoundary, { onError: handleError, children: (0, jsx_runtime_1.jsx)(AvatarScene, { uri: localUri, morphStateRef: morphStateRef, fov: fov, onReady: handleReady, onError: handleError }) }) }) }));
|
|
255
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(native_1.Canvas, { style: react_native_1.StyleSheet.absoluteFill, camera: { fov, position: CAMERA_POSITION, near: 0.01, far: 100 }, gl: { antialias: true, alpha: false }, onCreated: ({ gl }) => { gl.setClearColor(new THREE.Color('#1a1a2e')); }, children: (0, jsx_runtime_1.jsx)(GltfErrorBoundary, { onError: handleError, children: (0, jsx_runtime_1.jsx)(AvatarScene, { uri: localUri, morphStateRef: morphStateRef, fov: fov, onReady: handleReady, onError: handleError, onSceneReady: onSceneReady }) }) }) }));
|
|
288
256
|
});
|
|
289
257
|
exports.WgpuAvatar.displayName = 'WgpuAvatar';
|
|
290
258
|
const styles = react_native_1.StyleSheet.create({
|
package/dist/wgpu/morphTables.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
|
-
// Morph target tables for
|
|
3
|
+
// Morph target tables for avatar viseme/mood rendering
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MOOD_MORPHS = exports.DEFAULT_VISEME_WEIGHT = exports.VISEME_WEIGHTS = exports.VISEME_MORPH_ALIASES = exports.RHUBARB_TO_VISEME = void 0;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AuthedFileResult = {
|
|
2
|
+
uri: string;
|
|
3
|
+
size: number;
|
|
4
|
+
} | null;
|
|
5
|
+
/**
|
|
6
|
+
* Downloads a remote URL (with Bearer auth) to the local cache and returns a
|
|
7
|
+
* `file://` URI suitable for WgpuAvatar model source.
|
|
8
|
+
* The native model fetcher doesn't send auth headers, so we pre-fetch here.
|
|
9
|
+
* Also returns the file size in bytes for GPU memory budgeting.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useAuthedModelUri(remoteUrl: string | null): AuthedFileResult;
|
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
26
|
+
exports.useAuthedModelUri = void 0;
|
|
27
27
|
const react_1 = require("react");
|
|
28
28
|
const FileSystem = __importStar(require("expo-file-system/legacy"));
|
|
29
29
|
const studioApi_1 = require("../api/studioApi");
|
|
@@ -31,11 +31,11 @@ const studioApi_1 = require("../api/studioApi");
|
|
|
31
31
|
const failedUrls = new Set();
|
|
32
32
|
/**
|
|
33
33
|
* Downloads a remote URL (with Bearer auth) to the local cache and returns a
|
|
34
|
-
* `file://` URI suitable for
|
|
35
|
-
* The native
|
|
34
|
+
* `file://` URI suitable for WgpuAvatar model source.
|
|
35
|
+
* The native model fetcher doesn't send auth headers, so we pre-fetch here.
|
|
36
36
|
* Also returns the file size in bytes for GPU memory budgeting.
|
|
37
37
|
*/
|
|
38
|
-
function
|
|
38
|
+
function useAuthedModelUri(remoteUrl) {
|
|
39
39
|
const [result, setResult] = (0, react_1.useState)(null);
|
|
40
40
|
(0, react_1.useEffect)(() => {
|
|
41
41
|
if (!remoteUrl) {
|
|
@@ -57,7 +57,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
57
57
|
// Strip query params (access_token changes per session) so the same GLB is cached across sessions
|
|
58
58
|
const urlWithoutQuery = remoteUrl.split('?')[0];
|
|
59
59
|
const key = urlWithoutQuery.replace(/[^a-zA-Z0-9]/g, '_').slice(-100);
|
|
60
|
-
const dir = `${FileSystem.cacheDirectory}
|
|
60
|
+
const dir = `${FileSystem.cacheDirectory}model/`;
|
|
61
61
|
// Append .glb so native loaders can identify the format
|
|
62
62
|
const localPath = `${dir}${key}.glb`;
|
|
63
63
|
await FileSystem.makeDirectoryAsync(dir, { intermediates: true });
|
|
@@ -85,7 +85,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
85
85
|
const downloaded = await FileSystem.getInfoAsync(dlResult.uri);
|
|
86
86
|
const downloadedSize = downloaded.size;
|
|
87
87
|
if (!downloaded.exists || !downloadedSize || downloadedSize < 1000) {
|
|
88
|
-
console.error('[
|
|
88
|
+
console.error('[useAuthedModelUri] Download too small (' + (downloadedSize ?? 0) + 'B), likely an error response');
|
|
89
89
|
failedUrls.add(remoteUrl);
|
|
90
90
|
await FileSystem.deleteAsync(dlResult.uri);
|
|
91
91
|
return;
|
|
@@ -96,7 +96,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
96
96
|
position: 0,
|
|
97
97
|
});
|
|
98
98
|
if (dlHeader !== 'Z2xURg==') {
|
|
99
|
-
console.error('[
|
|
99
|
+
console.error('[useAuthedModelUri] Downloaded file is not a valid GLB (bad magic bytes)');
|
|
100
100
|
failedUrls.add(remoteUrl);
|
|
101
101
|
await FileSystem.deleteAsync(dlResult.uri);
|
|
102
102
|
return;
|
|
@@ -105,7 +105,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
105
105
|
setResult({ uri: dlResult.uri, size: downloadedSize });
|
|
106
106
|
}
|
|
107
107
|
catch (e) {
|
|
108
|
-
console.error('[
|
|
108
|
+
console.error('[useAuthedModelUri] Failed to download:', remoteUrl.slice(-60), e);
|
|
109
109
|
}
|
|
110
110
|
})();
|
|
111
111
|
return () => {
|
|
@@ -114,4 +114,4 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
114
114
|
}, [remoteUrl]);
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
|
-
exports.
|
|
117
|
+
exports.useAuthedModelUri = useAuthedModelUri;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talking-head-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Cross-platform 3D avatar component for React Native & web — lip-sync, gestures, accessories, and LLM integration. Powered by TalkingHead + Three.js.",
|
|
5
5
|
"main": "dist/index.web.js",
|
|
6
6
|
"browser": "dist/index.web.js",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"sideEffects": false,
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"@mediapipe/tasks-vision": "^0.10.34",
|
|
97
|
+
"expo-gl": "^55.0.10",
|
|
97
98
|
"zustand": "^5.0.12"
|
|
98
99
|
},
|
|
99
100
|
"peerDependencies": {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export type AuthedFileResult = {
|
|
2
|
-
uri: string;
|
|
3
|
-
size: number;
|
|
4
|
-
} | null;
|
|
5
|
-
/**
|
|
6
|
-
* Downloads a remote URL (with Bearer auth) to the local cache and returns a
|
|
7
|
-
* `file://` URI suitable for react-native-filament's <Model> source prop.
|
|
8
|
-
* The native Filament fetcher doesn't send auth headers, so we pre-fetch here.
|
|
9
|
-
* Also returns the file size in bytes for GPU memory budgeting.
|
|
10
|
-
*/
|
|
11
|
-
export declare function useAuthedFilamentUri(remoteUrl: string | null): AuthedFileResult;
|