Compare commits
18 Commits
8e65eeb501
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f48948e43 | |||
| 12ae95340f | |||
| 248226e553 | |||
| 7fdbf19951 | |||
| ae59fbe68b | |||
| b238139773 | |||
| eba9a3384b | |||
| 5a3332badf | |||
| c409215867 | |||
| 47f0961e22 | |||
| ed5669fe93 | |||
| 99da97fcb4 | |||
| 260c7e706c | |||
| 661aa63f9f | |||
| fe7d9de6f6 | |||
| b9cbb58a9d | |||
| ebbd21916e | |||
| 58cd883720 |
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cd /d/VscodeProject/zhengte.babylonjs-sdk && npm run build 2>&1 | head -50)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,6 @@ steps:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
|
||||
# 第三步:上传构建文件
|
||||
- name: 上传构建文件
|
||||
image: appleboy/drone-scp
|
||||
@ -52,8 +51,8 @@ steps:
|
||||
# from_secret: server_ssh_key
|
||||
port: 22
|
||||
source:
|
||||
- dist/*
|
||||
- dist/bblcdn/*
|
||||
- dist/**
|
||||
- vite.config.js
|
||||
target: /www/wwwroot/sdk.zguiy.com/zt/
|
||||
strip_components: 1
|
||||
when:
|
||||
|
||||
@ -1 +1 @@
|
||||
VITE_PUBLIC = https://cdn.files.zguiy.com/studio/
|
||||
VITE_PUBLIC = ./
|
||||
@ -1,39 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDK 全局挂载加载示例</title>
|
||||
<title>SDK ȫ<EFBFBD>ֹ<EFBFBD><EFBFBD>ؼ<EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD></title>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 非模块化:使用全局构建产物,加载后可通过 window.faceSDK.kernel 调用 -->
|
||||
<!-- 部署后把 src 改成实际访问路径,如 https://doc.zguiy.com/sdk/zt/assets/index.global.js -->
|
||||
<script src="https://sdk.zguiy.com/zt/assets/index.js"></script>
|
||||
|
||||
<script src="https://sdk.zguiy.com/zt/assets/index.global.js"></script>
|
||||
<script>
|
||||
const config = {
|
||||
container: 'renderDom',
|
||||
modelUrlList: ['./public/model/model.glb'],
|
||||
env: { hdrPath: '/hdr/my.env', intensity: 1.2, rotationY: 0.3 },
|
||||
onSuccess: () => console.log('SDK initialized (global)'),
|
||||
onError: (err) => console.error('SDK init error', err),
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
||||
};
|
||||
|
||||
function startSdk() {
|
||||
const kernel = window.faceSDK && window.faceSDK.kernel;
|
||||
if (!kernel) {
|
||||
console.error('SDK kernel not loaded');
|
||||
return;
|
||||
}
|
||||
kernel.init(config);
|
||||
}
|
||||
const sdkKernel = window.faceSDK && window.faceSDK.kernel;
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
startSdk();
|
||||
if (!sdkKernel) {
|
||||
console.error('faceSDK kernel is not available. Confirm index.global.js loaded correctly.');
|
||||
} else {
|
||||
window.addEventListener('load', startSdk);
|
||||
sdkKernel.init(config);
|
||||
|
||||
sdkKernel.on('model:load:progress', (data) => {
|
||||
console.log('加载模型中', data);
|
||||
});
|
||||
|
||||
sdkKernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
});
|
||||
|
||||
sdkKernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
69
examples/index.html
Normal file
69
examples/index.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDK ȫ<>ֹ<EFBFBD><D6B9>ؼ<EFBFBD><D8BC><EFBFBD>ʾ<EFBFBD><CABE></title>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<script src="https://sdk.zguiy.com/zt/assets/index.global.js"></script>
|
||||
<script>
|
||||
const config = {
|
||||
container: 'renderDom',
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
||||
};
|
||||
|
||||
const sdkKernel = window.faceSDK && window.faceSDK.kernel;
|
||||
|
||||
if (!sdkKernel) {
|
||||
console.error('faceSDK kernel is not available. Confirm index.global.js loaded correctly.');
|
||||
} else {
|
||||
sdkKernel.init(config);
|
||||
|
||||
sdkKernel.on('model:load:progress', (data) => {
|
||||
console.log('加载模型中', data);
|
||||
});
|
||||
|
||||
sdkKernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
});
|
||||
|
||||
sdkKernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,26 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDK 模块化加载示例</title>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 模块化:Dev 使用 /src/main.ts,构建后改为 /assets/index.js -->
|
||||
<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 = {
|
||||
container: 'renderDom',
|
||||
modelUrlList: ['/public/model/model.glb'],
|
||||
env: '/public/model/model.glb',
|
||||
onSuccess: () => console.log('SDK initialized (module)'),
|
||||
onError: (err) => console.error('SDK init error', err),
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/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);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
92
index.html
92
index.html
@ -1,40 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D模型展示SDK - TS版</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
<title>3D Model Showcase SDK - TS</title><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; }
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#renderDom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<canvas id="renderDom"></canvas>
|
||||
</div>
|
||||
|
||||
<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 = {
|
||||
container: 'renderDom',
|
||||
modelUrlList: ["/model/model.glb"],
|
||||
animationUrlList: [],
|
||||
idleAnimationUrlList: [],
|
||||
onSuccess: () => console.log('SDK initialized'),
|
||||
onError: (err) => console.error('SDK init error', err),
|
||||
container: document.querySelector('#renderDom'),
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||
};
|
||||
|
||||
kernel.init(config);
|
||||
|
||||
|
||||
|
||||
kernel.on('model:load:progress', (data) => {
|
||||
console.log('模型加载事件', data);
|
||||
});
|
||||
|
||||
|
||||
|
||||
kernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
kernel.on('all:ready', (data) => {
|
||||
console.log('所有模块加载完,', data);
|
||||
kernel.material.apply({
|
||||
target: 'Material__2',
|
||||
attribute: 'alpha',
|
||||
value: 0.5,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
kernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
|
||||
// 切换卷帘门开关
|
||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||
|
||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||
console.log('设置剖切:', clipHeight);
|
||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||
|
||||
// 验证剖切是否生效
|
||||
setTimeout(() => {
|
||||
const scene = kernel.mainApp?.appScene?.object;
|
||||
console.log('Scene:', scene);
|
||||
console.log('Scene clipPlane:', scene?.clipPlane);
|
||||
console.log('Scene meshes count:', scene?.meshes?.length);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
|
||||
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
||||
import { Tools } from '@babylonjs/core/Misc/tools';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
/**
|
||||
* 相机控制类- 负责创建和控制弧形旋转相机
|
||||
@ -17,16 +18,21 @@ export class AppCamera extends Monobehiver {
|
||||
/** 初始化相机 */
|
||||
Awake(): void {
|
||||
const scene = this.mainApp.appScene.object;
|
||||
const canvas = this.mainApp.appDom.renderDom;
|
||||
const canvas = AppConfig.container;
|
||||
if (!scene || !canvas) return;
|
||||
|
||||
// 创建弧形旋转相机:水平角70度,垂直角80度,距离5,目标点(0,1,0)
|
||||
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(80), 5, new Vector3(0, 0, 0), scene);
|
||||
// 创建弧形旋转相机:水平角70度,垂直角85度(接近上帝视角),距离5,目标点(0,2,0)
|
||||
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
|
||||
this.object.attachControl(canvas, true);
|
||||
this.object.minZ = 0.01; // 近裁剪面
|
||||
this.object.wheelPrecision =999999; // 滚轮缩放精度
|
||||
this.object.panningSensibility = 0;
|
||||
this.object.position = new Vector3(-0, 0, 100);
|
||||
|
||||
// 限制垂直角范围,实现上帝视角
|
||||
// this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角(接近90度,避免万向锁)
|
||||
// this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角
|
||||
|
||||
this.object.position = new Vector3(-0, 100, 0);
|
||||
this.setTarget(0, 2, 0);
|
||||
}
|
||||
|
||||
@ -42,8 +48,11 @@ export class AppCamera extends Monobehiver {
|
||||
/** 重置相机到默认位置 */
|
||||
reset(): void {
|
||||
if (!this.object) return;
|
||||
this.object.radius = 2; this.setTarget(0, 0, 0);
|
||||
this.object.position = new Vector3(0, 1.5, 2);
|
||||
this.object.radius = 5;
|
||||
this.object.alpha = Tools.ToRadians(60); // 水平角
|
||||
this.object.beta = Tools.ToRadians(60); // 垂直角(上帝视角)
|
||||
this.setTarget(0, 2, 0);
|
||||
this.object.position = new Vector3(-0, 100, 0);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
|
||||
@ -6,13 +6,12 @@ type ErrorCallback = ((error?: unknown) => void) | null | undefined;
|
||||
* 共享运行时配置对象
|
||||
*/
|
||||
export const AppConfig = {
|
||||
container: 'renderDom',
|
||||
container: document.querySelector('#renderDom') as HTMLCanvasElement,
|
||||
modelUrlList: [] as string[],
|
||||
success: null as OptionalCallback,
|
||||
error: null as ErrorCallback,
|
||||
env: {
|
||||
hdrPath:"",
|
||||
intensity: 1,
|
||||
rotationY: 0
|
||||
envPath: '/hdr/sanGiuseppeBridge.env',
|
||||
intensity: 1.5,
|
||||
rotationY: 0,
|
||||
background: true,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
/**
|
||||
* 负责获取渲染容器 DOM
|
||||
*/
|
||||
export class AppDom {
|
||||
private _renderDom: HTMLCanvasElement | null;
|
||||
|
||||
constructor() {
|
||||
this._renderDom = null;
|
||||
}
|
||||
|
||||
get renderDom(): HTMLCanvasElement | null {
|
||||
return this._renderDom;
|
||||
}
|
||||
|
||||
Awake(): void {
|
||||
const dom = document.getElementById(AppConfig.container) || document.querySelector('#renderDom');
|
||||
this._renderDom = (dom as HTMLCanvasElement) ?? null;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { Engine } from '@babylonjs/core/Engines/engine';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
/**
|
||||
* 渲染引擎管理类 - 负责创建和管理3D渲染引擎
|
||||
@ -15,7 +16,7 @@ export class AppEngin extends Monobehiver {
|
||||
}
|
||||
|
||||
Awake(): void {
|
||||
this.canvas = this.mainApp.appDom.renderDom;
|
||||
this.canvas = AppConfig.container;
|
||||
if (!this.canvas) {
|
||||
throw new Error('Render canvas not found');
|
||||
}
|
||||
|
||||
@ -15,28 +15,46 @@ export class AppEnv extends Monobehiver {
|
||||
|
||||
/** 初始化 - 创建默认HDR环境 */
|
||||
Awake(): void {
|
||||
this.createHDR(AppConfig.env);
|
||||
this.createHDR();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建HDR环境贴图
|
||||
* @param hdrPath HDR文件路径
|
||||
*/
|
||||
createHDR(options?: { hdrPath?: string; intensity?: number; rotationY?: number }): void {
|
||||
const hdrPath = options?.hdrPath || AppConfig.env.hdrPath || '/hdr/sanGiuseppeBridge.env';
|
||||
const intensity = options?.intensity ?? AppConfig.env.intensity ?? 1.5;
|
||||
const rotationY = options?.rotationY ?? AppConfig.env.rotationY ?? 0;
|
||||
createHDR(): void {
|
||||
const envPath = AppConfig.env.envPath;
|
||||
const intensity = AppConfig.env.intensity ?? 1.5;
|
||||
const rotationY = AppConfig.env.rotationY ?? 0;
|
||||
const scene = this.mainApp.appScene.object;
|
||||
if (!scene) return;
|
||||
if (this.object) {
|
||||
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;
|
||||
this.object = reflectionTexture;
|
||||
scene.backgroundTexture = reflectionTexture;
|
||||
|
||||
const box = scene.createDefaultSkybox(
|
||||
reflectionTexture,
|
||||
true,
|
||||
512,
|
||||
0,
|
||||
true
|
||||
);
|
||||
console.log('box', AppConfig.env.background);
|
||||
if (AppConfig.env.background) {
|
||||
if (box) box.visibility = 1;
|
||||
} else {
|
||||
if (box) box.visibility = 0;
|
||||
}
|
||||
// 保存环境纹理的引用
|
||||
this.object = reflectionTexture;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,13 +75,6 @@ export class AppEnv extends Monobehiver {
|
||||
if (this.object) this.object.rotationY = angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新环境配置
|
||||
*/
|
||||
updateEnvironment(options: { hdrPath?: string; intensity?: number; rotationY?: number }): void {
|
||||
this.createHDR(options);
|
||||
}
|
||||
|
||||
/** 清理资源 */
|
||||
clean(): void {
|
||||
if (this.object) {
|
||||
|
||||
@ -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,175 +34,9 @@ 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.diffuse = new Color3(1, 0.86, 0.80);
|
||||
light.specular = new Color3(1, 1, 1);
|
||||
light.intensity = 60;
|
||||
light.shadowMinZ = 0.01;
|
||||
light.shadowMaxZ = 100;
|
||||
light.range = 5000;
|
||||
|
||||
const generator = new ShadowGenerator(4096, light);
|
||||
generator.usePercentageCloserFiltering = true;
|
||||
generator.filteringQuality = ShadowGenerator.QUALITY_HIGH;
|
||||
generator.transparencyShadow = true;
|
||||
|
||||
this.lightList.push(light);
|
||||
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 { 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<LoadResult> {
|
||||
async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise<LoadResult> {
|
||||
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,30 @@ export class AppModel extends Monobehiver {
|
||||
this.isLoading = false;
|
||||
this.skeletonMerged = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指定模型
|
||||
* @param modelName 模型名称
|
||||
*/
|
||||
destroyModel(modelName: string): void {
|
||||
// 遍历模型字典,查找匹配的模型
|
||||
const keys = this.modelDic.Keys();
|
||||
for (const key of keys) {
|
||||
if (key.includes(modelName)) {
|
||||
const meshes = this.modelDic.Get(key);
|
||||
if (meshes) {
|
||||
// 销毁所有网格
|
||||
meshes.forEach(mesh => mesh?.dispose());
|
||||
// 从字典中移除
|
||||
this.modelDic.Remove(key);
|
||||
// 从加载的网格列表中移除
|
||||
this.loadedMeshes = this.loadedMeshes.filter(mesh => !meshes.includes(mesh));
|
||||
console.log(`Model destroyed: ${modelName}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.warn(`Model not found: ${modelName}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
143
src/babylonjs/AppRay.ts
Normal file
143
src/babylonjs/AppRay.ts
Normal file
@ -0,0 +1,143 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染热点
|
||||
* @param hotspots 热点数据
|
||||
*/
|
||||
renderHotspots(hotspots: any[]): void {
|
||||
console.log('Rendering hotspots:', hotspots);
|
||||
|
||||
// 这里需要根据实际的热点渲染逻辑实现
|
||||
// 示例实现:
|
||||
// 1. 清除现有的热点
|
||||
// 2. 根据热点数据创建新的热点标记
|
||||
// 3. 为热点添加交互事件
|
||||
|
||||
hotspots.forEach((hotspot, index) => {
|
||||
console.log(`Rendering hotspot ${index}:`, hotspot);
|
||||
// 这里需要根据实际的热点数据结构实现
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AppRay }
|
||||
@ -19,10 +19,5 @@ export class AppScene extends Monobehiver {
|
||||
this.object = new Scene(this.mainApp.appEngin.object);
|
||||
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
|
||||
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
|
||||
// 1. 开启色调映射(Tone mapping)
|
||||
// this.object.imageProcessingConfiguration.toneMappingEnabled = true;
|
||||
|
||||
// 2. 设置色调映射类型为ACES
|
||||
// this.object.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_ACES;
|
||||
}
|
||||
}
|
||||
|
||||
770
src/babylonjs/GameManager.ts
Normal file
770
src/babylonjs/GameManager.ts
Normal file
@ -0,0 +1,770 @@
|
||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3 } from "@babylonjs/core";
|
||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||
import { Nullable } from "@babylonjs/core/types";
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
type RollerDoorOptions = {
|
||||
/** 目标升起高度,缺省为初始 y + 3 */
|
||||
upY?: number;
|
||||
/** 落下终点,缺省为初始 y */
|
||||
downY?: number;
|
||||
/** 运动速度(单位/秒),缺省 1 */
|
||||
speed?: number;
|
||||
/** 自定义门体网格名列表,不传使用默认两个卷帘门 */
|
||||
meshNames?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏管理器类 - 负责管理游戏逻辑、材质和纹理
|
||||
*/
|
||||
export class GameManager extends Monobehiver {
|
||||
private materialDic: Dictionary<PBRMaterial>;
|
||||
private meshDic: Dictionary<any>;
|
||||
private oldTextureDic: Dictionary<any>;
|
||||
private rollerDoorMeshes: AbstractMesh[];
|
||||
private rollerDoorGroup: AbstractMesh | null;
|
||||
private rollerDoorInitialY: Map<string, number>;
|
||||
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||
private rollerDoorIsOpen: boolean;
|
||||
private rollerDoorNames: string[];
|
||||
private yClipPlane: Plane | null;
|
||||
private yClipTargets: string[] | null;
|
||||
private clipPlaneVisualization: Mesh | null;
|
||||
|
||||
// 记录加载失败的贴图
|
||||
private failedTextures: Array<{
|
||||
path: string;
|
||||
materialName?: string;
|
||||
textureType?: string;
|
||||
error?: string;
|
||||
timestamp: Date;
|
||||
}>;
|
||||
|
||||
constructor(mainApp: any) {
|
||||
super(mainApp);
|
||||
this.materialDic = new Dictionary<PBRMaterial>();
|
||||
this.meshDic = new Dictionary<any>();
|
||||
this.oldTextureDic = new Dictionary<any>();
|
||||
this.rollerDoorMeshes = [];
|
||||
this.rollerDoorGroup = null;
|
||||
this.rollerDoorInitialY = new Map();
|
||||
this.rollerDoorObserver = null;
|
||||
this.rollerDoorIsOpen = false;
|
||||
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
this.clipPlaneVisualization = null;
|
||||
this.failedTextures = [];
|
||||
}
|
||||
|
||||
/** 调试:返回当前场景中所有网格名称 */
|
||||
listMeshNames(): string[] {
|
||||
return this.meshDic.Keys();
|
||||
}
|
||||
|
||||
/** 初始化游戏管理器 */
|
||||
async Awake() {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化材质字典
|
||||
for (const mat of scene.materials) {
|
||||
if (!this.materialDic.Has(mat.name)) {
|
||||
// 初始化材质属性
|
||||
// mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
||||
this.materialDic.Set(mat.name, mat as PBRMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化网格字典
|
||||
for (const mesh of scene.meshes) {
|
||||
if (mesh instanceof Mesh) {
|
||||
|
||||
this.meshDic.Set(mesh.name, mesh);
|
||||
}
|
||||
}
|
||||
this.cacheRollerDoorMeshes();
|
||||
console.log('材质字典:', this.materialDic);
|
||||
this.setRollerDoorScale("Box006.001", new Vector3(0.12,0.02,0.118));
|
||||
|
||||
// 单独设置 Box005.001 的缩放为 (1, 2, 1)
|
||||
this.setRollerDoorScale("Box005.001", new Vector3(0.13,0.02,0.12));
|
||||
}
|
||||
|
||||
/** 初始化设置材质 */
|
||||
async initSetMaterial(oldObject: any) {
|
||||
if (!oldObject?.Component?.length) return;
|
||||
|
||||
|
||||
const { degreeId, Component } = oldObject;
|
||||
let degreeTextureDic = this.oldTextureDic.Get(degreeId) || {};
|
||||
const texturePromises: Promise<void>[] = [];
|
||||
|
||||
// 处理每个组件
|
||||
for (const component of Component) {
|
||||
const {
|
||||
name,
|
||||
albedoTexture,
|
||||
bumpTexture,
|
||||
alphaTexture,
|
||||
aoTexture,
|
||||
} = component;
|
||||
|
||||
if (!name) continue;
|
||||
|
||||
// 获取材质
|
||||
const mat = this.materialDic.Get(name);
|
||||
if (!mat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取或初始化纹理字典
|
||||
const textureDic = degreeTextureDic[name] || {
|
||||
albedo: null,
|
||||
bump: null,
|
||||
alpha: null,
|
||||
ao: null
|
||||
};
|
||||
|
||||
// 定义纹理任务
|
||||
const textureTasks = [
|
||||
{
|
||||
key: "albedo",
|
||||
path: albedoTexture,
|
||||
property: "albedoTexture"
|
||||
},
|
||||
{
|
||||
key: "bump",
|
||||
path: bumpTexture,
|
||||
property: "bumpTexture"
|
||||
},
|
||||
{
|
||||
key: "alpha",
|
||||
path: alphaTexture,
|
||||
property: "opacityTexture"
|
||||
},
|
||||
{
|
||||
key: "ao",
|
||||
path: aoTexture,
|
||||
property: "ambientTexture"
|
||||
}
|
||||
];
|
||||
|
||||
// 处理每个纹理任务
|
||||
for (const task of textureTasks) {
|
||||
const { key, path, property } = task;
|
||||
if (!path) continue;
|
||||
|
||||
const fullPath = this.getPublicUrl() + path;
|
||||
let texture = textureDic[key];
|
||||
|
||||
if (!texture) {
|
||||
try {
|
||||
texture = this.createTextureWithFallback(fullPath);
|
||||
if (!texture) {
|
||||
// 记录失败的贴图信息
|
||||
this.failedTextures.push({
|
||||
path: fullPath,
|
||||
materialName: name,
|
||||
textureType: key,
|
||||
error: '贴图创建失败',
|
||||
timestamp: new Date()
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 设置非ktx2格式的vScale
|
||||
if (!fullPath.toLowerCase().endsWith('.ktx2')) {
|
||||
texture.vScale = -1;
|
||||
}
|
||||
textureDic[key] = texture;
|
||||
} catch (error: any) {
|
||||
// 记录失败的贴图信息
|
||||
this.failedTextures.push({
|
||||
path: fullPath,
|
||||
materialName: name,
|
||||
textureType: key,
|
||||
error: error.message || error.toString(),
|
||||
timestamp: new Date()
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 将纹理赋值任务加入队列
|
||||
texturePromises.push(
|
||||
this.handleTextureAssignment(mat, textureDic, key, (texture) => {
|
||||
(mat as any)[property] = texture;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 更新纹理字典
|
||||
degreeTextureDic[name] = textureDic;
|
||||
}
|
||||
|
||||
// 等待所有纹理任务完成
|
||||
try {
|
||||
await Promise.all(texturePromises);
|
||||
|
||||
// 在所有贴图加载完成后设置材质属性
|
||||
for (const component of Component) {
|
||||
const { name, transparencyMode, bumpTextureLevel } = component;
|
||||
if (!name) continue;
|
||||
|
||||
const mat = this.materialDic.Get(name);
|
||||
if (!mat) continue;
|
||||
|
||||
mat.transparencyMode = transparencyMode;
|
||||
|
||||
if (mat.bumpTexture) {
|
||||
mat.bumpTexture.level = bumpTextureLevel;
|
||||
}
|
||||
|
||||
// 应用新的PBR材质属性
|
||||
this.applyPBRProperties(mat, component);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading textures:', error);
|
||||
} finally {
|
||||
if (this.mainApp.appDom?.load3D) {
|
||||
this.mainApp.appDom.load3D.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新后的纹理字典
|
||||
this.oldTextureDic.Set(degreeId, degreeTextureDic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用PBR材质属性
|
||||
* @param mat - PBR材质对象
|
||||
* @param component - 配置组件对象
|
||||
*/
|
||||
private applyPBRProperties(mat: PBRMaterial, component: any) {
|
||||
// 定义PBR属性映射任务
|
||||
const pbrTasks = [
|
||||
{
|
||||
key: "fresnel",
|
||||
value: component.fresnel,
|
||||
apply: (value: number) => {
|
||||
mat.indexOfRefraction = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "clearcoat",
|
||||
value: component.clearcoat,
|
||||
apply: (value: number) => {
|
||||
mat.clearCoat.isEnabled = true;
|
||||
mat.clearCoat.intensity = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "clearcoatRoughness",
|
||||
value: component.clearcoatRoughness,
|
||||
apply: (value: number) => {
|
||||
mat.clearCoat.roughness = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "roughness",
|
||||
value: component.roughness,
|
||||
apply: (value: number) => {
|
||||
mat.roughness = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "metallic",
|
||||
value: component.metallic,
|
||||
apply: (value: number) => {
|
||||
mat.metallic = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "alpha",
|
||||
value: component.alpha,
|
||||
apply: (value: number) => {
|
||||
mat.alpha = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "environmentIntensity",
|
||||
value: component.environmentIntensity,
|
||||
apply: (value: number) => {
|
||||
mat.environmentIntensity = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "baseColor",
|
||||
value: component.baseColor,
|
||||
apply: (value: any) => {
|
||||
if (value && typeof value === 'object') {
|
||||
const { r, g, b } = value;
|
||||
if (r !== null && r !== undefined &&
|
||||
g !== null && g !== undefined &&
|
||||
b !== null && b !== undefined) {
|
||||
mat.albedoColor.set(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 处理每个PBR属性任务
|
||||
for (const task of pbrTasks) {
|
||||
if (task.value !== null && task.value !== undefined) {
|
||||
try {
|
||||
task.apply(task.value);
|
||||
} catch (error) {
|
||||
console.warn('Error applying PBR property:', task.key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 通用的批量卸载贴图资源的方法 */
|
||||
private clearTextures(textureDic: Dictionary<any>): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
textureDic.Values().forEach((textures) => {
|
||||
for (const key in textures) {
|
||||
const texture = textures[key];
|
||||
if (texture && texture instanceof Texture) {
|
||||
texture.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
textureDic.Clear();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理纹理赋值 */
|
||||
private async handleTextureAssignment(mat: any, oldtextureDic: any, textureKey: string, assignCallback: (texture: Texture) => void) {
|
||||
const texture = oldtextureDic[textureKey];
|
||||
if (texture) {
|
||||
await this.checkTextureLoadedWithPromise(texture);
|
||||
assignCallback(texture);
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查纹理是否加载完成 */
|
||||
private checkTextureLoadedWithPromise(texture: Texture): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (texture.isReady()) {
|
||||
resolve();
|
||||
} else {
|
||||
texture.onLoadObservable.addOnce(() => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置相机位置 */
|
||||
reSet() {
|
||||
if (this.mainApp.appCamera?.object?.position) {
|
||||
this.mainApp.appCamera.object.position.set(160, 50, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** 卷帘门开合:再次调用会反向动作 */
|
||||
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||
}
|
||||
|
||||
/** 直接设置卷帘门状态 */
|
||||
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for roller door');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||
|
||||
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||
console.warn('Roller door group or meshes not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = Math.max(options?.speed ?? 1, 0.01);
|
||||
|
||||
// 计算目标高度
|
||||
let targetY: number;
|
||||
if (open) {
|
||||
// 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度
|
||||
if (options?.upY !== undefined) {
|
||||
targetY = options.upY;
|
||||
} else {
|
||||
// 找到所有门中最高的初始位置,让所有门都升到这个高度+3
|
||||
const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m =>
|
||||
this.rollerDoorInitialY.get(m.name) ?? m.position.y
|
||||
));
|
||||
targetY = maxBaseY + 3;
|
||||
}
|
||||
} else {
|
||||
// 下降时:回到初始位置
|
||||
targetY = 0;
|
||||
}
|
||||
|
||||
// 检查是否已经在目标位置
|
||||
if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) {
|
||||
this.rollerDoorIsOpen = open;
|
||||
return;
|
||||
}
|
||||
|
||||
this.rollerDoorIsOpen = open;
|
||||
this.stopRollerDoorAnimation();
|
||||
|
||||
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||
const dt = scene.getEngine().getDeltaTime() / 1000;
|
||||
const current = this.rollerDoorGroup!.position.y;
|
||||
const direction = targetY >= current ? 1 : -1;
|
||||
|
||||
// 使用固定速度变量
|
||||
const step = speed * dt;
|
||||
let next = current + direction * step;
|
||||
|
||||
if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) {
|
||||
next = targetY;
|
||||
this.stopRollerDoorAnimation();
|
||||
this.rollerDoorIsOpen = open;
|
||||
console.log('Roller door animation finished');
|
||||
}
|
||||
|
||||
// 移动透明盒子
|
||||
this.rollerDoorGroup!.position.y = next;
|
||||
|
||||
// 打印每个卷帘门的当前位置
|
||||
console.log('Roller door positions:');
|
||||
for (const mesh of this.rollerDoorMeshes) {
|
||||
console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 当前卷帘门是否开启 */
|
||||
isRollerDoorOpen(): boolean {
|
||||
return this.rollerDoorIsOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置卷帘门的缩放
|
||||
* @param meshName - 卷帘门网格名称
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setRollerDoorScale(meshName: string, scale: number | Vector3): void {
|
||||
const mesh = this.meshDic.Get(meshName);
|
||||
if (mesh) {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray());
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${meshName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所有卷帘门的缩放
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setAllRollerDoorsScale(scale: number | Vector3): void {
|
||||
this.rollerDoorMeshes.forEach(mesh => {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分
|
||||
* onlyMeshNames 指定只作用于哪些网格,其他网格不受影响
|
||||
*/
|
||||
setYAxisClip(
|
||||
height: number,
|
||||
keepAbove = true,
|
||||
onlyMeshNames?: string[],
|
||||
excludeMeshNames?: string[]
|
||||
): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for clipping');
|
||||
return;
|
||||
}
|
||||
const normal = new Vector3(0, keepAbove ? 1 : -1, 0);
|
||||
this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal);
|
||||
|
||||
// 如果指定了特定网格,只对这些网格应用剖切
|
||||
if (onlyMeshNames?.length) {
|
||||
this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames);
|
||||
} else {
|
||||
// 否则使用场景级别的剖切,作用于所有网格
|
||||
scene.clipPlane = this.yClipPlane;
|
||||
}
|
||||
|
||||
console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' });
|
||||
}
|
||||
|
||||
/** 关闭 Y 轴剖切 */
|
||||
clearYAxisClip(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene) {
|
||||
scene.clipPlane = null;
|
||||
}
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
|
||||
// 清除所有网格材质上的 clipPlane
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (mat && 'clipPlane' in mat) {
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cacheRollerDoorMeshes(customNames?: string[]): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
const names = customNames?.length ? customNames : this.rollerDoorNames;
|
||||
this.rollerDoorMeshes = [];
|
||||
|
||||
// 创建或获取 group 作为父级
|
||||
if (!this.rollerDoorGroup) {
|
||||
// 创建一个 AbstractMesh 作为组
|
||||
this.rollerDoorGroup = new AbstractMesh('rollerDoorGroup', scene);
|
||||
// 确保 group 的缩放为 1
|
||||
// 确保 group 的初始位置为 (0, 0, 0)
|
||||
this.rollerDoorGroup.position.set(0, 0, 0);
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const mesh = this.meshDic.Get(name);
|
||||
if (mesh) {
|
||||
this.rollerDoorMeshes.push(mesh);
|
||||
|
||||
// 保存网格的当前位置作为初始位置
|
||||
if (!this.rollerDoorInitialY.has(name)) {
|
||||
this.rollerDoorInitialY.set(name, mesh.position.y);
|
||||
}
|
||||
|
||||
// 保存网格的世界位置和缩放
|
||||
const worldPosition = mesh.getAbsolutePosition();
|
||||
const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);
|
||||
|
||||
// 将网格添加到 group 中
|
||||
mesh.parent = this.rollerDoorGroup;
|
||||
|
||||
// 调整网格的局部位置和缩放,保持世界位置和大小不变
|
||||
mesh.setAbsolutePosition(worldPosition);
|
||||
mesh.scaling.copyFrom(worldScaling);
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stopRollerDoorAnimation(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene && this.rollerDoorObserver) {
|
||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||
}
|
||||
this.rollerDoorObserver = null;
|
||||
}
|
||||
|
||||
/** 将 clipPlane 只作用到指定网格的材质 */
|
||||
private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void {
|
||||
const targetSet = new Set(targetNames);
|
||||
let appliedCount = 0;
|
||||
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (!mat) {
|
||||
console.log('[clipping] Mesh has no material:', mesh.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetSet.has(mesh.name)) {
|
||||
// 目标网格:应用剖切
|
||||
mat.clipPlane = plane;
|
||||
appliedCount++;
|
||||
console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name);
|
||||
} else {
|
||||
// 非目标网格:清除剖切
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount);
|
||||
if (appliedCount === 0) {
|
||||
console.warn('[clipping] No meshes found with names:', targetNames);
|
||||
console.log('[clipping] Available mesh names:', this.meshDic.Keys());
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取公共URL */
|
||||
private getPublicUrl(): string {
|
||||
// 尝试从环境变量获取
|
||||
if (import.meta && import.meta.env && import.meta.env.VITE_PUBLIC_URL) {
|
||||
return import.meta.env.VITE_PUBLIC_URL;
|
||||
}
|
||||
// 默认返回空字符串
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 清理资源 */
|
||||
dispose() {
|
||||
this.stopRollerDoorAnimation();
|
||||
this.clearYAxisClip();
|
||||
this.rollerDoorMeshes = [];
|
||||
this.rollerDoorInitialY.clear();
|
||||
this.rollerDoorIsOpen = false;
|
||||
|
||||
// 清理 rollerDoorGroup
|
||||
if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) {
|
||||
this.rollerDoorGroup.dispose();
|
||||
this.rollerDoorGroup = null;
|
||||
}
|
||||
|
||||
// 清理所有材质资源
|
||||
this.materialDic.Values().forEach((material) => {
|
||||
if (material && material.dispose) {
|
||||
material.dispose();
|
||||
}
|
||||
});
|
||||
this.materialDic.Clear();
|
||||
|
||||
// 清理所有贴图资源
|
||||
this.clearTextures(this.oldTextureDic);
|
||||
|
||||
// 清理所有网格
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
if (mesh && mesh.dispose) {
|
||||
mesh.dispose();
|
||||
}
|
||||
});
|
||||
this.meshDic.Clear();
|
||||
|
||||
// 清空失败贴图记录
|
||||
this.failedTextures = [];
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
update() { }
|
||||
|
||||
/** 尝试创建贴图的方法,支持多种格式回退 */
|
||||
private createTextureWithFallback(texturePath: string): Texture | null {
|
||||
const failureReasons: string[] = [];
|
||||
|
||||
try {
|
||||
const texture = new Texture(texturePath);
|
||||
|
||||
if (texture) {
|
||||
return texture;
|
||||
} else {
|
||||
failureReasons.push(`原始路径创建失败: ${texturePath}`);
|
||||
throw new Error('Texture creation returned null');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || error.toString();
|
||||
|
||||
// 特别处理KTX错误
|
||||
if (errorMessage.includes('KTX identifier') || errorMessage.includes('missing KTX') ||
|
||||
(texturePath.toLowerCase().endsWith('.ktx2') && errorMessage)) {
|
||||
this.failedTextures.push({
|
||||
path: texturePath,
|
||||
textureType: 'KTX2',
|
||||
error: `KTX错误: ${errorMessage}`,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
failureReasons.push(`原始路径加载异常: ${texturePath} - ${errorMessage}`);
|
||||
|
||||
// 如果是ktx2文件加载失败,尝试查找对应的jpg/png文件
|
||||
if (texturePath.toLowerCase().endsWith('.ktx2')) {
|
||||
// 尝试jpg格式
|
||||
const jpgPath = texturePath.replace(/\.ktx2$/i, '.jpg');
|
||||
try {
|
||||
const jpgTexture = new Texture(jpgPath);
|
||||
if (jpgTexture) {
|
||||
return jpgTexture;
|
||||
}
|
||||
} catch (jpgError: any) {
|
||||
failureReasons.push(`JPG回退失败: ${jpgPath} - ${jpgError}`);
|
||||
}
|
||||
|
||||
// 尝试png格式
|
||||
const pngPath = texturePath.replace(/\.ktx2$/i, '.png');
|
||||
try {
|
||||
const pngTexture = new Texture(pngPath);
|
||||
if (pngTexture) {
|
||||
return pngTexture;
|
||||
}
|
||||
} catch (pngError: any) {
|
||||
failureReasons.push(`PNG回退失败: ${pngPath} - ${pngError}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有格式都失败,记录详细失败信息
|
||||
this.failedTextures.push({
|
||||
path: texturePath,
|
||||
textureType: '回退机制',
|
||||
error: failureReasons.join('; '),
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质
|
||||
* @param target 目标对象
|
||||
* @param material 材质路径
|
||||
*/
|
||||
applyMaterial(target: string, attribute: string, value: number | string): void {
|
||||
// 这里需要根据实际的材质管理逻辑实现
|
||||
console.log(`Applying attribute ${attribute} to ${value}`);
|
||||
|
||||
// 示例实现:根据目标和材质路径应用材质
|
||||
// 1. 查找目标网格
|
||||
const targetMaterials: PBRMaterial[] = [];
|
||||
this.materialDic.Values().forEach(material => {
|
||||
if (material.name.includes(target)) {
|
||||
console.log(`${this.materialDic.Get(material.name)}`, material);
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
});
|
||||
|
||||
if (targetMaterials.length === 0) {
|
||||
console.warn(`Target not found: ${target}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理材质路径
|
||||
// 这里可以根据材质路径加载对应的材质配置
|
||||
// 例如:paint/blue 可以映射到特定的材质配置
|
||||
|
||||
// 3. 应用材质到目标网格
|
||||
targetMaterials.forEach(material => {
|
||||
if (material[attribute]) {
|
||||
material[attribute] = value;
|
||||
}
|
||||
console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`);
|
||||
// 这里需要根据实际的材质系统实现
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
* @description 主应用类,负责初始化和协调所有子模块
|
||||
*/
|
||||
|
||||
import { AppDom } from './AppDom';
|
||||
import { AppEngin } from './AppEngin';
|
||||
import { AppScene } from './AppScene';
|
||||
import { AppCamera } from './AppCamera';
|
||||
@ -11,29 +10,34 @@ import { AppLight } from './AppLight';
|
||||
import { AppEnv } from './AppEnv';
|
||||
import { AppModel } from './AppModel';
|
||||
import { AppConfig } from './AppConfig';
|
||||
import { AppRay } from './AppRay';
|
||||
import { GameManager } from './GameManager';
|
||||
import { EventBridge } from '../event/bridge';
|
||||
|
||||
/**
|
||||
* 主应用类 - 3D场景的核心控制器
|
||||
* 负责管理DOM、引擎、场景、相机、灯光、环境、模型和动画等子模块
|
||||
*/
|
||||
export class MainApp {
|
||||
appDom: AppDom;
|
||||
appEngin: AppEngin;
|
||||
appScene: AppScene;
|
||||
appCamera: AppCamera;
|
||||
appModel: AppModel;
|
||||
appLight: AppLight;
|
||||
appEnv: AppEnv;
|
||||
appRay: AppRay;
|
||||
gameManager: GameManager;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.appDom = new AppDom();
|
||||
this.appEngin = new AppEngin(this);
|
||||
this.appScene = new AppScene(this);
|
||||
this.appCamera = new AppCamera(this);
|
||||
this.appModel = new AppModel(this);
|
||||
this.appLight = new AppLight(this);
|
||||
this.appEnv = new AppEnv(this);
|
||||
this.appRay = new AppRay(this);
|
||||
this.gameManager = new GameManager(this);
|
||||
|
||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||
}
|
||||
@ -43,28 +47,28 @@ export class MainApp {
|
||||
* @param config 配置对象
|
||||
*/
|
||||
loadAConfig(config: any): void {
|
||||
AppConfig.container = config.container || 'renderDom';
|
||||
AppConfig.container = config.container;
|
||||
AppConfig.modelUrlList = config.modelUrlList || [];
|
||||
AppConfig.success = config.success;
|
||||
AppConfig.error = config.error;
|
||||
AppConfig.env = config.env
|
||||
AppConfig.env = config.env;
|
||||
}
|
||||
|
||||
loadModel(): void {
|
||||
this.appModel.loadModel();
|
||||
async loadModel(): Promise<void> {
|
||||
await this.appModel.loadModel();
|
||||
await this.gameManager.Awake();
|
||||
EventBridge.allReady({ scene: this.appScene.object });
|
||||
}
|
||||
|
||||
/** 唤醒/初始化所有子模块 */
|
||||
async Awake(): Promise<void> {
|
||||
this.appDom.Awake();
|
||||
this.appEngin.Awake();
|
||||
this.appScene.Awake();
|
||||
this.appCamera.Awake();
|
||||
this.appLight.Awake();
|
||||
this.appEnv.Awake();
|
||||
|
||||
this.appRay.Awake()
|
||||
this.appModel.initManagers();
|
||||
this.update();
|
||||
EventBridge.sceneReady({ scene: this.appScene.object });
|
||||
}
|
||||
|
||||
/** 启动渲染循环 */
|
||||
|
||||
68
src/event/bridge.ts
Normal file
68
src/event/bridge.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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);
|
||||
}
|
||||
static allReady(payload: SceneReadyPayload): Emitter {
|
||||
return emit("all: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 onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||
return on("all: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;
|
||||
};
|
||||
79
src/kernel/Adapter.ts
Normal file
79
src/kernel/Adapter.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { MainApp } from '../babylonjs/MainApp';
|
||||
|
||||
/**
|
||||
* Kernel 转接器类 - 封装 mainApp 的功能,提供统一<E7BB9F>?API 接口
|
||||
*/
|
||||
export class KernelAdapter {
|
||||
private mainApp: MainApp;
|
||||
|
||||
constructor(mainApp: MainApp) {
|
||||
this.mainApp = mainApp;
|
||||
}
|
||||
|
||||
/** 模型管理 */
|
||||
model = {
|
||||
/**
|
||||
* 销毁指定模<E5AE9A>? * @param modelName 模型名称
|
||||
*/
|
||||
destroy: (modelName: string): void => {
|
||||
this.mainApp.appModel.destroyModel(modelName);
|
||||
}
|
||||
};
|
||||
|
||||
/** 材质管理 */
|
||||
material = {
|
||||
/**
|
||||
* 应用材质
|
||||
* @param options 材质应用选项
|
||||
*/
|
||||
apply: (options: { target: string; attribute: string,value:number|string }): void => {
|
||||
this.mainApp.gameManager.applyMaterial(options.target, options.attribute,options.value);
|
||||
}
|
||||
};
|
||||
|
||||
/** 卷帘门控<E997A8>?*/
|
||||
door = {
|
||||
/** 再次调用会自动反向动<E59091>?*/
|
||||
toggle: (options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
||||
this.mainApp.gameManager.toggleRollerDoor(options);
|
||||
},
|
||||
/** 显式设置开/<2F>?*/
|
||||
setState: (open: boolean, options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
||||
this.mainApp.gameManager.setRollerDoorState(open, options);
|
||||
},
|
||||
/** 当前是否已开<E5B7B2>?*/
|
||||
isOpen: (): boolean => {
|
||||
return this.mainApp.gameManager.isRollerDoorOpen();
|
||||
}
|
||||
};
|
||||
|
||||
/** Y 轴剖<E8BDB4>?*/
|
||||
clipping = {
|
||||
/** <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>и߶ȣ<DFB6>keepAbove=true ʱ<><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϲ<EFBFBD><CFB2>֣<EFBFBD>onlyMeshNames Ϊ<><CEAA>Ĭ<EFBFBD>Ͻ<EFBFBD><CFBD><EFBFBD><EFBFBD>þ<EFBFBD><C3BE><EFBFBD><EFBFBD>ţ<EFBFBD>excludeMeshNames <20><><EFBFBD><EFBFBD><EFBFBD>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><D0B5><EFBFBD><EFBFBD><EFBFBD> */
|
||||
setY: (height: number, keepAbove = true, onlyMeshNames?: string[], excludeMeshNames?: string[]): void => {
|
||||
this.mainApp.gameManager.setYAxisClip(height, keepAbove, onlyMeshNames, excludeMeshNames);
|
||||
},
|
||||
/** 关闭剖切 */
|
||||
clear: (): void => {
|
||||
this.mainApp.gameManager.clearYAxisClip();
|
||||
}
|
||||
};
|
||||
|
||||
/** 热点管理 */
|
||||
hotspot = {
|
||||
/**
|
||||
* 渲染热点
|
||||
* @param hotspots 热点数据
|
||||
*/
|
||||
render: (hotspots: any[]): void => {
|
||||
this.mainApp.appRay.renderHotspots(hotspots);
|
||||
}
|
||||
};
|
||||
/** 调试工具 */
|
||||
debug = {
|
||||
/** 列出当前场景网格名称 */
|
||||
listMeshNames: (): string[] => {
|
||||
return this.mainApp.gameManager.listMeshNames();
|
||||
}
|
||||
};
|
||||
}
|
||||
72
src/main.ts
72
src/main.ts
@ -2,7 +2,9 @@ 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';
|
||||
import { KernelAdapter } from './kernel/Adapter';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -12,61 +14,50 @@ declare global {
|
||||
}
|
||||
|
||||
type InitParams = {
|
||||
container?: string;
|
||||
container?: string | HTMLCanvasElement;
|
||||
modelUrlList?: string[];
|
||||
animationUrlList?: string[];
|
||||
idleAnimationUrlList?: string[];
|
||||
onSuccess?: () => void;
|
||||
onError?: (error?: unknown) => void;
|
||||
apiConfig?: ConfiguratorParams;
|
||||
envConfig?: {
|
||||
env?: {
|
||||
hdrPath?: string;
|
||||
intensity?: number;
|
||||
rotationY?: number;
|
||||
background?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
let mainApp: MainApp | null = null;
|
||||
let kernelAdapter: KernelAdapter | null = null;
|
||||
|
||||
const kernel = {
|
||||
// 事件工具,提供给外部订阅/退订
|
||||
on,
|
||||
off,
|
||||
once,
|
||||
emit,
|
||||
/** 初始化应用 */
|
||||
init: async function (params: InitParams): Promise<void> {
|
||||
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();
|
||||
kernelAdapter = new KernelAdapter(mainApp);
|
||||
|
||||
// 展开转接器的属性和方法到kernel对象
|
||||
Object.assign(kernel, kernelAdapter);
|
||||
|
||||
const container = (typeof params.container === 'string'
|
||||
? (document.querySelector(params.container) || document.getElementById(params.container))
|
||||
: params.container || document.querySelector('#renderDom')) as HTMLCanvasElement | null;
|
||||
|
||||
if (!container) { throw new Error('Render canvas not found'); }
|
||||
|
||||
mainApp.loadAConfig({
|
||||
container: params.container || 'renderDom',
|
||||
container,
|
||||
modelUrlList: params.modelUrlList || [],
|
||||
success: params.onSuccess ?? null,
|
||||
error: params.onError ?? null,
|
||||
envConfig: params.envConfig
|
||||
env: params.env
|
||||
});
|
||||
|
||||
await mainApp.Awake();
|
||||
await mainApp.loadModel();
|
||||
},
|
||||
|
||||
/** 更新环境贴图配置(路径/强度/旋转) */
|
||||
setEnvironment: function (envConfig: { hdrPath?: string; intensity?: number; rotationY?: number }): void {
|
||||
if (!mainApp) {
|
||||
console.warn('mainApp is not initialized yet');
|
||||
return;
|
||||
}
|
||||
if (envConfig) {
|
||||
AppConfig.env = { ...AppConfig.env, ...envConfig };
|
||||
mainApp.appEnv.updateEnvironment(AppConfig.env);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -75,17 +66,4 @@ if (!window.faceSDK) {
|
||||
}
|
||||
window.faceSDK.kernel = kernel;
|
||||
|
||||
if (!window.yiyu) {
|
||||
window.yiyu = {};
|
||||
}
|
||||
window.yiyu.kernel = kernel;
|
||||
|
||||
window.yiyu.onAppLoaded = () => { };
|
||||
window.yiyu.onSingleSignFinished = (_text: string) => { };
|
||||
window.yiyu.onSentenceFinished = (_text: string) => { };
|
||||
window.yiyu.onSentenceChanged = (_data: unknown) => { };
|
||||
window.yiyu.onGloss = (_gloss: unknown) => { };
|
||||
|
||||
window.onload = () => { };
|
||||
|
||||
export { kernel };
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* @file compressor.js
|
||||
* @description 解压缩工具,使用pako完成与GoBetterStudio相同的deflate/raw解码
|
||||
*/
|
||||
|
||||
import pako from 'pako';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export const compressor = {
|
||||
/** 压缩字符串或字节数据 */
|
||||
compress(data: Uint8Array | string): Uint8Array {
|
||||
const source = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
||||
return pako.deflateRaw(source);
|
||||
},
|
||||
|
||||
/** 解压缩字节数据并返回字符串 */
|
||||
decompress(data: ArrayBuffer | Uint8Array): string {
|
||||
const input = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||
const uncompressed = pako.inflateRaw(input);
|
||||
return decoder.decode(uncompressed);
|
||||
}
|
||||
};
|
||||
|
||||
export default compressor;
|
||||
@ -1,73 +0,0 @@
|
||||
import md5 from 'js-md5';
|
||||
|
||||
const KEY_SIZE = 256 / 32;
|
||||
const ITERATIONS = 1000;
|
||||
|
||||
type EncryptedAsset = {
|
||||
value: Uint8Array;
|
||||
Timestamp: string;
|
||||
};
|
||||
|
||||
type Credential = {
|
||||
token: string;
|
||||
uid: string;
|
||||
};
|
||||
|
||||
type CryptoMaterial = {
|
||||
obj: Uint8Array;
|
||||
iv: Uint8Array;
|
||||
salt: Uint8Array;
|
||||
key: string;
|
||||
};
|
||||
|
||||
function prepareCryptoMaterial(data: EncryptedAsset, info: Credential): CryptoMaterial {
|
||||
const userIdPrefix = info.uid.slice(0, 16);
|
||||
const iv = new TextEncoder().encode(userIdPrefix);
|
||||
const obj = data.value.slice(7);
|
||||
const saltHex = md5(`${info.token}${data.Timestamp}`);
|
||||
const saltBytes = new Uint8Array((saltHex.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16)));
|
||||
return { obj, iv, salt: saltBytes, key: info.token };
|
||||
}
|
||||
|
||||
/** 从服务器返回的数据中异步解密出原始GLTF内容 */
|
||||
export async function decryptAsync(data: EncryptedAsset, info: Credential): Promise<ArrayBuffer | null> {
|
||||
const { obj, iv, salt, key } = prepareCryptoMaterial(data, info);
|
||||
const derivedKey = await generateAesKeyAsync(key, salt, KEY_SIZE, ITERATIONS);
|
||||
return aesDecryptAsync(obj, derivedKey, iv);
|
||||
}
|
||||
|
||||
async function generateAesKeyAsync(secret: string, salt: Uint8Array, keySize: number, iterations: number): Promise<CryptoKey> {
|
||||
const passwordBuffer = new TextEncoder().encode(secret);
|
||||
const baseKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
passwordBuffer,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
iterations,
|
||||
hash: 'SHA-1'
|
||||
},
|
||||
baseKey,
|
||||
{ name: 'AES-CBC', length: keySize * 32 },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
async function aesDecryptAsync(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer | null> {
|
||||
try {
|
||||
return await crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv },
|
||||
key,
|
||||
data
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error);
|
||||
return 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<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();
|
||||
}
|
||||
@ -10,8 +10,8 @@ export default defineConfig({
|
||||
lib: {
|
||||
entry: 'src/main.ts',
|
||||
name: 'kernel',
|
||||
formats: ['esm'],
|
||||
fileName: () => 'assets/index.js',
|
||||
formats: ['es', 'iife'],
|
||||
fileName: (format) => format === 'es' ? 'assets/index.js' : 'assets/index.global.js',
|
||||
},
|
||||
target: 'esnext',
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user