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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 继承
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -84,6 +84,9 @@ export interface TreeOptions {
|
||||
/** 节点选择回调 */
|
||||
onNodeSelect?: (node: BimTreeNode) => void;
|
||||
|
||||
/** 节点取消选择回调(再次点击已选中节点时触发) */
|
||||
onNodeDeselect?: (node: BimTreeNode) => void;
|
||||
|
||||
/** 节点展开/折叠回调 */
|
||||
onNodeExpand?: (node: BimTreeNode) => void;
|
||||
|
||||
|
||||
@@ -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)');
|
||||
}
|
||||
|
||||
253
src/components/walk-path-panel/index.css
Normal file
253
src/components/walk-path-panel/index.css
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/components/walk-path-panel/types.ts
Normal file
23
src/components/walk-path-panel/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 漫游点接口
|
||||
* 表示路径中的一个漫游点
|
||||
*/
|
||||
export interface RoamingPoint {
|
||||
/** 漫游点索引 */
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放选项接口
|
||||
* 配置漫游播放的参数
|
||||
*/
|
||||
export interface PlayOptions {
|
||||
/** 总播放时长(毫秒),不包括停留时间 */
|
||||
duration?: number;
|
||||
/** 是否循环播放 */
|
||||
loop?: boolean;
|
||||
/** 播放完成的回调 */
|
||||
onComplete?: () => void;
|
||||
/** 每个点播放完成的回调,用于高亮当前点 */
|
||||
onPointComplete?: (pointIndex: number) => void;
|
||||
}
|
||||
@@ -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 聊天管理器 */
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user