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

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