@linxai/3d-shared 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.
@@ -0,0 +1,136 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { SceneConfig, OrbitControlsConfig, ModelLoadConfig, AnimationStatesConfig } from './index'
3
+
4
+ /**
5
+ * Character 接口
6
+ * 只暴露最核心的 API,极简设计
7
+ */
8
+ export interface Character {
9
+ /** 加载的模型对象 */
10
+ model: any
11
+
12
+ /** 设置动画状态(如 'idle', 'speaking', 'dancing') */
13
+ setState: (state: string, mood?: string) => void
14
+
15
+ /** 获取当前状态和情绪 */
16
+ getState: () => { state: string; mood: string }
17
+
18
+ /** 开始说话(可选指定情绪) */
19
+ startSpeak: (emotion?: string) => void
20
+
21
+ /** 停止说话 */
22
+ stopSpeak: () => void
23
+
24
+ /** 追加字幕文本(支持流式,仅 web 平台) */
25
+ appendSubtitle?: (text: string) => void
26
+
27
+ /** 替换字幕文本(仅 web 平台) */
28
+ setSubtitle?: (text: string) => void
29
+
30
+ /** 清空字幕(仅 web 平台) */
31
+ clearSubtitle?: () => void
32
+ }
33
+
34
+ /**
35
+ * 内部 Character 接口
36
+ * 包含所有底层 API,仅供内部使用
37
+ * @internal
38
+ */
39
+ export interface CharacterInternal extends Character {
40
+ /** 动画剪辑列表(内部使用) */
41
+ clips: any[] | null
42
+ /** 根据 ID 获取动画剪辑(内部使用) */
43
+ getClip: (id: string) => any | null
44
+ /** 动画 Map (id -> clip)(内部使用) */
45
+ animationsMap: Map<string, any>
46
+ /** AnimationMixer 实例(内部使用) */
47
+ mixer: any | null
48
+
49
+ // 底层动画控制(内部使用)
50
+ play: (animationId: string, options?: { loop?: boolean; fadeIn?: number; fadeOut?: number }) => void
51
+ stop: (fadeOut?: number) => void
52
+ pause: () => void
53
+ resume: () => void
54
+ getCurrentAnimation: () => string | null
55
+ isPlaying: () => boolean
56
+ isSpeaking: () => boolean
57
+
58
+ // 情绪和表情控制(内部使用)
59
+ setMood: (mood: string) => void
60
+ applyEmotion: (emotion: string) => void
61
+ setMorphWeight: (morphName: string, value: number) => void
62
+ setDualMorph: (baseName: string, value: number) => void
63
+
64
+ // 程序化动画开关(内部使用)
65
+ setHeadSwayEnabled: (enabled: boolean) => void
66
+ setBlinkingEnabled: (enabled: boolean) => void
67
+ setLipSyncEnabled: (enabled: boolean) => void
68
+ }
69
+
70
+ /**
71
+ * 覆盖层配置(UI 相关)
72
+ * 字幕通过 Character.appendSubtitle/setSubtitle/clearSubtitle 控制
73
+ */
74
+ export interface OverlayConfig {
75
+ /** 是否显示浮层 */
76
+ show?: boolean
77
+ /** 是否显示说话按钮 */
78
+ showSpeakButton?: boolean
79
+ /** 是否正在加载/连接 */
80
+ isLoading?: boolean
81
+ /** 是否已连接 */
82
+ isConnected?: boolean
83
+ /** 按钮点击回调 */
84
+ onButtonClick?: () => void
85
+ /** 字幕样式 */
86
+ subtitleStyle?: any
87
+ /** 浮层容器样式 */
88
+ style?: any
89
+ }
90
+
91
+ /**
92
+ * CharacterView Props 接口
93
+ * 平台包需要实现这个接口
94
+ */
95
+ export interface CharacterViewProps {
96
+ // 核心配置(必需)
97
+ /** 模型配置 */
98
+ model: ModelLoadConfig
99
+ /** 动画状态机配置 */
100
+ animationStates?: AnimationStatesConfig
101
+
102
+ // 初始状态
103
+ /** 初始状态(默认 'idle') */
104
+ initialState?: string
105
+ /** 初始情绪(默认 'neutral') */
106
+ initialMood?: string
107
+ /** 是否启用欢迎动画 */
108
+ enableWelcome?: boolean
109
+
110
+ // 场景配置
111
+ /** 场景配置(灯光、相机等) */
112
+ sceneConfig?: SceneConfig
113
+ /** 轨道控制器配置 */
114
+ controlsConfig?: OrbitControlsConfig
115
+
116
+ // UI 配置
117
+ /** 覆盖层配置(字幕、按钮等) */
118
+ overlay?: OverlayConfig
119
+ /** Canvas 样式 */
120
+ style?: any
121
+ /** Canvas className */
122
+ className?: string
123
+
124
+ // 扩展内容
125
+ /** 额外的 3D 场景内容 */
126
+ children?: ReactNode
127
+
128
+ // 回调
129
+ /** 角色加载完成 */
130
+ onReady?: (character: Character) => void
131
+ /** 状态变化 */
132
+ onStateChange?: (state: string, mood: string) => void
133
+ /** 错误处理 */
134
+ onError?: (error: Error) => void
135
+ }
136
+
@@ -0,0 +1,197 @@
1
+ /**
2
+ * 光源配置
3
+ */
4
+ export interface LightConfig {
5
+ /** 环境光强度 */
6
+ ambientIntensity?: number
7
+ /** 点光源配置 */
8
+ pointLight?: {
9
+ position?: [number, number, number]
10
+ intensity?: number
11
+ } | null
12
+ /** 方向光配置 */
13
+ directionalLight?: {
14
+ position?: [number, number, number]
15
+ intensity?: number
16
+ } | null
17
+ }
18
+
19
+ /**
20
+ * GridHelper 配置
21
+ */
22
+ export interface GridHelperConfig {
23
+ /** 是否显示网格 */
24
+ visible?: boolean
25
+ /** 网格大小 */
26
+ size?: number
27
+ /** 网格分割数 */
28
+ divisions?: number
29
+ /** 中心线颜色 */
30
+ centerLineColor?: string
31
+ /** 网格线颜色 */
32
+ gridColor?: string
33
+ }
34
+
35
+ /**
36
+ * 3D 场景配置
37
+ */
38
+ export interface SceneConfig {
39
+ /** 背景颜色 */
40
+ backgroundColor?: string
41
+ /** 环境光强度(已废弃,使用 lights.ambientIntensity) */
42
+ ambientLightIntensity?: number
43
+ /** 光源配置 */
44
+ lights?: LightConfig
45
+ /** 相机位置 */
46
+ cameraPosition?: [number, number, number]
47
+ /** 相机视野角度 */
48
+ fov?: number
49
+ /** GridHelper 配置 */
50
+ gridHelper?: GridHelperConfig
51
+ }
52
+
53
+ /**
54
+ * OrbitControls 配置
55
+ */
56
+ export interface OrbitControlsConfig {
57
+ /** 是否启用阻尼 */
58
+ enableDamping?: boolean
59
+ /** 阻尼系数 */
60
+ dampingFactor?: number
61
+ /** 是否允许旋转 */
62
+ enableRotate?: boolean
63
+ /** 是否允许缩放 */
64
+ enableZoom?: boolean
65
+ /** 是否允许平移 */
66
+ enablePan?: boolean
67
+ /** 最小距离 */
68
+ minDistance?: number
69
+ /** 最大距离 */
70
+ maxDistance?: number
71
+ /** 最小极角 */
72
+ minPolarAngle?: number
73
+ /** 最大极角 */
74
+ maxPolarAngle?: number
75
+ /** 控制器目标点(摄像机看向的位置) */
76
+ target?: [number, number, number]
77
+ }
78
+
79
+ /**
80
+ * 动画配置
81
+ */
82
+ export interface AnimationConfig {
83
+ /** 动画 ID(用于重命名) */
84
+ id: string
85
+ /** 动画文件路径 (Web: URL string, Native: require() 结果或 Asset URI string) */
86
+ path: string | number
87
+ /** 动画描述(可选) */
88
+ description?: string
89
+ }
90
+
91
+ /**
92
+ * 动画状态机配置
93
+ */
94
+ export interface AnimationStateConfig {
95
+ /**
96
+ * 状态下的动画列表
97
+ * State 决定播放哪些动画,Mood 决定面部表情
98
+ */
99
+ animations: string[]
100
+ /** UI 显示标签(可选) */
101
+ label?: string
102
+ /** UI 显示图标(可选) */
103
+ icon?: string
104
+ /** 状态配置 */
105
+ config?: {
106
+ /** 是否循环播放 */
107
+ loop?: boolean
108
+ /** 持续时间(毫秒),用于自动切换到下一个动画 */
109
+ duration?: number
110
+ /** 淡入时间(秒) */
111
+ fadeIn?: number
112
+ /** 淡出时间(秒) */
113
+ fadeOut?: number
114
+ /** 是否随机播放(从动画列表中) */
115
+ random?: boolean
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 动画状态机总配置
121
+ */
122
+ export interface AnimationStatesConfig {
123
+ [state: string]: AnimationStateConfig
124
+ }
125
+
126
+ /**
127
+ * 模型加载配置
128
+ */
129
+ export interface ModelLoadConfig {
130
+ /** 模型路径 (Web: URL string, Native: require() 结果或 Asset URI string) */
131
+ path: string | number
132
+ /** 缩放比例 */
133
+ scale?: number | [number, number, number]
134
+ /** 位置偏移 */
135
+ position?: [number, number, number]
136
+ /** 旋转角度 */
137
+ rotation?: [number, number, number]
138
+ /** 是否启用阴影 */
139
+ castShadow?: boolean
140
+ /** 是否接收阴影 */
141
+ receiveShadow?: boolean
142
+ /** 动画配置列表 */
143
+ animations?: AnimationConfig[]
144
+ }
145
+
146
+ /**
147
+ * 模型实例配置
148
+ */
149
+ export interface ModelInstance {
150
+ /** 模型 ID */
151
+ id: string
152
+ /** 模型配置 */
153
+ config: ModelLoadConfig
154
+ /** 是否可见 */
155
+ visible?: boolean
156
+ }
157
+
158
+ /**
159
+ * 3D 场景状态
160
+ */
161
+ export interface SceneState {
162
+ /** 是否已加载 */
163
+ loaded: boolean
164
+ /** 加载进度 (0-1) */
165
+ progress?: number
166
+ /** 错误信息 */
167
+ error?: Error | null
168
+ }
169
+
170
+ /**
171
+ * 头部微动参数
172
+ */
173
+ export interface HeadSwayParams {
174
+ /** 摆动速度 */
175
+ speed: number
176
+ /** 摆动范围(弧度) */
177
+ range: number
178
+ /** 俯仰偏移(弧度) */
179
+ pitchOffset: number
180
+ }
181
+
182
+ /**
183
+ * 情绪表情配置
184
+ */
185
+ export interface EmotionConfig {
186
+ /** 情绪名称 */
187
+ emotion: string
188
+ /** 头部微动参数 */
189
+ headSway?: HeadSwayParams
190
+ /** Morph targets 配置 */
191
+ morphs?: {
192
+ [morphName: string]: number
193
+ }
194
+ }
195
+
196
+ // Character 接口(从独立文件导出)
197
+ export type { Character, CharacterViewProps, CharacterInternal, OverlayConfig } from './character'
@@ -0,0 +1,156 @@
1
+ import type { Object3D, Bone, SkinnedMesh } from 'three'
2
+
3
+ /**
4
+ * 查找模型中的头部骨骼(支持标准 Humanoid 骨骼命名)
5
+ */
6
+ export function findHeadBone(model: any): Bone | null {
7
+ if (!model?.scene) return null
8
+
9
+ const headBoneNames = ['Head', 'head', 'mixamorig:Head', 'mixamorigHead']
10
+
11
+ let headBone: Bone | null = null
12
+ model.scene.traverse((child: any) => {
13
+ if (child.isBone && headBoneNames.includes(child.name)) {
14
+ headBone = child as Bone
15
+ }
16
+ })
17
+
18
+ return headBone
19
+ }
20
+
21
+ /**
22
+ * 查找模型中所有包含 morph targets 的 SkinnedMesh
23
+ */
24
+ export function findMorphTargetMeshes(model: any): SkinnedMesh[] {
25
+ if (!model?.scene) return []
26
+
27
+ const meshes: SkinnedMesh[] = []
28
+ model.scene.traverse((child: any) => {
29
+ if (child.isSkinnedMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
30
+ meshes.push(child as SkinnedMesh)
31
+ }
32
+ })
33
+
34
+ return meshes
35
+ }
36
+
37
+ /**
38
+ * 设置单个 Morph Target 的权重
39
+ */
40
+ export function setMorphWeight(
41
+ meshes: SkinnedMesh[],
42
+ morphName: string,
43
+ value: number
44
+ ): void {
45
+ meshes.forEach(mesh => {
46
+ const index = mesh.morphTargetDictionary?.[morphName]
47
+ if (index !== undefined && mesh.morphTargetInfluences) {
48
+ mesh.morphTargetInfluences[index] = Math.max(0, Math.min(1, value))
49
+ }
50
+ })
51
+ }
52
+
53
+ /**
54
+ * 设置双侧 Morph Target(ARKit 标准,如 eyeBlinkLeft/eyeBlinkRight)
55
+ */
56
+ export function setDualMorph(
57
+ meshes: SkinnedMesh[],
58
+ baseName: string,
59
+ value: number
60
+ ): void {
61
+ const leftName = `${baseName}Left`
62
+ const rightName = `${baseName}Right`
63
+
64
+ setMorphWeight(meshes, leftName, value)
65
+ setMorphWeight(meshes, rightName, value)
66
+ }
67
+
68
+ /**
69
+ * 重置所有 lip-sync 相关的 morph targets
70
+ */
71
+ export function resetLipSyncMorphs(meshes: SkinnedMesh[]): void {
72
+ // 基础嘴部 morphs
73
+ setMorphWeight(meshes, 'jawOpen', 0)
74
+ setDualMorph(meshes, 'mouthFunnel', 0)
75
+
76
+ // 其他嘴部 morphs
77
+ const mouthMorphs = [
78
+ 'mouthClose',
79
+ 'mouthPucker',
80
+ 'mouthLeft',
81
+ 'mouthRight',
82
+ 'mouthRollLower',
83
+ 'mouthRollUpper',
84
+ 'mouthShrugLower',
85
+ 'mouthShrugUpper',
86
+ 'mouthStretch',
87
+ 'mouthPress',
88
+ 'mouthDimple',
89
+ 'mouthLowerDown',
90
+ 'mouthUpperUp',
91
+ ]
92
+
93
+ mouthMorphs.forEach(morphName => {
94
+ setMorphWeight(meshes, morphName, 0)
95
+ setDualMorph(meshes, morphName, 0)
96
+ })
97
+
98
+ // 重置眨眼
99
+ setDualMorph(meshes, 'eyeBlink', 0)
100
+ }
101
+
102
+ /**
103
+ * 获取情绪对应的头部微动参数
104
+ */
105
+ export function getEmotionHeadSwayParams(emotion: string) {
106
+ const D2R = Math.PI / 180
107
+
108
+ switch (emotion) {
109
+ case 'happy':
110
+ return { speed: 1.8, range: 7 * D2R, pitchOffset: -5 * D2R }
111
+ case 'sad':
112
+ return { speed: 0.6, range: 3 * D2R, pitchOffset: 8 * D2R }
113
+ case 'excited':
114
+ return { speed: 2.2, range: 9 * D2R, pitchOffset: -3 * D2R }
115
+ case 'neutral':
116
+ default:
117
+ return { speed: 1.0, range: 5 * D2R, pitchOffset: 0 }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 应用情绪表情 (ARKit morph targets)
123
+ */
124
+ export function applyEmotionMorphs(
125
+ meshes: SkinnedMesh[],
126
+ emotion: string
127
+ ): void {
128
+ // 重置基础表情
129
+ const morphsToReset = ['mouthSmile', 'mouthFrown', 'cheekSquint', 'eyeSquint', 'browDown']
130
+ morphsToReset.forEach(key => setDualMorph(meshes, key, 0))
131
+ setMorphWeight(meshes, 'browInnerUp', 0)
132
+
133
+ // 应用情绪表情
134
+ switch (emotion) {
135
+ case 'happy':
136
+ setDualMorph(meshes, 'mouthSmile', 0.7)
137
+ setDualMorph(meshes, 'cheekSquint', 0.5)
138
+ setDualMorph(meshes, 'eyeSquint', 0.2)
139
+ break
140
+ case 'sad':
141
+ setDualMorph(meshes, 'mouthFrown', 0.6)
142
+ setMorphWeight(meshes, 'browInnerUp', 0.7)
143
+ break
144
+ case 'excited':
145
+ setDualMorph(meshes, 'mouthSmile', 0.9)
146
+ setDualMorph(meshes, 'cheekSquint', 0.7)
147
+ setDualMorph(meshes, 'eyeSquint', 0.3)
148
+ setMorphWeight(meshes, 'browInnerUp', 0.4)
149
+ break
150
+ case 'neutral':
151
+ default:
152
+ setDualMorph(meshes, 'mouthSmile', 0.1)
153
+ break
154
+ }
155
+ }
156
+
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM"],
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "moduleResolution": "bundler",
17
+ "resolveJsonModule": true,
18
+ "allowSyntheticDefaultImports": true
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }
23
+
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/internal.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ external: ['react', 'react-native', 'three', '@react-three/fiber', '@react-three/drei'],
8
+ // 使用 React 17+ 的新 JSX runtime
9
+ esbuildOptions(options) {
10
+ options.jsx = 'automatic'
11
+ },
12
+ })
13
+