diff --git a/ScreenShot_2026-05-18_175704_601.png b/ScreenShot_2026-05-18_175704_601.png
new file mode 100644
index 0000000..0ab9646
Binary files /dev/null and b/ScreenShot_2026-05-18_175704_601.png differ
diff --git a/examples/app-global.js b/examples/app-global.js
index 2a10a1c..cb2c646 100644
--- a/examples/app-global.js
+++ b/examples/app-global.js
@@ -203,8 +203,8 @@ const isModelExists = (modelId) => {
return kernel.model.exists(modelId);
}
-//换棚子
-const executeEvent2 = async (result) => {
+//一般是换棚子/换颜色/显示放置区域
+ const executeEvent2 = async (result) => {
const kernel = getKernel();
// 检查是否有模型更换事件
@@ -214,10 +214,10 @@ const executeEvent2 = async (result) => {
let modelAlreadyExists = false;
if (hasModelChange) {
const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model');
- if (firstModelEvent) {
- const { category } = firstModelEvent.target_data;
- modelAlreadyExists = isModelExists(category);
- console.log(`检查模型 ${category} 是否存在:`, modelAlreadyExists);
+ if (firstModelEvent && firstModelEvent.target_data) {
+ const {name, category } = firstModelEvent.target_data;
+ modelAlreadyExists = kernel.model.exists(name+'_'+category);
+ console.log(`检查模型 ${name+'_'+category} 是否存在:`, modelAlreadyExists);
}
}
@@ -241,12 +241,10 @@ const executeEvent2 = async (result) => {
};
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
- console.log('替换百叶模型:', event);
- console.log('替换百叶模型类型:', category);
// 如果模型已存在,跳过加载
if (modelAlreadyExists) {
- console.log(`模型 ${category} 已存在,跳过加载`);
+ console.log(`模型 ${name+'_'+category} 已存在,跳过加载`);
continue;
}
@@ -269,7 +267,7 @@ const executeEvent2 = async (result) => {
modelControlType: model_control_type,
})
- console.log(`百叶模型已放置为 ${name}`);
+ console.log(`模型已放置为 ${name}`);
}
}
diff --git a/examples/index.global.js b/examples/index.global.js
index 9710cc6..20800a2 100644
--- a/examples/index.global.js
+++ b/examples/index.global.js
@@ -355762,14 +355762,26 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
const color = Color3.FromHexString(options.albedoColor);
material.albedoColor.copyFrom(color);
}
- if (options.albedoTexture) {
- material.albedoTexture = new Texture(options.albedoTexture);
+ if (options.albedoTexture !== void 0) {
+ if (options.albedoTexture) {
+ material.albedoTexture = new Texture(options.albedoTexture);
+ } else {
+ material.albedoTexture = null;
+ }
}
- if (options.normalMap) {
- material.bumpTexture = new Texture(options.normalMap);
+ if (options.normalMap !== void 0) {
+ if (options.normalMap) {
+ material.bumpTexture = new Texture(options.normalMap);
+ } else {
+ material.bumpTexture = null;
+ }
}
- if (options.metallicTexture) {
- material.metallicTexture = new Texture(options.metallicTexture);
+ if (options.metallicTexture !== void 0) {
+ if (options.metallicTexture) {
+ material.metallicTexture = new Texture(options.metallicTexture);
+ } else {
+ material.metallicTexture = null;
+ }
}
if (options.roughness !== void 0) {
material.roughness = options.roughness;
@@ -357185,6 +357197,19 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
*/
removeAll: () => {
this.mainApp.appModel.removeAll();
+ },
+ /**
+ * 检查模型是否已加载
+ * @param modelId 模型ID
+ * @returns 模型是否存在
+ * @example
+ * // 检查模型是否已加载,避免重复加载
+ * if (!kernel.model.exists('shed_001')) {
+ * await kernel.model.add({ modelId: 'shed_001', modelUrl: '...' });
+ * }
+ */
+ exists: (modelId) => {
+ return this.mainApp.appModel.exists(modelId);
}
};
/** 材质管理 */
diff --git a/index.html b/index.html
index 6a3a60f..9c27841 100644
--- a/index.html
+++ b/index.html
@@ -302,8 +302,8 @@
-
-
+
+
@@ -333,11 +333,11 @@
-
-
+
+
-
-
+
+
@@ -586,7 +586,7 @@
// 监听模型点击事件
window.addEventListener('model:click', (event) => {
console.log('模型被点击:', event.detail);
- const { meshName, materialName, modelControlType } = event.detail;
+ const { meshName, modelName, materialName, modelControlType } = event.detail;
const clickInfoDiv = document.getElementById('click-info');
const clickInfoContent = document.getElementById('click-info-content');
@@ -596,8 +596,8 @@
模型
- 网格名称:
- ${meshName}
+ 模型名称:
+ ${modelName || meshName}
`;
if (materialName) {
@@ -866,6 +866,8 @@
// DOM 2D转3D 示例:点击模型时显示信息框
if (data.pickedMesh && data.pickedPoint) {
const meshName = data.pickedMesh.name;
+ // 获取模型根节点名称(modelId)
+ const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName;
const position = data.pickedPoint; // 使用点击位置的坐标
currentMaterialName = data.materialName || ''; // 保存材质名
currentPickedMesh = data.pickedMesh; // 保存网格对象
@@ -873,7 +875,7 @@
// 获取已创建的DOM元素
const infoDiv = document.getElementById('model-info-box');
// 更新信息内容
- document.getElementById('info-name').textContent = `名称: ${meshName}`;
+ document.getElementById('info-name').textContent = `模型: ${modelName}`;
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
// 显示颜色按钮,隐藏旋转按钮
@@ -888,6 +890,8 @@
// 显示旋转控制UI
if (data.pickedMesh && data.pickedPoint) {
const meshName = data.pickedMesh.name;
+ // 获取模型根节点名称(modelId)
+ const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName;
const position = data.pickedPoint;
currentPickedMesh = data.pickedMesh; // 保存网格对象
diff --git a/index.js b/index.js
index 4340d96..fd8d3c8 100644
--- a/index.js
+++ b/index.js
@@ -37,9 +37,9 @@ export const init = async (customConfig = {}) => {
modelUrlList: [],
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
gizmo: {
- position: true,
- rotation: true,
- scale: true
+ position: false,
+ rotation: false,
+ scale: false
},
outline: {
enable: true,
@@ -67,7 +67,7 @@ export const getAutoLoadModelList = async () => {
const models = data.data // 这就是模型列表
models.forEach(model => {
- console.log(model.placement_zone);
+
if (model.placement_zone) {
const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone
kernel.dropZone.setData({
@@ -80,8 +80,9 @@ export const getAutoLoadModelList = async () => {
walls: walls
});
}
-
+
kernel.model.add({
+ modelName: model.name+'_'+model.category,
modelId: model.category,
modelUrl: model.file_url,
modelControlType: model.model_control_type,
@@ -148,18 +149,18 @@ export const executeEvent = async (dropzone_data, result) => {
if (event.event_type === 'change_model') {
console.log(event.target_data);
- const { id, name, file_url, model_control_type, category } = event.target_data;
- console.log('替换百叶模型:', event);
- console.log('替换百叶模型类型:', category);
+ const { name, file_url, model_control_type, category } = event.target_data;
+
// 生成唯一的模型ID
- const modelId = id + '_' + Date.now();
+ const modelId = Date.now();
// 先记录模型放置(会自动处理替换逻辑)
kernel.dropZone.recordModelPlacement(wallName, index, modelId);
console.log(Math.abs(rotation.y - 90), Math.abs(rotation.y - 90) > 5 ? 'x' : 'z');
// 加载并放置模型
await kernel.model.add({
+ modelName: name ,
modelId: modelId,
modelUrl: file_url,
modelControlType: model_control_type,
@@ -174,7 +175,7 @@ export const executeEvent = async (dropzone_data, result) => {
}
});
- console.log(`百叶模型已放置为 ${name}`);
+ console.log(`百叶模型已放置为 ${name+'_'+category}`);
}
if (event.event_type === 'change_color') {
@@ -202,12 +203,25 @@ export const executeEvent2 = async (result) => {
// 检查是否有模型更换事件
const hasModelChange = result.data.events.some(e => e.event_type === 'change_model');
- const modelExists = await kernel.model.exists(modelId);
- console.log(modelExists);
- // 只有在需要更换模型时才清除
+
+ // 检查新模型是否已经存在
+ let modelAlreadyExists = false;
if (hasModelChange) {
+ const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model');
+ if (firstModelEvent && firstModelEvent.target_data) {
+ const {name, category } = firstModelEvent.target_data;
+ modelAlreadyExists = kernel.model.exists(category);
+ console.log(`检查模型 ${name+'_'+category} 是否存在:`, modelAlreadyExists);
+ }
+ }
+
+ // 只有在需要更换模型且模型不存在时才清除
+ if (hasModelChange && !modelAlreadyExists) {
+ console.log('模型不存在,执行清除操作');
kernel.dropZone.clearZones();
kernel.model.removeAll();
+ } else if (modelAlreadyExists) {
+ console.log('模型已存在,跳过清除操作,仅更新材质');
}
// 先处理所有 change_model 事件
@@ -221,7 +235,11 @@ export const executeEvent2 = async (result) => {
};
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
-
+ // 如果模型已存在,跳过加载
+ if (modelAlreadyExists) {
+ console.log(`模型 ${name+'_'+category} 已存在,跳过加载`);
+ continue;
+ }
if (placement_zone) {
const { alpha, border_color, color, show_border, thickness, walls } = placement_zone
@@ -237,12 +255,13 @@ export const executeEvent2 = async (result) => {
// 加载并放置模型(使用 category 作为 modelId)
await kernel.model.add({
- modelId: category,
+ modelName: name,
+ modelId: category,
modelUrl: file_url,
modelControlType: model_control_type,
})
- console.log(`模型已放置为 ${name}`);
+ console.log(`模型已放置为 ${name+'_'+category}`);
}
}
diff --git a/src/babylonjs/AppDropZone.ts b/src/babylonjs/AppDropZone.ts
index 94b9d4b..52f0e47 100644
--- a/src/babylonjs/AppDropZone.ts
+++ b/src/babylonjs/AppDropZone.ts
@@ -145,18 +145,24 @@ export class AppDropZone {
return null;
};
- // 更新配置中的墙面分割数
- this.dropZoneConfig.walls = this.dropZoneConfig.walls.map(wall => {
- const newDivisions = matchWallName(wall.name);
- const finalDivisions = newDivisions !== null ? newDivisions : (wall.divisions || 1);
+ // 更新配置中的墙面分割数,只保留后端配置的墙面
+ this.dropZoneConfig.walls = this.dropZoneConfig.walls
+ .map(wall => {
+ const newDivisions = matchWallName(wall.name);
- console.log(`墙面 "${wall.name}" 匹配到分割数: ${finalDivisions}`);
+ // 如果后端没有配置这个墙面,返回 null 标记
+ if (newDivisions === null) {
+ return null;
+ }
- return {
- ...wall,
- divisions: finalDivisions
- };
- });
+ console.log(`墙面 "${wall.name}" 匹配到分割数: ${newDivisions}`);
+
+ return {
+ ...wall,
+ divisions: newDivisions
+ };
+ })
+ .filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面
// 清除旧的放置区域网格(不清除模型)
this.clearZones();
diff --git a/src/babylonjs/AppModel copy.ts b/src/babylonjs/AppModel copy.ts
new file mode 100644
index 0000000..a3ac236
--- /dev/null
+++ b/src/babylonjs/AppModel copy.ts
@@ -0,0 +1,748 @@
+import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
+import '@babylonjs/loaders/glTF';
+import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
+import { Mesh } from '@babylonjs/core/Meshes/mesh';
+import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
+import { Scene } from '@babylonjs/core/scene';
+import { Monobehiver } from '../base/Monobehiver';
+import { Dictionary } from '../utils/Dictionary';
+import { AppConfig } from './AppConfig';
+import { EventBridge } from '../event/bridge';
+import { DragConfig } from './AppModelDrag';
+
+type LoadResult = {
+ success: boolean;
+ meshes?: AbstractMesh[];
+ skeletons?: unknown[];
+ error?: string;
+};
+
+type ModelConfig = {
+ name: string;
+ url: string;
+};
+
+type ModelControlType = 'rotation' | 'color';
+
+type ModelTransform = {
+ position?: { x: number; y: number; z: number };
+ rotation?: { x: number; y: number; z: number };
+ scale?: { x: number; y: number; z: number };
+};
+
+type ModelMetadata = {
+ modelName: string;
+ modelId: string;
+ modelUrl: string;
+ modelControlType?: ModelControlType;
+ drag?: DragConfig;
+ transform?: ModelTransform;
+};
+
+/**
+ * 模型管理类 - 负责加载、缓存和管理3D模型
+ */
+export class AppModel extends Monobehiver {
+ private modelDic: Dictionary;
+ private modelMetadataDic: Dictionary;
+ private loadedMeshes: AbstractMesh[];
+ private isLoading: boolean;
+
+ constructor(mainApp: any) {
+ super(mainApp);
+ this.modelDic = new Dictionary();
+ this.modelMetadataDic = new Dictionary();
+ this.loadedMeshes = [];
+ this.isLoading = false;
+ }
+
+ initManagers(): void {
+ // 预留接口
+ }
+
+ /** 加载配置中的所有模型 */
+ async loadModel(): Promise {
+ if (!AppConfig.modelUrlList?.length || this.isLoading) return;
+
+ this.isLoading = true;
+ try {
+ await this.loadMultipleModels(AppConfig.modelUrlList);
+ EventBridge.modelLoaded({ urls: AppConfig.modelUrlList });
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+ /**
+ * 批量加载模型(内部方法)
+ * @param urls 模型URL数组
+ */
+ private async loadMultipleModels(urls: string[]): Promise {
+ const total = urls.length;
+ EventBridge.modelLoadProgress({ loaded: 0, total, urls, progress: 0, percentage: 0 });
+
+ for (let i = 0; i < urls.length; i++) {
+ const url = urls[i];
+ const result = await this.loadSingleModel(url, (event) => {
+ this.emitProgress(i, total, url, event);
+ });
+
+ this.emitProgress(i + 1, total, url, null, result.success);
+
+ if (!result.success) {
+ EventBridge.modelLoadError({ url, error: result.error });
+ }
+ }
+ }
+
+ /**
+ * 发送加载进度事件
+ */
+ private emitProgress(
+ loaded: number,
+ total: number,
+ url: string,
+ event: ISceneLoaderProgressEvent | null,
+ success?: boolean
+ ): void {
+ const currentProgress = event?.lengthComputable && event.total > 0
+ ? Math.min(1, event.loaded / event.total)
+ : 0;
+ const overallProgress = Math.min(1, (loaded + (event ? currentProgress : 0)) / total);
+
+ EventBridge.modelLoadProgress({
+ loaded: loaded + (event ? currentProgress : 0),
+ total,
+ url,
+ success,
+ progress: overallProgress,
+ percentage: Number((overallProgress * 100).toFixed(2)),
+ detail: event ? {
+ url,
+ lengthComputable: event.lengthComputable,
+ loadedBytes: event.loaded,
+ totalBytes: event.total
+ } : undefined
+ });
+ }
+
+ /**
+ * 加载单个模型文件
+ * @param modelUrl 模型URL
+ * @param onProgress 进度回调
+ */
+ private async loadSingleModel(
+ modelUrl: string,
+ onProgress?: (event: ISceneLoaderProgressEvent) => void
+ ): Promise {
+ try {
+ const scene = this.mainApp.appScene.object;
+ if (!scene) return { success: false, error: '场景未初始化' };
+
+ const result = await ImportMeshAsync(modelUrl, scene, { onProgress });
+ if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
+
+ this.loadedMeshes.push(...result.meshes);
+ return { success: true, meshes: result.meshes, skeletons: result.skeletons };
+ } catch (e: any) {
+ console.error(`模型加载失败: ${modelUrl}`, e);
+ return { success: false, error: e?.message };
+ }
+ }
+
+ /**
+ * 克隆模型材质,避免多个模型共享同名材质
+ * @param meshes 网格数组
+ * @param modelId 模型ID
+ */
+ private cloneMaterials(meshes: AbstractMesh[], modelId: string): void {
+ const scene = this.mainApp.appScene.object;
+ const clonedMaterials = new Map();
+
+ meshes.forEach(mesh => {
+ if (mesh.material) {
+ const originalMaterial = mesh.material;
+ const originalName = originalMaterial.name;
+
+ // 如果该材质还没有被克隆过,则克隆它
+ if (!clonedMaterials.has(originalName)) {
+ const newName = `${originalName}_${modelId}`;
+ const clonedMaterial = originalMaterial.clone(newName);
+ clonedMaterials.set(originalName, clonedMaterial);
+
+ }
+
+ // 应用克隆的材质
+ mesh.material = clonedMaterials.get(originalName);
+ }
+ });
+
+
+ }
+
+ /** 为网格设置阴影(投射和接收) */
+ private createModelRoot(modelId: string, meshes: AbstractMesh[]): AbstractMesh[] {
+ const scene = this.mainApp.appScene.object;
+ const root = new Mesh(`${modelId}__root`, scene);
+ const meshSet = new Set(meshes);
+ root.position.copyFrom(this.getMeshesBoundingCenter(meshes));
+
+ meshes.forEach(mesh => {
+ if (!mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)) {
+ mesh.setParent(root, true, true);
+ }
+ });
+
+ this.loadedMeshes.push(root);
+ return [root, ...meshes];
+ }
+
+ private getMeshesBoundingCenter(meshes: AbstractMesh[]): Vector3 {
+ const renderableMeshes = meshes.filter(mesh => !mesh.isDisposed() && mesh.getTotalVertices() > 0);
+ if (!renderableMeshes.length) return Vector3.Zero();
+
+ const min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
+ const max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
+
+ renderableMeshes.forEach(mesh => {
+ mesh.computeWorldMatrix(true);
+ const boundingBox = mesh.getBoundingInfo().boundingBox;
+ min.minimizeInPlace(boundingBox.minimumWorld);
+ max.maximizeInPlace(boundingBox.maximumWorld);
+ });
+
+ return min.add(max).scaleInPlace(0.5);
+ }
+
+
+ setupShadows(meshes: AbstractMesh[]): void {
+ const appLight = this.mainApp.appLight;
+ if (!appLight) return;
+
+ meshes.forEach(mesh => {
+ if (mesh.getTotalVertices() > 0) {
+ appLight.addShadowCaster(mesh);
+ mesh.receiveShadows = true;
+ }
+ });
+ }
+
+ /** 获取缓存的网格 */
+ getCachedMeshes(name: string): AbstractMesh[] | undefined {
+ return this.modelDic.Get(name);
+ }
+
+ /** 清理所有资源 */
+ clean(): void {
+ this.modelDic.Clear();
+ this.loadedMeshes.forEach(m => m?.dispose());
+ this.loadedMeshes = [];
+ this.isLoading = false;
+ }
+
+ /**
+ * 添加模型到场景(支持单个或批量)
+ * @param modelConfig 模型配置对象 或 模型配置数组
+ */
+ async add(
+ modelConfig: ModelMetadata | ModelMetadata[]
+ ): Promise {
+ // 批量加载
+ if (Array.isArray(modelConfig)) {
+ return await this.addMultiple(modelConfig);
+ }
+
+ // 单个加载
+ return await this.addSingle(
+ modelConfig.modelName,
+ modelConfig.modelId,
+ modelConfig.modelUrl,
+ modelConfig.modelControlType,
+ modelConfig.drag,
+ modelConfig.transform
+ );
+ }
+
+ /**
+ * 添加单个模型
+ */
+ private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise {
+ // 检查是否已存在
+ const existingMeshes = this.modelDic.Get(modelName+'_'+modelId);
+ if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
+ console.log(`模型 ${modelName} 已存在,直接显示`);
+ this.showMeshes(existingMeshes);
+ return { success: true, meshes: existingMeshes };
+ }
+
+ // 加载模型
+ const result = await this.loadSingleModel(modelUrl, (event) => {
+ this.emitSingleProgress(modelUrl, event);
+ });
+
+ if (result.success && result.meshes) {
+ // 克隆材质,确保每个模型有独立的材质
+ this.cloneMaterials(result.meshes, modelId);
+
+ result.meshes = this.createModelRoot(modelName+'_'+modelId, result.meshes);
+ this.modelDic.Set(modelName+'_'+modelId, result.meshes);
+
+ // 存储元数据
+ this.modelMetadataDic.Set(modelName+'_'+modelId, {
+ modelName: modelName,
+ modelId: modelId,
+ modelUrl: modelUrl,
+ modelControlType: modelControlType,
+ drag: drag,
+ transform: transform
+ });
+
+ // 应用 transform
+ if (transform) {
+ this.applyTransform(modelName+'_'+modelId, transform);
+ }
+
+ // 配置拖拽功能
+ if (drag) {
+ this.mainApp.appModelDrag?.configureDrag(modelName+'_'+modelId, drag);
+ }
+
+ // 更新 GameManager 的字典
+ this.mainApp.gameManager?.updateDictionaries();
+
+ EventBridge.modelLoaded({ urls: [modelUrl] });
+ } else {
+ EventBridge.modelLoadError({ url: modelUrl, error: result.error });
+ }
+
+ return result;
+ }
+
+ /**
+ * 批量添加模型
+ */
+ private async addMultiple(models: ModelMetadata[]): Promise<{ success: boolean; results: LoadResult[] }> {
+ const total = models.length;
+ const results: LoadResult[] = [];
+
+ EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
+
+ for (let i = 0; i < models.length; i++) {
+ const { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i];
+
+ const result = await this.loadSingleModel(modelUrl, (event) => {
+ this.emitProgress(i, total, modelUrl, event);
+ });
+
+ if (result.success && result.meshes) {
+ // 克隆材质,确保每个模型有独立的材质
+ this.cloneMaterials(result.meshes, modelId);
+
+ result.meshes = this.createModelRoot(modelId, result.meshes);
+ this.modelDic.Set(modelId, result.meshes);
+
+ // 存储元数据
+ this.modelMetadataDic.Set(modelId, {
+ modelName: modelName,
+ modelId: modelId,
+ modelUrl: modelUrl,
+ modelControlType: modelControlType,
+ drag: drag,
+ transform: transform
+ });
+
+ // 应用 transform
+ if (transform) {
+ this.applyTransform(modelId, transform);
+ }
+
+ // 配置拖拽功能
+ if (drag) {
+ this.mainApp.appModelDrag?.configureDrag(modelId, drag);
+ }
+ }
+
+ results.push(result);
+ this.emitProgress(i + 1, total, modelUrl, null, result.success);
+ }
+
+ // 批量加载完成后统一更新字典
+ this.mainApp.gameManager?.updateDictionaries();
+
+ EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) });
+
+ return {
+ success: results.every(r => r.success),
+ results
+ };
+ }
+
+ /**
+ * 显示网格
+ */
+ private showMeshes(meshes: AbstractMesh[]): void {
+ meshes.forEach(mesh => {
+ mesh.setEnabled(true);
+ mesh.getChildMeshes().forEach(child => child.setEnabled(true));
+ });
+ }
+
+ /**
+ * 发送单个模型加载进度
+ */
+ private emitSingleProgress(url: string, event: ISceneLoaderProgressEvent): void {
+ const progress = event.lengthComputable && event.total > 0
+ ? Math.min(1, event.loaded / event.total)
+ : 0;
+
+ EventBridge.modelLoadProgress({
+ loaded: progress,
+ total: 1,
+ url,
+ progress,
+ percentage: Number((progress * 100).toFixed(2)),
+ detail: {
+ url,
+ lengthComputable: event.lengthComputable,
+ loadedBytes: event.loaded,
+ totalBytes: event.total
+ }
+ });
+ }
+
+ /**
+ * 根据 mesh 名称查找 mesh 对象
+ * @param meshName mesh 名称
+ * @returns mesh 对象,未找到返回 undefined
+ */
+ private findMeshByName(meshName: string): AbstractMesh | undefined {
+ const keys = this.modelDic.Keys();
+ for (const key of keys) {
+ const meshes = this.modelDic.Get(key);
+ const found = meshes?.find(m => m.name === meshName);
+ if (found) return found;
+ }
+ return undefined;
+ }
+
+ /**
+ * 根据 mesh 查找所属的模型名称
+ * @param mesh 网格对象
+ * @returns 模型名称,未找到返回 undefined
+ */
+ findModelNameByMesh(mesh: AbstractMesh): string | undefined {
+
+ const keys = this.modelDic.Keys();
+ for (const key of keys) {
+ const meshes = this.modelDic.Get(key);
+ meshes.forEach(mesh => {
+ console.log(mesh.uniqueId);
+ console.log(mesh.name);
+
+ });
+ if (meshes?.some(m => m === mesh || m.uniqueId === mesh.uniqueId)) {
+
+ return key;
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * 根据 mesh 或 mesh 名称移除所属的整个模型
+ * @param meshOrName 网格对象或网格名称
+ * @returns 是否成功移除
+ */
+ remove(meshOrName: AbstractMesh | string): boolean {
+ let mesh: AbstractMesh | undefined;
+
+
+ // 判断传入的是对象还是字符串
+ if (typeof meshOrName === 'string') {
+ mesh = this.findMeshByName(meshOrName);
+ if (!mesh) {
+ console.warn(`未找到名为 ${meshOrName} 的网格`);
+ return false;
+ }
+ } else {
+ mesh = meshOrName;
+ }
+
+ const modelName = this.findModelNameByMesh(mesh);
+ if (modelName) {
+ this.removeByName(modelName);
+ return true;
+ }
+ console.warn('未找到该 mesh 所属的模型');
+ return false;
+ }
+
+ /**
+ * 替换模型
+ * @param modelConfig 模型配置对象
+ */
+ async replaceModel(modelConfig: ModelMetadata): Promise {
+
+
+
+ this.removeByName(modelConfig.modelId);
+ return await this.addSingle(
+ modelConfig.modelName,
+ modelConfig.modelId,
+ modelConfig.modelUrl,
+ modelConfig.modelControlType,
+ modelConfig.drag,
+ modelConfig.transform
+ );
+ }
+
+ /**
+ * 销毁指定模型
+ * @param modelName 模型名称
+ */
+ removeByName(modelName: string): void {
+ const meshes = this.modelDic.Get(modelName);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelName}`);
+ return;
+ }
+
+ this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true));
+ this.modelDic.Remove(modelName);
+ this.modelMetadataDic.Remove(modelName);
+ this.mainApp.gameManager?.updateDictionaries();
+
+
+ }
+
+ /**
+ * 清除所有已添加的模型并释放内存
+ * 主要用于切换尺寸后清除不适用的配件
+ */
+ removeAll(): void {
+ const modelNames = this.modelDic.Keys();
+
+
+
+ modelNames.forEach(modelName => {
+ const meshes = this.modelDic.Get(modelName);
+ if (meshes?.length) {
+ this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true));
+ }
+ });
+
+ this.modelDic.Clear();
+ this.modelMetadataDic.Clear();
+ this.mainApp.gameManager?.updateDictionaries();
+
+ console.log('所有模型已清除,内存已释放');
+ }
+
+ /**
+ * 获取模型元数据
+ * @param modelName 模型名称
+ */
+ getModelMetadata(modelName: string): ModelMetadata | undefined {
+ return this.modelMetadataDic.Get(modelName);
+ }
+
+ /**
+ * 根据网格查找模型元数据
+ * @param mesh 网格对象
+ */
+ getMetadataByMesh(mesh: AbstractMesh): ModelMetadata | undefined {
+ const modelName = this.findModelNameByMesh(mesh);
+ if (modelName) {
+ return this.modelMetadataDic.Get(modelName);
+ }
+ return undefined;
+ }
+
+ private getModelTransformTargets(meshes: AbstractMesh[]): AbstractMesh[] {
+ const meshSet = new Set(meshes);
+ const rootMeshes = meshes.filter(mesh => !mesh.parent || !meshSet.has(mesh.parent as AbstractMesh));
+
+ return rootMeshes.length ? rootMeshes : meshes.slice(0, 1);
+ }
+
+ getModelTransformTargetByMesh(mesh: AbstractMesh): AbstractMesh | undefined {
+ const modelName = this.findModelNameByMesh(mesh);
+ if (!modelName) return mesh;
+
+ const meshes = this.modelDic.Get(modelName);
+ if (!meshes?.length) return mesh;
+
+ return this.getModelTransformTargets(meshes)[0] ?? mesh;
+ }
+
+ getModelMeshesByMesh(mesh: AbstractMesh): AbstractMesh[] {
+ const modelName = this.findModelNameByMesh(mesh);
+ if (!modelName) return [mesh];
+
+ const meshes = this.modelDic.Get(modelName);
+ return meshes?.length ? meshes : [mesh];
+ }
+
+ /**
+ * 设置模型旋转
+ * @param modelId 模型ID
+ * @param rotation 旋转向量 {x, y, z}(默认使用角度)
+ * @param useDegrees 是否使用角度(默认true)
+ */
+ setRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
+ const meshes = this.modelDic.Get(modelId);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelId}`);
+ return;
+ }
+
+ // 如果使用角度,转换为弧度
+ const toRadians = (degrees: number) => degrees * Math.PI / 180;
+ const rotationValues = useDegrees ? {
+ x: toRadians(rotation.x),
+ y: toRadians(rotation.y),
+ z: toRadians(rotation.z)
+ } : rotation;
+
+ this.getModelTransformTargets(meshes).forEach(mesh => {
+ if (mesh.rotationQuaternion) {
+ mesh.rotationQuaternion = Quaternion.FromEulerAngles(
+ rotationValues.x,
+ rotationValues.y,
+ rotationValues.z
+ );
+ return;
+ }
+
+ mesh.rotation.set(rotationValues.x, rotationValues.y, rotationValues.z);
+ });
+ }
+
+ /**
+ * 累加模型旋转
+ * @param modelId 模型ID
+ * @param rotation 旋转向量 {x, y, z}(默认使用角度)
+ * @param useDegrees 是否使用角度(默认true)
+ */
+ addRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
+ const meshes = this.modelDic.Get(modelId);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelId}`);
+ return;
+ }
+
+ // 如果使用角度,转换为弧度
+ const toRadians = (degrees: number) => degrees * Math.PI / 180;
+ const rotationValues = useDegrees ? {
+ x: toRadians(rotation.x),
+ y: toRadians(rotation.y),
+ z: toRadians(rotation.z)
+ } : rotation;
+
+ this.getModelTransformTargets(meshes).forEach(mesh => {
+ mesh.addRotation(rotationValues.x, rotationValues.y, rotationValues.z);
+ });
+ }
+
+ /**
+ * 设置模型位置
+ * @param modelId 模型ID
+ * @param position 位置向量 {x, y, z}
+ */
+ setPosition(modelId: string, position: { x: number; y: number; z: number }): void {
+ const meshes = this.modelDic.Get(modelId);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelId}`);
+ return;
+ }
+
+ this.getModelTransformTargets(meshes).forEach(mesh => {
+ mesh.position.x = position.x;
+ mesh.position.y = position.y;
+ mesh.position.z = position.z;
+ });
+ }
+
+ /**
+ * 设置模型缩放
+ * @param modelId 模型ID
+ * @param scale 缩放向量 {x, y, z}
+ */
+ setScale(modelId: string, scale: { x: number; y: number; z: number }): void {
+ const meshes = this.modelDic.Get(modelId);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelId}`);
+ return;
+ }
+
+ this.getModelTransformTargets(meshes).forEach(mesh => {
+ mesh.scaling.x = scale.x;
+ mesh.scaling.y = scale.y;
+ mesh.scaling.z = scale.z;
+ });
+ }
+
+ /**
+ * 将模型放置到指定的放置区域
+ * @param modelId 模型ID
+ * @param zoneInfo 放置区域信息
+ * @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外)
+ */
+ placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
+ const meshes = this.modelDic.Get(modelId);
+ if (!meshes?.length) {
+ console.warn(`Model not found: ${modelId}`);
+ return;
+ }
+
+ // 计算放置位置:中心点 + 法线方向的偏移
+ const targetPosition = zoneInfo.center.add(zoneInfo.normal.scale(offsetDistance));
+
+ // 计算旋转角度:让模型面向墙面(法线的反方向)
+ const targetDirection = zoneInfo.normal.scale(-1);
+ const angle = Math.atan2(targetDirection.x, targetDirection.z);
+
+ this.getModelTransformTargets(meshes).forEach(mesh => {
+ // 设置位置
+ mesh.position.copyFrom(targetPosition);
+
+ // 设置旋转(只旋转Y轴,让模型面向墙面)
+ if (mesh.rotationQuaternion) {
+ mesh.rotationQuaternion = Quaternion.FromEulerAngles(0, angle, 0);
+ } else {
+ mesh.rotation.set(0, angle, 0);
+ }
+ });
+ }
+
+ /**
+ * 检查模型是否存在
+ * @param modelId 模型ID
+ * @returns 模型是否存在
+ */
+ exists(modelId: string): boolean {
+ return this.modelDic.Has(modelId);
+ }
+
+ /**
+ * 应用 transform 到模型
+ * @param modelId 模型ID
+ * @param transform 变换信息
+ */
+ private applyTransform(modelId: string, transform: ModelTransform): void {
+ // 应用位置
+ if (transform.position) {
+ this.setPosition(modelId, transform.position);
+ }
+
+ // 应用旋转(角度制)
+ if (transform.rotation) {
+ this.setRotation(modelId, transform.rotation, true);
+ }
+
+ // 应用缩放
+ if (transform.scale) {
+ this.setScale(modelId, transform.scale);
+ }
+ }
+}
diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts
index 66774c7..92c073f 100644
--- a/src/babylonjs/AppModel.ts
+++ b/src/babylonjs/AppModel.ts
@@ -31,6 +31,7 @@ type ModelTransform = {
};
type ModelMetadata = {
+ modelName: string;
modelId: string;
modelUrl: string;
modelControlType?: ModelControlType;
@@ -253,6 +254,7 @@ export class AppModel extends Monobehiver {
// 单个加载
return await this.addSingle(
+ modelConfig.modelName,
modelConfig.modelId,
modelConfig.modelUrl,
modelConfig.modelControlType,
@@ -264,9 +266,9 @@ export class AppModel extends Monobehiver {
/**
* 添加单个模型
*/
- private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise {
+ private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise {
// 检查是否已存在
- const existingMeshes = this.modelDic.Get(modelName);
+ const existingMeshes = this.modelDic.Get(modelId);
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
console.log(`模型 ${modelName} 已存在,直接显示`);
this.showMeshes(existingMeshes);
@@ -280,14 +282,15 @@ export class AppModel extends Monobehiver {
if (result.success && result.meshes) {
// 克隆材质,确保每个模型有独立的材质
- this.cloneMaterials(result.meshes, modelName);
+ this.cloneMaterials(result.meshes, modelId);
- result.meshes = this.createModelRoot(modelName, result.meshes);
- this.modelDic.Set(modelName, result.meshes);
+ result.meshes = this.createModelRoot(modelId, result.meshes);
+ this.modelDic.Set(modelId, result.meshes);
// 存储元数据
- this.modelMetadataDic.Set(modelName, {
- modelId: modelName,
+ this.modelMetadataDic.Set(modelId, {
+ modelName: modelName,
+ modelId: modelId,
modelUrl: modelUrl,
modelControlType: modelControlType,
drag: drag,
@@ -296,12 +299,12 @@ export class AppModel extends Monobehiver {
// 应用 transform
if (transform) {
- this.applyTransform(modelName, transform);
+ this.applyTransform(modelId, transform);
}
// 配置拖拽功能
if (drag) {
- this.mainApp.appModelDrag?.configureDrag(modelName, drag);
+ this.mainApp.appModelDrag?.configureDrag(modelId, drag);
}
// 更新 GameManager 的字典
@@ -325,7 +328,7 @@ export class AppModel extends Monobehiver {
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
for (let i = 0; i < models.length; i++) {
- const { modelId, modelUrl, modelControlType, drag, transform } = models[i];
+ const { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i];
const result = await this.loadSingleModel(modelUrl, (event) => {
this.emitProgress(i, total, modelUrl, event);
@@ -340,6 +343,7 @@ export class AppModel extends Monobehiver {
// 存储元数据
this.modelMetadataDic.Set(modelId, {
+ modelName: modelName,
modelId: modelId,
modelUrl: modelUrl,
modelControlType: modelControlType,
@@ -483,6 +487,7 @@ export class AppModel extends Monobehiver {
this.removeByName(modelConfig.modelId);
return await this.addSingle(
+ modelConfig.modelName,
modelConfig.modelId,
modelConfig.modelUrl,
modelConfig.modelControlType,
diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts
index 31b22b7..ed9deb8 100644
--- a/src/babylonjs/AppRay.ts
+++ b/src/babylonjs/AppRay.ts
@@ -143,9 +143,13 @@ class AppRay extends Monobehiver {
// 获取模型元数据
const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh);
+ // 获取模型名称(优先使用 modelName,如果没有则使用 modelId)
+ const modelName = this.mainApp.appModel.findModelNameByMesh(pickInfo.pickedMesh);
+console.log(modelName);
EventBridge.modelClick({
meshName: pickInfo.pickedMesh.name,
+ modelName: modelName,
pickedMesh: pickInfo.pickedMesh,
pickedPoint: pickInfo.pickedPoint,
materialName: materialName,
diff --git a/src/babylonjs/GameManager.ts b/src/babylonjs/GameManager.ts
index e9fc898..6187538 100644
--- a/src/babylonjs/GameManager.ts
+++ b/src/babylonjs/GameManager.ts
@@ -786,18 +786,33 @@ export class GameManager extends Monobehiver {
}
// 应用反照率纹理(颜色贴图)
- if (options.albedoTexture) {
- material.albedoTexture = new Texture(options.albedoTexture);
+ if (options.albedoTexture !== undefined) {
+ if (options.albedoTexture) {
+ material.albedoTexture = new Texture(options.albedoTexture);
+ } else {
+ // 传入空字符串或 null 时清空贴图
+ material.albedoTexture = null;
+ }
}
// 应用法线贴图
- if (options.normalMap) {
- material.bumpTexture = new Texture(options.normalMap);
+ if (options.normalMap !== undefined) {
+ if (options.normalMap) {
+ material.bumpTexture = new Texture(options.normalMap);
+ } else {
+ // 传入空字符串或 null 时清空贴图
+ material.bumpTexture = null;
+ }
}
// 应用金属度贴图
- if (options.metallicTexture) {
- material.metallicTexture = new Texture(options.metallicTexture);
+ if (options.metallicTexture !== undefined) {
+ if (options.metallicTexture) {
+ material.metallicTexture = new Texture(options.metallicTexture);
+ } else {
+ // 传入空字符串或 null 时清空贴图
+ material.metallicTexture = null;
+ }
}
// 应用粗糙度值
diff --git a/src/event/types.ts b/src/event/types.ts
index fc9ff1b..194b203 100644
--- a/src/event/types.ts
+++ b/src/event/types.ts
@@ -30,6 +30,7 @@ export type ModelLoadedPayload = {
export type ModelClickPayload = {
meshName?: string;
+ modelName?: string; // 模型根节点名称(modelId)
pickedMesh?: any;
pickedPoint?: any;
materialName?: string;
diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts
index ad6ecf1..a84af8b 100644
--- a/src/kernel/Adapter.ts
+++ b/src/kernel/Adapter.ts
@@ -5,6 +5,7 @@ import type { HotspotInput } from '../types/hotspot';
type ModelControlType = 'rotation' | 'color';
type ModelInput = {
+ modelName: string;
modelId: string;
modelUrl: string;
modelControlType?: ModelControlType;
@@ -68,6 +69,19 @@ export class KernelAdapter {
*/
removeAll: (): void => {
this.mainApp.appModel.removeAll();
+ },
+ /**
+ * 检查模型是否已加载
+ * @param modelId 模型ID
+ * @returns 模型是否存在
+ * @example
+ * // 检查模型是否已加载,避免重复加载
+ * if (!kernel.model.exists('shed_001')) {
+ * await kernel.model.add({ modelId: 'shed_001', modelUrl: '...' });
+ * }
+ */
+ exists: (modelId: string): boolean => {
+ return this.mainApp.appModel.exists(modelId);
}
};