talking-head-studio 0.4.0 → 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.
Files changed (42) hide show
  1. package/dist/TalkingHeadVisualization.d.ts +1 -1
  2. package/dist/TalkingHeadVisualization.js +13 -13
  3. package/dist/editor/AvatarCanvas.d.ts +3 -14
  4. package/dist/editor/AvatarCanvas.js +5 -4
  5. package/dist/editor/AvatarEditor.d.ts +1 -0
  6. package/dist/editor/AvatarEditor.js +6 -0
  7. package/dist/editor/AvatarEditor.native.d.ts +4 -0
  8. package/dist/editor/AvatarEditor.native.js +93 -0
  9. package/dist/editor/boneSnap.d.ts +25 -0
  10. package/dist/editor/boneSnap.js +93 -0
  11. package/dist/editor/index.d.ts +4 -0
  12. package/dist/editor/index.js +13 -1
  13. package/dist/editor/types.d.ts +18 -4
  14. package/dist/utils/avatarUtils.d.ts +2 -3
  15. package/dist/utils/avatarUtils.js +5 -6
  16. package/dist/wardrobe/wardrobeStore.d.ts +1 -1
  17. package/dist/wgpu/WgpuAvatar.d.ts +3 -0
  18. package/dist/wgpu/WgpuAvatar.js +55 -87
  19. package/dist/{filament → wgpu}/morphTables.js +1 -1
  20. package/dist/wgpu/useAuthedModelUri.d.ts +11 -0
  21. package/dist/{filament/useAuthedFilamentUri.js → wgpu/useAuthedModelUri.js} +9 -9
  22. package/package.json +2 -15
  23. package/dist/filament/FilamentAvatar.d.ts +0 -41
  24. package/dist/filament/FilamentAvatar.js +0 -755
  25. package/dist/filament/editor/FilamentEditor.d.ts +0 -16
  26. package/dist/filament/editor/FilamentEditor.js +0 -880
  27. package/dist/filament/editor/FilamentEditor.web.d.ts +0 -19
  28. package/dist/filament/editor/FilamentEditor.web.js +0 -58
  29. package/dist/filament/editor/PrecisionPanel.d.ts +0 -1
  30. package/dist/filament/editor/PrecisionPanel.js +0 -252
  31. package/dist/filament/editor/boneSnap.d.ts +0 -10
  32. package/dist/filament/editor/boneSnap.js +0 -97
  33. package/dist/filament/editor/index.d.ts +0 -5
  34. package/dist/filament/editor/index.js +0 -19
  35. package/dist/filament/index.d.ts +0 -6
  36. package/dist/filament/index.js +0 -24
  37. package/dist/filament/useAuthedFilamentUri.d.ts +0 -11
  38. /package/dist/{filament/editor → editor}/studioTheme.d.ts +0 -0
  39. /package/dist/{filament/editor → editor}/studioTheme.js +0 -0
  40. /package/dist/{filament → wgpu}/faceSqueezeAssets.d.ts +0 -0
  41. /package/dist/{filament → wgpu}/faceSqueezeAssets.js +0 -0
  42. /package/dist/{filament → wgpu}/morphTables.d.ts +0 -0
