@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,203 @@
1
+ import { useEffect, useRef, useCallback, useState } from 'react'
2
+ import { Bone } from 'three'
3
+ import { useFrame } from '@react-three/fiber'
4
+ import type { ModelLoadConfig, AnimationStatesConfig, CharacterInternal, Character } from '../types'
5
+ import type { ModelLoaderAdapter } from './useModelLoader'
6
+ import type { AnimationLoaderAdapter } from './useAnimationLoader'
7
+ import { useModelLoader } from './useModelLoader'
8
+ import { useAnimationLoader } from './useAnimationLoader'
9
+ import { useAnimationControl } from './useAnimationControl'
10
+ import { useAnimationStateMachine } from './useAnimationStateMachine'
11
+ import { useMorphControl } from './useMorphControl'
12
+ import { useProceduralAnimations } from './useProceduralAnimations'
13
+ import { useTalkingControl } from './useTalkingControl'
14
+ import { findHeadBone } from '../utils/characterUtils'
15
+
16
+ export interface UseCharacterControllerOptions {
17
+ /** 模型配置 */
18
+ model: ModelLoadConfig
19
+ /** 模型加载适配器 */
20
+ modelAdapter: ModelLoaderAdapter
21
+ /** 动画加载适配器 */
22
+ animationAdapter: AnimationLoaderAdapter
23
+ /** 动画状态机配置(可选) */
24
+ animationStates?: AnimationStatesConfig
25
+ /** 初始状态(默认 'idle') */
26
+ initialState?: string
27
+ /** 初始情绪 */
28
+ initialMood?: string
29
+ /** 是否启用 welcome 状态 */
30
+ enableWelcome?: boolean
31
+ /** 错误回调 */
32
+ onError?: (error: Error) => void
33
+ /** 状态变化回调 */
34
+ onStateChange?: (state: string, mood: string) => void
35
+ }
36
+
37
+ /**
38
+ * Character 控制器核心 Hook
39
+ * 处理模型/动画加载、AnimationMixer 管理、Character 对象构建
40
+ * 支持基础动画控制和状态机控制
41
+ *
42
+ * @param options - 配置选项
43
+ * @returns Character 对象和加载状态
44
+ */
45
+ export function useCharacterController(options: UseCharacterControllerOptions): {
46
+ loadedModel: any
47
+ character: CharacterInternal | null
48
+ isReady: boolean
49
+ } {
50
+ const {
51
+ model,
52
+ modelAdapter,
53
+ animationAdapter,
54
+ animationStates,
55
+ initialState = 'idle',
56
+ initialMood = 'neutral',
57
+ enableWelcome = false,
58
+ onError,
59
+ onStateChange
60
+ } = options
61
+
62
+ // 加载模型和动画
63
+ const { model: loadedModel, state } = useModelLoader(model, modelAdapter)
64
+ const { clips, animationsMap, state: animationState } = useAnimationLoader(model.animations, animationAdapter)
65
+
66
+ const headBoneRef = useRef<Bone | null>(null)
67
+ const [character, setCharacter] = useState<CharacterInternal | null>(null)
68
+
69
+ // 使用子 hooks 管理各个功能模块(注意顺序:morphControl 必须在 stateMachine 之前)
70
+ const animationControl = useAnimationControl({
71
+ model: loadedModel,
72
+ animationsMap,
73
+ })
74
+
75
+ const morphControl = useMorphControl(loadedModel)
76
+ const proceduralAnimations = useProceduralAnimations()
77
+
78
+ const stateMachine = useAnimationStateMachine({
79
+ animationStates,
80
+ initialState,
81
+ initialMood,
82
+ enableWelcome,
83
+ playAnimation: animationControl.play,
84
+ mixer: animationControl.mixer,
85
+ applyEmotion: morphControl.applyEmotion,
86
+ onStateChange,
87
+ })
88
+
89
+ // 用于记录当前状态下的动画列表和索引(用于顺序播放)
90
+ const currentAnimationListRef = useRef<string[]>([])
91
+ const currentAnimationIndexRef = useRef<number>(0)
92
+
93
+ // 用于记录目标状态(welcome 结束后要切换到的状态)
94
+ const targetStateAfterWelcomeRef = useRef<string>(initialState)
95
+
96
+ // 说话控制(需要在 stateMachine 之后初始化)
97
+ const talkingControl = useTalkingControl({
98
+ animationStates,
99
+ morphMeshes: morphControl.morphMeshes,
100
+ playAnimation: animationControl.play,
101
+ setState: stateMachine.setState,
102
+ })
103
+
104
+ // 处理错误
105
+ useEffect(() => {
106
+ if (state.error && onError) {
107
+ onError(state.error)
108
+ }
109
+ if (animationState.error && onError) {
110
+ onError(animationState.error)
111
+ }
112
+ }, [state.error, animationState.error, onError])
113
+
114
+ // 初始化头部骨骼
115
+ useEffect(() => {
116
+ if (loadedModel?.scene) {
117
+ headBoneRef.current = findHeadBone(loadedModel)
118
+
119
+ return () => {
120
+ headBoneRef.current = null
121
+ }
122
+ }
123
+ }, [loadedModel, morphControl.morphMeshes.length, animationControl.mixer])
124
+
125
+
126
+ // 资源加载完成后初始化(只执行一次)
127
+ useEffect(() => {
128
+ console.log('[useCharacterController] Check:', {
129
+ hasCharacter: !!character,
130
+ hasModel: !!loadedModel,
131
+ hasMixer: !!animationControl.mixer,
132
+ hasAnimationsMap: !!animationsMap,
133
+ animationsMapSize: animationsMap?.size,
134
+ })
135
+
136
+ if (character) return // 已经初始化过了
137
+ if (!loadedModel || !animationControl.mixer) return
138
+
139
+ // 检查动画是否需要加载
140
+ const hasAnimations = model.animations && model.animations.length > 0
141
+ if (hasAnimations && (!animationsMap || animationsMap.size === 0)) return
142
+
143
+ console.log('[useCharacterController] ✅ Creating character...')
144
+
145
+ // 构建 Character 对象(极简公共接口 + 完整内部接口)
146
+ const characterObj: CharacterInternal = {
147
+ model: loadedModel,
148
+ clips,
149
+ animationsMap,
150
+ getClip: (id: string) => animationsMap?.get(id) || null,
151
+ mixer: animationControl.mixer,
152
+
153
+ // 公共 API(仅 4 个核心方法)
154
+ setState: stateMachine.setState,
155
+ getState: stateMachine.getCurrentState,
156
+ startSpeak: talkingControl.startSpeak,
157
+ stopSpeak: talkingControl.stopSpeak,
158
+
159
+ // 内部 API(不对外暴露,但内部可用)
160
+ play: animationControl.play,
161
+ stop: animationControl.stop,
162
+ pause: animationControl.pause,
163
+ resume: animationControl.resume,
164
+ getCurrentAnimation: animationControl.getCurrentAnimation,
165
+ isPlaying: animationControl.isPlaying,
166
+ isSpeaking: talkingControl.isSpeaking,
167
+ setMood: stateMachine.setMood,
168
+ applyEmotion: morphControl.applyEmotion,
169
+ setMorphWeight: morphControl.setMorphWeight,
170
+ setDualMorph: morphControl.setDualMorph,
171
+ setHeadSwayEnabled: proceduralAnimations.setHeadSwayEnabled,
172
+ setBlinkingEnabled: proceduralAnimations.setBlinkingEnabled,
173
+ setLipSyncEnabled: proceduralAnimations.setLipSyncEnabled,
174
+ }
175
+
176
+ // 初始化状态机
177
+ stateMachine.initialize()
178
+
179
+ // 设置 character(这会触发 isReady = true)
180
+ setCharacter(characterObj)
181
+ console.log('[useCharacterController] ✅ Character created and set!')
182
+ }, [loadedModel, animationControl.mixer, animationsMap, character])
183
+
184
+ // 更新程序化动画
185
+ useFrame((_, delta) => {
186
+ // 更新程序化动画
187
+ proceduralAnimations.update(delta, {
188
+ headBone: headBoneRef.current,
189
+ morphMeshes: morphControl.morphMeshes,
190
+ emotion: talkingControl.getCurrentEmotion(),
191
+ isSpeaking: talkingControl.isSpeaking(),
192
+ })
193
+
194
+ // 更新说话手势调度
195
+ talkingControl.updateTalkGestures(delta)
196
+ })
197
+
198
+ return {
199
+ loadedModel,
200
+ character,
201
+ isReady: !!character,
202
+ }
203
+ }
@@ -0,0 +1,115 @@
1
+ import { useState, useEffect } from 'react'
2
+ import type { ModelLoadConfig, SceneState } from '../types'
3
+
4
+ /**
5
+ * 平台特定的模型加载适配器
6
+ * 平台包提供具体的加载实现
7
+ */
8
+ export interface ModelLoaderAdapter {
9
+ /**
10
+ * 加载模型文件
11
+ * @param path 模型文件路径
12
+ * @param onProgress 进度回调
13
+ * @returns Promise<GLTF 对象>
14
+ */
15
+ load: (
16
+ path: string | number,
17
+ onProgress?: (progress: number) => void
18
+ ) => Promise<any> // GLTF
19
+ }
20
+
21
+ /**
22
+ * 使用模型加载器的 Hook
23
+ * 在 shared 中实现核心逻辑,使用平台适配器加载
24
+ */
25
+ export function useModelLoader(
26
+ config: ModelLoadConfig,
27
+ adapter: ModelLoaderAdapter
28
+ ): { model: any | null; state: SceneState } {
29
+ const [state, setState] = useState<SceneState>({
30
+ loaded: false,
31
+ progress: 0,
32
+ })
33
+ const [model, setModel] = useState<any | null>(null)
34
+
35
+ useEffect(() => {
36
+ let mounted = true
37
+
38
+ const loadModel = async () => {
39
+ try {
40
+ setState({ loaded: false, progress: 0 })
41
+
42
+ const gltf = await adapter.load(config.path, (progress) => {
43
+ if (mounted) {
44
+ setState((prev) => ({
45
+ ...prev,
46
+ progress,
47
+ }))
48
+ }
49
+ })
50
+
51
+ if (mounted) {
52
+ setModel(gltf)
53
+ setState({
54
+ loaded: true,
55
+ progress: 1,
56
+ error: null,
57
+ })
58
+ }
59
+ } catch (error) {
60
+ if (mounted) {
61
+ setState({
62
+ loaded: false,
63
+ progress: 0,
64
+ error: error as Error,
65
+ })
66
+ }
67
+ }
68
+ }
69
+
70
+ loadModel()
71
+
72
+ return () => {
73
+ mounted = false
74
+ }
75
+ }, [config.path, adapter])
76
+
77
+ // 应用配置(缩放、位置、旋转等)
78
+ useEffect(() => {
79
+ if (model?.scene) {
80
+ const { scale, position, rotation, castShadow, receiveShadow } = config
81
+
82
+ if (scale) {
83
+ if (Array.isArray(scale)) {
84
+ model.scene.scale.set(...scale)
85
+ } else {
86
+ model.scene.scale.setScalar(scale)
87
+ }
88
+ }
89
+
90
+ if (position) {
91
+ model.scene.position.set(...position)
92
+ }
93
+
94
+ if (rotation) {
95
+ model.scene.rotation.set(...rotation)
96
+ }
97
+
98
+ // 遍历所有 mesh,设置阴影
99
+ model.scene.traverse((child: any) => {
100
+ if ('castShadow' in child) {
101
+ child.castShadow = castShadow ?? false
102
+ }
103
+ if ('receiveShadow' in child) {
104
+ child.receiveShadow = receiveShadow ?? false
105
+ }
106
+ })
107
+ }
108
+ }, [model, config])
109
+
110
+ return {
111
+ model,
112
+ state,
113
+ }
114
+ }
115
+
@@ -0,0 +1,46 @@
1
+ import { useRef, useCallback, useEffect } from 'react'
2
+ import type { SkinnedMesh } from 'three'
3
+ import {
4
+ findMorphTargetMeshes,
5
+ setMorphWeight as utilSetMorphWeight,
6
+ setDualMorph as utilSetDualMorph,
7
+ applyEmotionMorphs,
8
+ } from '../utils/characterUtils'
9
+
10
+ /**
11
+ * Morph Target 控制 Hook
12
+ * 管理模型的 morph targets(表情控制)
13
+ */
14
+ export function useMorphControl(model: any) {
15
+ const morphMeshesRef = useRef<SkinnedMesh[]>([])
16
+
17
+ // 初始化 morph meshes
18
+ useEffect(() => {
19
+ if (model?.scene) {
20
+ morphMeshesRef.current = findMorphTargetMeshes(model)
21
+ }
22
+ return () => {
23
+ morphMeshesRef.current = []
24
+ }
25
+ }, [model])
26
+
27
+ const setMorphWeight = useCallback((morphName: string, value: number) => {
28
+ utilSetMorphWeight(morphMeshesRef.current, morphName, value)
29
+ }, [])
30
+
31
+ const setDualMorph = useCallback((baseName: string, value: number) => {
32
+ utilSetDualMorph(morphMeshesRef.current, baseName, value)
33
+ }, [])
34
+
35
+ const applyEmotion = useCallback((emotion: string) => {
36
+ applyEmotionMorphs(morphMeshesRef.current, emotion)
37
+ }, [])
38
+
39
+ return {
40
+ morphMeshes: morphMeshesRef.current,
41
+ setMorphWeight,
42
+ setDualMorph,
43
+ applyEmotion,
44
+ }
45
+ }
46
+
@@ -0,0 +1,91 @@
1
+ import { useRef, useCallback } from 'react'
2
+ import type { Bone, SkinnedMesh } from 'three'
3
+ import {
4
+ setMorphWeight,
5
+ setDualMorph,
6
+ getEmotionHeadSwayParams,
7
+ } from '../utils/characterUtils'
8
+
9
+ export interface ProceduralAnimationsOptions {
10
+ headBone: Bone | null
11
+ morphMeshes: SkinnedMesh[]
12
+ emotion: string
13
+ isSpeaking: boolean
14
+ }
15
+
16
+ /**
17
+ * 程序化动画控制 Hook
18
+ * 管理头部微动、眨眼、lip-sync 等程序化动画
19
+ */
20
+ export function useProceduralAnimations() {
21
+ const elapsedTimeRef = useRef<number>(0)
22
+ const headSwayEnabledRef = useRef<boolean>(true)
23
+ const blinkingEnabledRef = useRef<boolean>(true)
24
+ const lipSyncEnabledRef = useRef<boolean>(true)
25
+
26
+ const setHeadSwayEnabled = useCallback((enabled: boolean) => {
27
+ headSwayEnabledRef.current = enabled
28
+ }, [])
29
+
30
+ const setBlinkingEnabled = useCallback((enabled: boolean) => {
31
+ blinkingEnabledRef.current = enabled
32
+ }, [])
33
+
34
+ const setLipSyncEnabled = useCallback((enabled: boolean) => {
35
+ lipSyncEnabledRef.current = enabled
36
+ }, [])
37
+
38
+ /**
39
+ * 更新程序化动画(在 useFrame 中调用)
40
+ */
41
+ const update = useCallback((delta: number, options: ProceduralAnimationsOptions) => {
42
+ elapsedTimeRef.current += delta
43
+ const time = elapsedTimeRef.current
44
+ const { headBone, morphMeshes, emotion, isSpeaking } = options
45
+
46
+ // 头部微动
47
+ if (headSwayEnabledRef.current && headBone) {
48
+ const { speed, range, pitchOffset } = getEmotionHeadSwayParams(emotion)
49
+
50
+ let yaw = Math.sin(time * speed) * range
51
+ let pitch = (Math.cos(time * speed * 0.7) * (range * 0.5)) + pitchOffset
52
+
53
+ if (isSpeaking) {
54
+ yaw *= 0.3
55
+ pitch += Math.sin(time * 8.0) * 0.03
56
+ }
57
+
58
+ headBone.rotation.y = yaw
59
+ headBone.rotation.x = pitch
60
+ }
61
+
62
+ // 眨眼
63
+ if (blinkingEnabledRef.current && morphMeshes.length > 0) {
64
+ const blinkSignal = Math.sin(time * 1.2)
65
+ const isBlinking = blinkSignal > 0.98
66
+ setDualMorph(morphMeshes, 'eyeBlink', isBlinking ? 1.0 : 0.0)
67
+ }
68
+
69
+ // Lip Sync
70
+ if (lipSyncEnabledRef.current && morphMeshes.length > 0) {
71
+ if (isSpeaking) {
72
+ const openAmount = (Math.sin(time * 18.0) + 1) * 0.25
73
+ setMorphWeight(morphMeshes, 'jawOpen', openAmount)
74
+
75
+ const funnelAmount = (Math.cos(time * 12.0) + 1) * 0.2
76
+ setDualMorph(morphMeshes, 'mouthFunnel', funnelAmount)
77
+ } else {
78
+ setMorphWeight(morphMeshes, 'jawOpen', 0)
79
+ setDualMorph(morphMeshes, 'mouthFunnel', 0)
80
+ }
81
+ }
82
+ }, [])
83
+
84
+ return {
85
+ update,
86
+ setHeadSwayEnabled,
87
+ setBlinkingEnabled,
88
+ setLipSyncEnabled,
89
+ }
90
+ }
91
+
@@ -0,0 +1,109 @@
1
+ import { useRef, useCallback } from 'react'
2
+ import type { SkinnedMesh } from 'three'
3
+ import type { AnimationStatesConfig } from '../types'
4
+ import {
5
+ resetLipSyncMorphs,
6
+ getEmotionHeadSwayParams,
7
+ applyEmotionMorphs,
8
+ } from '../utils/characterUtils'
9
+
10
+ export interface TalkingControlOptions {
11
+ animationStates?: AnimationStatesConfig
12
+ morphMeshes: SkinnedMesh[]
13
+ playAnimation: (animationId: string, options?: { loop?: boolean }) => void
14
+ setState: (state: string, mood?: string) => void
15
+ }
16
+
17
+ /**
18
+ * 说话控制 Hook
19
+ * 管理说话状态、情绪和手势调度
20
+ */
21
+ export function useTalkingControl(options: TalkingControlOptions) {
22
+ const { animationStates, morphMeshes, playAnimation, setState } = options
23
+
24
+ const isSpeakingRef = useRef<boolean>(false)
25
+ const currentEmotionRef = useRef<string>('neutral')
26
+ const talkGestureAnimationsRef = useRef<string[]>([])
27
+ const talkTimerRef = useRef<number>(0)
28
+ const nextTalkSwitchRef = useRef<number>(0)
29
+
30
+ const extractTalkGestures = useCallback(() => {
31
+ if (!animationStates?.speaking) return
32
+
33
+ const speakingConfig = animationStates.speaking
34
+ // animations 现在直接是 string[]
35
+ talkGestureAnimationsRef.current = speakingConfig.animations
36
+ }, [animationStates])
37
+
38
+ const triggerRandomTalkGesture = useCallback(() => {
39
+ if (talkGestureAnimationsRef.current.length === 0) {
40
+ extractTalkGestures()
41
+ }
42
+
43
+ if (talkGestureAnimationsRef.current.length === 0) {
44
+ return
45
+ }
46
+
47
+ const randomIndex = Math.floor(Math.random() * talkGestureAnimationsRef.current.length)
48
+ const nextAnim = talkGestureAnimationsRef.current[randomIndex]
49
+
50
+ playAnimation(nextAnim, { loop: true })
51
+
52
+ talkTimerRef.current = 0
53
+ nextTalkSwitchRef.current = 2.0 + Math.random() * 2.0
54
+ }, [playAnimation, extractTalkGestures])
55
+
56
+ const startSpeak = useCallback((emotion?: string) => {
57
+ const targetEmotion = emotion || currentEmotionRef.current
58
+ currentEmotionRef.current = targetEmotion
59
+ isSpeakingRef.current = true
60
+
61
+ applyEmotionMorphs(morphMeshes, targetEmotion)
62
+
63
+ if (animationStates?.speaking) {
64
+ setState('speaking', targetEmotion)
65
+ triggerRandomTalkGesture()
66
+ }
67
+ }, [animationStates, morphMeshes, setState, triggerRandomTalkGesture])
68
+
69
+ const stopSpeak = useCallback((resetEmotion?: string) => {
70
+ isSpeakingRef.current = false
71
+
72
+ const targetEmotion = resetEmotion || currentEmotionRef.current
73
+ if (resetEmotion) {
74
+ currentEmotionRef.current = resetEmotion
75
+ }
76
+
77
+ resetLipSyncMorphs(morphMeshes)
78
+ applyEmotionMorphs(morphMeshes, targetEmotion)
79
+
80
+ if (animationStates?.idle) {
81
+ setState('idle', targetEmotion)
82
+ }
83
+ }, [animationStates, morphMeshes, setState])
84
+
85
+ const isSpeaking = useCallback(() => isSpeakingRef.current, [])
86
+
87
+ const getCurrentEmotion = useCallback(() => currentEmotionRef.current, [])
88
+
89
+ /**
90
+ * 更新说话手势调度(在 useFrame 中调用)
91
+ */
92
+ const updateTalkGestures = useCallback((delta: number) => {
93
+ if (isSpeakingRef.current && animationStates?.speaking) {
94
+ talkTimerRef.current += delta
95
+ if (talkTimerRef.current > nextTalkSwitchRef.current) {
96
+ triggerRandomTalkGesture()
97
+ }
98
+ }
99
+ }, [animationStates, triggerRandomTalkGesture])
100
+
101
+ return {
102
+ startSpeak,
103
+ stopSpeak,
104
+ isSpeaking,
105
+ getCurrentEmotion,
106
+ updateTalkGestures,
107
+ }
108
+ }
109
+
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 3D-Shared 公共 API
3
+ * 只暴露用户需要的配置类型和通用组件
4
+ */
5
+
6
+ // ============================================================
7
+ // 类型定义 - 用户配置所需
8
+ // ============================================================
9
+ export type {
10
+ // 场景配置
11
+ SceneConfig,
12
+ LightConfig,
13
+ GridHelperConfig,
14
+ OrbitControlsConfig,
15
+
16
+ // 模型和动画配置
17
+ ModelLoadConfig,
18
+ AnimationConfig,
19
+ AnimationStatesConfig,
20
+ AnimationStateConfig,
21
+
22
+ // Character 接口(供用户调用,只包含高级 API)
23
+ Character,
24
+ CharacterViewProps,
25
+ OverlayConfig,
26
+ // 注意:不导出 CharacterInternal(内部使用)
27
+ } from './types'
28
+
29
+ // ============================================================
30
+ // 通用组件 - 跨平台复用
31
+ // ============================================================
32
+ export { SceneContent } from './components/SceneContent'
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 3D-Shared 内部 API
3
+ * 仅供 @linxai/3d-web 和 @linxai/3d-native 使用
4
+ * 不对最终用户暴露
5
+ */
6
+
7
+ // ============================================================
8
+ // 核心 Hook - 平台包只需要这一个
9
+ // ============================================================
10
+ export { useCharacterController } from './hooks/useCharacterController'
11
+ export type { UseCharacterControllerOptions } from './hooks/useCharacterController'
12
+
13
+ // ============================================================
14
+ // 适配器接口 - 平台包需要实现
15
+ // ============================================================
16
+ export type { ModelLoaderAdapter } from './hooks/useModelLoader'
17
+ export type { AnimationLoaderAdapter } from './hooks/useAnimationLoader'
18
+
19
+ // ============================================================
20
+ // 场景构建 - 平台包使用
21
+ // ============================================================
22
+ export type { SceneBuilder } from './components/SceneBuilder'
23
+ export { useSceneBuilder } from './components/SceneBuilder'
24
+ export { SceneContent } from './components/SceneContent'
25
+
26
+ // ============================================================
27
+ // Character 接口 - 平台包构建 Character 对象使用
28
+ // ============================================================
29
+ export type { Character, CharacterInternal, CharacterViewProps } from './types/character'
30
+
31
+ // ============================================================
32
+ // 上下文 - 平台包使用
33
+ // ============================================================
34
+ export { LoadingProvider, useLoading } from './contexts/LoadingContext'
35
+
36
+ // ============================================================
37
+ // 工具函数 - 平台包使用
38
+ // ============================================================
39
+ export {
40
+ findHeadBone,
41
+ findMorphTargetMeshes,
42
+ setMorphWeight,
43
+ setDualMorph,
44
+ resetLipSyncMorphs,
45
+ getEmotionHeadSwayParams,
46
+ applyEmotionMorphs,
47
+ } from './utils/characterUtils'
48
+
49
+ // ============================================================
50
+ // 类型定义 - 平台包使用
51
+ // ============================================================
52
+ export type { SceneConfig, SceneState } from './types'