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:
yuding
2026-02-04 18:20:30 +08:00
parent b12940f49c
commit 191c571f40
64 changed files with 10569 additions and 7065 deletions

View File

@@ -11,7 +11,7 @@ import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manag
import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager';
import { SectionBoxDialogManager } from './managers/section-box-dialog-manager';
import { WalkControlManager } from './managers/walk-control-manager';
import { MapDialogManager } from './managers/map-dialog-manager';
import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
import { ComponentDetailManager } from './managers/component-detail-manager';
import { AiChatManager } from './managers/ai-chat-manager';
import type { EngineOptions, ModelLoadOptions } from './components/engine';
@@ -41,7 +41,7 @@ export class BimEngine {
public sectionAxis: SectionAxisDialogManager | null = null;
public sectionBox: SectionBoxDialogManager | null = null;
public walkControl: WalkControlManager | null = null;
public map: MapDialogManager | null = null;
public engineInfo: EngineInfoDialogManager | null = null;
public componentDetail: ComponentDetailManager | null = null;
public aiChat: AiChatManager | null = null;
@@ -116,8 +116,8 @@ export class BimEngine {
this.sectionBox = new SectionBoxDialogManager();
this.walkControl = new WalkControlManager();
this.walkControl.init();
this.map = new MapDialogManager();
this.map.init();
this.engineInfo = new EngineInfoDialogManager();
this.engineInfo.init();
this.registry.engine3d = this.engine;
this.registry.dialog = this.dialog;
@@ -131,7 +131,7 @@ export class BimEngine {
this.registry.sectionAxis = this.sectionAxis;
this.registry.sectionBox = this.sectionBox;
this.registry.walkControl = this.walkControl;
this.registry.map = this.map;
this.registry.engineInfo = this.engineInfo;
this.componentDetail = new ComponentDetailManager();
this.registry.componentDetail = this.componentDetail;

View File

@@ -3,6 +3,8 @@ import { getIcon } from '../../../../../utils/icon-manager';
import { ManagerRegistry } from '../../../../../core/manager-registry';
export const createInfoButton = (): ButtonConfig => {
const registry = ManagerRegistry.getInstance();
return {
id: 'info',
groupId: 'group-2',
@@ -11,8 +13,7 @@ export const createInfoButton = (): ButtonConfig => {
icon: getIcon('信息'),
keepActive: false,
onClick: () => {
const registry = ManagerRegistry.getInstance();
registry.emit('ui:open-dialog', { id: 'info' });
registry.engineInfo?.show();
}
};
};

View File

@@ -5,14 +5,6 @@ import { ManagerRegistry } from '../../../../../core/manager-registry';
export const createMapButton = (): ButtonConfig => {
const registry = ManagerRegistry.getInstance();
registry.on('map:opened', () => {
registry.toolbar?.setBtnActive('map', true);
});
registry.on('map:closed', () => {
registry.toolbar?.setBtnActive('map', false);
});
return {
id: 'map',
groupId: 'group-1',
@@ -22,11 +14,7 @@ export const createMapButton = (): ButtonConfig => {
keepActive: true,
icon: getIcon('地图'),
onClick: () => {
if (registry.map?.isOpen()) {
registry.map?.hide();
} else {
registry.map?.show();
}
registry.engine3d?.toggleMiniMap();
}
};
};

View File

@@ -1,30 +0,0 @@
.bim-info-dialog-content {
padding: 16px;
font-family: sans-serif;
color: #333;
}
.bim-info-dialog-content h3 {
margin-top: 0;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
color: #0078d4;
}
.bim-info-dialog-content ul {
list-style: none;
padding: 0;
margin: 0;
}
.bim-info-dialog-content li {
margin-bottom: 8px;
font-size: 14px;
display: flex;
}
.bim-info-dialog-content li strong {
width: 80px;
color: #555;
}

View File

@@ -1,65 +0,0 @@
import './index.css';
import { BimDialog } from '../index';
/**
* BimInfoDialog (继承版)
* 这是一个展示项目信息的业务弹窗组件,直接继承自 BimDialog。
*/
export class BimInfoDialog extends BimDialog {
/**
* 构造函数
* @param container 父容器
*/
constructor(container: HTMLElement) {
// 1. 准备内容 DOM
const contentEl = document.createElement('div');
contentEl.className = 'bim-info-dialog-content';
const infoTitle = document.createElement('h3');
infoTitle.textContent = 'Model Information';
const infoList = document.createElement('ul');
infoList.innerHTML = `
<li><strong>Name:</strong> Sample Project</li>
<li><strong>Version:</strong> 1.0.0</li>
<li><strong>Date:</strong> ${new Date().toLocaleDateString()}</li>
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
`;
const actionBtn = document.createElement('button');
actionBtn.textContent = 'Update Status';
actionBtn.style.marginTop = '10px';
actionBtn.onclick = () => {
alert('Status updated!');
};
contentEl.appendChild(infoTitle);
contentEl.appendChild(infoList);
contentEl.appendChild(actionBtn);
// 2. 调用父类构造函数,传入特定的配置
super({
container: container,
title: 'dialog.testTitle',
content: contentEl,
width: 320,
height: 'auto',
position: 'center',
resizable: true,
draggable: true,
// 可以在这里添加特定的 onClose 逻辑
onClose: () => {
console.log('Info dialog closed');
},
onOpen: () => {
console.log('Info dialog opened');
}
});
// 3. 如果有特定于子类的初始化逻辑,可以在 super() 之后执行
// 例如this.element.classList.add('my-special-class');
}
// 不需要再手动实现 setTheme, destroy, close, init
// 它们都已从 BimDialog 继承
}

View File

@@ -513,6 +513,32 @@ export class Engine implements IBimComponent {
}
}
/**
* 剖切盒适应(缩放到场景整体包围盒)
* @remarks
* - 对接底层 `engine.clipping.scaleBox()`
* - 会确保当前处于剖切盒模式
*/
public scaleSectionBox(): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot scale section box: engine not initialized.');
return;
}
this.engine.clipping.scaleBox();
}
/**
* 反向剖切
* @remarks 对接底层 `engine.clipping.reverse()`
*/
public reverseSection(): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot reverse section: engine not initialized.');
return;
}
this.engine.clipping.reverse();
}
// ==================== 结束:剖切功能 ====================
// ==================== 漫游功能 ====================
@@ -845,17 +871,60 @@ export class Engine implements IBimComponent {
}
/**
* 隐藏指定模型
* @param models 要隐藏的模型对象
* 高亮指定模型构件
*
* 用于在 3D 场景中高亮显示指定的构件,常用于:
* - 点击构件树节点时高亮对应模型
* - 搜索结果定位
* - 批量选中构件
*
* @param models - 要高亮的模型数组,每个元素包含:
* - url: 模型资源 URL
* - ids: 构件 ID 数组
*
* @example
* engine.highlightModel([
* { url: 'https://xxx/models/xxx/', ids: [350518, 350520] }
* ]);
*/
public hideModels(models: any): void {
public highlightModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot highlight model: engine not initialized.');
return;
}
this.engine.modelToolModule.highlightModel(models);
}
public unhighlightAllModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot unhighlight models: engine not initialized.');
return;
}
this.engine.modelToolModule.unhighlightAllModels();
}
public viewScaleToModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot view scale to model: engine not initialized.');
return;
}
this.engine.modelToolModule.viewScaleToModel(models);
}
public hideModels(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot hide models: engine not initialized.');
return;
}
if (models) {
this.engine.modelToolModule.hideModel(models);
this.engine.modelToolModule.hideModel(models);
}
public showModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot show model: engine not initialized.');
return;
}
this.engine.modelToolModule.showModel(models);
}
/**
@@ -1004,4 +1073,3 @@ export class Engine implements IBimComponent {
}
}

View File

@@ -15,10 +15,12 @@ export interface EngineOptions {
showViewCube?: boolean;
}
/**
* 模型加载选项
* 用于配置模型的位置、旋转和缩放
*/
export interface EngineInfo {
totalVertices: number;
totalTriangles: number;
meshCount: number;
}
export interface ModelLoadOptions {
/** 模型初始位置 [x, y, z] */
position?: [number, number, number];

View File

@@ -1,49 +0,0 @@
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager } from '../../services/locale';
import { themeManager } from '../../services/theme';
/**
* 地图面板组件
*/
export class MapPanel implements IBimComponent {
public element!: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor() {}
public init(): void {
this.element = this.createPanel();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'map-panel';
panel.style.padding = '20px';
panel.style.color = '#fff';
panel.textContent = '地图内容待实现';
return panel;
}
public setLocales(): void {
// 更新文本
}
public setTheme(_theme: ThemeConfig): void {
// 应用主题
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -4,7 +4,7 @@ import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { MeasureConfig, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
import { MEASURE_TYPES, MEASURE_MODES_ORDERED, type MeasureMode } from '../../types/measure';
import { MEASURE_TYPES, MEASURE_MODES_ORDERED, getValueType, type MeasureMode, type MeasureValueType } from '../../types/measure';
/**
* 测量面板组件(只做 UI不实现真实测量
@@ -55,6 +55,7 @@ export class MeasurePanel implements IBimComponent {
private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map();
private toggleBtn!: HTMLButtonElement;
private toggleTextEl!: HTMLElement;
private mainValueRowEl!: HTMLElement;
private mainValueValueEl!: HTMLElement;
private mainValueLabelEl!: HTMLElement;
private mainNumberEl!: HTMLElement;
@@ -428,6 +429,7 @@ export class MeasurePanel implements IBimComponent {
// 主结果值(随模式变化)
const mainValueRow = document.createElement('div');
mainValueRow.className = 'bim-measure-row';
this.mainValueRowEl = mainValueRow;
const mainValueLabel = document.createElement('span');
mainValueLabel.className = 'label';
this.mainValueLabelEl = mainValueLabel;
@@ -764,7 +766,7 @@ export class MeasurePanel implements IBimComponent {
private renderResult(): void {
// 1) 根据模式决定结果区显示规则
// 你给的规则:
// - 距离:显示数值 + xyz
// - 距离:显示数值
// - 最小距离:只显示数值
// - 角度:--°
// - 标高:--m固定 m
@@ -773,14 +775,10 @@ export class MeasurePanel implements IBimComponent {
// - 坡度:--%
// - 空间体积:--mm³单位随设置变动即 unit³
this.mainValueLabelEl.style.display = '';
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const parts = this.formatMainValueParts(this.activeMode, this.result);
this.mainNumberEl.textContent = parts.numberText;
this.mainUnitEl.textContent = parts.unitText;
const showXyz = this.activeMode === 'distance' || this.activeMode === 'point';
if (showXyz) {
const isPointMode = this.activeMode === 'point';
if (isPointMode) {
this.mainValueRowEl.style.display = 'none';
this.xyzBoxEl.style.display = '';
const xyz = this.result?.xyz;
if (!xyz) {
@@ -789,12 +787,24 @@ export class MeasurePanel implements IBimComponent {
this.xyzZEl.textContent = '--';
return;
}
this.xyzXEl.textContent = this.formatNumberWithPrecision(xyz.x, this.config.precision);
this.xyzYEl.textContent = this.formatNumberWithPrecision(xyz.y, this.config.precision);
this.xyzZEl.textContent = this.formatNumberWithPrecision(xyz.z, this.config.precision);
this.xyzXEl.textContent = this.convertValue('length', xyz.x) + ' ' + this.getUnitText('length');
this.xyzYEl.textContent = this.convertValue('length', xyz.y) + ' ' + this.getUnitText('length');
this.xyzZEl.textContent = this.convertValue('length', xyz.z) + ' ' + this.getUnitText('length');
return;
}
this.mainValueRowEl.style.display = '';
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const value = this.result ? (this.result as any)[MEASURE_TYPES[this.activeMode].callBackType] : undefined;
if (this.activeMode === 'slope'||this.activeMode === 'angle') {
this.mainNumberEl.textContent = value ?? '--';
this.mainUnitEl.textContent = '';
} else {
const valueType = getValueType(this.activeMode);
this.mainNumberEl.textContent = this.convertValue(valueType, value);
this.mainUnitEl.textContent = this.getUnitText(valueType);
}
this.xyzBoxEl.style.display = 'none';
}
@@ -809,99 +819,69 @@ export class MeasurePanel implements IBimComponent {
return `measure.labels.value.${mode}`;
}
// 注意:旧的 formatMainValue/formatWithFixedUnit 已被 formatMainValueParts 替代,
// 以支持“数值与单位分色显示”和“无数据时仍展示单位”。
/**
* 统一的数值转换方法
* @param type 测量值类型length(长度)、area(面积)、angle(角度)、percent(百分比)、point(坐标)
* @param value 原始数值(单位:长度为米,面积为平方米)
* @returns 转换后的格式化字符串,无效值返回 '--'
*/
private convertValue(type: MeasureValueType, value: number | undefined | null): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return '--';
}
const unit = this.config.unit;
const precision = this.config.precision;
let converted: number;
switch (type) {
case 'length':
switch (unit) {
case 'mm': converted = value * 1000; break;
case 'cm': converted = value * 100; break;
case 'km': converted = value / 1000; break;
default: converted = value;
}
break;
case 'area':
switch (unit) {
case 'mm': converted = value * 1000 * 1000; break;
case 'cm': converted = value * 100 * 100; break;
case 'km': converted = value / 1000 / 1000; break;
default: converted = value;
}
break;
case 'angle':
case 'percent':
case 'point':
default:
converted = value;
}
return converted.toFixed(precision);
}
/**
* 基础数字格式化(按精度显示)
* 获取单位文本
* @param type 测量值类型
* @returns 对应的单位文本(如 mm、m²、°
*/
private formatNumberWithPrecision(value: number, precision: MeasurePrecision): string {
// 你要求精度可选0 / 0.0 / 0.00 / 0.000,因此这里不做 trim严格按 toFixed 输出
return value.toFixed(precision);
}
// 注意:旧的 formatLengthWithConfig 已被 formatLengthParts 替代。
private getUnitI18nKey(unit: MeasureUnit): string {
return `measure.units.${unit}`;
}
private formatMainValueParts(mode: MeasureMode, result: MeasureResult | null): { numberText: string; unitText: string } {
if (!result) {
return this.getEmptyValuePartsByMode(mode);
}
const config = MEASURE_TYPES[mode];
const value = (result as any)[config.resultField];
switch (config.valueType) {
case 'length':
case 'area':
return this.formatMeasureValue(value, config.valueType);
case 'angle':
return this.formatFixedUnitParts(value, t('measure.units.deg'));
case 'percent':
return this.formatFixedUnitParts(value, t('measure.units.percent'));
case 'point':
return { numberText: '--', unitText: '' };
default:
return { numberText: '--', unitText: '' };
}
}
private getEmptyValuePartsByMode(mode: MeasureMode): { numberText: string; unitText: string } {
const config = MEASURE_TYPES[mode];
switch (config.valueType) {
case 'length':
return { numberText: '--', unitText: t(this.getUnitI18nKey(this.config.unit)) };
case 'area':
return { numberText: '--', unitText: `${this.config.unit}²` };
case 'angle':
return { numberText: '--', unitText: t('measure.units.deg') };
case 'percent':
return { numberText: '--', unitText: t('measure.units.percent') };
case 'point':
return { numberText: '--', unitText: '' };
default:
return { numberText: '--', unitText: '' };
}
}
private formatFixedUnitParts(value: number | undefined, unitText: string): { numberText: string; unitText: string } {
if (value === null || value === undefined || Number.isNaN(value)) {
return { numberText: '--', unitText };
}
return { numberText: this.formatNumberWithPrecision(value, this.config.precision), unitText };
}
private formatMeasureValue(value: number | undefined, type: 'length' | 'area'): { numberText: string; unitText: string } {
private getUnitText(type: MeasureValueType): string {
const unit = this.config.unit;
const unitText = type === 'area' ? `${unit}²` : t(this.getUnitI18nKey(unit));
if (value === null || value === undefined || Number.isNaN(value)) {
return { numberText: '--', unitText };
switch (type) {
case 'length':
case 'point':
return unit;
case 'area':
return `${unit}²`;
case 'angle':
case 'percent':
return '°';
default:
return '';
}
let converted: number;
if (type === 'length') {
switch (unit) {
case 'mm': converted = value * 1000; break;
case 'cm': converted = value * 100; break;
case 'km': converted = value / 1000; break;
default: converted = value;
}
} else {
switch (unit) {
case 'mm': converted = value * 1000 * 1000; break;
case 'cm': converted = value * 100 * 100; break;
case 'km': converted = value / 1000 / 1000; break;
default: converted = value;
}
}
return { numberText: converted.toFixed(this.config.precision), unitText };
}
}

View File

@@ -7,9 +7,8 @@ export const fourMenuButton = (): MenuItemConfig => {
label: 'menu.info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog();
registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide();
}
};

View File

@@ -12,7 +12,7 @@ export const homeMenuButton = (): MenuItemConfig => {
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog();
registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide();
}
};

