1
This commit is contained in:
@ -13,5 +13,17 @@ export const AppConfig = {
|
||||
intensity: 1.5,
|
||||
rotationY: 0,
|
||||
background: true,
|
||||
}
|
||||
},
|
||||
gizmo: {
|
||||
position: true,
|
||||
rotation: false,
|
||||
scale: false,
|
||||
},
|
||||
outline: {
|
||||
enable: true,
|
||||
color: '#2196F3',
|
||||
thickness: 3.0,
|
||||
occlusionStrength: 0.9,
|
||||
occlusionThreshold: 0.0002,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
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';
|
||||
@ -19,17 +21,27 @@ type ModelConfig = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
type ModelControlType = 'rotation' | 'color';
|
||||
|
||||
type ModelMetadata = {
|
||||
modelId: string;
|
||||
modelUrl: string;
|
||||
modelControlType?: ModelControlType;
|
||||
};
|
||||
|
||||
/**
|
||||
* 模型管理类 - 负责加载、缓存和管理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;
|
||||
}
|
||||
@ -143,6 +155,40 @@ export class AppModel extends Monobehiver {
|
||||
});
|
||||
}
|
||||
/** 为网格设置阴影(投射和接收) */
|
||||
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;
|
||||
@ -170,30 +216,28 @@ export class AppModel extends Monobehiver {
|
||||
|
||||
/**
|
||||
* 添加模型到场景(支持单个或批量)
|
||||
* @param modelName 模型名称 或 模型配置数组
|
||||
* @param modelUrl 模型URL(单个模型时使用)
|
||||
* @param modelConfig 模型配置对象 或 模型配置数组
|
||||
*/
|
||||
async add(
|
||||
modelName: string | ModelConfig[],
|
||||
modelUrl?: string
|
||||
modelConfig: ModelMetadata | ModelMetadata[]
|
||||
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
||||
// 批量加载
|
||||
if (Array.isArray(modelName)) {
|
||||
return await this.addMultiple(modelName);
|
||||
if (Array.isArray(modelConfig)) {
|
||||
return await this.addMultiple(modelConfig);
|
||||
}
|
||||
|
||||
// 单个加载
|
||||
if (!modelUrl) {
|
||||
return { success: false, error: '缺少模型URL参数' };
|
||||
}
|
||||
|
||||
return await this.addSingle(modelName, modelUrl);
|
||||
return await this.addSingle(
|
||||
modelConfig.modelId,
|
||||
modelConfig.modelUrl,
|
||||
modelConfig.modelControlType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个模型
|
||||
*/
|
||||
private async addSingle(modelName: string, modelUrl: string): Promise<LoadResult> {
|
||||
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType): Promise<LoadResult> {
|
||||
// 检查是否已存在
|
||||
const existingMeshes = this.modelDic.Get(modelName);
|
||||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
||||
@ -209,8 +253,16 @@ export class AppModel extends Monobehiver {
|
||||
|
||||
if (result.success && result.meshes) {
|
||||
// this.cloneMaterials(result.meshes, modelName);
|
||||
result.meshes = this.createModelRoot(modelName, result.meshes);
|
||||
this.modelDic.Set(modelName, result.meshes);
|
||||
|
||||
// 存储元数据
|
||||
this.modelMetadataDic.Set(modelName, {
|
||||
modelId: modelName,
|
||||
modelUrl: modelUrl,
|
||||
modelControlType: modelControlType
|
||||
});
|
||||
|
||||
// 更新 GameManager 的字典
|
||||
this.mainApp.gameManager?.updateDictionaries();
|
||||
|
||||
@ -225,32 +277,40 @@ export class AppModel extends Monobehiver {
|
||||
/**
|
||||
* 批量添加模型
|
||||
*/
|
||||
private async addMultiple(models: ModelConfig[]): Promise<{ success: boolean; results: LoadResult[] }> {
|
||||
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 { name, url } = models[i];
|
||||
const { modelId, modelUrl, modelControlType } = models[i];
|
||||
|
||||
const result = await this.loadSingleModel(url, (event) => {
|
||||
this.emitProgress(i, total, url, event);
|
||||
const result = await this.loadSingleModel(modelUrl, (event) => {
|
||||
this.emitProgress(i, total, modelUrl, event);
|
||||
});
|
||||
|
||||
if (result.success && result.meshes) {
|
||||
this.cloneMaterials(result.meshes, name);
|
||||
this.modelDic.Set(name, result.meshes);
|
||||
result.meshes = this.createModelRoot(modelId, result.meshes);
|
||||
this.cloneMaterials(result.meshes, modelId);
|
||||
this.modelDic.Set(modelId, result.meshes);
|
||||
|
||||
// 存储元数据
|
||||
this.modelMetadataDic.Set(modelId, {
|
||||
modelId: modelId,
|
||||
modelUrl: modelUrl,
|
||||
modelControlType: modelControlType
|
||||
});
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
this.emitProgress(i + 1, total, url, null, result.success);
|
||||
this.emitProgress(i + 1, total, modelUrl, null, result.success);
|
||||
}
|
||||
|
||||
// 批量加载完成后统一更新字典
|
||||
this.mainApp.gameManager?.updateDictionaries();
|
||||
|
||||
EventBridge.modelLoaded({ urls: models.map(m => m.url) });
|
||||
EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) });
|
||||
|
||||
return {
|
||||
success: results.every(r => r.success),
|
||||
@ -352,14 +412,17 @@ export class AppModel extends Monobehiver {
|
||||
|
||||
/**
|
||||
* 替换模型
|
||||
* @param modelName 模型名称
|
||||
* @param newModelUrl 新模型URL
|
||||
* @param modelConfig 模型配置对象
|
||||
*/
|
||||
async replaceModel(modelName: string, newModelUrl: string): Promise<LoadResult> {
|
||||
console.log( modelName,this.modelDic);
|
||||
|
||||
this.removeByName(modelName);
|
||||
return await this.addSingle(modelName, newModelUrl);
|
||||
async replaceModel(modelConfig: ModelMetadata): Promise<LoadResult> {
|
||||
console.log(modelConfig.modelId, this.modelDic);
|
||||
|
||||
this.removeByName(modelConfig.modelId);
|
||||
return await this.addSingle(
|
||||
modelConfig.modelId,
|
||||
modelConfig.modelUrl,
|
||||
modelConfig.modelControlType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -375,6 +438,151 @@ export class AppModel extends Monobehiver {
|
||||
|
||||
meshes.forEach(mesh => mesh.dispose());
|
||||
this.modelDic.Remove(modelName);
|
||||
this.modelMetadataDic.Remove(modelName);
|
||||
console.log(`Model removed: ${modelName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型元数据
|
||||
* @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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
80
src/babylonjs/AppPositionGizmo.ts
Normal file
80
src/babylonjs/AppPositionGizmo.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||
import { PositionGizmo } from '@babylonjs/core/Gizmos/positionGizmo';
|
||||
import { UtilityLayerRenderer } from '@babylonjs/core/Rendering/utilityLayerRenderer';
|
||||
import { MainApp } from './MainApp';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
|
||||
export class AppPositionGizmo extends Monobehiver {
|
||||
private utilityLayer: UtilityLayerRenderer | null = null;
|
||||
private gizmo: PositionGizmo | null = null;
|
||||
private enabled = true;
|
||||
private rotationEnabled = false;
|
||||
private scaleEnabled = false;
|
||||
|
||||
constructor(mainApp: MainApp) {
|
||||
super(mainApp);
|
||||
}
|
||||
|
||||
Awake(): void {
|
||||
const scene = this.mainApp.appScene.object;
|
||||
if (!scene) return;
|
||||
|
||||
this.utilityLayer = new UtilityLayerRenderer(scene);
|
||||
this.gizmo = new PositionGizmo(this.utilityLayer);
|
||||
this.gizmo.updateGizmoRotationToMatchAttachedMesh = false;
|
||||
this.gizmo.updateGizmoPositionToMatchAttachedMesh = true;
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled;
|
||||
if (!enabled) {
|
||||
this.detach();
|
||||
}
|
||||
}
|
||||
|
||||
configure(options?: { position?: boolean; rotation?: boolean; scale?: boolean }): void {
|
||||
if (!options) return;
|
||||
|
||||
if (typeof options.position === 'boolean') {
|
||||
this.setEnabled(options.position);
|
||||
}
|
||||
|
||||
if (typeof options.rotation === 'boolean') {
|
||||
this.rotationEnabled = options.rotation;
|
||||
}
|
||||
|
||||
if (typeof options.scale === 'boolean') {
|
||||
this.scaleEnabled = options.scale;
|
||||
}
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.setEnabled(!this.enabled);
|
||||
}
|
||||
|
||||
attach(mesh: AbstractMesh | null): void {
|
||||
if (!this.enabled || !this.gizmo) return;
|
||||
this.gizmo.attachedMesh = mesh;
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
if (this.gizmo) {
|
||||
this.gizmo.attachedMesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
getAttachedMesh(): AbstractMesh | null {
|
||||
return this.gizmo?.attachedMesh ?? null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.gizmo?.dispose();
|
||||
this.utilityLayer?.dispose();
|
||||
this.gizmo = null;
|
||||
this.utilityLayer = null;
|
||||
}
|
||||
}
|
||||
@ -87,17 +87,35 @@ class AppRay extends Monobehiver {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mainApp.appDomTo3D.hideAll()
|
||||
|
||||
const materialName = pickInfo.pickedMesh.material?.name || '';
|
||||
const holdingShift = Boolean((evt as any).shiftKey);
|
||||
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
|
||||
if (holdingShift) {
|
||||
this.mainApp.appSelectionOutline.toggle(modelMeshes);
|
||||
} else {
|
||||
this.mainApp.appSelectionOutline.select(modelMeshes);
|
||||
}
|
||||
const transformTarget = this.mainApp.appModel.getModelTransformTargetByMesh(pickInfo.pickedMesh);
|
||||
this.mainApp.appPositionGizmo.attach(transformTarget ?? pickInfo.pickedMesh);
|
||||
|
||||
// 获取模型元数据
|
||||
const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh);
|
||||
|
||||
EventBridge.modelClick({
|
||||
meshName: pickInfo.pickedMesh.name,
|
||||
pickedMesh: pickInfo.pickedMesh,
|
||||
pickedPoint: pickInfo.pickedPoint,
|
||||
materialName: materialName,
|
||||
modelControlType: modelMetadata?.modelControlType,
|
||||
});
|
||||
}
|
||||
else{
|
||||
console.log(1111);
|
||||
|
||||
this.mainApp.appSelectionOutline.clear();
|
||||
this.mainApp.appPositionGizmo.detach();
|
||||
this.mainApp.appDomTo3D.hideAll()
|
||||
}
|
||||
}
|
||||
|
||||
159
src/babylonjs/AppSelectionOutline.ts
Normal file
159
src/babylonjs/AppSelectionOutline.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
||||
import { SelectionOutlineLayer } from '@babylonjs/core/Layers/selectionOutlineLayer';
|
||||
import '@babylonjs/core/Layers/effectLayerSceneComponent';
|
||||
import { MainApp } from './MainApp';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
|
||||
type OutlineConfig = {
|
||||
enable?: boolean;
|
||||
color?: Color3 | string;
|
||||
thickness?: number;
|
||||
width?: number;
|
||||
occlusionStrength?: number;
|
||||
occlusionThreshold?: number;
|
||||
};
|
||||
|
||||
export class AppSelectionOutline extends Monobehiver {
|
||||
private selectedMeshes: AbstractMesh[] = [];
|
||||
private outlineLayer: SelectionOutlineLayer | null = null;
|
||||
private enabled = true;
|
||||
private color = new Color3(0.1, 0.65, 1);
|
||||
private width = 0.08;
|
||||
private occlusionStrength = 0.9;
|
||||
private occlusionThreshold = 0.0002;
|
||||
|
||||
constructor(mainApp: MainApp) {
|
||||
super(mainApp);
|
||||
}
|
||||
|
||||
init(): void {
|
||||
const scene = this.mainApp.appScene.object;
|
||||
if (!scene || this.outlineLayer) return;
|
||||
|
||||
this.outlineLayer = new SelectionOutlineLayer('selection-outline', scene, {
|
||||
mainTextureRatio: 1,
|
||||
});
|
||||
this.applyLayerConfig();
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled;
|
||||
if (!enabled) {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
setStyle(options: { color?: Color3 | string; width?: number }): void {
|
||||
this.configure(options);
|
||||
}
|
||||
|
||||
configure(options?: OutlineConfig): void {
|
||||
if (!options) return;
|
||||
|
||||
if (typeof options.enable === 'boolean') {
|
||||
this.setEnabled(options.enable);
|
||||
}
|
||||
|
||||
if (options.color instanceof Color3) {
|
||||
this.color = options.color;
|
||||
} else if (typeof options.color === 'string') {
|
||||
this.color = Color3.FromHexString(options.color);
|
||||
}
|
||||
|
||||
if (typeof options.thickness === 'number') {
|
||||
this.width = options.thickness;
|
||||
} else if (typeof options.width === 'number') {
|
||||
this.width = options.width;
|
||||
}
|
||||
|
||||
if (typeof options.occlusionStrength === 'number') {
|
||||
this.occlusionStrength = options.occlusionStrength;
|
||||
}
|
||||
|
||||
if (typeof options.occlusionThreshold === 'number') {
|
||||
this.occlusionThreshold = options.occlusionThreshold;
|
||||
}
|
||||
|
||||
this.applyLayerConfig();
|
||||
this.rebuildLayerSelection(this.selectedMeshes);
|
||||
}
|
||||
|
||||
select(meshes: AbstractMesh | AbstractMesh[], additive = false): void {
|
||||
if (!this.enabled) return;
|
||||
this.init();
|
||||
|
||||
if (!additive) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
this.addGroup(this.normalizeMeshes(meshes));
|
||||
}
|
||||
|
||||
toggle(meshes: AbstractMesh | AbstractMesh[]): void {
|
||||
if (!this.enabled) return;
|
||||
this.init();
|
||||
|
||||
const targets = this.normalizeMeshes(meshes);
|
||||
if (targets.length && targets.every(mesh => this.isSelected(mesh))) {
|
||||
this.remove(targets);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addGroup(targets);
|
||||
}
|
||||
|
||||
remove(meshes: AbstractMesh | AbstractMesh[]): void {
|
||||
const targetIds = new Set(this.normalizeMeshes(meshes).map(mesh => mesh.uniqueId));
|
||||
this.selectedMeshes = this.selectedMeshes.filter(item => !targetIds.has(item.uniqueId));
|
||||
this.rebuildLayerSelection(this.selectedMeshes);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.outlineLayer?.clearSelection();
|
||||
this.selectedMeshes = [];
|
||||
}
|
||||
|
||||
getSelection(): AbstractMesh[] {
|
||||
return [...this.selectedMeshes];
|
||||
}
|
||||
|
||||
private addGroup(meshes: AbstractMesh[]): void {
|
||||
const newMeshes = meshes.filter(mesh => !this.isSelected(mesh));
|
||||
if (!newMeshes.length) return;
|
||||
|
||||
this.selectedMeshes.push(...newMeshes);
|
||||
this.rebuildLayerSelection(this.selectedMeshes);
|
||||
}
|
||||
|
||||
private isSelected(mesh: AbstractMesh): boolean {
|
||||
return this.selectedMeshes.some(item => item.uniqueId === mesh.uniqueId);
|
||||
}
|
||||
|
||||
private normalizeMeshes(meshes: AbstractMesh | AbstractMesh[]): AbstractMesh[] {
|
||||
const input = Array.isArray(meshes) ? meshes : [meshes];
|
||||
const uniqueMeshes = new Map<number, AbstractMesh>();
|
||||
|
||||
input.forEach(mesh => {
|
||||
if (!mesh || mesh.isDisposed() || mesh.metadata?.type === 'hotspot') return;
|
||||
if (!mesh.isEnabled() || mesh.getTotalVertices() <= 0 || !mesh.material) return;
|
||||
uniqueMeshes.set(mesh.uniqueId, mesh);
|
||||
});
|
||||
|
||||
return [...uniqueMeshes.values()];
|
||||
}
|
||||
|
||||
private rebuildLayerSelection(meshes: AbstractMesh[]): void {
|
||||
this.outlineLayer?.clearSelection();
|
||||
meshes.forEach(mesh => this.outlineLayer?.addSelection(mesh));
|
||||
}
|
||||
|
||||
private applyLayerConfig(): void {
|
||||
if (!this.outlineLayer) return;
|
||||
|
||||
this.outlineLayer.outlineColor = this.color;
|
||||
this.outlineLayer.outlineThickness = this.width;
|
||||
this.outlineLayer.occlusionStrength = this.occlusionStrength;
|
||||
this.outlineLayer.occlusionThreshold = this.occlusionThreshold;
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ import { GameManager } from './GameManager';
|
||||
import { EventBridge } from '../event/bridge';
|
||||
import { AppHotspot } from './AppHotspot';
|
||||
import { AppDomTo3D } from './AppDomTo3D';
|
||||
import { AppSelectionOutline } from './AppSelectionOutline';
|
||||
import { AppPositionGizmo } from './AppPositionGizmo';
|
||||
|
||||
/**
|
||||
* 主应用类 - 3D场景的核心控制器
|
||||
@ -30,6 +32,8 @@ export class MainApp {
|
||||
appRay: AppRay;
|
||||
appHotspot: AppHotspot;
|
||||
appDomTo3D: AppDomTo3D;
|
||||
appSelectionOutline: AppSelectionOutline;
|
||||
appPositionGizmo: AppPositionGizmo;
|
||||
gameManager: GameManager;
|
||||
|
||||
|
||||
@ -43,6 +47,8 @@ export class MainApp {
|
||||
this.appRay = new AppRay(this);
|
||||
this.appHotspot = new AppHotspot(this);
|
||||
this.appDomTo3D = new AppDomTo3D(this);
|
||||
this.appSelectionOutline = new AppSelectionOutline(this);
|
||||
this.appPositionGizmo = new AppPositionGizmo(this);
|
||||
this.gameManager = new GameManager(this);
|
||||
|
||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||
@ -55,7 +61,11 @@ export class MainApp {
|
||||
loadAConfig(config: any): void {
|
||||
AppConfig.container = config.container;
|
||||
AppConfig.modelUrlList = config.modelUrlList || [];
|
||||
AppConfig.env = config.env;
|
||||
AppConfig.env = { ...AppConfig.env, ...(config.env || {}) };
|
||||
AppConfig.gizmo = { ...AppConfig.gizmo, ...(config.gizmo || {}) };
|
||||
AppConfig.outline = { ...AppConfig.outline, ...(config.outline || {}) };
|
||||
this.appPositionGizmo.configure(AppConfig.gizmo);
|
||||
this.appSelectionOutline.configure(AppConfig.outline);
|
||||
}
|
||||
|
||||
async loadModel(): Promise<void> {
|
||||
@ -73,6 +83,8 @@ export class MainApp {
|
||||
this.appLight.Awake();
|
||||
this.appEnv.Awake();
|
||||
this.appRay.Awake();
|
||||
this.appSelectionOutline.init();
|
||||
this.appPositionGizmo.Awake();
|
||||
this.appDomTo3D.init();
|
||||
this.appModel.initManagers();
|
||||
this.update();
|
||||
@ -93,6 +105,7 @@ export class MainApp {
|
||||
async dispose(): Promise<void> {
|
||||
this.appModel?.clean();
|
||||
this.appEnv?.clean();
|
||||
this.appPositionGizmo?.dispose();
|
||||
// this.appHotspot?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user