@@ -28,7 +28,7 @@ export interface TalkingHeadVisualizationRef {
28
28
  /**
29
29
  * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
30
30
  *
31
- * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
31
+ * On native: uses WgpuAvatar (direct morph writes, no WebView bridge).
32
32
  * On web: uses TalkingHead WebView renderer.
33
33
  */
34
34
  export declare const TalkingHeadVisualization: React.ForwardRefExoticComponent<TalkingHeadVisualizationProps & React.RefAttributes<TalkingHeadVisualizationRef>>;
@@ -6,9 +6,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
7
  const react_native_1 = require("react-native");
8
8
  const TalkingHead_1 = require("./TalkingHead");
9
- const FilamentAvatar_1 = require("./filament/FilamentAvatar");
9
+ const WgpuAvatar_1 = require("./wgpu/WgpuAvatar");
10
10
  const avatarUtils_1 = require("./utils/avatarUtils");
11
- const faceSqueezeAssets_1 = require("./filament/faceSqueezeAssets");
11
+ const faceSqueezeAssets_1 = require("./wgpu/faceSqueezeAssets");
12
12
  // Cached fallback data URI — resolved once, reused across all instances
13
13
  let _fallbackDataUri = null;
14
14
  let _fallbackPromise = null;
@@ -41,16 +41,16 @@ function getLoadingLabel(stage) {
41
41
  /**
42
42
  * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
43
43
  *
44
- * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
44
+ * On native: uses WgpuAvatar (direct morph writes, no WebView bridge).
45
45
  * On web: uses TalkingHead WebView renderer.
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, Filament ref is wired via callback ref — store it here so
49
+ // On native, WgpuAvatar ref is wired via callback ref — store it here so
50
50
  // scheduleVisemes / sendAmplitude can route to it.
51
- const filamentRef = (0, react_1.useRef)(null);
52
- // Unified accessor — Filament on native, WebView on web
53
- const activeAvatar = (0, react_1.useCallback)(() => (filamentRef.current ?? avatarRef.current), []);
51
+ const wgpuRef = (0, react_1.useRef)(null);
52
+ // Unified accessor — WgpuAvatar on native, WebView on web
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);
56
56
  (0, react_1.useEffect)(() => {
@@ -114,9 +114,9 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
114
114
  if (lastScheduledVisemeKeyRef.current === scheduleKey)
115
115
  return;
116
116
  const av = activeAvatar();
117
- // Filament buffers pending schedules internally — no ready gate needed.
117
+ // WgpuAvatar buffers pending schedules internally — no ready gate needed.
118
118
  // WebView (avatarRef) still needs the ready gate.
119
- if (!av || (!filamentRef.current && !isAvatarReady)) {
119
+ if (!av || (!wgpuRef.current && !isAvatarReady)) {
120
120
  pendingVisemeScheduleRef.current = schedule;
121
121
  return;
122
122
  }
@@ -166,7 +166,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
166
166
  ready: isAvatarReady,
167
167
  });
168
168
  const av = activeAvatar();
169
- if (!av || (!filamentRef.current && !isAvatarReady)) {
169
+ if (!av || (!wgpuRef.current && !isAvatarReady)) {
170
170
  pendingVisemeScheduleRef.current = visemeSchedule;
171
171
  return;
172
172
  }
@@ -181,10 +181,10 @@ 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 Filament — direct morph writes, no WebView bridge.
184
+ // On native use WgpuAvatar — direct morph writes, no WebView bridge.
185
185
  if (react_native_1.Platform.OS !== 'web') {
186
- return ((0, jsx_runtime_1.jsx)(FilamentAvatar_1.FilamentAvatar, { focalLength: focalLength, ref: (fr) => {
187
- filamentRef.current = fr;
186
+ return ((0, jsx_runtime_1.jsx)(WgpuAvatar_1.WgpuAvatar, { focalLength: focalLength, ref: (fr) => {
187
+ wgpuRef.current = fr;
188
188
  if (typeof ref === 'function')
189
189
  ref(fr);
190
190
  else if (ref)
@@ -1,16 +1,5 @@
1
- /// <reference types="react" />
2
- import type { AvatarAppearance } from '../appearance';
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: !editingAssetId }), (0, jsx_runtime_1.jsx)(AvatarModel_1.AvatarModel, { url: avatarUrl, scale: 1, onSkeletonReady: handleSkeletonReady, onBoundsReady: handleBoundsReady }), equippedItems.map((asset) => {
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 = editingAssetId === asset.id;
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;
@@ -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';
@@ -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; } });
@@ -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?: React.CSSProperties;
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
- * react-native-filament's useModel(). Never base64-encodes — Filament reads
4
- * file:// directly on both iOS and Android.
3
+ * WgpuAvatar model loading.
5
4
  */
6
- export declare function resolveFilamentAssetUri(module: unknown): Promise<string | null>;
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.resolveFilamentAssetUri = void 0;
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
- * react-native-filament's useModel(). Never base64-encodes — Filament reads
10
- * file:// directly on both iOS and Android.
9
+ * WgpuAvatar model loading.
11
10
  */
12
- async function resolveFilamentAssetUri(module) {
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 Filament asset:', e);
19
+ console.error('[AssetUtils] Failed to resolve asset:', e);
21
20
  return null;
22
21
  }
23
22
  }
24
- exports.resolveFilamentAssetUri = resolveFilamentAssetUri;
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 (Filament mode) */
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 {};