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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user