talking-head-studio 0.2.5 → 0.2.7
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/README.md +8 -7
- package/dist/TalkingHead.d.ts +0 -1
- package/dist/TalkingHead.js +31 -29
- package/dist/TalkingHead.web.d.ts +1 -3
- package/dist/TalkingHead.web.js +72 -43
- package/dist/appearance/apply.d.ts +0 -1
- package/dist/appearance/apply.js +8 -5
- package/dist/appearance/index.d.ts +0 -1
- package/dist/appearance/index.js +9 -3
- package/dist/appearance/matchers.d.ts +0 -1
- package/dist/appearance/matchers.js +4 -1
- package/dist/appearance/schema.d.ts +0 -1
- package/dist/appearance/schema.js +4 -1
- package/dist/editor/AvatarCanvas.d.ts +1 -2
- package/dist/editor/AvatarCanvas.js +44 -60
- package/dist/editor/AvatarCanvasErrorBoundary.d.ts +1 -2
- package/dist/editor/AvatarCanvasErrorBoundary.js +9 -14
- package/dist/editor/AvatarModel.d.ts +1 -2
- package/dist/editor/AvatarModel.js +17 -13
- package/dist/editor/RigidAccessory.d.ts +1 -2
- package/dist/editor/RigidAccessory.js +19 -17
- package/dist/editor/SkinnedClothing.d.ts +1 -2
- package/dist/editor/SkinnedClothing.js +46 -9
- package/dist/editor/index.d.ts +0 -1
- package/dist/editor/index.js +11 -4
- package/dist/editor/types.d.ts +0 -1
- package/dist/editor/types.js +2 -1
- package/dist/html.d.ts +0 -1
- package/dist/html.js +8 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.js +20 -2
- package/dist/index.web.d.ts +0 -1
- package/dist/index.web.js +20 -2
- package/dist/sketchfab/api.d.ts +0 -1
- package/dist/sketchfab/api.js +10 -4
- package/dist/sketchfab/categories.d.ts +0 -1
- package/dist/sketchfab/categories.js +5 -2
- package/dist/sketchfab/index.d.ts +0 -1
- package/dist/sketchfab/index.js +13 -3
- package/dist/sketchfab/types.d.ts +0 -1
- package/dist/sketchfab/types.js +2 -1
- package/dist/sketchfab/useSketchfabSearch.d.ts +0 -1
- package/dist/sketchfab/useSketchfabSearch.js +20 -17
- package/dist/voice/convertToWav.d.ts +0 -1
- package/dist/voice/convertToWav.js +4 -1
- package/dist/voice/index.d.ts +0 -1
- package/dist/voice/index.js +9 -3
- package/dist/voice/useAudioPlayer.d.ts +0 -1
- package/dist/voice/useAudioPlayer.js +7 -4
- package/dist/voice/useAudioRecording.d.ts +0 -1
- package/dist/voice/useAudioRecording.js +20 -17
- package/package.json +10 -8
- package/dist/TalkingHead.d.ts.map +0 -1
- package/dist/TalkingHead.web.d.ts.map +0 -1
- package/dist/__tests__/TalkingHead.test.d.ts +0 -2
- package/dist/__tests__/TalkingHead.test.d.ts.map +0 -1
- package/dist/__tests__/TalkingHead.test.js +0 -23
- package/dist/__tests__/sketchfab.test.d.ts +0 -2
- package/dist/__tests__/sketchfab.test.d.ts.map +0 -1
- package/dist/__tests__/sketchfab.test.js +0 -21
- package/dist/appearance/apply.d.ts.map +0 -1
- package/dist/appearance/index.d.ts.map +0 -1
- package/dist/appearance/matchers.d.ts.map +0 -1
- package/dist/appearance/schema.d.ts.map +0 -1
- package/dist/editor/AvatarCanvas.d.ts.map +0 -1
- package/dist/editor/AvatarCanvasErrorBoundary.d.ts.map +0 -1
- package/dist/editor/AvatarModel.d.ts.map +0 -1
- package/dist/editor/RigidAccessory.d.ts.map +0 -1
- package/dist/editor/SkinnedClothing.d.ts.map +0 -1
- package/dist/editor/index.d.ts.map +0 -1
- package/dist/editor/types.d.ts.map +0 -1
- package/dist/html.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.web.d.ts.map +0 -1
- package/dist/sketchfab/api.d.ts.map +0 -1
- package/dist/sketchfab/categories.d.ts.map +0 -1
- package/dist/sketchfab/index.d.ts.map +0 -1
- package/dist/sketchfab/types.d.ts.map +0 -1
- package/dist/sketchfab/useSketchfabSearch.d.ts.map +0 -1
- package/dist/voice/convertToWav.d.ts.map +0 -1
- package/dist/voice/index.d.ts.map +0 -1
- package/dist/voice/useAudioPlayer.d.ts.map +0 -1
- package/dist/voice/useAudioRecording.d.ts.map +0 -1
- package/src/TalkingHead.tsx +0 -276
- package/src/TalkingHead.web.tsx +0 -220
- package/src/__tests__/TalkingHead.test.tsx +0 -32
- package/src/__tests__/sketchfab.test.ts +0 -24
- package/src/appearance/apply.ts +0 -94
- package/src/appearance/index.ts +0 -4
- package/src/appearance/matchers.ts +0 -43
- package/src/appearance/schema.ts +0 -35
- package/src/editor/AvatarCanvas.tsx +0 -167
- package/src/editor/AvatarCanvasErrorBoundary.tsx +0 -64
- package/src/editor/AvatarModel.tsx +0 -49
- package/src/editor/RigidAccessory.tsx +0 -130
- package/src/editor/SkinnedClothing.tsx +0 -114
- package/src/editor/index.ts +0 -5
- package/src/editor/r3f-shim.d.ts +0 -34
- package/src/editor/types.ts +0 -30
- package/src/html.ts +0 -678
- package/src/index.ts +0 -11
- package/src/index.web.ts +0 -8
- package/src/sketchfab/api.ts +0 -82
- package/src/sketchfab/categories.ts +0 -127
- package/src/sketchfab/index.ts +0 -6
- package/src/sketchfab/types.ts +0 -40
- package/src/sketchfab/useSketchfabSearch.ts +0 -110
- package/src/voice/convertToWav.ts +0 -87
- package/src/voice/index.ts +0 -7
- package/src/voice/useAudioPlayer.ts +0 -78
- package/src/voice/useAudioRecording.ts +0 -207
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { useGLTF } from '@react-three/drei';
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
4
|
-
import { Box3, Vector3 } from 'three';
|
|
5
|
-
import { applyAppearanceToObject3D } from '../appearance';
|
|
6
|
-
import type { AvatarAppearance } from '../appearance';
|
|
7
|
-
|
|
8
|
-
interface AvatarModelProps {
|
|
9
|
-
url: string;
|
|
10
|
-
appearance?: AvatarAppearance;
|
|
11
|
-
scale?: number;
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
onSkeletonReady?: (skeleton: any) => void;
|
|
14
|
-
onBoundsReady?: (center: typeof Vector3.prototype, height: number) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function AvatarModel({
|
|
18
|
-
url,
|
|
19
|
-
appearance,
|
|
20
|
-
scale = 1,
|
|
21
|
-
onSkeletonReady,
|
|
22
|
-
onBoundsReady,
|
|
23
|
-
}: AvatarModelProps) {
|
|
24
|
-
const { scene } = useGLTF(url);
|
|
25
|
-
const groupRef = useRef(null);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (!scene) return;
|
|
29
|
-
scene.traverse((child: any) => {
|
|
30
|
-
if (child.isSkinnedMesh && child.skeleton) {
|
|
31
|
-
onSkeletonReady?.(child.skeleton);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
if (onBoundsReady) {
|
|
35
|
-
const box = new Box3().setFromObject(scene);
|
|
36
|
-
const center = new Vector3();
|
|
37
|
-
box.getCenter(center);
|
|
38
|
-
const height = box.max.y - box.min.y;
|
|
39
|
-
onBoundsReady(center, height);
|
|
40
|
-
}
|
|
41
|
-
}, [scene, onSkeletonReady, onBoundsReady]);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (!scene) return;
|
|
45
|
-
applyAppearanceToObject3D(scene, appearance ?? { version: 1 as const });
|
|
46
|
-
}, [scene, appearance]);
|
|
47
|
-
|
|
48
|
-
return <primitive ref={groupRef} object={scene} scale={scale} />;
|
|
49
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { useGLTF, PivotControls } from '@react-three/drei';
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { Box3, Vector3 } from 'three';
|
|
5
|
-
import type { AssetPlacement } from './types';
|
|
6
|
-
|
|
7
|
-
interface RigidAccessoryProps {
|
|
8
|
-
assetId: string;
|
|
9
|
-
url: string;
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
avatarScene: any;
|
|
12
|
-
attachBone: string;
|
|
13
|
-
offsetPosition?: [number, number, number];
|
|
14
|
-
offsetRotation?: [number, number, number];
|
|
15
|
-
scale?: number;
|
|
16
|
-
isEditing?: boolean;
|
|
17
|
-
onPlacementChange?: (assetId: string, placement: AssetPlacement) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function RigidAccessory({
|
|
21
|
-
assetId,
|
|
22
|
-
url,
|
|
23
|
-
avatarScene,
|
|
24
|
-
attachBone,
|
|
25
|
-
offsetPosition,
|
|
26
|
-
offsetRotation,
|
|
27
|
-
scale = 1,
|
|
28
|
-
isEditing = false,
|
|
29
|
-
onPlacementChange,
|
|
30
|
-
}: RigidAccessoryProps) {
|
|
31
|
-
const { scene } = useGLTF(url);
|
|
32
|
-
|
|
33
|
-
// Clone uniquely for this instance
|
|
34
|
-
const clone = useMemo(() => scene.clone(true), [scene]);
|
|
35
|
-
const [basePos, setBasePos] = useState<typeof Vector3.prototype | null>(null);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!avatarScene) return;
|
|
39
|
-
|
|
40
|
-
let targetBone: any = null;
|
|
41
|
-
avatarScene.traverse((child: any) => {
|
|
42
|
-
if (child.isBone && child.name === attachBone) {
|
|
43
|
-
targetBone = child;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!targetBone) return;
|
|
48
|
-
|
|
49
|
-
const box = new Box3().setFromObject(clone);
|
|
50
|
-
const meshMin = box.min;
|
|
51
|
-
const meshCenter = new Vector3();
|
|
52
|
-
box.getCenter(meshCenter);
|
|
53
|
-
|
|
54
|
-
const boneWorldPos = new Vector3();
|
|
55
|
-
targetBone.getWorldPosition(boneWorldPos);
|
|
56
|
-
|
|
57
|
-
setBasePos(new Vector3(
|
|
58
|
-
boneWorldPos.x - meshCenter.x,
|
|
59
|
-
boneWorldPos.y - meshMin.y,
|
|
60
|
-
boneWorldPos.z - meshCenter.z,
|
|
61
|
-
));
|
|
62
|
-
}, [avatarScene, attachBone, clone]);
|
|
63
|
-
|
|
64
|
-
// Apply transforms live
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (!basePos) return;
|
|
67
|
-
|
|
68
|
-
clone.position.copy(basePos);
|
|
69
|
-
|
|
70
|
-
if (offsetPosition) {
|
|
71
|
-
clone.position.x += offsetPosition[0];
|
|
72
|
-
clone.position.y += offsetPosition[1];
|
|
73
|
-
clone.position.z += offsetPosition[2];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (offsetRotation) {
|
|
77
|
-
clone.rotation.set(...offsetRotation);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (scale !== undefined) {
|
|
81
|
-
clone.scale.set(scale, scale, scale);
|
|
82
|
-
}
|
|
83
|
-
}, [basePos, offsetPosition, offsetRotation, scale, clone]);
|
|
84
|
-
|
|
85
|
-
const handleDragEnd = () => {
|
|
86
|
-
if (!basePos) return;
|
|
87
|
-
|
|
88
|
-
// The PivotControls autoTransform updates the object directly
|
|
89
|
-
const newOffsetPos: [number, number, number] = [
|
|
90
|
-
clone.position.x - basePos.x,
|
|
91
|
-
clone.position.y - basePos.y,
|
|
92
|
-
clone.position.z - basePos.z,
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
const newOffsetRot: [number, number, number] = [
|
|
96
|
-
clone.rotation.x,
|
|
97
|
-
clone.rotation.y,
|
|
98
|
-
clone.rotation.z,
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const currentScale = clone.scale.x;
|
|
102
|
-
|
|
103
|
-
onPlacementChange?.(assetId, {
|
|
104
|
-
bone: attachBone,
|
|
105
|
-
position: newOffsetPos,
|
|
106
|
-
rotation: newOffsetRot,
|
|
107
|
-
scale: currentScale,
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
if (!basePos) return null;
|
|
112
|
-
|
|
113
|
-
if (isEditing) {
|
|
114
|
-
return (
|
|
115
|
-
<PivotControls
|
|
116
|
-
anchor={[0, 0, 0]}
|
|
117
|
-
scale={100}
|
|
118
|
-
fixed={true}
|
|
119
|
-
depthTest={false}
|
|
120
|
-
activeAxes={[true, true, true]}
|
|
121
|
-
onDragEnd={handleDragEnd}
|
|
122
|
-
autoTransform={true}
|
|
123
|
-
>
|
|
124
|
-
<primitive object={clone} />
|
|
125
|
-
</PivotControls>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return <primitive object={clone} />;
|
|
130
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { useGLTF } from '@react-three/drei';
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
4
|
-
import * as THREE from 'three';
|
|
5
|
-
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils.js';
|
|
6
|
-
|
|
7
|
-
interface SkinnedClothingProps {
|
|
8
|
-
url: string;
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
avatarSkeleton: any;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Rebind a skinned mesh's bones to the avatar skeleton by matching bone names.
|
|
15
|
-
* This handles hair packs / clothing that share the same rig convention but
|
|
16
|
-
* were exported as separate files with their own skeleton.
|
|
17
|
-
*/
|
|
18
|
-
function rebindToAvatarSkeleton(mesh: THREE.SkinnedMesh, avatarSkeleton: THREE.Skeleton) {
|
|
19
|
-
const avatarBonesByName = new Map<string, THREE.Bone>();
|
|
20
|
-
avatarSkeleton.bones.forEach((bone: THREE.Bone) => avatarBonesByName.set(bone.name, bone));
|
|
21
|
-
|
|
22
|
-
// Build a new bone array for this mesh by matching names
|
|
23
|
-
const newBones: THREE.Bone[] = [];
|
|
24
|
-
const oldBones = mesh.skeleton.bones;
|
|
25
|
-
const boneIndexMap = new Map<number, number>(); // old index → new index
|
|
26
|
-
|
|
27
|
-
oldBones.forEach((oldBone: THREE.Bone, oldIdx: number) => {
|
|
28
|
-
const matched = avatarBonesByName.get(oldBone.name);
|
|
29
|
-
if (matched) {
|
|
30
|
-
boneIndexMap.set(oldIdx, newBones.length);
|
|
31
|
-
newBones.push(matched);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (newBones.length === 0) {
|
|
36
|
-
// No bone names matched at all — just bind directly and hope for the best
|
|
37
|
-
mesh.bind(avatarSkeleton);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Remap skinIndex buffer to point at the new bone indices
|
|
42
|
-
const skinIndex = mesh.geometry.attributes.skinIndex;
|
|
43
|
-
const remappedIndex = skinIndex.array.slice();
|
|
44
|
-
|
|
45
|
-
for (let i = 0; i < remappedIndex.length; i++) {
|
|
46
|
-
const oldIdx = remappedIndex[i];
|
|
47
|
-
const newIdx = boneIndexMap.get(oldIdx);
|
|
48
|
-
remappedIndex[i] = newIdx !== undefined ? newIdx : 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
mesh.geometry.setAttribute(
|
|
52
|
-
'skinIndex',
|
|
53
|
-
new THREE.Uint16BufferAttribute(remappedIndex, skinIndex.itemSize),
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// Build inverse bind matrices for the matched bones
|
|
57
|
-
const inverseBindMatrices: number[] = [];
|
|
58
|
-
newBones.forEach((bone: THREE.Bone) => {
|
|
59
|
-
const m = new THREE.Matrix4();
|
|
60
|
-
// Use the avatar skeleton's inverse bind matrix if bone index aligns,
|
|
61
|
-
// otherwise compute from world matrix
|
|
62
|
-
const avatarIdx = avatarSkeleton.bones.indexOf(bone);
|
|
63
|
-
if (avatarIdx >= 0 && avatarSkeleton.boneInverses[avatarIdx]) {
|
|
64
|
-
m.copy(avatarSkeleton.boneInverses[avatarIdx]);
|
|
65
|
-
} else {
|
|
66
|
-
m.copy(bone.matrixWorld).invert();
|
|
67
|
-
}
|
|
68
|
-
inverseBindMatrices.push(...m.elements);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const newSkeleton = new THREE.Skeleton(
|
|
72
|
-
newBones,
|
|
73
|
-
newBones.map((_: THREE.Bone, i: number) => {
|
|
74
|
-
const m = new THREE.Matrix4();
|
|
75
|
-
m.fromArray(inverseBindMatrices, i * 16);
|
|
76
|
-
return m;
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
mesh.bind(newSkeleton);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function SkinnedClothing({ url, avatarSkeleton }: SkinnedClothingProps) {
|
|
84
|
-
const { scene } = useGLTF(url);
|
|
85
|
-
const groupRef = useRef<any>(null);
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
const group = groupRef.current;
|
|
89
|
-
if (!group || !scene || !avatarSkeleton) return;
|
|
90
|
-
|
|
91
|
-
const clone = SkeletonUtils.clone(scene as any);
|
|
92
|
-
|
|
93
|
-
(clone as any).traverse((child: any) => {
|
|
94
|
-
if (child.isSkinnedMesh) {
|
|
95
|
-
rebindToAvatarSkeleton(child as THREE.SkinnedMesh, avatarSkeleton);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
group.add(clone as any);
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
group.remove(clone as any);
|
|
103
|
-
(clone as any).traverse((child: any) => {
|
|
104
|
-
if (child.geometry) child.geometry.dispose();
|
|
105
|
-
if (child.material) {
|
|
106
|
-
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
|
107
|
-
materials.forEach((m: any) => m.dispose());
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
}, [scene, avatarSkeleton]);
|
|
112
|
-
|
|
113
|
-
return <group ref={groupRef} />;
|
|
114
|
-
}
|
package/src/editor/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { AvatarCanvas } from './AvatarCanvas';
|
|
2
|
-
export { AvatarModel } from './AvatarModel';
|
|
3
|
-
export { RigidAccessory } from './RigidAccessory';
|
|
4
|
-
export { SkinnedClothing } from './SkinnedClothing';
|
|
5
|
-
export type { AvatarEditorProps, AssetPlacement, EquippedAsset } from './types';
|
package/src/editor/r3f-shim.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal JSX intrinsic shims for React Three Fiber elements.
|
|
3
|
-
* These are provided at runtime by @react-three/fiber (a peer dependency).
|
|
4
|
-
* This file prevents TS errors when the peer dep types are not installed.
|
|
5
|
-
*/
|
|
6
|
-
import type React from 'react';
|
|
7
|
-
|
|
8
|
-
declare module 'react' {
|
|
9
|
-
namespace JSX {
|
|
10
|
-
interface IntrinsicElements {
|
|
11
|
-
// R3F primitives
|
|
12
|
-
primitive: {
|
|
13
|
-
object: any;
|
|
14
|
-
ref?: React.Ref<any>;
|
|
15
|
-
scale?: any;
|
|
16
|
-
[key: string]: any;
|
|
17
|
-
};
|
|
18
|
-
group: {
|
|
19
|
-
ref?: React.Ref<any>;
|
|
20
|
-
[key: string]: any;
|
|
21
|
-
};
|
|
22
|
-
// Three.js light intrinsics
|
|
23
|
-
ambientLight: {
|
|
24
|
-
intensity?: number;
|
|
25
|
-
[key: string]: any;
|
|
26
|
-
};
|
|
27
|
-
directionalLight: {
|
|
28
|
-
position?: [number, number, number];
|
|
29
|
-
intensity?: number;
|
|
30
|
-
[key: string]: any;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/editor/types.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { AvatarAppearance } from '../appearance';
|
|
2
|
-
import type React from 'react';
|
|
3
|
-
|
|
4
|
-
export interface AssetPlacement {
|
|
5
|
-
bone: string;
|
|
6
|
-
position: [number, number, number];
|
|
7
|
-
rotation: [number, number, number];
|
|
8
|
-
scale?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface EquippedAsset {
|
|
12
|
-
id: string;
|
|
13
|
-
url: string;
|
|
14
|
-
type: 'rigid' | 'skinned';
|
|
15
|
-
slot?: string;
|
|
16
|
-
attach_bone?: string;
|
|
17
|
-
offset_position?: [number, number, number];
|
|
18
|
-
offset_rotation?: [number, number, number];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface AvatarEditorProps {
|
|
22
|
-
avatarUrl: string;
|
|
23
|
-
appearance?: AvatarAppearance;
|
|
24
|
-
equipped?: EquippedAsset[];
|
|
25
|
-
placements?: Record<string, AssetPlacement>;
|
|
26
|
-
editingAssetId?: string | null;
|
|
27
|
-
onPlacementChange?: (assetId: string, placement: AssetPlacement) => void;
|
|
28
|
-
className?: string;
|
|
29
|
-
style?: React.CSSProperties;
|
|
30
|
-
}
|