View File

@@ -8,9 +8,8 @@ export const infoMenuButton = (): MenuItemConfig => {
group: 'info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog();
registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide();
}
};

View File

@@ -7,9 +7,8 @@ export const secondMenuButton = (): MenuItemConfig => {
label: 'menu.info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog();
registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide();
}
};

View File

@@ -31,6 +31,7 @@ export class BimTree implements IBimComponent {
// 事件回调 (由 Manager 注入)
public onNodeCheck?: (node: BimTreeNode) => void;
public onNodeSelect?: (node: BimTreeNode) => void;
public onNodeDeselect?: (node: BimTreeNode) => void;
public onNodeExpand?: (node: BimTreeNode) => void;
constructor(options: TreeOptions) {
@@ -61,6 +62,7 @@ export class BimTree implements IBimComponent {
// 初始化回调
if (options.onNodeCheck) this.onNodeCheck = options.onNodeCheck;
if (options.onNodeSelect) this.onNodeSelect = options.onNodeSelect;
if (options.onNodeDeselect) this.onNodeDeselect = options.onNodeDeselect;
if (options.onNodeExpand) this.onNodeExpand = options.onNodeExpand;
}
@@ -333,18 +335,26 @@ export class BimTree implements IBimComponent {
/**
* 处理节点选择 (高亮)
* 点击已选中节点时切换为取消选中
*/
private handleNodeSelect(node: BimTreeNode) {
// 如果之前有选中的,先取消选中
if (this.selectedNode && this.selectedNode !== node) {
// 再次点击已选中的节点 → 取消选中
if (this.selectedNode === node) {
node.setSelected(false);
this.selectedNode = null;
if (this.onNodeDeselect) this.onNodeDeselect(node);
return;
}
// 取消之前选中的节点
if (this.selectedNode) {
this.selectedNode.setSelected(false);
}
// 设置当前为选中
// 选中当前节点
node.setSelected(true);
this.selectedNode = node;
// 触发外部回调
if (this.onNodeSelect) this.onNodeSelect(node);
}
@@ -421,6 +431,11 @@ export class BimTree implements IBimComponent {
this.nodeMap.forEach(node => node.toggleExpand(expanded));
}
public checkAllNodes(checked: boolean): void {
const state = checked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked;
this.nodeMap.forEach(node => node.setChecked(state, false, true));
}
public getCheckedNodes(includeHalfChecked: boolean = false): TreeNodeConfig[] {
const result: TreeNodeConfig[] = [];
this.nodeMap.forEach(node => {

View File

@@ -208,13 +208,8 @@ export class BimTreeNode {
this.setChecked(newChecked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true);
}
/**
* 设置选中状态 (API调用或联动)
* @param state 新状态
* @param fireEvent 是否触发事件
*/
public setChecked(state: TreeNodeCheckState, fireEvent: boolean = false) {
if (this.checkState === state) return;
public setChecked(state: TreeNodeCheckState, fireEvent: boolean = false, force: boolean = false) {
if (!force && this.checkState === state) return;
this.checkState = state;
this.config.checked = (state === TreeNodeCheckState.Checked);

View File

@@ -84,6 +84,9 @@ export interface TreeOptions {
/** 节点选择回调 */
onNodeSelect?: (node: BimTreeNode) => void;
/** 节点取消选择回调(再次点击已选中节点时触发) */
onNodeDeselect?: (node: BimTreeNode) => void;
/** 节点展开/折叠回调 */
onNodeExpand?: (node: BimTreeNode) => void;

View File

@@ -24,7 +24,7 @@ export class WalkControlPanel implements IBimComponent {
// DOM 引用 - 左侧按钮
private planViewBtn!: HTMLButtonElement;
private pathModeBtn!: HTMLButtonElement;
private walkModeBtn!: HTMLButtonElement;
// private walkModeBtn!: HTMLButtonElement;
// DOM 引用 - 中间设置区
private settingsContainer!: HTMLElement;
@@ -138,15 +138,15 @@ export class WalkControlPanel implements IBimComponent {
this.options.onPathModeToggle?.(newMode === 'path');
});
this.walkModeBtn = this.createIconButton('walk', () => {
const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk';
this.setMode(newMode);
this.options.onWalkModeToggle?.(newMode === 'walk');
});
// this.walkModeBtn = this.createIconButton('walk', () => {
// const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk';
// this.setMode(newMode);
// this.options.onWalkModeToggle?.(newMode === 'walk');
// });
container.appendChild(this.planViewBtn);
container.appendChild(this.pathModeBtn);
container.appendChild(this.walkModeBtn);
// container.appendChild(this.walkModeBtn);
return container;
}
@@ -339,7 +339,7 @@ export class WalkControlPanel implements IBimComponent {
this.pathModeBtn.classList.toggle('active', this.state.mode === 'path');
// 漫游按钮
this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk');
// this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk');
}
private updateSettingsView(): void {
@@ -434,7 +434,7 @@ export class WalkControlPanel implements IBimComponent {
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-bg-base', theme.bgBase ?? '#152232');
style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
style.setProperty('--bim-bg-inset', theme.bgInset ?? '#152232');
@@ -443,16 +443,16 @@ export class WalkControlPanel implements IBimComponent {
style.setProperty('--bim-border-subtle', theme.borderSubtle ?? '#1e293b');
style.setProperty('--bim-divider', theme.divider ?? '#334155');
style.setProperty('--bim-shadow-xl', theme.shadowXl ?? '0 20px 40px rgba(0, 0, 0, 0.3)');
style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
style.setProperty('--bim-primary-hover', theme.primaryHover ?? '#60a5fa');
style.setProperty('--bim-primary-subtle', theme.primarySubtle ?? 'rgba(59, 130, 246, 0.15)');
style.setProperty('--bim-text-primary', theme.textPrimary ?? '#ffffff');
style.setProperty('--bim-text-inverse', theme.textInverse ?? '#152232');
style.setProperty('--bim-icon-default', theme.iconDefault ?? '#ffffff');
style.setProperty('--bim-component-bg-hover', theme.componentBgHover ?? 'rgba(248, 250, 252, 0.06)');
style.setProperty('--bim-component-bg-active', theme.componentBgActive ?? 'rgba(248, 250, 252, 0.1)');
}

View File

@@ -0,0 +1,253 @@
/* 路径漫游面板根容器 */
.walk-path-panel {
padding: 16px;
box-sizing: border-box;
}
/* ==================== 按钮样式 ==================== */
/* 按钮通用样式 */
.walk-path-btn {
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
box-sizing: border-box;
}
/* 按钮组 */
.walk-path-btn-group {
display: flex;
gap: 8px;
margin-top: 16px;
}
/* 播放按钮 */
.walk-path-btn-play {
flex: 1;
height: 32px;
padding: 0 16px;
background: var(--bim-primary, #3b82f6);
color: #fff;
}
.walk-path-btn-play:hover:not(:disabled) {
opacity: 0.9;
}
.walk-path-btn-play:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 停止按钮 */
.walk-path-btn-stop {
flex: 1;
height: 32px;
padding: 0 16px;
background: #ef4444;
color: #fff;
}
.walk-path-btn-stop:hover:not(:disabled) {
opacity: 0.9;
}
.walk-path-btn-stop:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 小按钮 */
.walk-path-btn-small {
height: 28px;
padding: 0 12px;
font-size: 12px;
background: var(--bim-bg-elevated, #1f2d3e);
color: var(--bim-text-primary, #fff);
border: 1px solid var(--bim-border-default, #334155);
}
.walk-path-btn-small:hover {
background: var(--bim-border-default, #334155);
}
/* 危险按钮(删除) */
.walk-path-btn-danger {
color: #ef4444;
}
/* ==================== 表单样式 ==================== */
/* 设置区域 */
.walk-path-settings {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
/* 表单组 */
.walk-path-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.walk-path-form-group label {
font-size: 12px;
color: var(--bim-text-secondary, #94a3b8);
}
/* 行内表单组(复选框) */
.walk-path-form-group-inline {
flex-direction: row;
align-items: center;
}
/* 输入框 */
.walk-path-input {
padding: 8px 12px;
border: 1px solid var(--bim-border-default, #334155);
border-radius: 4px;
background: var(--bim-bg-elevated, #1f2d3e);
color: var(--bim-text-primary, #fff);
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.walk-path-input:focus {
outline: none;
border-color: var(--bim-primary, #3b82f6);
}
/* 输入框包装器(带单位) */
.walk-path-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.walk-path-input-wrapper .walk-path-input {
flex: 1;
}
/* 单位文本 */
.walk-path-unit {
color: var(--bim-text-secondary, #94a3b8);
font-size: 14px;
}
/* 复选框 */
.walk-path-checkbox {
width: 16px;
height: 16px;
margin-right: 8px;
}
/* ==================== 漫游点区域 ==================== */
/* 漫游点区域容器 */
.walk-path-points-section {
margin-bottom: 16px;
}
/* 操作栏 */
.walk-path-points-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
/* 漫游点列表 */
.walk-path-points-list {
height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
/* 空状态提示 */
.walk-path-empty {
text-align: center;
padding: 32px 16px;
color: var(--bim-text-secondary, #94a3b8);
font-size: 14px;
}
/* ==================== 漫游点项 ==================== */
/* 漫游点项容器 */
.walk-path-point-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: var(--bim-bg-elevated, #1f2d3e);
border-radius: 4px;
transition: all 0.2s;
}
.walk-path-point-item:hover {
background: var(--bim-border-default, #334155);
}
/* 悬浮时显示操作按钮 */
.walk-path-point-item:hover .walk-path-point-actions {
opacity: 1;
}
/* 播放中的点高亮样式 */
.walk-path-point-item-active {
background: var(--bim-primary, #3b82f6);
}
.walk-path-point-item-active .walk-path-point-name {
color: #fff;
}
.walk-path-point-item-active .walk-path-point-actions {
opacity: 1;
}
/* 漫游点名称 */
.walk-path-point-name {
font-size: 14px;
color: var(--bim-text-primary, #fff);
}
/* 操作按钮容器 */
.walk-path-point-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
/* 图标按钮 */
.walk-path-btn-icon {
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--bim-text-primary, #fff);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.walk-path-btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 危险图标按钮 */
.walk-path-btn-icon-danger:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}

View File

@@ -1,53 +1,408 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager } from '../../services/locale';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import { ManagerRegistry } from '../../core/manager-registry';
import type { RoamingPoint } from './types';
/**
* 路径漫游面板组件(暂时空内容)
* 路径漫游面板组件
* 提供漫游点的添加、删除、跳转和播放功能
*/
export class WalkPathPanel implements IBimComponent {
public element!: HTMLElement;
/** 管理器注册表实例 */
private registry = ManagerRegistry.getInstance();
/** 国际化订阅取消函数 */
private unsubscribeLocale: (() => void) | null = null;
/** 主题订阅取消函数 */
private unsubscribeTheme: (() => void) | null = null;
constructor() {
// 暂时无配置
}
/** 漫游点列表 */
private points: RoamingPoint[] = [];
/** 漫游时间(毫秒) */
private duration: number = 10000;
/** 是否循环播放 */
private loop: boolean = false;
/** 当前播放中的点索引,-1 表示未播放 */
private playingPointIndex: number = -1;
/** 是否正在播放 */
private isPlaying: boolean = false;
/**
* 初始化组件
* 创建 DOM 元素,订阅国际化和主题变化,加载已有漫游点
*/
public init(): void {
this.element = this.createPanel();
// 创建根元素
this.element = document.createElement('div');
this.element.className = 'walk-path-panel';
// 订阅
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
// 订阅国际化变化
this.unsubscribeLocale = localeManager.subscribe(() => this.render());
// 订阅主题变化
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
// 从引擎加载已有的漫游点
this.loadPointsFromEngine();
// 渲染界面
this.render();
// 应用当前主题
this.setTheme(themeManager.getTheme());
}
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'walk-path-panel';
panel.style.padding = '20px';
panel.style.color = 'var(--bim-text-color, #fff)';
panel.textContent = '路径漫游内容待实现';
return panel;
/**
* 从引擎加载已有的漫游点
* 在面板打开时调用,获取底层引擎中已存在的漫游点
*/
private loadPointsFromEngine(): void {
const enginePoints = this.registry.engine3d?.pathRoamingGetPoints() ?? [];
// 将引擎返回的点转换为本地格式
this.points = enginePoints.map((_: any, index: number) => ({
index: index
}));
}
/**
* 渲染整个面板
* 清空现有内容并重新构建界面
*/
private render(): void {
this.element.innerHTML = '';
// 渲染路径设置区域
const settings = this.createSettingsSection();
this.element.appendChild(settings);
// 渲染漫游点区域
const pointsSection = this.createPointsSection();
this.element.appendChild(pointsSection);
// 渲染播放按钮
const playBtn = this.createPlayButton();
this.element.appendChild(playBtn);
}
/**
* 创建路径设置区域
* 包含漫游时间和循环播放设置
*/
private createSettingsSection(): HTMLElement {
const section = document.createElement('div');
section.className = 'walk-path-settings';
// ===== 漫游时间 =====
const durationGroup = document.createElement('div');
durationGroup.className = 'walk-path-form-group';
const durationLabel = document.createElement('label');
durationLabel.textContent = t('walkControl.path.duration');
const durationWrapper = document.createElement('div');
durationWrapper.className = 'walk-path-input-wrapper';
const durationInput = document.createElement('input');
durationInput.type = 'number';
durationInput.className = 'walk-path-input';
durationInput.value = String(this.duration / 1000);
durationInput.min = '1';
durationInput.oninput = (e) => {
// 更新漫游时间,转换为毫秒
const val = parseInt((e.target as HTMLInputElement).value) || 1;
this.duration = val * 1000;
};
const durationUnit = document.createElement('span');
durationUnit.className = 'walk-path-unit';
durationUnit.textContent = t('walkControl.path.durationUnit');
durationWrapper.appendChild(durationInput);
durationWrapper.appendChild(durationUnit);
durationGroup.appendChild(durationLabel);
durationGroup.appendChild(durationWrapper);
// ===== 循环播放 =====
const loopGroup = document.createElement('div');
loopGroup.className = 'walk-path-form-group walk-path-form-group-inline';
const loopCheckbox = document.createElement('input');
loopCheckbox.type = 'checkbox';
loopCheckbox.id = 'walk-path-loop-checkbox';
loopCheckbox.className = 'walk-path-checkbox';
loopCheckbox.checked = this.loop;
loopCheckbox.onchange = (e) => {
// 更新循环播放状态
this.loop = (e.target as HTMLInputElement).checked;
};
const loopLabel = document.createElement('label');
loopLabel.htmlFor = 'walk-path-loop-checkbox';
loopLabel.textContent = t('walkControl.path.loop');
loopGroup.appendChild(loopCheckbox);
loopGroup.appendChild(loopLabel);
section.appendChild(durationGroup);
section.appendChild(loopGroup);
return section;
}
/**
* 创建漫游点区域
* 包含操作按钮和漫游点列表
*/
private createPointsSection(): HTMLElement {
const section = document.createElement('div');
section.className = 'walk-path-points-section';
// ===== 操作栏 =====
const toolbar = document.createElement('div');
toolbar.className = 'walk-path-points-toolbar';
// 添加漫游点按钮
const addBtn = document.createElement('button');
addBtn.className = 'walk-path-btn walk-path-btn-small';
addBtn.textContent = `+ ${t('walkControl.path.addPoint')}`;
addBtn.onclick = () => this.addPoint();
// 删除全部按钮
const deleteAllBtn = document.createElement('button');
deleteAllBtn.className = 'walk-path-btn walk-path-btn-small walk-path-btn-danger';
deleteAllBtn.textContent = t('walkControl.path.deleteAll');
deleteAllBtn.onclick = () => this.deleteAllPoints();
toolbar.appendChild(addBtn);
toolbar.appendChild(deleteAllBtn);
section.appendChild(toolbar);
// ===== 漫游点列表 =====
const list = document.createElement('div');
list.className = 'walk-path-points-list';
if (this.points.length === 0) {
// 空状态提示
const empty = document.createElement('div');
empty.className = 'walk-path-empty';
empty.textContent = t('walkControl.path.noPoints');
list.appendChild(empty);
} else {
// 渲染每个漫游点
this.points.forEach((_, index) => {
const item = this.createPointItem(index);
list.appendChild(item);
});
}
section.appendChild(list);
return section;
}
/**
* 创建单个漫游点项
* @param index 漫游点索引
*/
private createPointItem(index: number): HTMLElement {
const item = document.createElement('div');
item.className = 'walk-path-point-item';
// 如果是当前播放的点,添加高亮样式
if (this.playingPointIndex === index) {
item.classList.add('walk-path-point-item-active');
}
// 漫游点名称
const name = document.createElement('span');
name.className = 'walk-path-point-name';
name.textContent = `${t('walkControl.path.point')}${index}`;
// 操作按钮容器
const actions = document.createElement('div');
actions.className = 'walk-path-point-actions';
// 跳转按钮
const jumpBtn = document.createElement('button');
jumpBtn.className = 'walk-path-btn-icon';
jumpBtn.innerHTML = '▶';
jumpBtn.title = t('walkControl.path.play');
jumpBtn.onclick = (e) => {
e.stopPropagation();
this.jumpToPoint(index);
};
// 删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.className = 'walk-path-btn-icon walk-path-btn-icon-danger';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.deletePoint(index);
};
actions.appendChild(jumpBtn);
actions.appendChild(deleteBtn);
item.appendChild(name);
item.appendChild(actions);
return item;
}
/**
* 创建播放/停止按钮组
*/
private createPlayButton(): HTMLElement {
const wrapper = document.createElement('div');
wrapper.className = 'walk-path-btn-group';
const playBtn = document.createElement('button');
playBtn.className = 'walk-path-btn walk-path-btn-play';
playBtn.textContent = `${t('walkControl.path.play')}`;
playBtn.disabled = this.points.length === 0 || this.isPlaying;
playBtn.onclick = () => this.playPath();
const stopBtn = document.createElement('button');
stopBtn.className = 'walk-path-btn walk-path-btn-stop';
stopBtn.textContent = `${t('walkControl.path.stop')}`;
stopBtn.disabled = !this.isPlaying;
stopBtn.onclick = () => this.stopPath();
wrapper.appendChild(playBtn);
wrapper.appendChild(stopBtn);
return wrapper;
}
// ==================== 操作方法 ====================
/**
* 添加漫游点
* 将当前相机位置添加为新的漫游点
*/
private addPoint(): void {
// 调用引擎添加漫游点
this.registry.engine3d?.pathRoamingAddPoint();
// 更新本地状态
const newIndex = this.points.length;
this.points.push({ index: newIndex });
// 重新渲染
this.render();
}
/**
* 删除指定漫游点
* @param index 要删除的漫游点索引
*/
private deletePoint(index: number): void {
// 调用引擎删除漫游点
this.registry.engine3d?.pathRoamingRemovePoint(index);
// 从本地列表中移除
this.points.splice(index, 1);
// 重新索引
this.points.forEach((p, i) => p.index = i);
// 重新渲染
this.render();
}
/**
* 删除所有漫游点
*/
private deleteAllPoints(): void {
// 调用引擎清除所有漫游点
this.registry.engine3d?.pathRoamingClearPoints();
// 清空本地列表
this.points = [];
// 重新渲染
this.render();
}
/**
* 跳转到指定漫游点
* @param index 目标漫游点索引
*/
private jumpToPoint(index: number): void {
this.registry.engine3d?.pathRoamingJumpToPoint(index);
}
/**
* 播放漫游
* 按顺序播放所有漫游点
*/
private playPath(): void {
if (this.points.length === 0) return;
this.isPlaying = true;
this.render();
console.log('[WalkPathPanel] 开始播放漫游', { duration: this.duration, loop: this.loop, pointsCount: this.points.length });
this.registry.engine3d?.pathRoamingPlay({
duration: this.duration,
loop: this.loop,
onPointComplete: (pointIndex: number) => {
console.log('[WalkPathPanel] onPointComplete', { pointIndex });
this.playingPointIndex = pointIndex;
this.render();
},
onComplete: () => {
console.log('[WalkPathPanel] onComplete 播放完成');
this.isPlaying = false;
this.playingPointIndex = -1;
this.render();
}
});
}
/**
* 停止漫游
*/
private stopPath(): void {
console.log('[WalkPathPanel] 停止漫游');
this.registry.engine3d?.pathRoamingStop();
this.isPlaying = false;
this.playingPointIndex = -1;
this.render();
}
// ==================== 生命周期 ====================
/**
* 更新国际化文本
* 当语言切换时重新渲染整个面板
*/
public setLocales(): void {
// 更新文本
// 重新渲染以更新所有文本
this.render();
}
public setTheme(_theme: ThemeConfig): void {
// 应用主题
/**
* 应用主题
* @param theme 主题配置
*/
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
// 设置 CSS 变量
this.element.style.setProperty('--bim-text-primary', theme.textPrimary ?? '#fff');
this.element.style.setProperty('--bim-text-secondary', theme.textSecondary ?? '#94a3b8');
this.element.style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
this.element.style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
this.element.style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
}
/**
* 销毁组件
* 清理订阅和 DOM 元素
*/
public destroy(): void {
// 如果正在播放,先停止
if (this.isPlaying) {
this.stopPath();
}
// 取消订阅
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
// 移除 DOM 元素
if (this.element?.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}

View File

@@ -0,0 +1,23 @@
/**
* 漫游点接口
* 表示路径中的一个漫游点
*/
export interface RoamingPoint {
/** 漫游点索引 */
index: number;
}
/**
* 播放选项接口
* 配置漫游播放的参数
*/
export interface PlayOptions {
/** 总播放时长(毫秒),不包括停留时间 */
duration?: number;
/** 是否循环播放 */
loop?: boolean;
/** 播放完成的回调 */
onComplete?: () => void;
/** 每个点播放完成的回调,用于高亮当前点 */
onPointComplete?: (pointIndex: number) => void;
}

View File

@@ -14,12 +14,12 @@ import type { ConstructTreeManagerBtn } from '../managers/construct-tree-manager
import type { MeasureDialogManager } from '../managers/measure-dialog-manager';
import type { WalkControlManager } from '../managers/walk-control-manager';
import type { MapDialogManager } from '../managers/map-dialog-manager';
import type { SectionPlaneDialogManager } from '../managers/section-plane-dialog-manager';
import type { SectionAxisDialogManager } from '../managers/section-axis-dialog-manager';
import type { SectionBoxDialogManager } from '../managers/section-box-dialog-manager';
import type { WalkPathDialogManager } from '../managers/walk-path-dialog-manager';
import type { WalkPlanViewDialogManager } from '../managers/walk-plan-view-dialog-manager';
import type { EngineInfoDialogManager } from '../managers/engine-info-dialog-manager';
import type { ComponentDetailManager } from '../managers/component-detail-manager';
import type { AiChatManager } from '../managers/ai-chat-manager';
@@ -55,8 +55,6 @@ export class ManagerRegistry {
public measure: MeasureDialogManager | null = null;
/** 漫游控制管理器 */
public walkControl: WalkControlManager | null = null;
/** 地图对话框管理器 */
public map: MapDialogManager | null = null;
/** 拾取面剖切对话框管理器 */
public sectionPlane: SectionPlaneDialogManager | null = null;
/** 轴向剖切对话框管理器 */
@@ -67,6 +65,8 @@ export class ManagerRegistry {
public walkPath: WalkPathDialogManager | null = null;
/** 漫游平面图对话框管理器 */
public walkPlanView: WalkPlanViewDialogManager | null = null;
/** 引擎信息对话框管理器 */
public engineInfo: EngineInfoDialogManager | null = null;
/** 构件详情管理器 */
public componentDetail: ComponentDetailManager | null = null;
/** AI 聊天管理器 */

View File

@@ -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);

View File

@@ -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 = [];

View 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;
}
}

View File

@@ -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 要半透明的模型对象

View File

@@ -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', {});
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -27,14 +27,13 @@ export interface EngineEvents {
'walk:gravity-toggle': { enabled: boolean };
'walk:collision-toggle': { enabled: boolean };
// 地图事件
'map:opened': {};
'map:closed': {};
// 构件选中事件
'component:selected': { url: string; id: string };
'component:deselected': {};
// 右键菜单事件
'menu:show-all': {};
// 剖切事件
'section:move': { x?: { min: number; max: number }; y?: { min: number; max: number }; z?: { min: number; max: number } };

View File

@@ -1,24 +1,3 @@
export type MeasureMode =
| 'clearHeight'
| 'clearDistance'
| 'distance'
| 'elevation'
| 'point'
| 'angle'
| 'area'
| 'slope';
export type MeasureValueType = 'length' | 'area' | 'angle' | 'percent' | 'point';
export interface MeasureTypeConfig {
key: MeasureMode;
engineKey: string;
valueType: MeasureValueType;
icon: string;
order: number;
resultField: string;
}
const ICONS = {
clearHeight: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净高</text></svg>`,
clearDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净距</text></svg>`,
@@ -28,83 +7,88 @@ const ICONS = {
angle: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/></svg>`,
area: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">面积</text></svg>`,
slope: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/></svg>`,
};
} as const;
export const MEASURE_TYPES: Record<MeasureMode, MeasureTypeConfig> = {
export const MEASURE_TYPES = {
distance: {
key: 'distance',
callBackType: 'distance',
icon: ICONS.distance,
order: 0,
},
clearHeight: {
key: 'clearHeight',
engineKey: 'clear-height',
valueType: 'length',
callBackType: 'clear-height',
icon: ICONS.clearHeight,
order: 0,
resultField: 'clearHeightMm',
order: 1,
},
clearDistance: {
key: 'clearDistance',
engineKey: 'clear-distance',
valueType: 'length',
callBackType: 'clear-distance',
icon: ICONS.clearDistance,
order: 1,
resultField: 'clearDistanceMm',
},
distance: {
key: 'distance',
engineKey: 'distance',
valueType: 'length',
icon: ICONS.distance,
order: 2,
resultField: 'distanceMm',
},
elevation: {
key: 'elevation',
engineKey: 'elevation',
valueType: 'length',
callBackType: 'elevation',
icon: ICONS.elevation,
order: 3,
resultField: 'elevationMm',
},
point: {
key: 'point',
engineKey: 'point',
valueType: 'point',
callBackType: 'point',
icon: ICONS.point,
order: 4,
resultField: 'xyz',
},
angle: {
key: 'angle',
engineKey: 'angle',
valueType: 'angle',
callBackType: 'angle',
icon: ICONS.angle,
order: 5,
resultField: 'angleDeg',
},
area: {
key: 'area',
engineKey: 'area',
valueType: 'area',
callBackType: 'area',
icon: ICONS.area,
order: 6,
resultField: 'areaM2',
},
slope: {
key: 'slope',
engineKey: 'slope',
valueType: 'percent',
callBackType: 'slope',
icon: ICONS.slope,
order: 7,
resultField: 'slopePercent',
},
};
} as const;
export type MeasureMode = keyof typeof MEASURE_TYPES;
export type CallBackType = typeof MEASURE_TYPES[MeasureMode]['callBackType'];
export type MeasureTypeConfig = typeof MEASURE_TYPES[MeasureMode];
export type MeasureValueType = 'length' | 'area' | 'angle' | 'percent' | 'point';
export const MEASURE_MODES_ORDERED: MeasureMode[] = Object.values(MEASURE_TYPES)
.sort((a, b) => a.order - b.order)
.map((t) => t.key);
export const ENGINE_TYPE_TO_MODE: Record<string, MeasureMode> = Object.fromEntries(
Object.values(MEASURE_TYPES).map((t) => [t.engineKey, t.key])
) as Record<string, MeasureMode>;
export function getValueType(key: MeasureMode): MeasureValueType {
switch (key) {
case 'clearHeight':
case 'clearDistance':
case 'distance':
case 'elevation':
return 'length';
case 'area':
return 'area';
case 'angle':
return 'angle';
case 'slope':
return 'percent';
case 'point':
default:
return 'point';
}
}
export const MODE_TO_ENGINE_TYPE: Record<MeasureMode, string> = Object.fromEntries(
Object.values(MEASURE_TYPES).map((t) => [t.key, t.engineKey])
) as Record<MeasureMode, string>;
export function getModeBycallBackType(callBackType: CallBackType): MeasureMode | undefined {
return (Object.values(MEASURE_TYPES) as MeasureTypeConfig[]).find(t => t.callBackType === callBackType)?.key;
}