This commit is contained in:
@ -1,26 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SDK 模块化加载示例</title>
|
<title>SDK 模块化加载示例</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
}
|
||||||
|
#app { width: 100vw; height: 100vh; position: relative; }
|
||||||
|
#renderDom { width: 100%; height: 100%; display: block; }
|
||||||
|
</style>
|
||||||
<canvas id="renderDom"></canvas>
|
<canvas id="renderDom"></canvas>
|
||||||
|
|
||||||
<!-- 模块化:Dev 使用 /src/main.ts,构建后改为 /assets/index.js -->
|
<!-- 模块化:Dev 使用 /src/main.ts,构建后改为 /assets/index.js -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
||||||
import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
container: 'renderDom',
|
container: 'renderDom',
|
||||||
modelUrlList: ['/public/model/model.glb'],
|
modelUrlList: ['./public/model/model.glb'],
|
||||||
env: { hdrPath: '/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
env: { envPath: './public/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
||||||
onSuccess: () => console.log('SDK initialized (module)'),
|
onSuccess: () => console.log('SDK initialized (module)'),
|
||||||
onError: (err) => console.error('SDK init error', err),
|
onError: (err) => console.error('SDK init error', err),
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
69
index.html
69
index.html
@ -1,38 +1,77 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>3D模型展示SDK - TS版</title>
|
<title>3D模型展示SDK - TS版</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
}
|
}
|
||||||
#app { width: 100vw; height: 100vh; position: relative; }
|
|
||||||
#renderDom { width: 100%; height: 100%; display: block; }
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderDom {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<canvas id="renderDom"></canvas>
|
<canvas id="renderDom"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { kernel } from './src/main.ts';
|
import { kernel } from './src/main.ts';
|
||||||
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
|
||||||
const config = {
|
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||||
container: 'renderDom',
|
|
||||||
modelUrlList: ['/public/model/model.glb'],
|
const config = {
|
||||||
env: { envPath: '/public/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
container: 'renderDom',
|
||||||
onSuccess: () => console.log('SDK initialized (module)'),
|
modelUrlList: ['/public/model/model.glb'],
|
||||||
onError: (err) => console.error('SDK init error', err),
|
env: { envPath: '/public/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
kernel.init(config);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
kernel.on('model:load:progress', (data) => {
|
||||||
|
console.log('模型加载事件', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
kernel.on('model:loaded', (data) => {
|
||||||
|
console.log('模型加载完成', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
kernel.on('model:click', (data) => {
|
||||||
|
console.log('模型点击事件', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
kernel.init(config);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@ -26,7 +26,7 @@ export class AppCamera extends Monobehiver {
|
|||||||
this.object.minZ = 0.01; // 近裁剪面
|
this.object.minZ = 0.01; // 近裁剪面
|
||||||
this.object.wheelPrecision =999999; // 滚轮缩放精度
|
this.object.wheelPrecision =999999; // 滚轮缩放精度
|
||||||
this.object.panningSensibility = 0;
|
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);
|
this.setTarget(0, 2, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ export const AppConfig = {
|
|||||||
success: null as OptionalCallback,
|
success: null as OptionalCallback,
|
||||||
error: null as ErrorCallback,
|
error: null as ErrorCallback,
|
||||||
env: {
|
env: {
|
||||||
envPath:"",
|
envPath: '/hdr/sanGiuseppeBridge.env',
|
||||||
intensity: 1,
|
intensity: 1.5,
|
||||||
rotationY: 0
|
rotationY: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class AppEnv extends Monobehiver {
|
|||||||
|
|
||||||
/** 初始化 - 创建默认HDR环境 */
|
/** 初始化 - 创建默认HDR环境 */
|
||||||
Awake(): void {
|
Awake(): void {
|
||||||
this.createHDR();
|
this.createHDR();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +23,7 @@ export class AppEnv extends Monobehiver {
|
|||||||
* @param hdrPath HDR文件路径
|
* @param hdrPath HDR文件路径
|
||||||
*/
|
*/
|
||||||
createHDR(): void {
|
createHDR(): void {
|
||||||
const hdrPath = AppConfig.env.envPath;
|
const envPath = AppConfig.env.envPath;
|
||||||
const intensity = AppConfig.env.intensity ?? 1.5;
|
const intensity = AppConfig.env.intensity ?? 1.5;
|
||||||
const rotationY = AppConfig.env.rotationY ?? 0;
|
const rotationY = AppConfig.env.rotationY ?? 0;
|
||||||
const scene = this.mainApp.appScene.object;
|
const scene = this.mainApp.appScene.object;
|
||||||
@ -32,7 +32,7 @@ export class AppEnv extends Monobehiver {
|
|||||||
this.object.dispose();
|
this.object.dispose();
|
||||||
this.object = null;
|
this.object = null;
|
||||||
}
|
}
|
||||||
const reflectionTexture = CubeTexture.CreateFromPrefilteredData(hdrPath, scene);
|
const reflectionTexture = CubeTexture.CreateFromPrefilteredData(envPath, scene);
|
||||||
reflectionTexture.rotationY = rotationY;
|
reflectionTexture.rotationY = rotationY;
|
||||||
scene.environmentIntensity = intensity;
|
scene.environmentIntensity = intensity;
|
||||||
scene.environmentTexture = reflectionTexture;
|
scene.environmentTexture = reflectionTexture;
|
||||||
|
|||||||
@ -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 { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
|
||||||
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent';
|
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent';
|
||||||
import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector';
|
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 { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
|
||||||
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
||||||
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
|
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
|
||||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
|
||||||
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
||||||
|
|
||||||
type DebugMarkers = {
|
type DebugMarkers = {
|
||||||
@ -21,7 +20,7 @@ type DebugMarkers = {
|
|||||||
* 灯光管理类- 负责创建和管理场景灯光
|
* 灯光管理类- 负责创建和管理场景灯光
|
||||||
*/
|
*/
|
||||||
export class AppLight extends Monobehiver {
|
export class AppLight extends Monobehiver {
|
||||||
lightList: SpotLight[];
|
lightList: DirectionalLight[];
|
||||||
shadowGenerator: ShadowGenerator | null;
|
shadowGenerator: ShadowGenerator | null;
|
||||||
debugMarkers?: DebugMarkers;
|
debugMarkers?: DebugMarkers;
|
||||||
coneMesh?: Mesh;
|
coneMesh?: Mesh;
|
||||||
@ -35,22 +34,17 @@ export class AppLight extends Monobehiver {
|
|||||||
|
|
||||||
/** 初始化灯光并开启阴影 */
|
/** 初始化灯光并开启阴影 */
|
||||||
Awake(): void {
|
Awake(): void {
|
||||||
const light = new SpotLight(
|
const light = new DirectionalLight(
|
||||||
"mainLight",
|
"mainLight",
|
||||||
new Vector3(-0.6, 2.12, 2),
|
|
||||||
new Vector3(0, -0.5, -1),
|
new Vector3(0, -0.5, -1),
|
||||||
Math.PI * 0.6, // angle 弧度
|
|
||||||
);
|
);
|
||||||
|
|
||||||
light.angle = 1.5;
|
light.position = new Vector3(-0.6, 2.12, 2);
|
||||||
light.innerAngle = 1;
|
|
||||||
light.exponent = 2;
|
|
||||||
light.diffuse = new Color3(1, 0.86, 0.80);
|
light.diffuse = new Color3(1, 0.86, 0.80);
|
||||||
light.specular = new Color3(1, 1, 1);
|
light.specular = new Color3(1, 1, 1);
|
||||||
light.intensity = 60;
|
light.intensity = 1;
|
||||||
light.shadowMinZ = 0.01;
|
light.shadowMinZ = 0.01;
|
||||||
light.shadowMaxZ = 100;
|
light.shadowMaxZ = 100;
|
||||||
light.range = 5000;
|
|
||||||
|
|
||||||
const generator = new ShadowGenerator(4096, light);
|
const generator = new ShadowGenerator(4096, light);
|
||||||
generator.usePercentageCloserFiltering = true;
|
generator.usePercentageCloserFiltering = true;
|
||||||
@ -61,149 +55,6 @@ export class AppLight extends Monobehiver {
|
|||||||
this.shadowGenerator = generator;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { ImportMeshAsync } from '@babylonjs/core/Loading/sceneLoader';
|
import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
|
||||||
import '@babylonjs/loaders/glTF';
|
import '@babylonjs/loaders/glTF';
|
||||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||||
import { Scene } from '@babylonjs/core/scene';
|
import { Scene } from '@babylonjs/core/scene';
|
||||||
|
import { ActionManager, ExecuteCodeAction } from '@babylonjs/core/Actions';
|
||||||
import { Monobehiver } from '../base/Monobehiver';
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
import { Dictionary } from '../utils/Dictionary';
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
import { AppConfig } from './AppConfig';
|
import { AppConfig } from './AppConfig';
|
||||||
|
import { EventBridge } from '../event/bridge';
|
||||||
|
|
||||||
type LoadResult = {
|
type LoadResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -44,10 +46,45 @@ export class AppModel extends Monobehiver {
|
|||||||
if (!AppConfig.modelUrlList?.length || this.isLoading) return;
|
if (!AppConfig.modelUrlList?.length || this.isLoading) return;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
for (const url of AppConfig.modelUrlList) {
|
const total = AppConfig.modelUrlList.length;
|
||||||
await this.loadSingleModel(url);
|
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 {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@ -57,7 +94,7 @@ export class AppModel extends Monobehiver {
|
|||||||
* 加载单个模型
|
* 加载单个模型
|
||||||
* @param modelUrl 模型URL
|
* @param modelUrl 模型URL
|
||||||
*/
|
*/
|
||||||
async loadSingleModel(modelUrl: string): Promise<LoadResult> {
|
async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise<LoadResult> {
|
||||||
try {
|
try {
|
||||||
const cached = this.getCachedMeshes(modelUrl);
|
const cached = this.getCachedMeshes(modelUrl);
|
||||||
if (cached) return { success: true, meshes: cached };
|
if (cached) return { success: true, meshes: cached };
|
||||||
@ -65,16 +102,11 @@ export class AppModel extends Monobehiver {
|
|||||||
const scene: Scene | null = this.mainApp.appScene.object;
|
const scene: Scene | null = this.mainApp.appScene.object;
|
||||||
if (!scene) return { success: false, error: '场景未初始化' };
|
if (!scene) return { success: false, error: '场景未初始化' };
|
||||||
|
|
||||||
// ImportMeshAsync的签名与当前调用不完全一致,使用any规避编译报错
|
const result = await ImportMeshAsync(modelUrl, scene, { onProgress });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const result: any = await (ImportMeshAsync as any)(modelUrl, scene);
|
|
||||||
if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
|
if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
|
||||||
|
|
||||||
this.modelDic.Set(modelUrl, result.meshes);
|
this.modelDic.Set(modelUrl, result.meshes);
|
||||||
this.loadedMeshes.push(...result.meshes);
|
this.loadedMeshes.push(...result.meshes);
|
||||||
|
|
||||||
this.setupShadows(result.meshes as AbstractMesh[]);
|
|
||||||
|
|
||||||
return { success: true, meshes: result.meshes, skeletons: result.skeletons };
|
return { success: true, meshes: result.meshes, skeletons: result.skeletons };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`模型加载失败: ${modelUrl}`, e);
|
console.error(`模型加载失败: ${modelUrl}`, e);
|
||||||
@ -116,4 +148,5 @@ export class AppModel extends Monobehiver {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.skeletonMerged = false;
|
this.skeletonMerged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/babylonjs/AppRay.ts
Normal file
124
src/babylonjs/AppRay.ts
Normal file
@ -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 }
|
||||||
@ -11,6 +11,8 @@ import { AppLight } from './AppLight';
|
|||||||
import { AppEnv } from './AppEnv';
|
import { AppEnv } from './AppEnv';
|
||||||
import { AppModel } from './AppModel';
|
import { AppModel } from './AppModel';
|
||||||
import { AppConfig } from './AppConfig';
|
import { AppConfig } from './AppConfig';
|
||||||
|
import { AppRay } from './AppRay';
|
||||||
|
import { EventBridge } from '../event/bridge';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主应用类 - 3D场景的核心控制器
|
* 主应用类 - 3D场景的核心控制器
|
||||||
@ -24,6 +26,7 @@ export class MainApp {
|
|||||||
appModel: AppModel;
|
appModel: AppModel;
|
||||||
appLight: AppLight;
|
appLight: AppLight;
|
||||||
appEnv: AppEnv;
|
appEnv: AppEnv;
|
||||||
|
appRay: AppRay;
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -34,6 +37,7 @@ export class MainApp {
|
|||||||
this.appModel = new AppModel(this);
|
this.appModel = new AppModel(this);
|
||||||
this.appLight = new AppLight(this);
|
this.appLight = new AppLight(this);
|
||||||
this.appEnv = new AppEnv(this);
|
this.appEnv = new AppEnv(this);
|
||||||
|
this.appRay = new AppRay(this);
|
||||||
|
|
||||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||||
}
|
}
|
||||||
@ -45,10 +49,7 @@ export class MainApp {
|
|||||||
loadAConfig(config: any): void {
|
loadAConfig(config: any): void {
|
||||||
AppConfig.container = config.container || 'renderDom';
|
AppConfig.container = config.container || 'renderDom';
|
||||||
AppConfig.modelUrlList = config.modelUrlList || [];
|
AppConfig.modelUrlList = config.modelUrlList || [];
|
||||||
AppConfig.env = config.env
|
AppConfig.env = config.env;
|
||||||
AppConfig.success = config.success;
|
|
||||||
AppConfig.error = config.error;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadModel(): void {
|
loadModel(): void {
|
||||||
@ -63,9 +64,10 @@ export class MainApp {
|
|||||||
this.appCamera.Awake();
|
this.appCamera.Awake();
|
||||||
this.appLight.Awake();
|
this.appLight.Awake();
|
||||||
this.appEnv.Awake();
|
this.appEnv.Awake();
|
||||||
|
this.appRay.Awake()
|
||||||
this.appModel.initManagers();
|
this.appModel.initManagers();
|
||||||
this.update();
|
this.update();
|
||||||
|
EventBridge.sceneReady({ scene: this.appScene.object });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 启动渲染循环 */
|
/** 启动渲染循环 */
|
||||||
|
|||||||
63
src/event/bridge.ts
Normal file
63
src/event/bridge.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/event/bus.ts
Normal file
82
src/event/bus.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
type Listener = {
|
||||||
|
callback: (...args: unknown[]) => void;
|
||||||
|
context?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Emitter {
|
||||||
|
private _events: Record<string, Listener[]> = {};
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
37
src/event/types.ts
Normal file
37
src/event/types.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
23
src/main.ts
23
src/main.ts
@ -2,7 +2,8 @@ import { MainApp } from './babylonjs/MainApp';
|
|||||||
import { AppConfig } from './babylonjs/AppConfig';
|
import { AppConfig } from './babylonjs/AppConfig';
|
||||||
import configurator, { ConfiguratorParams } from './components/conf';
|
import configurator, { ConfiguratorParams } from './components/conf';
|
||||||
import auth from './components/auth';
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -14,10 +15,11 @@ declare global {
|
|||||||
type InitParams = {
|
type InitParams = {
|
||||||
container?: string;
|
container?: string;
|
||||||
modelUrlList?: string[];
|
modelUrlList?: string[];
|
||||||
|
apiConfig?: ConfiguratorParams;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onError?: (error?: unknown) => void;
|
onError?: (error?: unknown) => void;
|
||||||
env?: {
|
env?: {
|
||||||
envPath?: string;
|
hdrPath?: string;
|
||||||
intensity?: number;
|
intensity?: number;
|
||||||
rotationY?: number;
|
rotationY?: number;
|
||||||
};
|
};
|
||||||
@ -26,10 +28,26 @@ type InitParams = {
|
|||||||
let mainApp: MainApp | null = null;
|
let mainApp: MainApp | null = null;
|
||||||
|
|
||||||
const kernel = {
|
const kernel = {
|
||||||
|
// 事件工具,提供给外部订阅/退订
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
once,
|
||||||
|
emit,
|
||||||
/** 初始化应用 */
|
/** 初始化应用 */
|
||||||
init: async function (params: InitParams): Promise<void> {
|
init: async function (params: InitParams): Promise<void> {
|
||||||
if (!params) { console.error('params is required'); return; }
|
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 = new MainApp();
|
||||||
mainApp.loadAConfig({
|
mainApp.loadAConfig({
|
||||||
container: params.container || 'renderDom',
|
container: params.container || 'renderDom',
|
||||||
@ -42,7 +60,6 @@ const kernel = {
|
|||||||
await mainApp.Awake();
|
await mainApp.Awake();
|
||||||
await mainApp.loadModel();
|
await mainApp.loadModel();
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.faceSDK) {
|
if (!window.faceSDK) {
|
||||||
|
|||||||
@ -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<string, Listener[]> = {};
|
|
||||||
|
|
||||||
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<string, EventMeta>;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user