From b9cbb58a9d0e858fe4f8410b95ccf1e812964eaf Mon Sep 17 00:00:00 2001 From: yinsx Date: Mon, 5 Jan 2026 16:09:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/module-demo.html | 22 ++++- index.html | 69 ++++++++++++---- src/babylonjs/AppCamera.ts | 2 +- src/babylonjs/AppConfig.ts | 4 +- src/babylonjs/AppEnv.ts | 6 +- src/babylonjs/AppLight.ts | 159 ++----------------------------------- src/babylonjs/AppModel.ts | 55 ++++++++++--- src/babylonjs/AppRay.ts | 124 +++++++++++++++++++++++++++++ src/babylonjs/MainApp.ts | 12 +-- src/event/bridge.ts | 63 +++++++++++++++ src/event/bus.ts | 82 +++++++++++++++++++ src/event/types.ts | 37 +++++++++ src/main.ts | 23 +++++- src/utils/event.ts | 123 ---------------------------- 14 files changed, 460 insertions(+), 321 deletions(-) create mode 100644 src/babylonjs/AppRay.ts create mode 100644 src/event/bridge.ts create mode 100644 src/event/bus.ts create mode 100644 src/event/types.ts delete mode 100644 src/utils/event.ts diff --git a/examples/module-demo.html b/examples/module-demo.html index 71cb6a2..97c29a4 100644 --- a/examples/module-demo.html +++ b/examples/module-demo.html @@ -1,26 +1,40 @@ + SDK 模块化加载示例 + + - + + \ No newline at end of file diff --git a/index.html b/index.html index 3750b4d..aca3388 100644 --- a/index.html +++ b/index.html @@ -1,38 +1,77 @@ + 3D模型展示SDK - TS版 +
- + + \ No newline at end of file diff --git a/src/babylonjs/AppCamera.ts b/src/babylonjs/AppCamera.ts index 042ec86..bff9595 100644 --- a/src/babylonjs/AppCamera.ts +++ b/src/babylonjs/AppCamera.ts @@ -26,7 +26,7 @@ export class AppCamera extends Monobehiver { this.object.minZ = 0.01; // 近裁剪面 this.object.wheelPrecision =999999; // 滚轮缩放精度 this.object.panningSensibility = 0; - this.object.position = new Vector3(-0, 0, 100); + this.object.position = new Vector3(-0, 0, 100); this.setTarget(0, 2, 0); } diff --git a/src/babylonjs/AppConfig.ts b/src/babylonjs/AppConfig.ts index ce0a151..5f15f77 100644 --- a/src/babylonjs/AppConfig.ts +++ b/src/babylonjs/AppConfig.ts @@ -11,8 +11,8 @@ export const AppConfig = { success: null as OptionalCallback, error: null as ErrorCallback, env: { - envPath:"", - intensity: 1, + envPath: '/hdr/sanGiuseppeBridge.env', + intensity: 1.5, rotationY: 0 } }; diff --git a/src/babylonjs/AppEnv.ts b/src/babylonjs/AppEnv.ts index d740dfe..9646cc2 100644 --- a/src/babylonjs/AppEnv.ts +++ b/src/babylonjs/AppEnv.ts @@ -15,7 +15,7 @@ export class AppEnv extends Monobehiver { /** 初始化 - 创建默认HDR环境 */ Awake(): void { - this.createHDR(); + this.createHDR(); } /** @@ -23,7 +23,7 @@ export class AppEnv extends Monobehiver { * @param hdrPath HDR文件路径 */ createHDR(): void { - const hdrPath = AppConfig.env.envPath; + const envPath = AppConfig.env.envPath; const intensity = AppConfig.env.intensity ?? 1.5; const rotationY = AppConfig.env.rotationY ?? 0; const scene = this.mainApp.appScene.object; @@ -32,7 +32,7 @@ export class AppEnv extends Monobehiver { this.object.dispose(); this.object = null; } - const reflectionTexture = CubeTexture.CreateFromPrefilteredData(hdrPath, scene); + const reflectionTexture = CubeTexture.CreateFromPrefilteredData(envPath, scene); reflectionTexture.rotationY = rotationY; scene.environmentIntensity = intensity; scene.environmentTexture = reflectionTexture; diff --git a/src/babylonjs/AppLight.ts b/src/babylonjs/AppLight.ts index 56fcdc3..3008565 100644 --- a/src/babylonjs/AppLight.ts +++ b/src/babylonjs/AppLight.ts @@ -1,4 +1,4 @@ -import { SpotLight } from '@babylonjs/core/Lights/spotLight'; +import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'; import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector'; @@ -7,7 +7,6 @@ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { Color3 } from '@babylonjs/core/Maths/math.color'; import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager'; -import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; import { Mesh } from '@babylonjs/core/Meshes/mesh'; type DebugMarkers = { @@ -21,7 +20,7 @@ type DebugMarkers = { * 灯光管理类- 负责创建和管理场景灯光 */ export class AppLight extends Monobehiver { - lightList: SpotLight[]; + lightList: DirectionalLight[]; shadowGenerator: ShadowGenerator | null; debugMarkers?: DebugMarkers; coneMesh?: Mesh; @@ -35,22 +34,17 @@ export class AppLight extends Monobehiver { /** 初始化灯光并开启阴影 */ Awake(): void { - const light = new SpotLight( + const light = new DirectionalLight( "mainLight", - new Vector3(-0.6, 2.12, 2), new Vector3(0, -0.5, -1), - Math.PI * 0.6, // angle 弧度 ); - light.angle = 1.5; - light.innerAngle = 1; - light.exponent = 2; + light.position = new Vector3(-0.6, 2.12, 2); light.diffuse = new Color3(1, 0.86, 0.80); light.specular = new Color3(1, 1, 1); - light.intensity = 60; + light.intensity = 1; light.shadowMinZ = 0.01; light.shadowMaxZ = 100; - light.range = 5000; const generator = new ShadowGenerator(4096, light); generator.usePercentageCloserFiltering = true; @@ -61,149 +55,6 @@ export class AppLight extends Monobehiver { this.shadowGenerator = generator; } - /** 将网格添加为阴影投射者 */ - addShadowCaster(mesh: AbstractMesh): void { - if (this.shadowGenerator) { - this.shadowGenerator.addShadowCaster(mesh); - } - } - /** 设置主光源强度 */ - setIntensity(intensity: number): void { - if (this.lightList[0]) this.lightList[0].intensity = intensity; - } - /** 创建灯光可视化调试器 - W键拖拽位置,E键旋转方向 */ - enableLightDebug(): void { - const scene = this.mainApp.appScene.object; - const light = this.lightList[0]; - if (!light || !scene) return; - - const marker = MeshBuilder.CreateSphere("lightMarker", { diameter: 0.3 }, scene); - marker.position = light.position.clone(); - const mat = new StandardMaterial("lightMat", scene); - mat.emissiveColor = Color3.Yellow(); - marker.material = mat; - - const arrow = MeshBuilder.CreateCylinder("lightArrow", { height: 1, diameterTop: 0, diameterBottom: 0.1 }, scene); - arrow.parent = marker; - arrow.position.set(0, 0, 0.6); - arrow.rotation.x = Math.PI / 2; - const arrowMat = new StandardMaterial("arrowMat", scene); - arrowMat.emissiveColor = Color3.Red(); - arrow.material = arrowMat; - - const dir = light.direction.normalize(); - marker.rotation.y = Math.atan2(dir.x, dir.z); - marker.rotation.x = -Math.asin(dir.y); - - const gizmoManager = new GizmoManager(scene); - gizmoManager.attachableMeshes = [marker]; - gizmoManager.usePointerToAttachGizmos = false; - gizmoManager.attachToMesh(marker); - - scene.onBeforeRenderObservable.add(() => { - light.position.copyFrom(marker.position); - const forward = new Vector3(0, 0, 1); - const rotationMatrix = marker.getWorldMatrix().getRotationMatrix(); - light.direction = Vector3.TransformNormal(forward, rotationMatrix).normalize(); - }); - - const onKey = (e: KeyboardEvent) => { - if (e.key === 'w' || e.key === 'W') { - gizmoManager.positionGizmoEnabled = true; - gizmoManager.rotationGizmoEnabled = false; - } else if (e.key === 'e' || e.key === 'E') { - gizmoManager.positionGizmoEnabled = false; - gizmoManager.rotationGizmoEnabled = true; - } - }; - window.addEventListener('keydown', onKey); - gizmoManager.positionGizmoEnabled = true; - - this.debugMarkers = { marker, arrow, gizmoManager, onKey }; - } - - /** 隐藏灯光调试器 */ - disableLightDebug(): void { - if (this.debugMarkers) { - window.removeEventListener('keydown', this.debugMarkers.onKey); - this.debugMarkers.gizmoManager.dispose(); - this.debugMarkers.arrow.dispose(); - this.debugMarkers.marker.dispose(); - this.debugMarkers = undefined; - } - } - - /** 创建聚光灯可视化Gizmo - 带光锥范围 */ - createLightGizmo(): void { - const scene = this.mainApp.appScene.object; - const light = this.lightList[0]; - if (!light || !scene) return; - - const coneLength = 3; - const updateCone = () => { - if (this.coneMesh) this.coneMesh.dispose(); - const radius = Math.tan(light.angle) * coneLength; - const cone = MeshBuilder.CreateCylinder("lightCone", { - height: coneLength, - diameterTop: radius * 2, - diameterBottom: 0 - }, scene); - const mat = new StandardMaterial("coneMat", scene); - mat.emissiveColor = Color3.Yellow(); - mat.alpha = 0.2; - mat.wireframe = true; - cone.material = mat; - - cone.position = light.position.add(light.direction.scale(coneLength / 2)); - const up = new Vector3(0, 1, 0); - const axis = Vector3.Cross(up, light.direction).normalize(); - const angle = Math.acos(Vector3.Dot(up, light.direction.normalize())); - if (axis.length() > 0.001) cone.rotationQuaternion = Quaternion.RotationAxis(axis, angle); - - this.coneMesh = cone; - }; - - updateCone(); - this.updateCone = updateCone; - } - - /** 创建angle和innerAngle调试滑动条 */ - createAngleSliders(): void { - const light = this.lightList[0]; - if (!light) return; - - const container = document.createElement('div'); - container.style.cssText = 'position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:5px;color:#fff;font-size:12px;z-index:1000'; - - const createSlider = (label: string, value: number, min: number, max: number, onChange: (v: number) => void) => { - const wrap = document.createElement('div'); - wrap.style.marginBottom = '8px'; - const lbl = document.createElement('div'); - lbl.textContent = `${label}: ${value}`; - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = String(min); - slider.max = String(max); - slider.value = String(value); - slider.style.width = '150px'; - slider.oninput = () => { - lbl.textContent = `${label}: ${slider.value}`; - onChange(Number(slider.value)); - }; - wrap.append(lbl, slider); - return wrap; - }; - - const toRad = (deg: number) => deg * Math.PI / 180; - const toDeg = (rad: number) => Math.round(rad * 180 / Math.PI); - - container.append( - createSlider('angle', toDeg(light.angle), 1, 180, v => { light.angle = toRad(v); this.updateCone?.(); }), - createSlider('innerAngle', toDeg(light.innerAngle), 0, 180, v => { light.innerAngle = toRad(v); }) - ); - - document.body.appendChild(container); - } } diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts index cf189f8..af1795f 100644 --- a/src/babylonjs/AppModel.ts +++ b/src/babylonjs/AppModel.ts @@ -1,10 +1,12 @@ -import { ImportMeshAsync } from '@babylonjs/core/Loading/sceneLoader'; +import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader'; import '@babylonjs/loaders/glTF'; import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; import { Scene } from '@babylonjs/core/scene'; +import { ActionManager, ExecuteCodeAction } from '@babylonjs/core/Actions'; import { Monobehiver } from '../base/Monobehiver'; import { Dictionary } from '../utils/Dictionary'; import { AppConfig } from './AppConfig'; +import { EventBridge } from '../event/bridge'; type LoadResult = { success: boolean; @@ -44,10 +46,45 @@ export class AppModel extends Monobehiver { if (!AppConfig.modelUrlList?.length || this.isLoading) return; this.isLoading = true; try { - for (const url of AppConfig.modelUrlList) { - await this.loadSingleModel(url); + const total = AppConfig.modelUrlList.length; + EventBridge.modelLoadProgress({ loaded: 0, total, urls: AppConfig.modelUrlList, progress: 0, percentage: 0 }); + for (let i = 0; i < AppConfig.modelUrlList.length; i++) { + const url = AppConfig.modelUrlList[i]; + const handleProgress = (event: ISceneLoaderProgressEvent): void => { + const currentProgress = event.lengthComputable && event.total > 0 + ? Math.min(1, event.loaded / event.total) + : 0; + const overallProgress = Math.min(1, (i + currentProgress) / total); + EventBridge.modelLoadProgress({ + loaded: i + currentProgress, + total, + url, + progress: overallProgress, + percentage: Number((overallProgress * 100).toFixed(2)), + detail: { + url, + lengthComputable: event.lengthComputable, + loadedBytes: event.loaded, + totalBytes: event.total + } + }); + }; + + const result = await this.loadSingleModel(url, handleProgress); + const overallProgress = Math.min(1, (i + 1) / total); + EventBridge.modelLoadProgress({ + loaded: i + 1, + total, + url, + success: result.success, + progress: overallProgress, + percentage: Number((overallProgress * 100).toFixed(2)) + }); + if (!result.success) { + EventBridge.modelLoadError({ url, error: result.error }); + } } - + EventBridge.modelLoaded({ urls: AppConfig.modelUrlList }); } finally { this.isLoading = false; } @@ -57,7 +94,7 @@ export class AppModel extends Monobehiver { * 加载单个模型 * @param modelUrl 模型URL */ - async loadSingleModel(modelUrl: string): Promise { + async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise { try { const cached = this.getCachedMeshes(modelUrl); if (cached) return { success: true, meshes: cached }; @@ -65,16 +102,11 @@ export class AppModel extends Monobehiver { const scene: Scene | null = this.mainApp.appScene.object; if (!scene) return { success: false, error: '场景未初始化' }; - // ImportMeshAsync的签名与当前调用不完全一致,使用any规避编译报错 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result: any = await (ImportMeshAsync as any)(modelUrl, scene); + const result = await ImportMeshAsync(modelUrl, scene, { onProgress }); if (!result?.meshes?.length) return { success: false, error: '未找到网格' }; this.modelDic.Set(modelUrl, result.meshes); this.loadedMeshes.push(...result.meshes); - - this.setupShadows(result.meshes as AbstractMesh[]); - return { success: true, meshes: result.meshes, skeletons: result.skeletons }; } catch (e: any) { console.error(`模型加载失败: ${modelUrl}`, e); @@ -116,4 +148,5 @@ export class AppModel extends Monobehiver { this.isLoading = false; this.skeletonMerged = false; } + } diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts new file mode 100644 index 0000000..66edfcc --- /dev/null +++ b/src/babylonjs/AppRay.ts @@ -0,0 +1,124 @@ +import { + type IPointerEvent, + PickingInfo, + PointerEventTypes, + Vector3, + AbstractMesh, + Color3, + PBRMaterial, + StandardMaterial, + HighlightLayer, + PointerInfo, +} from '@babylonjs/core' +import { MainApp } from './MainApp' +import { Monobehiver } from '../base/Monobehiver'; +import { EventBridge } from '../event/bridge'; + +class AppRay extends Monobehiver { + oldPoint: Vector3 = Vector3.Zero() + newPoint: Vector3 = Vector3.Zero() + private highlightLayer: HighlightLayer | null = null + private originalMaterial: any = null + private highlightedMesh: AbstractMesh | null = null + + constructor(mainApp: MainApp) { + super(mainApp) + } + + Awake() { + this.setupHighlightLayer() + this.setupUnifiedEventHandling() + } + + // 设置高亮层 + setupHighlightLayer() { + // 高亮层创建已禁用 + return + } + + // 设置统一的事件处理 + setupUnifiedEventHandling() { + // 使用观察者模式而不是直接覆盖事件处理器 + this.mainApp.appScene.object.onPointerObservable.add((pointerInfo: PointerInfo) => { + const { type, event, pickInfo } = pointerInfo; + + // 检查事件类型并转换 + const pointerEvent = event as IPointerEvent; + + // 只处理鼠标和触摸事件 + if (pointerEvent.pointerType !== "mouse" && pointerEvent.pointerType !== "touch") { + return; + } + + // 处理非主要触摸点 + if (pointerEvent.pointerType === "touch" && (pointerEvent as any).isPrimary === false) { + return; + } + + if (type === PointerEventTypes.POINTERDOWN) { + this.oldPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY); + } else if (type === PointerEventTypes.POINTERUP) { + this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY); + const distance = Vector3.Distance(this.oldPoint, this.newPoint); + + // 只有在没有移动的情况下才处理单击 + if (distance < 5) { // 增加一些容差 + this.handleSingleClick(pointerEvent, pickInfo); + } + } + }); + } + + // 处理单击 + handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) { + if (pickInfo && pickInfo.pickedMesh) { + EventBridge.modelClick({ + meshName: pickInfo.pickedMesh.name, + }); + } + } + + + + // 高亮显示网格 - 已禁用 + highlightMesh(mesh: AbstractMesh) { + // 高亮功能已禁用 + return + } + + // 使用材质方式高亮 - 已禁用 + highlightWithMaterial(mesh: AbstractMesh) { + // 材质高亮功能已禁用 + return + } + + // 清除高亮 + clearHighlight() { + try { + // 清除高亮层 + if (this.highlightLayer && this.highlightedMesh) { + try { + this.highlightLayer.removeMesh(this.highlightedMesh as any) + } catch (error) { + console.warn('高亮层移除失败:', error) + } + } + + // 恢复原始材质 + if (this.highlightedMesh && this.originalMaterial) { + const material = this.highlightedMesh.material as PBRMaterial + if (material && this.originalMaterial.albedoColor) { + material.albedoColor = this.originalMaterial.albedoColor + material.emissiveColor = this.originalMaterial.emissiveColor + } + } + + this.highlightedMesh = null + this.originalMaterial = null + } catch (error) { + console.error('清除高亮失败:', error) + } + } +} + +export { AppRay } diff --git a/src/babylonjs/MainApp.ts b/src/babylonjs/MainApp.ts index dce00b9..7d6e2c3 100644 --- a/src/babylonjs/MainApp.ts +++ b/src/babylonjs/MainApp.ts @@ -11,6 +11,8 @@ import { AppLight } from './AppLight'; import { AppEnv } from './AppEnv'; import { AppModel } from './AppModel'; import { AppConfig } from './AppConfig'; +import { AppRay } from './AppRay'; +import { EventBridge } from '../event/bridge'; /** * 主应用类 - 3D场景的核心控制器 @@ -24,6 +26,7 @@ export class MainApp { appModel: AppModel; appLight: AppLight; appEnv: AppEnv; + appRay: AppRay; constructor() { @@ -34,6 +37,7 @@ export class MainApp { this.appModel = new AppModel(this); this.appLight = new AppLight(this); this.appEnv = new AppEnv(this); + this.appRay = new AppRay(this); window.addEventListener("resize", () => this.appEngin.handleResize()); } @@ -45,10 +49,7 @@ export class MainApp { loadAConfig(config: any): void { AppConfig.container = config.container || 'renderDom'; AppConfig.modelUrlList = config.modelUrlList || []; - AppConfig.env = config.env - AppConfig.success = config.success; - AppConfig.error = config.error; - + AppConfig.env = config.env; } loadModel(): void { @@ -63,9 +64,10 @@ export class MainApp { this.appCamera.Awake(); this.appLight.Awake(); this.appEnv.Awake(); - + this.appRay.Awake() this.appModel.initManagers(); this.update(); + EventBridge.sceneReady({ scene: this.appScene.object }); } /** 启动渲染循环 */ diff --git a/src/event/bridge.ts b/src/event/bridge.ts new file mode 100644 index 0000000..2726fa2 --- /dev/null +++ b/src/event/bridge.ts @@ -0,0 +1,63 @@ +import { emit, on, once, off, Emitter } from './bus'; +import { + ModelClickPayload, + ModelLoadedPayload, + ModelLoadErrorPayload, + ModelLoadProgressPayload, + SceneReadyPayload +} from './types'; + +/** + * Centralized event helpers to avoid spreading raw event strings. + */ +export class EventBridge { + // Emits + static modelLoadProgress(payload: ModelLoadProgressPayload): Emitter { + return emit("model:load:progress", payload); + } + + static modelLoadError(payload: ModelLoadErrorPayload): Emitter { + return emit("model:load:error", payload); + } + + static modelLoaded(payload: ModelLoadedPayload): Emitter { + return emit("model:loaded", payload); + } + + static modelClick(payload: ModelClickPayload): Emitter { + return emit("model:click", payload); + } + + static sceneReady(payload: SceneReadyPayload): Emitter { + return emit("scene:ready", payload); + } + + // Listeners + static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter { + return on("model:load:progress", callback, context); + } + + static onModelLoadError(callback: (payload: ModelLoadErrorPayload) => void, context?: unknown): Emitter { + return on("model:load:error", callback, context); + } + + static onModelLoaded(callback: (payload: ModelLoadedPayload) => void, context?: unknown): Emitter { + return on("model:loaded", callback, context); + } + + static onModelClick(callback: (payload: ModelClickPayload) => void, context?: unknown): Emitter { + return on("model:click", callback, context); + } + + static onSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter { + return on("scene:ready", callback, context); + } + + static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter { + return once("scene:ready", callback, context); + } + + static off(eventName?: string, callback?: (...args: unknown[]) => void): Emitter { + return off(eventName, callback); + } +} diff --git a/src/event/bus.ts b/src/event/bus.ts new file mode 100644 index 0000000..9644937 --- /dev/null +++ b/src/event/bus.ts @@ -0,0 +1,82 @@ +type Listener = { + callback: (...args: unknown[]) => void; + context?: unknown; +}; + +export class Emitter { + private _events: Record = {}; + + on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this { + if (!this._events[name]) { + this._events[name] = []; + } + this._events[name].push({ callback, context }); + return this; + } + + once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this { + const onceWrapper = (...args: unknown[]) => { + this.off(name, onceWrapper); + callback.apply(context, args); + }; + return this.on(name, onceWrapper, context); + } + + off(name?: string, callback?: (...args: unknown[]) => void): this { + if (!name) { + this._events = {}; + return this; + } + if (!this._events[name]) return this; + if (!callback) { + delete this._events[name]; + return this; + } + this._events[name] = this._events[name].filter( + listener => listener.callback !== callback + ); + return this; + } + + removeAllListeners(): this { + this._events = {}; + return this; + } + + emit(name: string, ...args: unknown[]): this { + if (!this._events[name]) return this; + this._events[name].forEach(listener => { + listener.callback.apply(listener.context, args); + }); + return this; + } + + listenerCount(name: string): number { + return this._events[name]?.length ?? 0; + } +} + +export class EventBus extends Emitter { } + +export const eventBus = new EventBus(); + +export const on = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => { + return eventBus.on(eventName, callback, context); +}; + +export const off = (eventName?: string, callback?: (...args: unknown[]) => void): Emitter => { + return eventBus.off(eventName, callback); +}; + +export const once = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => { + return eventBus.once(eventName, callback, context); +}; + +export const emit = (eventName: string, ...args: unknown[]): Emitter => { + return eventBus.emit(eventName, ...args); +}; + +export const removeAllListeners = (eventName?: string): Emitter => { + if (eventName) return eventBus.off(eventName); + return eventBus.removeAllListeners(); +}; diff --git a/src/event/types.ts b/src/event/types.ts new file mode 100644 index 0000000..e95cb07 --- /dev/null +++ b/src/event/types.ts @@ -0,0 +1,37 @@ +import { Scene } from '@babylonjs/core/scene'; + + +export type ModelLoadProgressDetail = { + url?: string; + lengthComputable?: boolean; + loadedBytes?: number; + totalBytes?: number; +}; + +export type ModelLoadProgressPayload = { + loaded: number; + total: number; + url?: string; + urls?: string[]; + success?: boolean; + progress?: number; + percentage?: number; + detail?: ModelLoadProgressDetail; +}; + +export type ModelLoadErrorPayload = { + url: string; + error?: unknown; +}; + +export type ModelLoadedPayload = { + urls: string[]; +}; + +export type ModelClickPayload = { + meshName?: string; +}; + +export type SceneReadyPayload = { + scene: Scene | null; +}; diff --git a/src/main.ts b/src/main.ts index dd601d3..5000392 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,8 @@ import { MainApp } from './babylonjs/MainApp'; import { AppConfig } from './babylonjs/AppConfig'; import configurator, { ConfiguratorParams } from './components/conf'; import auth from './components/auth'; -import { on } from './utils/event'; +import { on, off, once, emit } from './event/bus'; +import { EventBridge } from './event/bridge'; declare global { interface Window { @@ -14,10 +15,11 @@ declare global { type InitParams = { container?: string; modelUrlList?: string[]; + apiConfig?: ConfiguratorParams; onSuccess?: () => void; onError?: (error?: unknown) => void; env?: { - envPath?: string; + hdrPath?: string; intensity?: number; rotationY?: number; }; @@ -26,10 +28,26 @@ type InitParams = { let mainApp: MainApp | null = null; const kernel = { + // 事件工具,提供给外部订阅/退订 + on, + off, + once, + emit, /** 初始化应用 */ init: async function (params: InitParams): Promise { if (!params) { console.error('params is required'); return; } + if (params.apiConfig) { + await configurator.init(params.apiConfig); + if (params.apiConfig.name) { + const userInfo = await auth.login(params.apiConfig.name); + if (!userInfo) { + console.error('failed to fetch user'); + return; + } + } + } + mainApp = new MainApp(); mainApp.loadAConfig({ container: params.container || 'renderDom', @@ -42,7 +60,6 @@ const kernel = { await mainApp.Awake(); await mainApp.loadModel(); }, - }; if (!window.faceSDK) { diff --git a/src/utils/event.ts b/src/utils/event.ts deleted file mode 100644 index 7132077..0000000 --- a/src/utils/event.ts +++ /dev/null @@ -1,123 +0,0 @@ -type Listener = { - callback: (...args: unknown[]) => void; - context?: unknown; -}; - -type EventMeta = { - type: string; - description: string; - listeners: Listener[]; -}; - -/** - * 基础事件发射器 - */ -export class Emitter { - private _events: Record = {}; - - on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this { - if (!this._events[name]) { - this._events[name] = []; - } - this._events[name].push({ callback, context }); - return this; - } - - once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this { - const onceWrapper = (...args: unknown[]) => { - this.off(name, onceWrapper); - callback.apply(context, args); - }; - return this.on(name, onceWrapper, context); - } - - off(name?: string, callback?: (...args: unknown[]) => void): this { - if (!name) { - this._events = {}; - return this; - } - if (!this._events[name]) return this; - if (!callback) { - delete this._events[name]; - return this; - } - this._events[name] = this._events[name].filter( - listener => listener.callback !== callback - ); - return this; - } - - removeAllListeners(): this { - this._events = {}; - return this; - } - - emit(name: string, ...args: unknown[]): this { - if (!this._events[name]) return this; - this._events[name].forEach(listener => { - listener.callback.apply(listener.context, args); - }); - return this; - } - - listenerCount(name: string): number { - return this._events[name]?.length ?? 0; - } -} - -/** - * 全局事件管理器 - */ -export class EventManager extends Emitter { - private eventMap: Map; - - constructor() { - super(); - this.eventMap = new Map(); - } - - registerEvent(type: string, description: string): void { - this.eventMap.set(type, { type, description, listeners: [] }); - } - - getRegisteredEvents(): EventMeta[] { - return Array.from(this.eventMap.values()); - } -} - -// 创建全局事件管理器实例 -export const eventManager = new EventManager(); - -// 注册标准事件类型(描述使用英文,避免编码问题) -eventManager.registerEvent('load', 'resource load complete'); -eventManager.registerEvent('load-progress', 'resource load progress'); -eventManager.registerEvent('load-error', 'resource load error'); -eventManager.registerEvent('animation-start', 'animation start'); -eventManager.registerEvent('animation-end', 'animation end'); -eventManager.registerEvent('animation-loop', 'animation loop'); -eventManager.registerEvent('model-change', 'model change'); -eventManager.registerEvent('camera-change', 'camera change'); -eventManager.registerEvent('scene-ready', 'scene ready'); -eventManager.registerEvent('dispose', 'component disposed'); - -// 导出便捷函数 -export function on(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter { - return eventManager.on(eventName, callback, context); -} - -export function off(eventName?: string, callback?: (...args: unknown[]) => void): Emitter { - return eventManager.off(eventName, callback); -} - -export function once(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter { - return eventManager.once(eventName, callback, context); -} - -export function emit(eventName: string, ...args: unknown[]): Emitter { - return eventManager.emit(eventName, ...args); -} - -export function removeAllListeners(eventName?: string): Emitter { - if (eventName) return eventManager.off(eventName); - return eventManager.removeAllListeners(); -}