refactor: sync managers and section box actions
Wire section box scale/reverse/reset to clipping APIs and sync demo artifacts.
This commit is contained in:
@@ -1,64 +1,257 @@
|
||||
/**
|
||||
* @file construct-tree-manager-btn.ts
|
||||
* @description 构件树管理器 - 负责管理构件树按钮和对话框
|
||||
*
|
||||
* 功能概述:
|
||||
* 1. 在界面左上角显示构件树按钮
|
||||
* 2. 点击按钮打开构件树对话框,包含三个选项卡:楼层树、类型树、专业树
|
||||
* 3. 点击树节点时,如果节点有 ids,则高亮对应的 3D 模型构件
|
||||
*
|
||||
* 数据流:
|
||||
* 1. 从 3D 引擎获取原始树数据 (getLevelTreeData/getTypeTreeData/getMajorTreeData)
|
||||
* 2. 通过 transformTreeData 转换为 SDK 标准的 TreeNodeConfig 格式
|
||||
* 3. 创建 BimTree 组件渲染树结构
|
||||
* 4. 用户点击节点时,通过 onNodeSelect 回调触发模型高亮
|
||||
*/
|
||||
|
||||
import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type';
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { BimButtonGroup } from '../components/button-group';
|
||||
import { BimTree } from '../components/tree';
|
||||
import { TreeNodeConfig } from '../components/tree/types';
|
||||
import { TreeNodeConfig, TreeNodeCheckState } from '../components/tree/types';
|
||||
import type { BimTreeNode } from '../components/tree/tree-node';
|
||||
import { BimDialog } from '../components/dialog';
|
||||
import { BimTab } from '../components/tab';
|
||||
import { getIcon } from '../utils/icon-manager';
|
||||
|
||||
function transformTreeData(apiData: any[]): TreeNodeConfig[] {
|
||||
if (!apiData || apiData.length === 0) return [];
|
||||
// ============================================================================
|
||||
// 类型定义
|
||||
// ============================================================================
|
||||
|
||||
return apiData.map((model, modelIndex) => {
|
||||
const transformNode = (node: any, index: number): TreeNodeConfig => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
return {
|
||||
id: node.id || `node-${modelIndex}-${index}`,
|
||||
label: node.name || node.label || '未命名',
|
||||
expanded: false,
|
||||
clickAction: hasChildren ? 'expand' : 'select',
|
||||
children: hasChildren
|
||||
? node.children.map((child: any, childIndex: number) => transformNode(child, childIndex))
|
||||
: undefined,
|
||||
data: node
|
||||
};
|
||||
};
|
||||
|
||||
const hasChildren = model.children && model.children.length > 0;
|
||||
return {
|
||||
id: `model-${modelIndex}`,
|
||||
label: model.name || '模型',
|
||||
expanded: true,
|
||||
clickAction: 'expand',
|
||||
children: hasChildren
|
||||
? model.children.map((child: any, childIndex: number) => transformNode(child, childIndex))
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
/**
|
||||
* 3D 引擎返回的原始树节点数据结构
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* name: "标高 1",
|
||||
* id: null,
|
||||
* ids: ["350518", "350520", ...], // 构件 ID 数组,用于高亮模型
|
||||
* children: [...],
|
||||
* isLeaf: false
|
||||
* }
|
||||
*/
|
||||
interface EngineTreeNode {
|
||||
/** 节点显示名称 */
|
||||
name?: string;
|
||||
/** 节点 ID(可能为 null) */
|
||||
id?: string | null;
|
||||
/** 构件 ID 数组 - 用于调用 highlightModel 高亮模型 */
|
||||
ids?: string[] | null;
|
||||
/** 子节点列表 */
|
||||
children?: EngineTreeNode[] | null;
|
||||
/** 是否为叶子节点 */
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3D 引擎返回的原始模型数据结构(最外层)
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* name: "栈桥模型",
|
||||
* url: "https://xxx.com/models/xxx/", // 模型 URL,用于 highlightModel
|
||||
* children: [...]
|
||||
* }
|
||||
*/
|
||||
interface EngineModelData {
|
||||
/** 模型显示名称 */
|
||||
name?: string;
|
||||
/** 模型资源 URL - 调用 highlightModel 时需要此参数 */
|
||||
url: string;
|
||||
/** 子节点列表 */
|
||||
children?: EngineTreeNode[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的节点数据,包含模型 URL
|
||||
* 转换后存储在 TreeNodeConfig.data 中
|
||||
*/
|
||||
interface TransformedNodeData extends EngineTreeNode {
|
||||
/** 模型 URL - 从最外层 model.url 传递下来 */
|
||||
_modelUrl: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 工具函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 使用 SHA-256 算法对 ids 数组生成唯一哈希值
|
||||
*
|
||||
* 为什么需要 hash:
|
||||
* - 原始数据中 node.id 可能为 null
|
||||
* - ids 数组可能很长(几百个元素),不适合直接作为 ID
|
||||
* - 需要一个稳定且唯一的标识符用于树组件的 nodeMap
|
||||
*
|
||||
* @param ids - 构件 ID 数组
|
||||
* @returns 64 位十六进制哈希字符串
|
||||
*
|
||||
* @example
|
||||
* hashIds(["350518", "350520"]) // => "a1b2c3d4e5f6..."
|
||||
*/
|
||||
async function hashIds(ids: string[]): Promise<string> {
|
||||
const str = JSON.stringify(ids);
|
||||
const data = new TextEncoder().encode(str);
|
||||
const buf = await crypto.subtle.digest('SHA-256', data);
|
||||
return Array.from(new Uint8Array(buf))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集节点及其所有子孙节点的 ids
|
||||
* 用于父级节点操作时,获取其下所有叶子节点对应的构件 ids
|
||||
*/
|
||||
function collectAllIds(node: BimTreeNode): string[] {
|
||||
const result: string[] = [];
|
||||
const data = node.config.data as TransformedNodeData | undefined;
|
||||
if (data?.ids?.length) {
|
||||
result.push(...data.ids);
|
||||
}
|
||||
for (const child of node.children || []) {
|
||||
result.push(...collectAllIds(child));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 3D 引擎返回的原始树数据转换为 SDK 标准的 TreeNodeConfig 格式
|
||||
*
|
||||
* 转换过程:
|
||||
* 1. 遍历最外层的模型数组,提取每个模型的 url
|
||||
* 2. 递归转换每个节点,将 url 注入到所有子节点的 data 中
|
||||
* 3. 如果节点有 ids 数组,使用 SHA-256 生成唯一 ID
|
||||
*
|
||||
* 点击行为:
|
||||
* - 点击节点内容:触发 onNodeSelect(高亮+跳转)
|
||||
* - 点击箭头:展开/折叠子节点
|
||||
*
|
||||
* 数据结构转换示意:
|
||||
*
|
||||
* 输入:[{ name, url, children: [{ name, ids, children }] }]
|
||||
* 输出:[{ id, label, data: { ids, _modelUrl }, children }]
|
||||
*
|
||||
* @param apiData - 3D 引擎返回的原始树数据
|
||||
* @returns 转换后的 TreeNodeConfig 数组
|
||||
*/
|
||||
let nodeIdCounter = 0;
|
||||
|
||||
async function transformTreeData(apiData: EngineModelData[]): Promise<TreeNodeConfig[]> {
|
||||
if (!apiData || apiData.length === 0) return [];
|
||||
|
||||
/**
|
||||
* 递归转换单个节点
|
||||
* @param node - 原始节点数据
|
||||
* @param modelUrl - 从最外层传递的模型 URL
|
||||
*/
|
||||
const transformNode = async (node: EngineTreeNode, modelUrl: string): Promise<TreeNodeConfig> => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
// 生成节点 ID:优先使用 ids 哈希,其次使用原始 id,最后生成唯一 ID
|
||||
let id: string;
|
||||
if (node.ids?.length) {
|
||||
id = await hashIds(node.ids);
|
||||
} else if (node.id) {
|
||||
id = node.id;
|
||||
} else {
|
||||
id = `node_${++nodeIdCounter}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
label: node.name || '未命名',
|
||||
expanded: false,
|
||||
checked: true,
|
||||
children: hasChildren
|
||||
? await Promise.all(node.children!.map(child => transformNode(child, modelUrl)))
|
||||
: undefined,
|
||||
data: {
|
||||
...node,
|
||||
_modelUrl: modelUrl
|
||||
} as TransformedNodeData
|
||||
};
|
||||
};
|
||||
|
||||
// 遍历最外层模型数组
|
||||
return Promise.all(apiData.map(async (model) => {
|
||||
const hasChildren = model.children && model.children.length > 0;
|
||||
// ⭐ 提取模型 URL,将传递给所有子节点
|
||||
const modelUrl = model.url;
|
||||
|
||||
return {
|
||||
id: modelUrl,
|
||||
label: model.name || '模型',
|
||||
expanded: true,
|
||||
checked: true,
|
||||
children: hasChildren
|
||||
? await Promise.all(model.children!.map(child => transformNode(child, modelUrl)))
|
||||
: undefined,
|
||||
data: {
|
||||
_modelUrl: modelUrl
|
||||
} as TransformedNodeData
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 管理器类
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 构件树管理器
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理构件树按钮的生命周期(创建、显示/隐藏、销毁)
|
||||
* 2. 管理构件树对话框(打开、关闭、内容渲染)
|
||||
* 3. 处理树节点点击事件,调用 3D 引擎高亮模型
|
||||
*
|
||||
* 使用示例:
|
||||
* ```typescript
|
||||
* const manager = new ConstructTreeManagerBtn(container);
|
||||
* manager.openConstructTreeDialog(); // 打开构件树对话框
|
||||
* ```
|
||||
*/
|
||||
export class ConstructTreeManagerBtn extends BaseManager {
|
||||
/** 按钮组实例 */
|
||||
/** 按钮组实例 - 用于渲染构件树按钮 */
|
||||
private toolbar: BimButtonGroup | null = null;
|
||||
/** 按钮容器元素 */
|
||||
private toolbarContainer: HTMLElement | null = null;
|
||||
/** 主容器元素 */
|
||||
/** 主容器元素 - 按钮挂载的父容器 */
|
||||
private container: HTMLElement;
|
||||
/** 构件树对话框实例 */
|
||||
private dialog: BimDialog | null = null;
|
||||
|
||||
/**
|
||||
* 创建构件树管理器
|
||||
* @param container - 按钮挂载的容器元素
|
||||
*/
|
||||
constructor(container: HTMLElement) {
|
||||
super();
|
||||
this.container = container;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/** 初始化按钮 */
|
||||
/**
|
||||
* 初始化按钮
|
||||
* 创建按钮容器和按钮组,添加构件树按钮
|
||||
*/
|
||||
private init() {
|
||||
// 创建按钮容器
|
||||
this.toolbarContainer = document.createElement('div');
|
||||
this.toolbarContainer.id = 'bim-construct-tree';
|
||||
this.container.appendChild(this.toolbarContainer);
|
||||
|
||||
// 创建按钮组
|
||||
this.toolbar = new BimButtonGroup({
|
||||
container: this.toolbarContainer,
|
||||
showLabel: false,
|
||||
@@ -69,6 +262,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
});
|
||||
this.toolbar.init();
|
||||
this.toolbar.addGroup('construct-tree');
|
||||
|
||||
// 添加构件树按钮
|
||||
this.toolbar.addButton({
|
||||
id: 'construct-tree-btn',
|
||||
groupId: 'construct-tree',
|
||||
@@ -82,31 +277,103 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
this.toolbar.render();
|
||||
}
|
||||
|
||||
public openConstructTreeDialog() {
|
||||
/**
|
||||
* 打开构件树对话框
|
||||
*
|
||||
* 流程:
|
||||
* 1. 隐藏按钮组
|
||||
* 2. 从 3D 引擎获取三种树数据(楼层/类型/专业)
|
||||
* 3. 转换数据格式
|
||||
* 4. 创建三个 BimTree 实例
|
||||
* 5. 创建选项卡组件
|
||||
* 6. 创建对话框并显示
|
||||
*/
|
||||
public async openConstructTreeDialog() {
|
||||
// 隐藏按钮组,避免遮挡对话框
|
||||
this.setVisible(false);
|
||||
|
||||
// 从 3D 引擎获取原始树数据
|
||||
const levelTreeData = this.registry.engine3d?.getLevelTreeData() ?? [];
|
||||
const typeTreeData = this.registry.engine3d?.getTypeTreeData() ?? [];
|
||||
const majorTreeData = this.registry.engine3d?.getMajorTreeData() ?? [];
|
||||
|
||||
console.log('[ConstructTree] 构件树数据 (Level):', levelTreeData);
|
||||
console.log('[ConstructTree] 类型树数据 (Type):', typeTreeData);
|
||||
console.log('[ConstructTree] 专业树数据 (Major):', majorTreeData);
|
||||
// 调试日志:输出原始数据
|
||||
console.log('[ConstructTree] 原始数据 (Level):', levelTreeData);
|
||||
console.log('[ConstructTree] 原始数据 (Type):', typeTreeData);
|
||||
console.log('[ConstructTree] 原始数据 (Major):', majorTreeData);
|
||||
|
||||
const createTree = (data: any[]) => {
|
||||
/**
|
||||
* 创建树组件
|
||||
* @param data - 原始树数据
|
||||
* @param label - 调试用标签
|
||||
*/
|
||||
const createTree = async (data: any[], label: string) => {
|
||||
// 转换数据格式
|
||||
const transformedData = await transformTreeData(data);
|
||||
console.log(`[ConstructTree] 转换后数据 (${label}):`, transformedData);
|
||||
|
||||
const tree = new BimTree({
|
||||
data: transformTreeData(data),
|
||||
data: transformedData,
|
||||
checkable: true,
|
||||
indent: 0,
|
||||
enableSearch: true,
|
||||
checkStrictly: true,
|
||||
defaultExpandAll: true,
|
||||
|
||||
/**
|
||||
* 节点勾选回调 - 显示/隐藏模型构件
|
||||
* 勾选 → showModel,取消勾选 → hideModels
|
||||
*/
|
||||
onNodeCheck: (node) => {
|
||||
console.log('onNodeCheck', node);
|
||||
const nodeData = node.config.data as TransformedNodeData | undefined;
|
||||
if (!nodeData?._modelUrl) return;
|
||||
|
||||
const ids = nodeData.ids?.length
|
||||
? nodeData.ids
|
||||
: collectAllIds(node);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
const modelParam = [{
|
||||
url: nodeData._modelUrl,
|
||||
ids: ids.map(Number)
|
||||
}];
|
||||
|
||||
if (node.checkState === TreeNodeCheckState.Checked) {
|
||||
this.registry.engine3d?.showModel(modelParam);
|
||||
} else {
|
||||
this.registry.engine3d?.hideModels(modelParam);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 节点选中回调 - 高亮并跳转到模型构件
|
||||
*/
|
||||
onNodeSelect: (node) => {
|
||||
console.log('onNodeSelect', node);
|
||||
const nodeData = node.config.data as TransformedNodeData | undefined;
|
||||
if (!nodeData?._modelUrl) return;
|
||||
|
||||
const ids = nodeData.ids?.length
|
||||
? nodeData.ids
|
||||
: collectAllIds(node);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
const modelParam = [{
|
||||
url: nodeData._modelUrl,
|
||||
ids: ids.map(Number)
|
||||
}];
|
||||
|
||||
this.registry.engine3d?.unhighlightAllModels();
|
||||
this.registry.engine3d?.highlightModel(modelParam);
|
||||
this.registry.engine3d?.viewScaleToModel(modelParam);
|
||||
},
|
||||
|
||||
// 再次点击已选中节点时取消高亮
|
||||
onNodeDeselect: () => {
|
||||
this.registry.engine3d?.unhighlightAllModels();
|
||||
},
|
||||
|
||||
onNodeExpand: () => {
|
||||
this.dialog?.fitWidth();
|
||||
},
|
||||
@@ -115,10 +382,12 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
return tree;
|
||||
};
|
||||
|
||||
const componentTree = createTree(levelTreeData);
|
||||
const typeTree = createTree(typeTreeData);
|
||||
const majorTree = createTree(majorTreeData);
|
||||
// 创建三个树实例
|
||||
const componentTree = await createTree(levelTreeData, 'Level');
|
||||
const typeTree = await createTree(typeTreeData, 'Type');
|
||||
const majorTree = await createTree(majorTreeData, 'Major');
|
||||
|
||||
// 创建选项卡面板容器
|
||||
const componentPanel = document.createElement('div');
|
||||
componentPanel.className = 'construct-tab__panel-content';
|
||||
componentPanel.appendChild(componentTree.element);
|
||||
@@ -131,10 +400,23 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
majorPanel.className = 'construct-tab__panel-content';
|
||||
majorPanel.appendChild(majorTree.element);
|
||||
|
||||
// 创建选项卡组件
|
||||
const tabMount = document.createElement('div');
|
||||
tabMount.className = 'construct-tab__container';
|
||||
tabMount.style.height = '100%';
|
||||
tabMount.style.overflow = 'hidden';
|
||||
|
||||
/**
|
||||
* 重置所有树的勾选状态并显示所有模型
|
||||
* 在 Tab 切换和对话框初始化时调用
|
||||
*/
|
||||
const resetAllTrees = () => {
|
||||
this.registry.engine3d?.showAllModels();
|
||||
componentTree.checkAllNodes(true);
|
||||
typeTree.checkAllNodes(true);
|
||||
majorTree.checkAllNodes(true);
|
||||
};
|
||||
|
||||
const tab = new BimTab({
|
||||
container: tabMount,
|
||||
tabs: [
|
||||
@@ -144,11 +426,21 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
],
|
||||
activeId: 'component',
|
||||
onChange: () => {
|
||||
resetAllTrees();
|
||||
this.dialog?.fitWidth();
|
||||
}
|
||||
});
|
||||
tab.init();
|
||||
|
||||
// BimTab 初始化时不触发 onChange,需要手动调用
|
||||
resetAllTrees();
|
||||
|
||||
// 监听右键菜单"显示全部"事件
|
||||
const unsubscribeShowAll = this.registry.on('menu:show-all', () => {
|
||||
resetAllTrees();
|
||||
});
|
||||
|
||||
// 创建对话框
|
||||
this.dialog = this.registry.dialog!.create({
|
||||
title: 'constructTree.title',
|
||||
minWidth: 320,
|
||||
@@ -157,6 +449,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
position: { x: 20, y: 20 },
|
||||
resizable: false,
|
||||
onClose: () => {
|
||||
unsubscribeShowAll();
|
||||
tab.destroy();
|
||||
componentTree.destroy();
|
||||
typeTree.destroy();
|
||||
@@ -181,8 +474,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 添加按钮组
|
||||
* @param groupId 组 ID
|
||||
* @param beforeGroupId 插入位置
|
||||
* @param groupId - 组 ID
|
||||
* @param beforeGroupId - 插入位置
|
||||
*/
|
||||
public addGroup(groupId: string, beforeGroupId?: string) {
|
||||
this.toolbar?.addGroup(groupId, beforeGroupId);
|
||||
@@ -191,7 +484,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 添加按钮
|
||||
* @param config 按钮配置
|
||||
* @param config - 按钮配置
|
||||
*/
|
||||
public addButton(config: ButtonConfig) {
|
||||
this.toolbar?.addButton(config);
|
||||
@@ -200,8 +493,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 设置按钮可见性
|
||||
* @param id 按钮 ID
|
||||
* @param v 是否可见
|
||||
* @param id - 按钮 ID
|
||||
* @param v - 是否可见
|
||||
*/
|
||||
public setButtonVisibility(id: string, v: boolean) {
|
||||
this.toolbar?.updateButtonVisibility(id, v);
|
||||
@@ -209,7 +502,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 设置是否显示标签
|
||||
* @param show 是否显示
|
||||
* @param show - 是否显示
|
||||
*/
|
||||
public setShowLabel(show: boolean) {
|
||||
this.toolbar?.setShowLabel(show);
|
||||
@@ -217,7 +510,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 设置按钮组可见性
|
||||
* @param visible 是否可见
|
||||
* @param visible - 是否可见
|
||||
*/
|
||||
public setVisible(visible: boolean) {
|
||||
if (this.toolbarContainer) {
|
||||
@@ -227,7 +520,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 设置背景颜色
|
||||
* @param color 颜色值
|
||||
* @param color - 颜色值
|
||||
*/
|
||||
public setBackgroundColor(color: string) {
|
||||
this.toolbar?.setBackgroundColor(color);
|
||||
@@ -235,7 +528,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
|
||||
/**
|
||||
* 设置按钮组颜色
|
||||
* @param colors 颜色配置
|
||||
* @param colors - 颜色配置
|
||||
*/
|
||||
public setColors(colors: ButtonGroupColors) {
|
||||
this.toolbar?.setColors(colors);
|
||||
|
||||
@@ -1,41 +1,18 @@
|
||||
/**
|
||||
* 对话框管理器
|
||||
* 负责创建和管理所有对话框实例
|
||||
*/
|
||||
import { BimDialog } from '../components/dialog';
|
||||
import { BimInfoDialog } from '../components/dialog/bimInfoDialog';
|
||||
import type { DialogOptions } from '../components/dialog/index.type';
|
||||
import type { ThemeConfig } from '../themes/types';
|
||||
import { themeManager } from '../services/theme';
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
|
||||
/**
|
||||
* 对话框管理器
|
||||
* 统一管理对话框的创建、主题更新和销毁
|
||||
*/
|
||||
export class DialogManager extends BaseManager {
|
||||
/** 容器元素 */
|
||||
private container: HTMLElement;
|
||||
/** 活跃的对话框列表 */
|
||||
private activeDialogs: BimDialog[] = [];
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
super();
|
||||
this.container = container;
|
||||
|
||||
this.subscribe('ui:open-dialog', (payload) => {
|
||||
console.log('[DialogManager] Received open-dialog event:', payload);
|
||||
if (payload.id === 'info') {
|
||||
this.showInfoDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对话框
|
||||
* @param options 对话框配置选项
|
||||
* @returns 对话框实例
|
||||
*/
|
||||
public create(options: Omit<DialogOptions, 'container'>): BimDialog {
|
||||
const dialog = new BimDialog({
|
||||
container: this.container,
|
||||
@@ -51,15 +28,6 @@ export class DialogManager extends BaseManager {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/** 显示信息对话框 */
|
||||
public showInfoDialog() {
|
||||
new BimInfoDialog(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有对话框的主题
|
||||
* @param theme 主题配置
|
||||
*/
|
||||
public updateTheme(theme: ThemeConfig) {
|
||||
this.activeDialogs.forEach(dialog => {
|
||||
if (dialog.setTheme) {
|
||||
@@ -68,7 +36,6 @@ export class DialogManager extends BaseManager {
|
||||
});
|
||||
}
|
||||
|
||||
/** 销毁管理器和所有对话框 */
|
||||
public destroy() {
|
||||
this.activeDialogs.forEach(d => d.destroy());
|
||||
this.activeDialogs = [];
|
||||
|
||||
55
src/managers/engine-info-dialog-manager.ts
Normal file
55
src/managers/engine-info-dialog-manager.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { BaseDialogManager } from '../core/base-dialog-manager';
|
||||
import { t } from '../services/locale';
|
||||
import type { EngineInfo } from '../components/engine';
|
||||
|
||||
export class EngineInfoDialogManager extends BaseDialogManager {
|
||||
protected get dialogId() { return 'engine-info-dialog'; }
|
||||
protected get dialogTitle() { return 'info.dialogTitle'; }
|
||||
protected get dialogWidth() { return 280; }
|
||||
|
||||
public init(): void {}
|
||||
|
||||
protected getDialogPosition() {
|
||||
const container = this.registry.container;
|
||||
if (!container) return { x: 100, y: 100 };
|
||||
|
||||
const containerWidth = container.clientWidth;
|
||||
const containerHeight = container.clientHeight;
|
||||
|
||||
return {
|
||||
x: (containerWidth - this.dialogWidth) / 2,
|
||||
y: (containerHeight - 150) / 2
|
||||
};
|
||||
}
|
||||
|
||||
protected createContent(): HTMLElement {
|
||||
const info: EngineInfo | null = this.registry.engine3d?.getEngineInfo() ?? null;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'engine-info-content';
|
||||
content.style.cssText = 'padding: 16px;';
|
||||
|
||||
const createRow = (label: string, value: number | string) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px;';
|
||||
|
||||
const labelEl = document.createElement('span');
|
||||
labelEl.style.cssText = 'color: var(--bim-text-secondary, #94a3b8);';
|
||||
labelEl.textContent = label;
|
||||
|
||||
const valueEl = document.createElement('span');
|
||||
valueEl.style.cssText = 'color: var(--bim-text-primary, #fff); font-weight: 500;';
|
||||
valueEl.textContent = String(value);
|
||||
|
||||
row.appendChild(labelEl);
|
||||
row.appendChild(valueEl);
|
||||
return row;
|
||||
};
|
||||
|
||||
content.appendChild(createRow(t('info.meshCount'), info?.meshCount ?? '-'));
|
||||
content.appendChild(createRow(t('info.totalTriangles'), info?.totalTriangles ?? '-'));
|
||||
content.appendChild(createRow(t('info.totalVertices'), info?.totalVertices ?? '-'));
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,7 @@ export class EngineManager extends BaseManager {
|
||||
order: 8,
|
||||
onClick: () => {
|
||||
this.showAllModels();
|
||||
this.emit('menu:show-all', {});
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
@@ -400,6 +401,28 @@ export class EngineManager extends BaseManager {
|
||||
this.engineInstance.fitSectionBoxToModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 剖切盒适应(缩放到场景整体包围盒)
|
||||
* @remarks 对接底层 clipping.scaleBox()
|
||||
*/
|
||||
public scaleSectionBox(): void {
|
||||
if (!this.engineInstance) {
|
||||
return;
|
||||
}
|
||||
this.engineInstance.scaleSectionBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* 反向剖切
|
||||
* @remarks 对接底层 clipping.reverse()
|
||||
*/
|
||||
public reverseSection(): void {
|
||||
if (!this.engineInstance) {
|
||||
return;
|
||||
}
|
||||
this.engineInstance.reverseSection();
|
||||
}
|
||||
|
||||
/** 激活框选放大功能 */
|
||||
public activateZoomBox(): void {
|
||||
if (!this.engineInstance) {
|
||||
@@ -569,13 +592,35 @@ export class EngineManager extends BaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏指定模型
|
||||
* @param models 要隐藏的模型对象
|
||||
* 高亮指定模型构件
|
||||
*
|
||||
* @param models - 要高亮的模型数组,格式: [{ url: string, ids: string[] }]
|
||||
*
|
||||
* @example
|
||||
* manager.highlightModel([
|
||||
* { url: 'https://xxx/models/xxx/', ids: [350518, 350520] }
|
||||
* ]);
|
||||
*/
|
||||
public hideModels(models: any): void {
|
||||
public highlightModel(models: { url: string; ids: number[] }[]): void {
|
||||
this.engineInstance?.highlightModel(models);
|
||||
}
|
||||
|
||||
public unhighlightAllModels(): void {
|
||||
this.engineInstance?.unhighlightAllModels();
|
||||
}
|
||||
|
||||
public viewScaleToModel(models: { url: string; ids: number[] }[]): void {
|
||||
this.engineInstance?.viewScaleToModel(models);
|
||||
}
|
||||
|
||||
public hideModels(models: { url: string; ids: number[] }[]): void {
|
||||
this.engineInstance?.hideModels(models);
|
||||
}
|
||||
|
||||
public showModel(models: { url: string; ids: number[] }[]): void {
|
||||
this.engineInstance?.showModel(models);
|
||||
}
|
||||
|
||||
/**
|
||||
* 半透明指定模型
|
||||
* @param models 要半透明的模型对象
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* 地图对话框管理器
|
||||
* 负责管理地图/平面图对话框的显示和交互
|
||||
*/
|
||||
import { BaseDialogManager } from '../core/base-dialog-manager';
|
||||
import { MapPanel } from '../components/map-panel';
|
||||
|
||||
/**
|
||||
* 地图对话框管理器
|
||||
* 继承自 BaseDialogManager,提供地图面板的对话框管理功能
|
||||
*/
|
||||
export class MapDialogManager extends BaseDialogManager {
|
||||
/** 地图面板实例 */
|
||||
private panel: MapPanel | null = null;
|
||||
|
||||
/** 对话框唯一标识 */
|
||||
protected get dialogId() { return 'map-dialog'; }
|
||||
/** 对话框标题(国际化 key) */
|
||||
protected get dialogTitle() { return 'map.dialogTitle'; }
|
||||
/** 对话框宽度 */
|
||||
protected get dialogWidth() { return 300; }
|
||||
/** 对话框高度 */
|
||||
protected get dialogHeight(): number { return 400; }
|
||||
|
||||
/** 初始化 */
|
||||
public init(): void {}
|
||||
|
||||
/**
|
||||
* 获取对话框位置
|
||||
* 定位在容器左下角
|
||||
*/
|
||||
protected getDialogPosition() {
|
||||
const container = this.registry.container;
|
||||
if (!container) return { x: 20, y: 100 };
|
||||
|
||||
const paddingLeft = 20;
|
||||
const paddingBottom = 20;
|
||||
const containerHeight = container.clientHeight;
|
||||
|
||||
return {
|
||||
x: paddingLeft,
|
||||
y: containerHeight - this.dialogHeight - paddingBottom
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建对话框内容 */
|
||||
protected createContent(): HTMLElement {
|
||||
this.panel = new MapPanel();
|
||||
this.panel.init();
|
||||
return this.panel.element;
|
||||
}
|
||||
|
||||
/** 对话框创建后的回调 */
|
||||
protected onDialogCreated(): void {
|
||||
this.emit('map:opened', {});
|
||||
}
|
||||
|
||||
/** 对话框关闭时的回调 */
|
||||
protected onDialogClose(): void {
|
||||
this.emit('map:closed', {});
|
||||
}
|
||||
|
||||
/** 销毁前的清理 */
|
||||
protected onBeforeDestroy(): void {
|
||||
if (this.panel) {
|
||||
this.panel.destroy();
|
||||
this.panel = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 隐藏对话框 */
|
||||
public hide(): void {
|
||||
super.hide();
|
||||
this.emit('map:closed', {});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { BaseDialogManager } from '../core/base-dialog-manager';
|
||||
import { MeasurePanel } from '../components/measure-panel';
|
||||
import type { MeasureConfig, MeasureResult } from '../components/measure-panel/types';
|
||||
import { ENGINE_TYPE_TO_MODE, MEASURE_TYPES, type MeasureMode } from '../types/measure';
|
||||
import { MEASURE_TYPES, getModeBycallBackType, getValueType, type MeasureMode, type CallBackType } from '../types/measure';
|
||||
|
||||
interface EngineMeasureData {
|
||||
id: string;
|
||||
point1?: { x: number; y: number; z: number };
|
||||
point2?: { x: number; y: number; z: number };
|
||||
text: number;
|
||||
type: string;
|
||||
textX?: number;
|
||||
textY?: number;
|
||||
textZ?: number;
|
||||
type: CallBackType;
|
||||
isSelect: boolean;
|
||||
container: any;
|
||||
}
|
||||
@@ -80,20 +81,24 @@ export class MeasureDialogManager extends BaseDialogManager {
|
||||
const engine = this.registry.engine3d?.getEngine();
|
||||
if (engine?.events) {
|
||||
const handler = (data: EngineMeasureData) => {
|
||||
console.log('[MeasureDialogManager] 测量值变化:', data);
|
||||
console.log('[MeasureDialogManager] 测量值回调:', data);
|
||||
if (data && this.panel) {
|
||||
this.handleMeasureChanged(data);
|
||||
}
|
||||
};
|
||||
engine.events.on('measure-changed', handler);
|
||||
this.unsubscribeMeasureChanged = () => engine.events.off('measure-changed', handler);
|
||||
engine.events.on('measure-click', handler);
|
||||
this.unsubscribeMeasureChanged = () => {
|
||||
engine.events.off('measure-changed', handler);
|
||||
engine.events.on('measure-click', handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMeasureChanged(data: EngineMeasureData): void {
|
||||
if (!this.panel) return;
|
||||
|
||||
const targetMode = ENGINE_TYPE_TO_MODE[data.type];
|
||||
const targetMode = getModeBycallBackType(data.type);
|
||||
if (!targetMode) {
|
||||
console.warn('[MeasureDialogManager] 未知测量类型:', data.type);
|
||||
return;
|
||||
@@ -112,10 +117,12 @@ export class MeasureDialogManager extends BaseDialogManager {
|
||||
const config = MEASURE_TYPES[mode];
|
||||
const result: MeasureResult = {};
|
||||
|
||||
if (config.valueType === 'point' && data.point1) {
|
||||
result.xyz = { x: data.point1.x, y: data.point1.y, z: data.point1.z };
|
||||
if (getValueType(mode) === 'point') {
|
||||
if (data.textX !== undefined && data.textY !== undefined && data.textZ !== undefined) {
|
||||
result.xyz = { x: data.textX, y: data.textY, z: data.textZ };
|
||||
}
|
||||
} else {
|
||||
(result as any)[config.resultField] = data.text;
|
||||
(result as any)[config.callBackType] = data.text;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -57,14 +57,21 @@ export class SectionBoxDialogManager extends BaseDialogManager {
|
||||
}
|
||||
},
|
||||
onReverseToggle: (isReversed) => {
|
||||
// 底层暂不支持反向功能
|
||||
console.log('[SectionBoxDialogManager] 反向切换(底层暂不支持):', isReversed);
|
||||
console.log('[SectionBoxDialogManager] 反向切换:', isReversed);
|
||||
// 底层 reverse() 为“切换一次”,这里不使用 isReversed 作为入参,只要用户点击就触发。
|
||||
this.registry.engine3d?.reverseSection();
|
||||
},
|
||||
onFitToModel: () => {
|
||||
console.log('[SectionBoxDialogManager] Fit to model not supported in new API');
|
||||
// 对接底层 scaleBox():缩放剖切盒到场景整体包围盒
|
||||
this.registry.engine3d?.scaleSectionBox();
|
||||
},
|
||||
onReset: () => {
|
||||
console.log('[SectionBoxDialogManager] Reset not supported in new API');
|
||||
// 重置定义:关闭剖切再打开剖切盒。
|
||||
// UI 侧会自行将滑块强制恢复到 0-100,并将隐藏/反向按钮恢复为关闭状态。
|
||||
this.registry.engine3d?.deactivateSection();
|
||||
this.registry.engine3d?.activeSection('box');
|
||||
// 确保剖切可见(避免上一次处于隐藏状态导致“看起来没重置”)
|
||||
this.registry.engine3d?.recoverSection();
|
||||
},
|
||||
onRangeChange: (range) => {
|
||||
this.registry.engine3d?.setSectionBoxRange(range);
|
||||
|
||||
@@ -41,12 +41,8 @@ export class WalkControlManager extends BaseManager {
|
||||
|
||||
this.panel = new WalkControlPanel({
|
||||
onPlanViewToggle: (isActive) => {
|
||||
console.log('[WalkControl] 地图:', isActive);
|
||||
if (isActive) {
|
||||
this.registry.map?.show();
|
||||
} else {
|
||||
this.registry.map?.hide();
|
||||
}
|
||||
console.log('[WalkControl] 小地图:', isActive);
|
||||
this.registry.engine3d?.toggleMiniMap();
|
||||
this.emit('walk:plan-view-toggle', { isActive });
|
||||
},
|
||||
onPathModeToggle: (isActive) => {
|
||||
@@ -94,17 +90,7 @@ export class WalkControlManager extends BaseManager {
|
||||
});
|
||||
this.panel.init();
|
||||
|
||||
if (this.registry.map?.isOpen()) {
|
||||
this.panel.setPlanViewActive(true);
|
||||
}
|
||||
|
||||
this.subscribe('map:opened', () => {
|
||||
this.panel?.setPlanViewActive(true);
|
||||
});
|
||||
|
||||
this.subscribe('map:closed', () => {
|
||||
this.panel?.setPlanViewActive(false);
|
||||
});
|
||||
|
||||
if (this.registry.container) {
|
||||
this.panel.element.style.position = 'absolute';
|
||||
|
||||
@@ -20,26 +20,26 @@ export class WalkPathDialogManager extends BaseDialogManager {
|
||||
/** 对话框宽度 */
|
||||
protected get dialogWidth() { return 300; }
|
||||
/** 对话框高度 */
|
||||
protected get dialogHeight(): number { return 400; }
|
||||
protected get dialogHeight(): number { return 450; }
|
||||
|
||||
/** 初始化 */
|
||||
public init(): void {}
|
||||
|
||||
/**
|
||||
* 获取对话框位置
|
||||
* 定位在容器右侧居中
|
||||
* 定位在容器右上角
|
||||
*/
|
||||
protected getDialogPosition() {
|
||||
const container = this.registry.container;
|
||||
if (!container) return { x: 100, y: 100 };
|
||||
|
||||
const paddingRight = 20;
|
||||
const paddingTop = 20;
|
||||
const containerWidth = container.clientWidth;
|
||||
const containerHeight = container.clientHeight;
|
||||
|
||||
return {
|
||||
x: containerWidth - this.dialogWidth - paddingRight,
|
||||
y: (containerHeight - this.dialogHeight) / 2
|
||||
y: paddingTop
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user