mbt-3d 0.1.0
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 +22 -0
- package/dist/index.d.ts +556 -0
- package/dist/mbt-3d.cjs.js +2 -0
- package/dist/mbt-3d.cjs.js.map +1 -0
- package/dist/mbt-3d.es.js +420 -0
- package/dist/mbt-3d.es.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# mbt-3d
|
|
2
|
+
|
|
3
|
+
React components for 3D character viewing with animations and morph targets.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install mbt-3d
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer Dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install react react-dom three @react-three/fiber @react-three/drei
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- React >= 18.0.0
|
|
20
|
+
- Three.js >= 0.150.0
|
|
21
|
+
- @react-three/fiber >= 8.0.0
|
|
22
|
+
- @react-three/drei >= 9.0.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { ForwardRefExoticComponent } from 'react';
|
|
2
|
+
import { JSX } from 'react/jsx-runtime';
|
|
3
|
+
import { JSX as JSX_2 } from 'react';
|
|
4
|
+
import { RefAttributes } from 'react';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AnimatedModel - 3D model with animation support and bone attachment capability
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* This component:
|
|
12
|
+
* - Loads GLB/GLTF models with animations using useGLTF
|
|
13
|
+
* - Properly clones skinned meshes using SkeletonUtils for multiple instances
|
|
14
|
+
* - Plays animations with automatic crossfading
|
|
15
|
+
* - Supports morph targets for character customization
|
|
16
|
+
* - Provides context for BoneAttachment children
|
|
17
|
+
* - Exposes imperative API via ref for external animation control
|
|
18
|
+
*
|
|
19
|
+
* The ref provides these methods:
|
|
20
|
+
* - `playAnimation(name, options)` - Play an animation with optional loop/restore
|
|
21
|
+
* - `stopAnimation()` - Stop current animation
|
|
22
|
+
* - `getAnimationNames()` - Get list of available animations
|
|
23
|
+
* - `getGroup()` - Get the THREE.Group for advanced manipulation
|
|
24
|
+
* - `setMorphTarget(name, value)` - Set morph target value
|
|
25
|
+
* - `getMorphTargetNames()` - Get available morph target names
|
|
26
|
+
*
|
|
27
|
+
* @param ref - React ref for imperative API access
|
|
28
|
+
* @param url - URL or path to GLB/GLTF model file with animations. Example: `"/models/character.glb"`
|
|
29
|
+
* @param position - Model position in 3D space as [x, y, z]. Example: `[0, -1, 0]`. Default: `[0, 0, 0]`
|
|
30
|
+
* @param rotation - Model rotation in radians as [x, y, z]. Example: `[0, Math.PI, 0]`. Default: `[0, 0, 0]`
|
|
31
|
+
* @param scale - Uniform scale (number) or per-axis scale [x, y, z]. Examples: `0.5` or `[1, 2, 1]`. Default: `1`
|
|
32
|
+
* @param defaultAnimation - Name of animation to play on load. If not specified, plays first animation. Example: `"Idle"`
|
|
33
|
+
* @param morphTargets - Morph target values as key-value pairs where value is 0-1. Example: `{ muscular: 0.5, thin: 0.2 }`
|
|
34
|
+
* @param children - Child components, typically BoneAttachment components for attaching items
|
|
35
|
+
* @param onLoad - Callback when model loads. Receives AnimatedModelInfo object: `{ meshes, materials, bones, nodeCount, animations, morphTargetNames }`
|
|
36
|
+
* @param onError - Callback when model fails to load. Receives Error object
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const modelRef = useRef<AnimatedModelHandle>(null);
|
|
41
|
+
*
|
|
42
|
+
* <AnimatedModel
|
|
43
|
+
* ref={modelRef}
|
|
44
|
+
* url="/models/character.glb"
|
|
45
|
+
* defaultAnimation="Idle"
|
|
46
|
+
* morphTargets={{ muscular: 0.5 }}
|
|
47
|
+
* position={[0, -1, 0]}
|
|
48
|
+
* >
|
|
49
|
+
* <BoneAttachment bone="hand_r">
|
|
50
|
+
* <Model url="/models/sword.glb" />
|
|
51
|
+
* </BoneAttachment>
|
|
52
|
+
* </AnimatedModel>
|
|
53
|
+
*
|
|
54
|
+
* // Control animations from outside
|
|
55
|
+
* modelRef.current?.playAnimation('Attack', { loop: false });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare const AnimatedModel: ForwardRefExoticComponent<AnimatedModelProps & RefAttributes<AnimatedModelHandle>>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Imperative handle for AnimatedModel (accessed via ref)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* ref.current?.playAnimation('Attack', { loop: false });
|
|
66
|
+
* ref.current?.setMorphTarget('muscular', 0.8);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare interface AnimatedModelHandle {
|
|
70
|
+
/**
|
|
71
|
+
* Play an animation by name
|
|
72
|
+
* @param name - Animation name (supports partial matching)
|
|
73
|
+
* @param options - Playback options
|
|
74
|
+
*/
|
|
75
|
+
playAnimation: (name: string, options?: {
|
|
76
|
+
/** Loop the animation continuously. Default: false */
|
|
77
|
+
loop?: boolean;
|
|
78
|
+
/** Duration of crossfade in seconds. Default: 0.2 */
|
|
79
|
+
crossFadeDuration?: number;
|
|
80
|
+
/** Restore default animation when this one finishes. Default: true */
|
|
81
|
+
restoreDefault?: boolean;
|
|
82
|
+
}) => void;
|
|
83
|
+
/** Stop the currently playing animation */
|
|
84
|
+
stopAnimation: () => void;
|
|
85
|
+
/** Get array of all available animation names */
|
|
86
|
+
getAnimationNames: () => string[];
|
|
87
|
+
/** Get the THREE.Group object for advanced manipulation */
|
|
88
|
+
getGroup: () => THREE.Group | null;
|
|
89
|
+
/**
|
|
90
|
+
* Set a morph target value
|
|
91
|
+
* @param name - Morph target name
|
|
92
|
+
* @param value - Value between 0 and 1
|
|
93
|
+
*/
|
|
94
|
+
setMorphTarget: (name: string, value: number) => void;
|
|
95
|
+
/** Get array of all available morph target names */
|
|
96
|
+
getMorphTargetNames: () => string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Extended model information including animation and morph target data
|
|
101
|
+
*/
|
|
102
|
+
export declare interface AnimatedModelInfo extends ModelInfo {
|
|
103
|
+
/** Names of all animations in the model */
|
|
104
|
+
animations: string[];
|
|
105
|
+
/** Names of all morph targets (blend shapes) */
|
|
106
|
+
morphTargetNames: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Props for AnimatedModel component
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* const ref = useRef<AnimatedModelHandle>(null);
|
|
115
|
+
*
|
|
116
|
+
* <AnimatedModel
|
|
117
|
+
* ref={ref}
|
|
118
|
+
* url="/character.glb"
|
|
119
|
+
* defaultAnimation="Idle"
|
|
120
|
+
* morphTargets={{ muscular: 0.5 }}
|
|
121
|
+
* position={[0, -1, 0]}
|
|
122
|
+
* >
|
|
123
|
+
* <BoneAttachment bone="hand_r">
|
|
124
|
+
* <Model url="/sword.glb" />
|
|
125
|
+
* </BoneAttachment>
|
|
126
|
+
* </AnimatedModel>
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare interface AnimatedModelProps extends Omit<ModelProps, 'onLoad'> {
|
|
130
|
+
/** Child components (typically BoneAttachment components) */
|
|
131
|
+
children?: React.ReactNode;
|
|
132
|
+
/** Name of animation to play on load (auto-plays first if not specified) */
|
|
133
|
+
defaultAnimation?: string;
|
|
134
|
+
/** Morph target values as key-value pairs { targetName: value } where value is 0-1 */
|
|
135
|
+
morphTargets?: Record<string, number>;
|
|
136
|
+
/** Callback when model loads with extended metadata including animations */
|
|
137
|
+
onLoad?: (info: AnimatedModelInfo) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* BoneAttachment - Attach models to bones of an AnimatedModel parent
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* This component:
|
|
145
|
+
* - Must be used as a child of AnimatedModel
|
|
146
|
+
* - Uses React Three Fiber's createPortal to attach to a bone
|
|
147
|
+
* - Automatically follows bone transformations during animations
|
|
148
|
+
* - Supports position/rotation/scale offsets relative to the bone
|
|
149
|
+
*
|
|
150
|
+
* Common bone names:
|
|
151
|
+
* - Hand bones: "hand_r", "hand_l", "DEF-handR", "DEF-handL"
|
|
152
|
+
* - Spine/Back: "spine", "spine_upper", "back"
|
|
153
|
+
* - Head: "head", "neck"
|
|
154
|
+
*
|
|
155
|
+
* @param children - Child components to attach to the bone (typically Model components)
|
|
156
|
+
* @param bone - Bone name to attach to. Must match bone name in the parent AnimatedModel. Example: `"hand_r"` or `"DEF-handR"`
|
|
157
|
+
* @param position - Position offset relative to the bone as [x, y, z]. Example: `[0.1, 0, 0]`. Default: `[0, 0, 0]`
|
|
158
|
+
* @param rotation - Rotation offset relative to the bone in radians as [x, y, z]. Example: `[0, Math.PI/2, 0]`. Default: `[0, 0, 0]`
|
|
159
|
+
* @param scale - Uniform scale (number) or per-axis scale [x, y, z]. Examples: `0.7` or `[1, 2, 1]`. Default: `1`
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <AnimatedModel url="/character.glb">
|
|
164
|
+
* // Sword in right hand
|
|
165
|
+
* <BoneAttachment
|
|
166
|
+
* bone="hand_r"
|
|
167
|
+
* position={[0.1, 0, 0]}
|
|
168
|
+
* rotation={[0, Math.PI/2, 0]}
|
|
169
|
+
* >
|
|
170
|
+
* <Model url="/sword.glb" scale={0.7} />
|
|
171
|
+
* </BoneAttachment>
|
|
172
|
+
*
|
|
173
|
+
* // Shield on back
|
|
174
|
+
* <BoneAttachment bone="spine_upper">
|
|
175
|
+
* <Model url="/shield.glb" />
|
|
176
|
+
* </BoneAttachment>
|
|
177
|
+
* </AnimatedModel>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export declare function BoneAttachment({ children, bone, position, rotation, scale, }: BoneAttachmentProps): JSX_2.Element | null;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Props for BoneAttachment component
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* Must be used as a child of AnimatedModel
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```tsx
|
|
190
|
+
* <AnimatedModel url="/character.glb">
|
|
191
|
+
* <BoneAttachment
|
|
192
|
+
* bone="hand_r"
|
|
193
|
+
* position={[0.1, 0, 0]}
|
|
194
|
+
* rotation={[0, Math.PI/2, 0]}
|
|
195
|
+
* >
|
|
196
|
+
* <Model url="/sword.glb" />
|
|
197
|
+
* </BoneAttachment>
|
|
198
|
+
* </AnimatedModel>
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export declare interface BoneAttachmentProps {
|
|
202
|
+
/** Child components to attach to the bone */
|
|
203
|
+
children: React.ReactNode;
|
|
204
|
+
/**
|
|
205
|
+
* Bone name to attach to. Common names:
|
|
206
|
+
* - Hands: "hand_r", "hand_l", "DEF-handR", "DEF-handL"
|
|
207
|
+
* - Spine/Back: "spine", "spine_upper", "back"
|
|
208
|
+
* - Head: "head", "neck"
|
|
209
|
+
*/
|
|
210
|
+
bone: string;
|
|
211
|
+
/** Position offset relative to the bone [x, y, z]. Default: [0, 0, 0] */
|
|
212
|
+
position?: [number, number, number];
|
|
213
|
+
/** Rotation offset relative to the bone in radians [x, y, z]. Default: [0, 0, 0] */
|
|
214
|
+
rotation?: [number, number, number];
|
|
215
|
+
/** Uniform scale (number) or per-axis scale [x, y, z]. Default: 1 */
|
|
216
|
+
scale?: number | [number, number, number];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Model - Simple 3D model component for loading and displaying GLB/GLTF files
|
|
221
|
+
*
|
|
222
|
+
* @remarks
|
|
223
|
+
* This component:
|
|
224
|
+
* - Loads GLB/GLTF models using drei's useGLTF hook
|
|
225
|
+
* - Automatically clones the scene for independent instances
|
|
226
|
+
* - Configures shadows on all meshes
|
|
227
|
+
* - Supports position, rotation, and scale transforms
|
|
228
|
+
* - Provides onLoad callback with model metadata
|
|
229
|
+
*
|
|
230
|
+
* @param url - URL or path to GLB/GLTF model file. Example: `"/models/sword.glb"`
|
|
231
|
+
* @param position - Model position in 3D space as [x, y, z]. Example: `[0, 0, 0]`. Default: `[0, 0, 0]`
|
|
232
|
+
* @param rotation - Model rotation in radians as [x, y, z]. Example: `[0, Math.PI/2, 0]`. Default: `[0, 0, 0]`
|
|
233
|
+
* @param scale - Uniform scale (number) or per-axis scale [x, y, z]. Examples: `0.5` or `[1, 2, 1]`. Default: `1`
|
|
234
|
+
* @param onLoad - Callback when model finishes loading. Receives ModelInfo object with metadata: `{ meshes, materials, bones, nodeCount }`
|
|
235
|
+
* @param onError - Callback when model fails to load. Receives Error object
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```tsx
|
|
239
|
+
* <Model
|
|
240
|
+
* url="/models/sword.glb"
|
|
241
|
+
* position={[0, 0, 0]}
|
|
242
|
+
* rotation={[0, Math.PI/2, 0]}
|
|
243
|
+
* scale={0.5}
|
|
244
|
+
* onLoad={(info) => console.log('Loaded:', info)}
|
|
245
|
+
* />
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export declare function Model({ url, position, rotation, scale, onLoad, onError, }: ModelProps): JSX.Element;
|
|
249
|
+
|
|
250
|
+
export declare namespace Model {
|
|
251
|
+
var preload: (url: string) => void;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Metadata information about a loaded model
|
|
256
|
+
*/
|
|
257
|
+
export declare interface ModelInfo {
|
|
258
|
+
/** Names of all mesh objects in the model */
|
|
259
|
+
meshes: string[];
|
|
260
|
+
/** Names of all materials used by the model */
|
|
261
|
+
materials: string[];
|
|
262
|
+
/** Names of all bones (for skinned/rigged meshes) */
|
|
263
|
+
bones: string[];
|
|
264
|
+
/** Total number of nodes in the scene graph */
|
|
265
|
+
nodeCount: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Props for Model component
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```tsx
|
|
273
|
+
* <Model
|
|
274
|
+
* url="/models/sword.glb"
|
|
275
|
+
* position={[0, 0, 0]}
|
|
276
|
+
* rotation={[0, Math.PI/2, 0]}
|
|
277
|
+
* scale={0.5}
|
|
278
|
+
* />
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export declare interface ModelProps {
|
|
282
|
+
/** URL or path to GLB/GLTF model file */
|
|
283
|
+
url: string;
|
|
284
|
+
/** Model position in 3D space [x, y, z]. Default: [0, 0, 0] */
|
|
285
|
+
position?: [number, number, number];
|
|
286
|
+
/** Model rotation in radians [x, y, z]. Default: [0, 0, 0] */
|
|
287
|
+
rotation?: [number, number, number];
|
|
288
|
+
/** Uniform scale (number) or per-axis scale [x, y, z]. Default: 1 */
|
|
289
|
+
scale?: number | [number, number, number];
|
|
290
|
+
/** Callback when model finishes loading with metadata */
|
|
291
|
+
onLoad?: (info: ModelInfo) => void;
|
|
292
|
+
/** Callback when model fails to load */
|
|
293
|
+
onError?: (error: Error) => void;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* MorphableModel - 3D model with morph targets for shape customization
|
|
298
|
+
*
|
|
299
|
+
* @remarks
|
|
300
|
+
* This component:
|
|
301
|
+
* - Loads GLB/GLTF models with morph targets (blend shapes)
|
|
302
|
+
* - Dynamically discovers available morph targets from the model
|
|
303
|
+
* - Applies morph target values in real-time
|
|
304
|
+
* - Provides imperative API via ref for programmatic control
|
|
305
|
+
*
|
|
306
|
+
* Morph targets are commonly used for:
|
|
307
|
+
* - Character customization (muscular, thin, fat)
|
|
308
|
+
* - Facial expressions (smile, frown, blink)
|
|
309
|
+
* - Clothing fit adjustments
|
|
310
|
+
*
|
|
311
|
+
* @param ref - React ref for imperative API access
|
|
312
|
+
* @param url - URL or path to GLB/GLTF model file with morph targets. Example: `"/models/character.glb"`
|
|
313
|
+
* @param position - Model position in 3D space as [x, y, z]. Example: `[0, -1, 0]`. Default: `[0, 0, 0]`
|
|
314
|
+
* @param rotation - Model rotation in radians as [x, y, z]. Example: `[0, Math.PI, 0]`. Default: `[0, 0, 0]`
|
|
315
|
+
* @param scale - Uniform scale (number) or per-axis scale [x, y, z]. Examples: `0.5` or `[1, 2, 1]`. Default: `1`
|
|
316
|
+
* @param morphTargets - Morph target values as key-value pairs where value is 0-1. Example: `{ muscular: 0.5, thin: 0.2 }`
|
|
317
|
+
* @param onMorphTargetsFound - Callback when morph targets are discovered from the model. Receives array of morph target names: `["muscular", "thin", "fat"]`
|
|
318
|
+
* @param onLoad - Callback when model finishes loading. Receives ModelInfo object with metadata: `{ meshes, materials, bones, nodeCount }`
|
|
319
|
+
* @param onError - Callback when model fails to load. Receives Error object
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```tsx
|
|
323
|
+
* const modelRef = useRef<MorphableModelHandle>(null);
|
|
324
|
+
* const [morphs, setMorphs] = useState({ muscular: 0.5, thin: 0.2 });
|
|
325
|
+
*
|
|
326
|
+
* <MorphableModel
|
|
327
|
+
* ref={modelRef}
|
|
328
|
+
* url="/models/character.glb"
|
|
329
|
+
* morphTargets={morphs}
|
|
330
|
+
* onMorphTargetsFound={(names) => console.log('Available:', names)}
|
|
331
|
+
* position={[0, -1, 0]}
|
|
332
|
+
* />
|
|
333
|
+
*
|
|
334
|
+
* // Control programmatically
|
|
335
|
+
* modelRef.current?.setMorphTarget('muscular', 0.8);
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export declare const MorphableModel: ForwardRefExoticComponent<MorphableModelProps & RefAttributes<MorphableModelHandle>>;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Imperative handle for MorphableModel (accessed via ref)
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```tsx
|
|
345
|
+
* ref.current?.setMorphTarget('muscular', 0.8);
|
|
346
|
+
* const names = ref.current?.getMorphTargetNames();
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
export declare interface MorphableModelHandle {
|
|
350
|
+
/**
|
|
351
|
+
* Set a morph target value
|
|
352
|
+
* @param name - Morph target name
|
|
353
|
+
* @param value - Value between 0 and 1
|
|
354
|
+
*/
|
|
355
|
+
setMorphTarget: (name: string, value: number) => void;
|
|
356
|
+
/** Get array of all available morph target names */
|
|
357
|
+
getMorphTargetNames: () => string[];
|
|
358
|
+
/** Get current values of all morph targets */
|
|
359
|
+
getMorphTargetValues: () => Record<string, number>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Props for MorphableModel component
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```tsx
|
|
367
|
+
* <MorphableModel
|
|
368
|
+
* url="/character.glb"
|
|
369
|
+
* morphTargets={{ muscular: 0.5, thin: 0.2 }}
|
|
370
|
+
* onMorphTargetsFound={(names) => console.log(names)}
|
|
371
|
+
* />
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
export declare interface MorphableModelProps extends ModelProps {
|
|
375
|
+
/** Morph target values as key-value pairs { targetName: value } where value is 0-1 */
|
|
376
|
+
morphTargets?: Record<string, number>;
|
|
377
|
+
/** Callback when morph targets are discovered from the model */
|
|
378
|
+
onMorphTargetsFound?: (names: string[]) => void;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export declare function preloadModel(url: string): void;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Scene3D - Main 3D scene container with built-in canvas, lighting, and controls
|
|
385
|
+
*
|
|
386
|
+
* @remarks
|
|
387
|
+
* This component wraps React Three Fiber's Canvas and provides:
|
|
388
|
+
* - OrbitControls for camera rotation (preserves state between model changes)
|
|
389
|
+
* - Configurable lighting (ambient + spot light)
|
|
390
|
+
* - Optional background (image URL or hex color like "#1a1a2e")
|
|
391
|
+
* - Optional contact shadows under models
|
|
392
|
+
* - Shadow support
|
|
393
|
+
*
|
|
394
|
+
* @param children - React children to render inside the 3D scene
|
|
395
|
+
* @param camera - Camera configuration object. Example: `{ position: [0, 2, 5], fov: 45 }`
|
|
396
|
+
* @param camera.position - Camera position as [x, y, z]. Example: `[0, 2, 5]`. Default: `[0, 2, 5]`
|
|
397
|
+
* @param camera.fov - Field of view in degrees. Example: `45`. Default: `45`
|
|
398
|
+
* @param controls - OrbitControls configuration object. Example: `{ enablePan: false, minDistance: 2, maxDistance: 10 }`
|
|
399
|
+
* @param controls.enabled - Enable/disable all controls. Default: `true`
|
|
400
|
+
* @param controls.enablePan - Enable camera panning with middle mouse. Default: `true`
|
|
401
|
+
* @param controls.enableZoom - Enable camera zoom with scroll wheel. Default: `true`
|
|
402
|
+
* @param controls.enableRotate - Enable camera rotation with left mouse. Default: `true`
|
|
403
|
+
* @param controls.minDistance - Minimum zoom distance. Example: `2`
|
|
404
|
+
* @param controls.maxDistance - Maximum zoom distance. Example: `10`
|
|
405
|
+
* @param controls.minPolarAngle - Minimum vertical rotation angle in radians. Example: `Math.PI / 4`
|
|
406
|
+
* @param controls.maxPolarAngle - Maximum vertical rotation angle in radians. Example: `Math.PI / 1.8`
|
|
407
|
+
* @param controls.autoRotate - Auto-rotate camera around the center. Default: `false`
|
|
408
|
+
* @param controls.autoRotateSpeed - Auto-rotation speed. Default: `2`
|
|
409
|
+
* @param background - Background: image URL (e.g., `"/bg.jpg"`) or hex color (e.g., `"#1a1a2e"`)
|
|
410
|
+
* @param shadows - Enable shadow rendering. Default: `true`
|
|
411
|
+
* @param ambientIntensity - Ambient light intensity (0-1). Example: `0.5`. Default: `0.5`
|
|
412
|
+
* @param spotLight - Spot light configuration object. Example: `{ position: [5, 10, 5], intensity: 50 }`
|
|
413
|
+
* @param spotLight.position - Light position as [x, y, z]. Example: `[5, 10, 5]`. Default: `[5, 10, 5]`
|
|
414
|
+
* @param spotLight.intensity - Light intensity. Example: `50`. Default: `50`
|
|
415
|
+
* @param spotLight.castShadow - Enable shadow casting. Default: `true`
|
|
416
|
+
* @param contactShadows - Contact shadows configuration. Can be `true` for default settings or object with custom settings. Example: `{ position: [0, -1, 0], opacity: 0.5, blur: 2 }`
|
|
417
|
+
* @param contactShadows.position - Shadow position as [x, y, z]. Example: `[0, -1, 0]`. Default: `[0, -1, 0]`
|
|
418
|
+
* @param contactShadows.opacity - Shadow opacity (0-1). Example: `0.5`. Default: `0.5`
|
|
419
|
+
* @param contactShadows.blur - Shadow blur amount. Example: `2`. Default: `2`
|
|
420
|
+
* @param style - Container style object. Example: `{ width: 400, height: 500 }`
|
|
421
|
+
* @param className - Container CSS class name
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```tsx
|
|
425
|
+
* <Scene3D
|
|
426
|
+
* camera={{ position: [0, 2, 5], fov: 45 }}
|
|
427
|
+
* background="#1a1a2e"
|
|
428
|
+
* shadows
|
|
429
|
+
* style={{ width: 400, height: 500 }}
|
|
430
|
+
* >
|
|
431
|
+
* <AnimatedModel url="/model.glb" position={[0, -1, 0]} />
|
|
432
|
+
* </Scene3D>
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
export declare function Scene3D({ children, camera, controls, background, shadows, ambientIntensity, spotLight, contactShadows, style, className, }: Scene3DProps): JSX.Element;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Props for Scene3D component
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* <Scene3D
|
|
443
|
+
* camera={{ position: [0, 2, 5], fov: 45 }}
|
|
444
|
+
* controls={{ enablePan: false, minDistance: 2 }}
|
|
445
|
+
* background="#1a1a2e"
|
|
446
|
+
* shadows
|
|
447
|
+
* >
|
|
448
|
+
* {children}
|
|
449
|
+
* </Scene3D>
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
export declare interface Scene3DProps {
|
|
453
|
+
/** React children to render inside the 3D scene */
|
|
454
|
+
children: React.ReactNode;
|
|
455
|
+
/** Camera configuration */
|
|
456
|
+
camera?: {
|
|
457
|
+
/** Camera position [x, y, z]. Default: [0, 2, 5] */
|
|
458
|
+
position?: [number, number, number];
|
|
459
|
+
/** Field of view in degrees. Default: 45 */
|
|
460
|
+
fov?: number;
|
|
461
|
+
};
|
|
462
|
+
/** OrbitControls configuration for camera rotation */
|
|
463
|
+
controls?: {
|
|
464
|
+
/** Enable/disable all controls. Default: true */
|
|
465
|
+
enabled?: boolean;
|
|
466
|
+
/** Enable camera panning (middle mouse). Default: true */
|
|
467
|
+
enablePan?: boolean;
|
|
468
|
+
/** Enable camera zoom (scroll wheel). Default: true */
|
|
469
|
+
enableZoom?: boolean;
|
|
470
|
+
/** Enable camera rotation (left mouse). Default: true */
|
|
471
|
+
enableRotate?: boolean;
|
|
472
|
+
/** Minimum zoom distance */
|
|
473
|
+
minDistance?: number;
|
|
474
|
+
/** Maximum zoom distance */
|
|
475
|
+
maxDistance?: number;
|
|
476
|
+
/** Minimum vertical rotation angle (radians) */
|
|
477
|
+
minPolarAngle?: number;
|
|
478
|
+
/** Maximum vertical rotation angle (radians) */
|
|
479
|
+
maxPolarAngle?: number;
|
|
480
|
+
/** Auto-rotate camera around the center. Default: false */
|
|
481
|
+
autoRotate?: boolean;
|
|
482
|
+
/** Auto-rotation speed. Default: 2 */
|
|
483
|
+
autoRotateSpeed?: number;
|
|
484
|
+
};
|
|
485
|
+
/** Background: image URL ("/bg.jpg") or hex color ("#1a1a2e") */
|
|
486
|
+
background?: string;
|
|
487
|
+
/** Enable shadow rendering. Default: true */
|
|
488
|
+
shadows?: boolean;
|
|
489
|
+
/** Ambient light intensity. Default: 0.5 */
|
|
490
|
+
ambientIntensity?: number;
|
|
491
|
+
/** Spot light configuration */
|
|
492
|
+
spotLight?: {
|
|
493
|
+
/** Light position [x, y, z]. Default: [5, 10, 5] */
|
|
494
|
+
position?: [number, number, number];
|
|
495
|
+
/** Light intensity. Default: 50 */
|
|
496
|
+
intensity?: number;
|
|
497
|
+
/** Enable shadow casting. Default: true */
|
|
498
|
+
castShadow?: boolean;
|
|
499
|
+
};
|
|
500
|
+
/** Contact shadows configuration (ground shadows under models) */
|
|
501
|
+
contactShadows?: boolean | {
|
|
502
|
+
/** Shadow position [x, y, z]. Default: [0, -1, 0] */
|
|
503
|
+
position?: [number, number, number];
|
|
504
|
+
/** Shadow opacity 0-1. Default: 0.5 */
|
|
505
|
+
opacity?: number;
|
|
506
|
+
/** Shadow blur amount. Default: 2 */
|
|
507
|
+
blur?: number;
|
|
508
|
+
};
|
|
509
|
+
/** Container style (e.g., { width: 400, height: 500 }) */
|
|
510
|
+
style?: React.CSSProperties;
|
|
511
|
+
/** Container CSS class name */
|
|
512
|
+
className?: string;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export declare function useAnimationController(animations: THREE.AnimationClip[], scene: THREE.Object3D, options?: UseAnimationControllerOptions): {
|
|
516
|
+
playAnimation: (name: string, opts?: {
|
|
517
|
+
loop?: boolean;
|
|
518
|
+
crossFadeDuration?: number;
|
|
519
|
+
restoreDefault?: boolean;
|
|
520
|
+
}) => void;
|
|
521
|
+
stopAnimation: () => void;
|
|
522
|
+
getAnimationNames: () => string[];
|
|
523
|
+
actions: {
|
|
524
|
+
[x: string]: THREE.AnimationAction | null;
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
declare interface UseAnimationControllerOptions {
|
|
529
|
+
defaultAnimation?: string;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export declare function useClonedModel(url: string, options?: UseClonedModelOptions): {
|
|
533
|
+
scene: THREE.Object3D<THREE.Object3DEventMap>;
|
|
534
|
+
animations: THREE.AnimationClip[];
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
declare interface UseClonedModelOptions {
|
|
538
|
+
onLoad?: (info: ModelInfo) => void;
|
|
539
|
+
onError?: (error: Error) => void;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export declare function useMorphTargets(scene: THREE.Object3D, initialValues?: Record<string, number>): {
|
|
543
|
+
setMorphTarget: (name: string, value: number) => void;
|
|
544
|
+
getMorphTargetNames: () => string[];
|
|
545
|
+
getMorphTargetValues: () => {
|
|
546
|
+
[x: string]: number;
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export { }
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
export declare namespace Model {
|
|
554
|
+
var preload: (url: string) => void;
|
|
555
|
+
}
|
|
556
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("react/jsx-runtime"),a=require("react"),D=require("@react-three/fiber"),M=require("@react-three/drei"),W=require("three");function _(e){const n=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const r in e)if(r!=="default"){const t=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(n,r,t.get?t:{enumerable:!0,get:()=>e[r]})}}return n.default=e,Object.freeze(n)}const w=_(W);function $({background:e}){const n=e==null?void 0:e.startsWith("#");return e?n?y.jsx("color",{attach:"background",args:[e]}):y.jsx(z,{url:e}):null}function z({url:e}){const n=M.useTexture(e);return a.useMemo(()=>{n.colorSpace=w.SRGBColorSpace},[n]),y.jsx("primitive",{attach:"background",object:n})}function V({children:e,camera:n={},controls:r={},background:t,shadows:s=!0,ambientIntensity:c=.5,spotLight:u={},contactShadows:b=!0,style:d,className:m}){const o={position:n.position||[0,2,5],fov:n.fov||45},i={enabled:r.enabled??!0,enablePan:r.enablePan??!0,enableZoom:r.enableZoom??!0,enableRotate:r.enableRotate??!0,minDistance:r.minDistance,maxDistance:r.maxDistance,minPolarAngle:r.minPolarAngle,maxPolarAngle:r.maxPolarAngle,autoRotate:r.autoRotate??!1,autoRotateSpeed:r.autoRotateSpeed??2},f={position:u.position||[5,10,5],intensity:u.intensity??50,castShadow:u.castShadow??!0},p=typeof b=="object"?{position:b.position||[0,-1,0],opacity:b.opacity??.5,blur:b.blur??2}:b?{position:[0,-1,0],opacity:.5,blur:2}:null;return y.jsx("div",{style:d,className:m,children:y.jsxs(D.Canvas,{shadows:s,camera:{position:o.position,fov:o.fov},style:{width:"100%",height:"100%"},children:[y.jsx(a.Suspense,{fallback:null,children:y.jsx($,{background:t})}),y.jsx("ambientLight",{intensity:c}),y.jsx("spotLight",{position:f.position,intensity:f.intensity,castShadow:f.castShadow}),y.jsx(a.Suspense,{fallback:null,children:e}),y.jsx(M.OrbitControls,{makeDefault:!0,enabled:i.enabled,enablePan:i.enablePan,enableZoom:i.enableZoom,enableRotate:i.enableRotate,minDistance:i.minDistance,maxDistance:i.maxDistance,minPolarAngle:i.minPolarAngle,maxPolarAngle:i.maxPolarAngle,autoRotate:i.autoRotate,autoRotateSpeed:i.autoRotateSpeed}),p&&y.jsx(M.ContactShadows,{position:p.position,opacity:p.opacity,blur:p.blur})]})})}function G({url:e,position:n=[0,0,0],rotation:r=[0,0,0],scale:t=1,onLoad:s,onError:c}){const{scene:u}=M.useGLTF(e),b=a.useMemo(()=>{const m=u.clone(),o=[],i=new Set,f=[];let p=0;return m.traverse(h=>{if(p++,h.type==="Bone"&&f.push(h.name),h.isMesh){const l=h;o.push(l.name),l.castShadow=!0,l.receiveShadow=!0,(Array.isArray(l.material)?l.material:[l.material]).forEach(g=>i.add(g.name))}}),s&&setTimeout(()=>{s({meshes:o.sort(),materials:Array.from(i).sort(),bones:f.sort(),nodeCount:p})},0),m},[u,s]),d=typeof t=="number"?[t,t,t]:t;return y.jsx("group",{position:n,rotation:r,scale:d,children:y.jsx("primitive",{object:b})})}G.preload=e=>{M.useGLTF.preload(e)};function L(e){const n=new Map,r=new Map,t=e.clone();return F(e,t,function(s,c){n.set(c,s),r.set(s,c)}),t.traverse(function(s){if(!s.isSkinnedMesh)return;const c=s,u=n.get(s),b=u.skeleton.bones;c.skeleton=u.skeleton.clone(),c.bindMatrix.copy(u.bindMatrix),c.skeleton.bones=b.map(function(d){return r.get(d)}),c.bind(c.skeleton,c.bindMatrix)}),t}function F(e,n,r){r(e,n);for(let t=0;t<e.children.length;t++)F(e.children[t],n.children[t],r)}function q(e,n,r){const{actions:t,names:s}=M.useAnimations(e,n),c=a.useRef(null),u=a.useRef(r==null?void 0:r.defaultAnimation);a.useEffect(()=>{if(s.length===0)return;const o=u.current;let i=s[0];if(o){const p=s.find(h=>h===o||h.includes(o));p&&(i=p)}const f=t[i];f&&(f.reset().fadeIn(.5).play(),c.current=f)},[t,s]);const b=a.useCallback((o,i)=>{const{loop:f=!1,crossFadeDuration:p=.2,restoreDefault:h=!0}=i||{};let l=t[o];if(!l){const g=Object.keys(t).find(S=>S.toLowerCase().includes(o.toLowerCase())||o.toLowerCase().includes(S.toLowerCase()));g&&(l=t[g])}if(!l){console.warn(`Animation "${o}" not found. Available: ${s.join(", ")}`);return}const x=c.current;if(!(x===l&&l.isRunning())&&(x&&x!==l&&x.fadeOut(p),l.reset(),l.fadeIn(p),l.setLoop(f?w.LoopRepeat:w.LoopOnce,f?1/0:1),l.clampWhenFinished=!f,l.play(),f||l.getMixer().update(0),c.current=l,h&&!f&&u.current)){const g=l.getMixer(),S=j=>{if(j.action===l){g.removeEventListener("finished",S);const A=t[u.current];A&&(l.fadeOut(p),A.reset().fadeIn(p).play(),c.current=A)}};g.addEventListener("finished",S)}},[t,s]),d=a.useCallback(()=>{var o;(o=c.current)==null||o.fadeOut(.2),c.current=null},[]),m=a.useCallback(()=>s,[s]);return{playAnimation:b,stopAnimation:d,getAnimationNames:m,actions:t}}function O(e,n){const r=a.useRef(n||{}),t=a.useRef([]),s=a.useRef([]);a.useEffect(()=>{const d=new Set,m=[];e.traverse(o=>{o instanceof w.Mesh&&o.morphTargetDictionary&&o.morphTargetInfluences&&(m.push(o),Object.keys(o.morphTargetDictionary).forEach(i=>{d.add(i)}))}),t.current=Array.from(d).sort(),s.current=m},[e]),D.useFrame(()=>{const d=r.current;s.current.forEach(m=>{!m.morphTargetDictionary||!m.morphTargetInfluences||Object.entries(d).forEach(([o,i])=>{const f=m.morphTargetDictionary[o];f!==void 0&&(m.morphTargetInfluences[f]=i)})})}),a.useEffect(()=>{n&&(r.current={...n})},[n]);const c=a.useCallback((d,m)=>{r.current[d]=Math.max(0,Math.min(1,m))},[]),u=a.useCallback(()=>t.current,[]),b=a.useCallback(()=>({...r.current}),[]);return{setMorphTarget:c,getMorphTargetNames:u,getMorphTargetValues:b}}const H=a.createContext(null);function J(){const e=a.useContext(H);if(!e)throw new Error("BoneAttachment must be used within an AnimatedModel");return e}const P=a.forwardRef(({url:e,position:n=[0,0,0],rotation:r=[0,0,0],scale:t=1,defaultAnimation:s,morphTargets:c,onLoad:u,onError:b,children:d},m)=>{const o=a.useRef(null),i=a.useRef([]),{scene:f,animations:p}=M.useGLTF(e),h=a.useMemo(()=>L(f),[f]),{playAnimation:l,stopAnimation:x,getAnimationNames:g}=q(p,h,{defaultAnimation:s}),{setMorphTarget:S}=O(h,c);a.useEffect(()=>{if(!h)return;const T=setTimeout(()=>{const C=[],k=[],B=new Set,N=new Set;let I=0;h.traverse(R=>{if(I++,R.type==="Bone"&&C.push(R.name),R.isMesh){const v=R;k.push(v.name),v.castShadow=!0,v.receiveShadow=!0,(Array.isArray(v.material)?v.material:[v.material]).forEach(E=>{B.add(E.name),E.shadowSide=w.DoubleSide}),v.morphTargetDictionary&&Object.keys(v.morphTargetDictionary).forEach(E=>{N.add(E)})}}),i.current=Array.from(N).sort(),u==null||u({meshes:k.sort(),materials:Array.from(B).sort(),bones:C.sort(),nodeCount:I,animations:p.map(R=>R.name),morphTargetNames:i.current})},0);return()=>clearTimeout(T)},[h,p,u]),a.useImperativeHandle(m,()=>({playAnimation:l,stopAnimation:x,getAnimationNames:g,getGroup:()=>o.current,setMorphTarget:S,getMorphTargetNames:()=>i.current}));const j=a.useMemo(()=>({scene:h,getBone:T=>h.getObjectByName(T)||null}),[h]),A=typeof t=="number"?[t,t,t]:t;return y.jsx(H.Provider,{value:j,children:y.jsxs("group",{ref:o,position:n,rotation:r,scale:A,children:[y.jsx("primitive",{object:h}),d]})})});P.displayName="AnimatedModel";P.preload=e=>{M.useGLTF.preload(e)};const Z=a.forwardRef(({url:e,position:n=[0,0,0],rotation:r=[0,0,0],scale:t=1,morphTargets:s,onMorphTargetsFound:c,onLoad:u,onError:b},d)=>{const{scene:m}=M.useGLTF(e),o=a.useMemo(()=>m.clone(),[m]),{setMorphTarget:i,getMorphTargetNames:f,getMorphTargetValues:p}=O(o,s);a.useEffect(()=>{const l=f();l.length>0&&(c==null||c(l))},[o,f,c]),a.useEffect(()=>{if(!o)return;const l=[],x=new Set,g=[];let S=0;o.traverse(j=>{if(S++,j.type==="Bone"&&g.push(j.name),j.isMesh){const A=j;l.push(A.name),A.castShadow=!0,A.receiveShadow=!0,(Array.isArray(A.material)?A.material:[A.material]).forEach(C=>x.add(C.name))}}),u==null||u({meshes:l.sort(),materials:Array.from(x).sort(),bones:g.sort(),nodeCount:S})},[o,u]),a.useImperativeHandle(d,()=>({setMorphTarget:i,getMorphTargetNames:f,getMorphTargetValues:p}));const h=typeof t=="number"?[t,t,t]:t;return y.jsx("group",{position:n,rotation:r,scale:h,children:y.jsx("primitive",{object:o})})});Z.displayName="MorphableModel";function K({children:e,bone:n,position:r=[0,0,0],rotation:t=[0,0,0],scale:s=1}){const{getBone:c}=J(),[u,b]=a.useState(null);if(a.useEffect(()=>{const m=c(n);m?b(m):console.warn(`Bone "${n}" not found in model`)},[n,c]),!u)return null;const d=typeof s=="number"?[s,s,s]:s;return D.createPortal(y.jsx("group",{position:r,rotation:t,scale:d,children:e}),u)}function Q(e,n){const{scene:r,animations:t}=M.useGLTF(e),s=a.useMemo(()=>L(r),[r]);return a.useEffect(()=>{var m;if(!s)return;const c=[],u=[],b=new Set;let d=0;s.traverse(o=>{if(d++,o.type==="Bone"&&c.push(o.name),o.isMesh){const i=o;u.push(i.name),i.castShadow=!0,i.receiveShadow=!0,(Array.isArray(i.material)?i.material:[i.material]).forEach(p=>{b.add(p.name),p.shadowSide=w.DoubleSide})}}),(m=n==null?void 0:n.onLoad)==null||m.call(n,{meshes:u.sort(),materials:Array.from(b).sort(),bones:c.sort(),nodeCount:d})},[s,n]),{scene:s,animations:t}}function U(e){M.useGLTF.preload(e)}exports.AnimatedModel=P;exports.BoneAttachment=K;exports.Model=G;exports.MorphableModel=Z;exports.Scene3D=V;exports.preloadModel=U;exports.useAnimationController=q;exports.useClonedModel=Q;exports.useMorphTargets=O;
|
|
2
|
+
//# sourceMappingURL=mbt-3d.cjs.js.map
|