1
This commit is contained in:
BIN
ScreenShot_2026-05-18_175704_601.png
Normal file
BIN
ScreenShot_2026-05-18_175704_601.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@ -203,8 +203,8 @@ const isModelExists = (modelId) => {
|
|||||||
return kernel.model.exists(modelId);
|
return kernel.model.exists(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//换棚子
|
//一般是换棚子/换颜色/显示放置区域
|
||||||
const executeEvent2 = async (result) => {
|
const executeEvent2 = async (result) => {
|
||||||
const kernel = getKernel();
|
const kernel = getKernel();
|
||||||
|
|
||||||
// 检查是否有模型更换事件
|
// 检查是否有模型更换事件
|
||||||
@ -214,10 +214,10 @@ const executeEvent2 = async (result) => {
|
|||||||
let modelAlreadyExists = false;
|
let modelAlreadyExists = false;
|
||||||
if (hasModelChange) {
|
if (hasModelChange) {
|
||||||
const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model');
|
const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model');
|
||||||
if (firstModelEvent) {
|
if (firstModelEvent && firstModelEvent.target_data) {
|
||||||
const { category } = firstModelEvent.target_data;
|
const {name, category } = firstModelEvent.target_data;
|
||||||
modelAlreadyExists = isModelExists(category);
|
modelAlreadyExists = kernel.model.exists(name+'_'+category);
|
||||||
console.log(`检查模型 ${category} 是否存在:`, modelAlreadyExists);
|
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;
|
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
console.log('替换百叶模型类型:', category);
|
|
||||||
|
|
||||||
// 如果模型已存在,跳过加载
|
// 如果模型已存在,跳过加载
|
||||||
if (modelAlreadyExists) {
|
if (modelAlreadyExists) {
|
||||||
console.log(`模型 ${category} 已存在,跳过加载`);
|
console.log(`模型 ${name+'_'+category} 已存在,跳过加载`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +267,7 @@ const executeEvent2 = async (result) => {
|
|||||||
modelControlType: model_control_type,
|
modelControlType: model_control_type,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`百叶模型已放置为 ${name}`);
|
console.log(`模型已放置为 ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -355762,14 +355762,26 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
|
|||||||
const color = Color3.FromHexString(options.albedoColor);
|
const color = Color3.FromHexString(options.albedoColor);
|
||||||
material.albedoColor.copyFrom(color);
|
material.albedoColor.copyFrom(color);
|
||||||
}
|
}
|
||||||
if (options.albedoTexture) {
|
if (options.albedoTexture !== void 0) {
|
||||||
material.albedoTexture = new Texture(options.albedoTexture);
|
if (options.albedoTexture) {
|
||||||
|
material.albedoTexture = new Texture(options.albedoTexture);
|
||||||
|
} else {
|
||||||
|
material.albedoTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options.normalMap) {
|
if (options.normalMap !== void 0) {
|
||||||
material.bumpTexture = new Texture(options.normalMap);
|
if (options.normalMap) {
|
||||||
|
material.bumpTexture = new Texture(options.normalMap);
|
||||||
|
} else {
|
||||||
|
material.bumpTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options.metallicTexture) {
|
if (options.metallicTexture !== void 0) {
|
||||||
material.metallicTexture = new Texture(options.metallicTexture);
|
if (options.metallicTexture) {
|
||||||
|
material.metallicTexture = new Texture(options.metallicTexture);
|
||||||
|
} else {
|
||||||
|
material.metallicTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options.roughness !== void 0) {
|
if (options.roughness !== void 0) {
|
||||||
material.roughness = options.roughness;
|
material.roughness = options.roughness;
|
||||||
@ -357185,6 +357197,19 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
|
|||||||
*/
|
*/
|
||||||
removeAll: () => {
|
removeAll: () => {
|
||||||
this.mainApp.appModel.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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/** 材质管理 */
|
/** 材质管理 */
|
||||||
|
|||||||
24
index.html
24
index.html
@ -302,8 +302,8 @@
|
|||||||
<button class="option-btn" data-option="size-3">10x13EM星空篷</button>
|
<button class="option-btn" data-option="size-3">10x13EM星空篷</button>
|
||||||
<button class="option-btn" data-option="size-4">全铁3x6</button>
|
<button class="option-btn" data-option="size-4">全铁3x6</button>
|
||||||
<button class="option-btn" data-option="size-1">10x12</button>
|
<button class="option-btn" data-option="size-1">10x12</button>
|
||||||
<button class="option-btn" data-option="size-2">10x10星空篷</button>
|
<button class="option-btn" data-option="size-2">SPF111S1010W</button>
|
||||||
<button class="option-btn" data-option="size-3">10x13星空篷</button>
|
<button class="option-btn" data-option="size-3">SPF111S1013W</button>
|
||||||
<button class="option-btn" data-option="size-4">10x20星空篷</button>
|
<button class="option-btn" data-option="size-4">10x20星空篷</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -333,11 +333,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content">
|
<div class="category-content">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="louver-1">整体</button>
|
<button class="option-btn" data-option="louver-1">SPFPDS13FTW</button>
|
||||||
<button class="option-btn" data-option="louver-2">3m百叶</button>
|
<button class="option-btn" data-option="louver-2">SPFPDS13FTC</button>
|
||||||
<button class="option-btn" data-option="louver-3">3m下拉帘</button>
|
<button class="option-btn" data-option="louver-3">3m下拉帘</button>
|
||||||
<button class="option-btn" data-option="louver-4">百叶4</button>
|
<button class="option-btn" data-option="louver-4">SPFSW13FTC</button>
|
||||||
<button class="option-btn" data-option="louver-4">卷帘小</button>
|
<button class="option-btn" data-option="louver-4">SPFSW10FTW</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -586,7 +586,7 @@
|
|||||||
// 监听模型点击事件
|
// 监听模型点击事件
|
||||||
window.addEventListener('model:click', (event) => {
|
window.addEventListener('model:click', (event) => {
|
||||||
console.log('模型被点击:', event.detail);
|
console.log('模型被点击:', event.detail);
|
||||||
const { meshName, materialName, modelControlType } = event.detail;
|
const { meshName, modelName, materialName, modelControlType } = event.detail;
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
const clickInfoDiv = document.getElementById('click-info');
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
const clickInfoContent = document.getElementById('click-info-content');
|
||||||
@ -596,8 +596,8 @@
|
|||||||
<span class="click-info-value">模型</span>
|
<span class="click-info-value">模型</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="click-info-item">
|
<div class="click-info-item">
|
||||||
<span class="click-info-label">网格名称:</span>
|
<span class="click-info-label">模型名称:</span>
|
||||||
<span class="click-info-value">${meshName}</span>
|
<span class="click-info-value">${modelName || meshName}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (materialName) {
|
if (materialName) {
|
||||||
@ -866,6 +866,8 @@
|
|||||||
// DOM 2D转3D 示例:点击模型时显示信息框
|
// DOM 2D转3D 示例:点击模型时显示信息框
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
const meshName = data.pickedMesh.name;
|
const meshName = data.pickedMesh.name;
|
||||||
|
// 获取模型根节点名称(modelId)
|
||||||
|
const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName;
|
||||||
const position = data.pickedPoint; // 使用点击位置的坐标
|
const position = data.pickedPoint; // 使用点击位置的坐标
|
||||||
currentMaterialName = data.materialName || ''; // 保存材质名
|
currentMaterialName = data.materialName || ''; // 保存材质名
|
||||||
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
@ -873,7 +875,7 @@
|
|||||||
// 获取已创建的DOM元素
|
// 获取已创建的DOM元素
|
||||||
const infoDiv = document.getElementById('model-info-box');
|
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)}]`;
|
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
||||||
|
|
||||||
// 显示颜色按钮,隐藏旋转按钮
|
// 显示颜色按钮,隐藏旋转按钮
|
||||||
@ -888,6 +890,8 @@
|
|||||||
// 显示旋转控制UI
|
// 显示旋转控制UI
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
const meshName = data.pickedMesh.name;
|
const meshName = data.pickedMesh.name;
|
||||||
|
// 获取模型根节点名称(modelId)
|
||||||
|
const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName;
|
||||||
const position = data.pickedPoint;
|
const position = data.pickedPoint;
|
||||||
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
|
|
||||||
|
|||||||
49
index.js
49
index.js
@ -37,9 +37,9 @@ export const init = async (customConfig = {}) => {
|
|||||||
modelUrlList: [],
|
modelUrlList: [],
|
||||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
||||||
gizmo: {
|
gizmo: {
|
||||||
position: true,
|
position: false,
|
||||||
rotation: true,
|
rotation: false,
|
||||||
scale: true
|
scale: false
|
||||||
},
|
},
|
||||||
outline: {
|
outline: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@ -67,7 +67,7 @@ export const getAutoLoadModelList = async () => {
|
|||||||
const models = data.data // 这就是模型列表
|
const models = data.data // 这就是模型列表
|
||||||
|
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
console.log(model.placement_zone);
|
|
||||||
if (model.placement_zone) {
|
if (model.placement_zone) {
|
||||||
const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone
|
const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone
|
||||||
kernel.dropZone.setData({
|
kernel.dropZone.setData({
|
||||||
@ -82,6 +82,7 @@ export const getAutoLoadModelList = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kernel.model.add({
|
kernel.model.add({
|
||||||
|
modelName: model.name+'_'+model.category,
|
||||||
modelId: model.category,
|
modelId: model.category,
|
||||||
modelUrl: model.file_url,
|
modelUrl: model.file_url,
|
||||||
modelControlType: model.model_control_type,
|
modelControlType: model.model_control_type,
|
||||||
@ -148,18 +149,18 @@ export const executeEvent = async (dropzone_data, result) => {
|
|||||||
if (event.event_type === 'change_model') {
|
if (event.event_type === 'change_model') {
|
||||||
console.log(event.target_data);
|
console.log(event.target_data);
|
||||||
|
|
||||||
const { id, name, file_url, model_control_type, category } = event.target_data;
|
const { name, file_url, model_control_type, category } = event.target_data;
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
console.log('替换百叶模型类型:', category);
|
|
||||||
|
|
||||||
// 生成唯一的模型ID
|
// 生成唯一的模型ID
|
||||||
const modelId = id + '_' + Date.now();
|
const modelId = Date.now();
|
||||||
|
|
||||||
// 先记录模型放置(会自动处理替换逻辑)
|
// 先记录模型放置(会自动处理替换逻辑)
|
||||||
kernel.dropZone.recordModelPlacement(wallName, index, modelId);
|
kernel.dropZone.recordModelPlacement(wallName, index, modelId);
|
||||||
console.log(Math.abs(rotation.y - 90), Math.abs(rotation.y - 90) > 5 ? 'x' : 'z');
|
console.log(Math.abs(rotation.y - 90), Math.abs(rotation.y - 90) > 5 ? 'x' : 'z');
|
||||||
// 加载并放置模型
|
// 加载并放置模型
|
||||||
await kernel.model.add({
|
await kernel.model.add({
|
||||||
|
modelName: name ,
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
modelUrl: file_url,
|
modelUrl: file_url,
|
||||||
modelControlType: model_control_type,
|
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') {
|
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 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) {
|
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.dropZone.clearZones();
|
||||||
kernel.model.removeAll();
|
kernel.model.removeAll();
|
||||||
|
} else if (modelAlreadyExists) {
|
||||||
|
console.log('模型已存在,跳过清除操作,仅更新材质');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先处理所有 change_model 事件
|
// 先处理所有 change_model 事件
|
||||||
@ -221,7 +235,11 @@ export const executeEvent2 = async (result) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
|
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
|
||||||
|
// 如果模型已存在,跳过加载
|
||||||
|
if (modelAlreadyExists) {
|
||||||
|
console.log(`模型 ${name+'_'+category} 已存在,跳过加载`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (placement_zone) {
|
if (placement_zone) {
|
||||||
const { alpha, border_color, color, show_border, thickness, walls } = placement_zone
|
const { alpha, border_color, color, show_border, thickness, walls } = placement_zone
|
||||||
@ -237,12 +255,13 @@ export const executeEvent2 = async (result) => {
|
|||||||
|
|
||||||
// 加载并放置模型(使用 category 作为 modelId)
|
// 加载并放置模型(使用 category 作为 modelId)
|
||||||
await kernel.model.add({
|
await kernel.model.add({
|
||||||
modelId: category,
|
modelName: name,
|
||||||
|
modelId: category,
|
||||||
modelUrl: file_url,
|
modelUrl: file_url,
|
||||||
modelControlType: model_control_type,
|
modelControlType: model_control_type,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`模型已放置为 ${name}`);
|
console.log(`模型已放置为 ${name+'_'+category}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,18 +145,24 @@ export class AppDropZone {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新配置中的墙面分割数
|
// 更新配置中的墙面分割数,只保留后端配置的墙面
|
||||||
this.dropZoneConfig.walls = this.dropZoneConfig.walls.map(wall => {
|
this.dropZoneConfig.walls = this.dropZoneConfig.walls
|
||||||
const newDivisions = matchWallName(wall.name);
|
.map(wall => {
|
||||||
const finalDivisions = newDivisions !== null ? newDivisions : (wall.divisions || 1);
|
const newDivisions = matchWallName(wall.name);
|
||||||
|
|
||||||
console.log(`墙面 "${wall.name}" 匹配到分割数: ${finalDivisions}`);
|
// 如果后端没有配置这个墙面,返回 null 标记
|
||||||
|
if (newDivisions === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
console.log(`墙面 "${wall.name}" 匹配到分割数: ${newDivisions}`);
|
||||||
...wall,
|
|
||||||
divisions: finalDivisions
|
return {
|
||||||
};
|
...wall,
|
||||||
});
|
divisions: newDivisions
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面
|
||||||
|
|
||||||
// 清除旧的放置区域网格(不清除模型)
|
// 清除旧的放置区域网格(不清除模型)
|
||||||
this.clearZones();
|
this.clearZones();
|
||||||
|
|||||||
748
src/babylonjs/AppModel copy.ts
Normal file
748
src/babylonjs/AppModel copy.ts
Normal file
@ -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<AbstractMesh[]>;
|
||||||
|
private modelMetadataDic: Dictionary<ModelMetadata>;
|
||||||
|
private loadedMeshes: AbstractMesh[];
|
||||||
|
private isLoading: boolean;
|
||||||
|
|
||||||
|
constructor(mainApp: any) {
|
||||||
|
super(mainApp);
|
||||||
|
this.modelDic = new Dictionary<AbstractMesh[]>();
|
||||||
|
this.modelMetadataDic = new Dictionary<ModelMetadata>();
|
||||||
|
this.loadedMeshes = [];
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
initManagers(): void {
|
||||||
|
// 预留接口
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载配置中的所有模型 */
|
||||||
|
async loadModel(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<LoadResult> {
|
||||||
|
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<string, any>();
|
||||||
|
|
||||||
|
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<AbstractMesh>(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<LoadResult | { success: boolean; results: LoadResult[] }> {
|
||||||
|
// 批量加载
|
||||||
|
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<LoadResult> {
|
||||||
|
// 检查是否已存在
|
||||||
|
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<LoadResult> {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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<AbstractMesh>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ type ModelTransform = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ModelMetadata = {
|
type ModelMetadata = {
|
||||||
|
modelName: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
modelControlType?: ModelControlType;
|
modelControlType?: ModelControlType;
|
||||||
@ -253,6 +254,7 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
// 单个加载
|
// 单个加载
|
||||||
return await this.addSingle(
|
return await this.addSingle(
|
||||||
|
modelConfig.modelName,
|
||||||
modelConfig.modelId,
|
modelConfig.modelId,
|
||||||
modelConfig.modelUrl,
|
modelConfig.modelUrl,
|
||||||
modelConfig.modelControlType,
|
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<LoadResult> {
|
private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise<LoadResult> {
|
||||||
// 检查是否已存在
|
// 检查是否已存在
|
||||||
const existingMeshes = this.modelDic.Get(modelName);
|
const existingMeshes = this.modelDic.Get(modelId);
|
||||||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
||||||
console.log(`模型 ${modelName} 已存在,直接显示`);
|
console.log(`模型 ${modelName} 已存在,直接显示`);
|
||||||
this.showMeshes(existingMeshes);
|
this.showMeshes(existingMeshes);
|
||||||
@ -280,14 +282,15 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
if (result.success && result.meshes) {
|
if (result.success && result.meshes) {
|
||||||
// 克隆材质,确保每个模型有独立的材质
|
// 克隆材质,确保每个模型有独立的材质
|
||||||
this.cloneMaterials(result.meshes, modelName);
|
this.cloneMaterials(result.meshes, modelId);
|
||||||
|
|
||||||
result.meshes = this.createModelRoot(modelName, result.meshes);
|
result.meshes = this.createModelRoot(modelId, result.meshes);
|
||||||
this.modelDic.Set(modelName, result.meshes);
|
this.modelDic.Set(modelId, result.meshes);
|
||||||
|
|
||||||
// 存储元数据
|
// 存储元数据
|
||||||
this.modelMetadataDic.Set(modelName, {
|
this.modelMetadataDic.Set(modelId, {
|
||||||
modelId: modelName,
|
modelName: modelName,
|
||||||
|
modelId: modelId,
|
||||||
modelUrl: modelUrl,
|
modelUrl: modelUrl,
|
||||||
modelControlType: modelControlType,
|
modelControlType: modelControlType,
|
||||||
drag: drag,
|
drag: drag,
|
||||||
@ -296,12 +299,12 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
// 应用 transform
|
// 应用 transform
|
||||||
if (transform) {
|
if (transform) {
|
||||||
this.applyTransform(modelName, transform);
|
this.applyTransform(modelId, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置拖拽功能
|
// 配置拖拽功能
|
||||||
if (drag) {
|
if (drag) {
|
||||||
this.mainApp.appModelDrag?.configureDrag(modelName, drag);
|
this.mainApp.appModelDrag?.configureDrag(modelId, drag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 GameManager 的字典
|
// 更新 GameManager 的字典
|
||||||
@ -325,7 +328,7 @@ export class AppModel extends Monobehiver {
|
|||||||
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
||||||
|
|
||||||
for (let i = 0; i < models.length; i++) {
|
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) => {
|
const result = await this.loadSingleModel(modelUrl, (event) => {
|
||||||
this.emitProgress(i, total, modelUrl, event);
|
this.emitProgress(i, total, modelUrl, event);
|
||||||
@ -340,6 +343,7 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
// 存储元数据
|
// 存储元数据
|
||||||
this.modelMetadataDic.Set(modelId, {
|
this.modelMetadataDic.Set(modelId, {
|
||||||
|
modelName: modelName,
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
modelUrl: modelUrl,
|
modelUrl: modelUrl,
|
||||||
modelControlType: modelControlType,
|
modelControlType: modelControlType,
|
||||||
@ -483,6 +487,7 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
this.removeByName(modelConfig.modelId);
|
this.removeByName(modelConfig.modelId);
|
||||||
return await this.addSingle(
|
return await this.addSingle(
|
||||||
|
modelConfig.modelName,
|
||||||
modelConfig.modelId,
|
modelConfig.modelId,
|
||||||
modelConfig.modelUrl,
|
modelConfig.modelUrl,
|
||||||
modelConfig.modelControlType,
|
modelConfig.modelControlType,
|
||||||
|
|||||||
@ -143,9 +143,13 @@ class AppRay extends Monobehiver {
|
|||||||
|
|
||||||
// 获取模型元数据
|
// 获取模型元数据
|
||||||
const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh);
|
const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh);
|
||||||
|
// 获取模型名称(优先使用 modelName,如果没有则使用 modelId)
|
||||||
|
const modelName = this.mainApp.appModel.findModelNameByMesh(pickInfo.pickedMesh);
|
||||||
|
console.log(modelName);
|
||||||
|
|
||||||
EventBridge.modelClick({
|
EventBridge.modelClick({
|
||||||
meshName: pickInfo.pickedMesh.name,
|
meshName: pickInfo.pickedMesh.name,
|
||||||
|
modelName: modelName,
|
||||||
pickedMesh: pickInfo.pickedMesh,
|
pickedMesh: pickInfo.pickedMesh,
|
||||||
pickedPoint: pickInfo.pickedPoint,
|
pickedPoint: pickInfo.pickedPoint,
|
||||||
materialName: materialName,
|
materialName: materialName,
|
||||||
|
|||||||
@ -786,18 +786,33 @@ export class GameManager extends Monobehiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 应用反照率纹理(颜色贴图)
|
// 应用反照率纹理(颜色贴图)
|
||||||
if (options.albedoTexture) {
|
if (options.albedoTexture !== undefined) {
|
||||||
material.albedoTexture = new Texture(options.albedoTexture);
|
if (options.albedoTexture) {
|
||||||
|
material.albedoTexture = new Texture(options.albedoTexture);
|
||||||
|
} else {
|
||||||
|
// 传入空字符串或 null 时清空贴图
|
||||||
|
material.albedoTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用法线贴图
|
// 应用法线贴图
|
||||||
if (options.normalMap) {
|
if (options.normalMap !== undefined) {
|
||||||
material.bumpTexture = new Texture(options.normalMap);
|
if (options.normalMap) {
|
||||||
|
material.bumpTexture = new Texture(options.normalMap);
|
||||||
|
} else {
|
||||||
|
// 传入空字符串或 null 时清空贴图
|
||||||
|
material.bumpTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用金属度贴图
|
// 应用金属度贴图
|
||||||
if (options.metallicTexture) {
|
if (options.metallicTexture !== undefined) {
|
||||||
material.metallicTexture = new Texture(options.metallicTexture);
|
if (options.metallicTexture) {
|
||||||
|
material.metallicTexture = new Texture(options.metallicTexture);
|
||||||
|
} else {
|
||||||
|
// 传入空字符串或 null 时清空贴图
|
||||||
|
material.metallicTexture = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用粗糙度值
|
// 应用粗糙度值
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export type ModelLoadedPayload = {
|
|||||||
|
|
||||||
export type ModelClickPayload = {
|
export type ModelClickPayload = {
|
||||||
meshName?: string;
|
meshName?: string;
|
||||||
|
modelName?: string; // 模型根节点名称(modelId)
|
||||||
pickedMesh?: any;
|
pickedMesh?: any;
|
||||||
pickedPoint?: any;
|
pickedPoint?: any;
|
||||||
materialName?: string;
|
materialName?: string;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { HotspotInput } from '../types/hotspot';
|
|||||||
type ModelControlType = 'rotation' | 'color';
|
type ModelControlType = 'rotation' | 'color';
|
||||||
|
|
||||||
type ModelInput = {
|
type ModelInput = {
|
||||||
|
modelName: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
modelControlType?: ModelControlType;
|
modelControlType?: ModelControlType;
|
||||||
@ -68,6 +69,19 @@ export class KernelAdapter {
|
|||||||
*/
|
*/
|
||||||
removeAll: (): void => {
|
removeAll: (): void => {
|
||||||
this.mainApp.appModel.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: string): boolean => {
|
||||||
|
return this.mainApp.appModel.exists(modelId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user