提交代码

This commit is contained in:
yuding
2026-04-20 10:38:42 +08:00
parent 0b730da6f4
commit 9a185699ae
36 changed files with 17961 additions and 13149 deletions

View File

@@ -11,7 +11,7 @@ import { themeManager } from './services/theme';
import type { LocaleType } from './locales/types';
import type { ThemeType } from './themes/types';
import type { EngineEvents } from './types/events';
import './iflow-engine-base.css';
/**
* BimEngine2d 构造选项
* 合并引擎配置与主题/语言设置

View File

@@ -11,6 +11,7 @@ import { themeManager } from './services/theme';
import type { LocaleType } from './locales/types';
import type { ThemeType } from './themes/types';
import type { EngineEvents } from './types/events';
import './iflow-engine-base.css';
/**
* BimEngine720 构造选项

View File

@@ -1,5 +1,6 @@
declare const __APP_VERSION__: string;
import './bim-engine.css';
import './iflow-engine-base.css';
import { DialogManager } from './managers/dialog-manager';
import { EngineManager } from './managers/engine-manager';
import { RightKeyManager } from './managers/right-key-manager';

View File

@@ -16,8 +16,6 @@ import type { SectionBoxRange } from '../section-box-panel/types';
import type { ManagerRegistry } from '../../core/manager-registry';
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
import { createEngine as createEngineSDK } from 'iflow-engine-base';
//import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
import "../../../../bim_engine_base/dist/iflow-engine-base.css"
export type {
EngineOptions,
@@ -177,6 +175,22 @@ export class Engine implements IBimComponent {
console.warn('[Engine] 底层引擎不支持 events.on 方法,无法监听点击事件');
}
const oneClickEncoding = (this.engine as any).oneClickEncoding;
if (oneClickEncoding?.on) {
oneClickEncoding.on('encoding-start', (data: any) => {
console.log('[Engine] 底层 encoding-start 事件触发:', data);
this.registry.emit('encoding:start', data);
});
oneClickEncoding.on('encoding-complete', (data: any) => {
console.log('[Engine] 底层 encoding-complete 事件触发:', data);
this.registry.emit('encoding:complete', data);
});
oneClickEncoding.on('encoding-error', (data: any) => {
console.log('[Engine] 底层 encoding-error 事件触发:', data);
this.registry.emit('encoding:error', data);
});
}
} catch (error) {
console.error('[Engine] Failed to initialize engine:', error);
this._isInitialized = false;
@@ -252,6 +266,36 @@ export class Engine implements IBimComponent {
this.engine?.events?.off(event, handler);
}
/**
* 订阅一键编码事件
* @param event 事件名称:'encoding-start' | 'encoding-complete' | 'encoding-error'
* @param handler 事件处理函数
*/
public onOneClickEncodingEvent(event: string, handler: (...args: any[]) => void): void {
this.engine?.oneClickEncoding?.on(event, handler);
}
/**
* 取消订阅一键编码事件
* @param event 事件名称
* @param handler 事件处理函数
*/
public offOneClickEncodingEvent(event: string, handler: (...args: any[]) => void): void {
this.engine?.oneClickEncoding?.off(event, handler);
}
/**
* 启动一键编码
* @remarks 调用底层 engine.oneClickEncoding.start(),需先订阅相关事件
*/
public startOneClickEncoding(): void {
if (!this._isInitialized || !this.engine?.oneClickEncoding) {
console.warn('[Engine] oneClickEncoding not available.');
return;
}
this.engine.oneClickEncoding.start();
}
/**
* 暂停渲染
*/
@@ -631,6 +675,32 @@ export class Engine implements IBimComponent {
this.engine.clipping.reverse();
}
/**
* 设置剖切面填充开关
* @param enabled 是否启用填充
* @remarks 对接底层 `engine.clipping.setFillCutFace(enabled)`
*/
public setFillCutFace(enabled: boolean): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot set fill cut face: engine not initialized.');
return;
}
console.log('[Engine] Setting fill cut face:', enabled);
this.engine.clipping.setFillCutFace(enabled);
}
/**
* 获取剖切面填充状态
* @returns true=已启用填充false=未启用
* @remarks 对接底层 `engine.clipping.getFillCutFace()`
*/
public getFillCutFace(): boolean {
if (!this._isInitialized || !this.engine?.clipping) {
return false;
}
return this.engine.clipping.getFillCutFace?.() ?? false;
}
// ==================== 结束:剖切功能 ====================
// ==================== 相机切换 ====================
@@ -1767,6 +1837,49 @@ export class Engine implements IBimComponent {
// ==================== 结束:构件操作 ====================
/**
* 读取所有已加载模型的缓存编码数据
* @remarks 遍历 engine.models对每个模型调用 oneClickEncoding.readModelCodeFormStoge(url)
*/
public readModelCodeFormStoge(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Cannot read model code form stoge: engine not initialized.');
return;
}
let count = 0;
this.engine.models.forEach((m: any) => {
this.engine.oneClickEncoding.readModelCodeFormStoge(m.url);
count++;
});
console.log('readModelCodeFormStoge-success:', count);
}
/**
* 检查所有已加载模型是否已有编码数据
* @returns 当所有模型都存在编码时返回 true否则返回 false
*/
public hasModelCode(): boolean {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Cannot check model code: engine not initialized.');
return false;
}
const models = (this.engine as any).models;
if (!Array.isArray(models) || models.length === 0) {
return false;
}
const oneClickEncoding = (this.engine as any).oneClickEncoding;
if (!oneClickEncoding || typeof oneClickEncoding.exitModelCode !== 'function') {
console.warn('[Engine] oneClickEncoding.exitModelCode is not available.');
return false;
}
return models.every((m: any) => {
if (!m || typeof m.url !== 'string') {
return false;
}
return oneClickEncoding.exitModelCode(m.url) === true;
});
}
/**
* 销毁组件 (接口实现)
* 清理资源、取消订阅、销毁引擎实例

View File

@@ -25,29 +25,29 @@ export class RadialToolbar implements IBimComponent {
private maxInteractiveRadius = 220;
private readonly closeDelay: number;
private readonly itemsPerRing: number;
// 主按钮直径px
private readonly MAIN_BUTTON_SIZE = 60;
// 子按钮直径px
private readonly SUB_BUTTON_SIZE = 46;
// 第一环子按钮中心”到“主按钮中心的距离px
// 不做防重叠兜底,允许在小半径时出现重叠。
// 第一环"子按钮中心"到"主按钮中心"的距离px
private readonly BASE_RADIUS = 80;
// 多环时相邻两环的中心半径差px
private readonly RING_GAP = 40;
// 扇形展开角度范围:当前 180~270实际就是 90 度)
private readonly RING_GAP = 60;
// 扇形展开角度范围 (180为正左边/下270为正上)
private readonly FAN_START_DEG = 170;
private readonly FAN_END_DEG = 280;
// 扇形边缘留白px防止按钮贴边或裁切
private readonly CANVAS_PADDING = 28;
// 【自定义每环数量】:你可以随意修改这里的数字!多出来的按钮会自动折叠到更外围的新环。
private readonly FIXED_RING_CAPACITIES = [4, 6, 8];
constructor(options: RadialToolbarOptions) {
this.container = options.container;
this.items = options.items === undefined ? this.createDefaultItems() : [...options.items];
this.mainButtonLabel = options.mainButtonLabel ?? 'toolbar.home';
this.onMainButtonClick = options.onMainButtonClick;
this.itemsPerRing = Math.max(3, options.itemsPerRing ?? 5);
this.closeDelay = Math.max(100, options.closeDelay ?? 260);
this.wrapper = this.createWrapper();
@@ -223,38 +223,69 @@ export class RadialToolbar implements IBimComponent {
return btn;
}
/**
* 根据预设的数组计算容量(不足自动向后排)
*/
private getRingCapacities(total: number): number[] {
const capacities: number[] = [];
let remaining = total;
for (const cap of this.FIXED_RING_CAPACITIES) {
if (remaining <= 0) break;
const actualCount = Math.min(cap, remaining);
capacities.push(actualCount);
remaining -= actualCount;
}
// 如果配置的环数不够用,剩下的全部折叠到最后一环
if (remaining > 0) {
capacities.push(remaining);
}
return capacities;
}
private updateItemPositions(): void {
const total = this.itemElements.length;
if (total === 0) return;
const fanSpan = this.FAN_END_DEG - this.FAN_START_DEG;
const ringCapacities = this.getRingCapacities(total);
this.itemElements.forEach((btn, globalIndex) => {
const ringIndex = Math.floor(globalIndex / this.itemsPerRing);
const ringStart = ringIndex * this.itemsPerRing;
const ringCount = Math.min(this.itemsPerRing, total - ringStart);
const ringLocalIndex = globalIndex - ringStart;
const ratio = ringCount === 1 ? 0.5 : ringLocalIndex / (ringCount - 1);
const angleDeg = this.FAN_START_DEG + fanSpan * ratio;
const angleRad = (angleDeg * Math.PI) / 180;
const radius = this.getBaseRadius(ringCount) + ringIndex * this.RING_GAP;
let globalIndex = 0;
ringCapacities.forEach((ringCount, ringIndex) => {
const radius = this.BASE_RADIUS + ringIndex * this.RING_GAP;
const x = Math.cos(angleRad) * radius;
const y = Math.sin(angleRad) * radius;
for (let ringLocalIndex = 0; ringLocalIndex < ringCount; ringLocalIndex++) {
const btn = this.itemElements[globalIndex];
const openDelay = (ringLocalIndex + ringIndex * 0.5) * 0.045;
const closeDelay = (ringCount - 1 - ringLocalIndex + ringIndex * 0.4) * 0.032;
// 【核心逻辑】:首尾固定:第一个按钮在最下面(180),最后一个在最上面(270),中间按剩余空间均分
// 如果该环只有1个按钮让它待在180度的起始位置
const ratio = ringCount <= 1 ? 0 : ringLocalIndex / (ringCount - 1);
btn.style.setProperty('--rt-x', `${x.toFixed(2)}px`);
btn.style.setProperty('--rt-y', `${y.toFixed(2)}px`);
btn.style.setProperty('--rt-open-delay', `${openDelay.toFixed(3)}s`);
btn.style.setProperty('--rt-close-delay', `${closeDelay.toFixed(3)}s`);
const angleDeg = this.FAN_START_DEG + fanSpan * ratio;
const angleRad = (angleDeg * Math.PI) / 180;
const x = Math.cos(angleRad) * radius;
const y = Math.sin(angleRad) * radius;
const openDelay = (ringLocalIndex + ringIndex * 0.5) * 0.045;
const closeDelay = (ringCount - 1 - ringLocalIndex + ringIndex * 0.4) * 0.032;
btn.style.setProperty('--rt-x', `${x.toFixed(2)}px`);
btn.style.setProperty('--rt-y', `${y.toFixed(2)}px`);
btn.style.setProperty('--rt-open-delay', `${openDelay.toFixed(3)}s`);
btn.style.setProperty('--rt-close-delay', `${closeDelay.toFixed(3)}s`);
globalIndex++;
}
});
}
private updateLayoutMetrics(): void {
const total = this.items.length;
const ringCount = Math.max(1, Math.ceil(total / this.itemsPerRing));
const maxItemsPerRing = Math.max(1, Math.min(total, this.itemsPerRing));
const maxRadius = this.getBaseRadius(maxItemsPerRing) + (ringCount - 1) * this.RING_GAP;
const ringCapacities = this.getRingCapacities(total);
const ringCount = ringCapacities.length;
const maxRadius = this.BASE_RADIUS + (ringCount - 1) * this.RING_GAP;
const size = Math.ceil(maxRadius + this.MAIN_BUTTON_SIZE + this.SUB_BUTTON_SIZE + this.CANVAS_PADDING * 2);
this.maxInteractiveRadius = maxRadius + this.SUB_BUTTON_SIZE * 0.7;
@@ -265,10 +296,6 @@ export class RadialToolbar implements IBimComponent {
this.wrapper.style.setProperty('--rt-main-offset', `${(this.MAIN_BUTTON_SIZE - this.SUB_BUTTON_SIZE) / 2}px`);
}
private getBaseRadius(_itemsInRing: number): number {
return this.BASE_RADIUS;
}
private scheduleCollapse(): void {
if (this.timer) {
clearTimeout(this.timer);
@@ -354,7 +381,7 @@ export class RadialToolbar implements IBimComponent {
if (!item) {
return;
}
const text = t(item.label);
const text = item.title ?? t(item.label);
el.title = text;
el.setAttribute('aria-label', text);
this.applyItemActiveClass(el, item);
@@ -362,7 +389,7 @@ export class RadialToolbar implements IBimComponent {
if (!item.icon) {
const iconEl = el.querySelector('.radial-sub-btn-icon');
if (iconEl) {
iconEl.textContent = this.getFallbackLabel(item.label);
iconEl.textContent = this.getFallbackLabel(item.title ?? item.label);
}
}
});
@@ -431,6 +458,12 @@ export class RadialToolbar implements IBimComponent {
}
}
public addItem(item: RadialMenuItem): void {
this.items.push(item);
this.renderItems();
this.updateLayoutMetrics();
}
public init(): void { }
public setLocales(): void {

View File

@@ -1,6 +1,8 @@
export interface RadialMenuItem {
id: string;
label: string;
/** 直接显示的文本(优先级高于 label 的国际化翻译) */
title?: string;
icon?: string;
onClick?: (item: RadialMenuItem) => void;
isToggle?: boolean;
@@ -14,6 +16,5 @@ export interface RadialToolbarOptions {
mainButtonIcon?: string;
mainButtonLabel?: string;
onMainButtonClick?: () => void;
itemsPerRing?: number;
closeDelay?: number;
}

View File

@@ -15,15 +15,16 @@ export class SectionAxisPanel implements IBimComponent {
public element: HTMLElement;
private options: SectionAxisPanelOptions;
// 状态
private isHidden: boolean = false;
private isFilled: boolean = false;
private activeAxis: SectionAxis = 'x';
// DOM 引用 - 第一行
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
// DOM 引用 - 第二行
private axisXBtn!: HTMLButtonElement;
@@ -37,6 +38,7 @@ export class SectionAxisPanel implements IBimComponent {
constructor(options: SectionAxisPanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
this.activeAxis = options.defaultAxis ?? 'x';
this.element = this.createDom();
}
@@ -60,7 +62,7 @@ export class SectionAxisPanel implements IBimComponent {
this.setTheme(themeManager.getTheme());
// 初始化按钮状态
this.updateHideButtonState();
this.updateButtonStates();
this.updateAxisButtonsState();
}
@@ -86,41 +88,39 @@ export class SectionAxisPanel implements IBimComponent {
public setLocales(): void {
this.hideLabelEl.textContent = t('sectionAxis.actions.hide');
this.reverseLabelEl.textContent = t('sectionAxis.actions.reverse');
// XYZ按钮的文字不需要国际化保持为单个字母
this.fillLabelEl.textContent = t('sectionAxis.actions.fill');
this.hideBtn.title = t('sectionAxis.actions.hide');
this.reverseBtn.title = t('sectionAxis.actions.reverse');
this.fillBtn.title = t('sectionAxis.actions.fill');
this.axisXBtn.title = t('sectionAxis.actions.axisX');
this.axisYBtn.title = t('sectionAxis.actions.axisY');
this.axisZBtn.title = t('sectionAxis.actions.axisZ');
}
/**
* 设置隐藏状态
*/
public setHiddenState(isHidden: boolean): void {
this.isHidden = isHidden;
this.updateHideButtonState();
this.updateButtonStates();
}
/**
* 获取隐藏状态
*/
public getHiddenState(): boolean {
return this.isHidden;
}
/**
* 设置激活的轴向
*/
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
public setActiveAxis(axis: SectionAxis): void {
this.activeAxis = axis;
this.updateAxisButtonsState();
}
/**
* 获取激活的轴向
*/
public getActiveAxis(): SectionAxis {
return this.activeAxis;
}
@@ -151,6 +151,12 @@ export class SectionAxisPanel implements IBimComponent {
const row1 = document.createElement('div');
row1.className = 'section-axis-row-1';
this.fillBtn = this.createButton(
'fill',
getIcon('填充'),
() => this.handleFillToggle()
);
this.hideBtn = this.createButton(
'hide',
getIcon('隐藏'),
@@ -163,6 +169,7 @@ export class SectionAxisPanel implements IBimComponent {
() => this.handleReverse()
);
row1.appendChild(this.fillBtn);
row1.appendChild(this.hideBtn);
row1.appendChild(this.reverseBtn);
@@ -188,7 +195,7 @@ export class SectionAxisPanel implements IBimComponent {
* 创建按钮(带图标)
*/
private createButton(
type: 'hide' | 'reverse',
type: 'hide' | 'reverse' | 'fill',
iconSvg: string,
onClick: () => void
): HTMLButtonElement {
@@ -207,11 +214,12 @@ export class SectionAxisPanel implements IBimComponent {
label.className = 'section-axis-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
} else if (type === 'fill') {
this.fillLabelEl = label;
}
// 点击事件
@@ -249,16 +257,22 @@ export class SectionAxisPanel implements IBimComponent {
*/
private handleHideToggle(): void {
this.isHidden = !this.isHidden;
this.updateHideButtonState();
this.updateButtonStates();
if (this.options.onHideToggle) {
this.options.onHideToggle(this.isHidden);
}
}
/**
* 处理反向按钮点击
*/
private handleFillToggle(): void {
this.isFilled = !this.isFilled;
this.updateButtonStates();
if (this.options.onFillToggle) {
this.options.onFillToggle(this.isFilled);
}
}
private handleReverse(): void {
if (this.options.onReverse) {
this.options.onReverse();
@@ -281,15 +295,9 @@ export class SectionAxisPanel implements IBimComponent {
}
}
/**
* 更新隐藏按钮状态
*/
private updateHideButtonState(): void {
if (this.isHidden) {
this.hideBtn.classList.add('active');
} else {
this.hideBtn.classList.remove('active');
}
private updateButtonStates(): void {
this.hideBtn.classList.toggle('active', this.isHidden);
this.fillBtn.classList.toggle('active', this.isFilled);
}
/**

View File

@@ -7,30 +7,11 @@ export type SectionAxis = 'x' | 'y' | 'z';
* 轴向剖切面板配置选项
*/
export interface SectionAxisPanelOptions {
/**
* 隐藏按钮切换回调
* @param isHidden 是否隐藏
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 反向按钮回调
*/
onFillToggle?: (isFilled: boolean) => void;
onReverse?: () => void;
/**
* 轴向切换回调
* @param axis 当前激活的轴向
*/
onAxisChange?: (axis: SectionAxis) => void;
/**
* 默认激活的轴向(默认 'x'
*/
defaultAxis?: SectionAxis;
/**
* 初始隐藏状态(默认 false
*/
defaultHidden?: boolean;
defaultFill?: boolean;
}

View File

@@ -18,15 +18,18 @@ export class SectionBoxPanel implements IBimComponent {
private isHidden: boolean = false;
private isReversed: boolean = false;
private isFilled: boolean = false;
private range: SectionBoxRange;
private hideBtn!: HTMLButtonElement;
private fitBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private fitLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
private xLabelEl!: HTMLElement;
private yLabelEl!: HTMLElement;
private zLabelEl!: HTMLElement;
@@ -61,6 +64,7 @@ export class SectionBoxPanel implements IBimComponent {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isReversed = options.defaultReversed ?? false;
this.isFilled = options.defaultFill ?? false;
this.range = JSON.parse(JSON.stringify(options.defaultRange ?? DEFAULT_RANGE));
}
@@ -96,6 +100,15 @@ export class SectionBoxPanel implements IBimComponent {
return this.isReversed;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
public setRange(range: Partial<SectionBoxRange>): void {
if (range.x) this.range.x = { ...this.range.x, ...range.x };
if (range.y) this.range.y = { ...this.range.y, ...range.y };
@@ -126,6 +139,12 @@ export class SectionBoxPanel implements IBimComponent {
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'section-box-row-buttons';
this.fillBtn = this.createButton('fill', t('sectionBox.actions.fill'), () => {
this.isFilled = !this.isFilled;
this.updateButtonStates();
this.options.onFillToggle?.(this.isFilled);
}, 'fill');
this.hideBtn = this.createButton('hide', t('sectionBox.actions.hide'), () => {
this.isHidden = !this.isHidden;
this.updateButtonStates();
@@ -138,7 +157,7 @@ export class SectionBoxPanel implements IBimComponent {
this.resetBtn = this.createButton('reset', t('sectionBox.actions.reset'), () => this.reset(), 'reset');
[this.hideBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
[this.fillBtn, this.hideBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
const slidersContainer = document.createElement('div');
slidersContainer.className = 'section-box-sliders';
@@ -164,7 +183,8 @@ export class SectionBoxPanel implements IBimComponent {
hide: '隐藏',
reverse: '反向',
fit: '适应到模型',
reset: '重置'
reset: '重置',
fill: '填充'
};
const icon = document.createElement('div');
@@ -178,6 +198,7 @@ export class SectionBoxPanel implements IBimComponent {
if (ref === 'hide') this.hideLabelEl = labelEl;
else if (ref === 'fit') this.fitLabelEl = labelEl;
else if (ref === 'reset') this.resetLabelEl = labelEl;
else if (ref === 'fill') this.fillLabelEl = labelEl;
btn.appendChild(icon);
btn.appendChild(labelEl);
@@ -320,12 +341,14 @@ export class SectionBoxPanel implements IBimComponent {
public setLocales(): void {
if (!this.hideLabelEl) return;
this.fillLabelEl.textContent = t('sectionBox.actions.fill');
this.hideLabelEl.textContent = t('sectionBox.actions.hide');
this.fitLabelEl.textContent = t('sectionBox.actions.fitToModel');
this.resetLabelEl.textContent = t('sectionBox.actions.reset');
this.xLabelEl.textContent = t('sectionBox.axes.x');
this.yLabelEl.textContent = t('sectionBox.axes.y');
this.zLabelEl.textContent = t('sectionBox.axes.z');
this.fillBtn.title = t('sectionBox.actions.fill');
this.hideBtn.title = t('sectionBox.actions.hide');
this.fitBtn.title = t('sectionBox.actions.fitToModel');
this.resetBtn.title = t('sectionBox.actions.reset');

View File

@@ -24,46 +24,14 @@ export interface SectionBoxRange {
* 剖切盒面板配置选项
*/
export interface SectionBoxPanelOptions {
/**
* 隐藏按钮切换回调
* @param isHidden 是否隐藏剖切盒
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 反向按钮切换回调
* @param isReversed 是否反向
*/
onReverseToggle?: (isReversed: boolean) => void;
/**
* 适应到模型按钮回调
*/
onFitToModel?: () => void;
/**
* 重置按钮回调
*/
onReset?: () => void;
/**
* 范围变化回调
* @param range 当前范围值
*/
onRangeChange?: (range: SectionBoxRange) => void;
/**
* 默认隐藏状态(默认 false
*/
onFillToggle?: (isFilled: boolean) => void;
defaultHidden?: boolean;
/**
* 默认反向状态(默认 false
*/
defaultReversed?: boolean;
/**
* 默认范围值
*/
defaultFill?: boolean;
defaultRange?: SectionBoxRange;
}

View File

@@ -11,16 +11,18 @@ export interface SectionDockPanelOptions {
defaultType?: SectionDockType | null;
defaultAxis?: SectionDockAxis;
defaultHidden?: boolean;
defaultFill?: boolean;
onTypeChange?: (type: SectionDockType, axis: SectionDockAxis) => void;
onAxisChange?: (axis: SectionDockAxis) => void;
onHideToggle?: (hidden: boolean) => void;
onReverse?: () => void;
onReset?: (type: SectionDockType, axis: SectionDockAxis) => void;
onFitToModel?: () => void;
onFillToggle?: (isFilled: boolean) => void;
}
interface ToolDefinition {
key: 'hide' | 'reverse' | 'reset' | 'fit';
key: 'hide' | 'reverse' | 'reset' | 'fit' | 'fill';
iconName: string;
textKey: string;
onClick: () => void;
@@ -40,12 +42,14 @@ export class SectionDockPanel implements IBimComponent {
private activeType: SectionDockType | null;
private activeAxis: SectionDockAxis;
private isHidden: boolean;
private isFilled: boolean;
constructor(options: SectionDockPanelOptions = {}) {
this.options = options;
this.activeType = options.defaultType ?? null;
this.activeAxis = options.defaultAxis ?? 'x';
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
const { root, toolContainer, axisPanel, divider } = this.createDom();
this.element = root;
@@ -112,6 +116,15 @@ export class SectionDockPanel implements IBimComponent {
return this.activeAxis;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.renderTools();
}
public getFillState(): boolean {
return this.isFilled;
}
private createDom(): { root: HTMLElement; toolContainer: HTMLElement; axisPanel: HTMLElement; divider: HTMLElement } {
const root = document.createElement('div');
root.className = 'section-dock-panel';
@@ -257,6 +270,18 @@ export class SectionDockPanel implements IBimComponent {
}
private getToolsForType(type: SectionDockType): ToolDefinition[] {
const fillTool: ToolDefinition = {
key: 'fill',
iconName: '填充',
textKey: type === 'box' ? 'sectionBox.actions.fill' : type === 'axis' ? 'sectionAxis.actions.fill' : 'sectionPlane.actions.fill',
isActive: this.isFilled,
onClick: () => {
this.isFilled = !this.isFilled;
this.options.onFillToggle?.(this.isFilled);
this.renderTools();
}
};
const hideTool: ToolDefinition = {
key: 'hide',
iconName: '隐藏',
@@ -280,6 +305,7 @@ export class SectionDockPanel implements IBimComponent {
if (type === 'axis') {
return [
fillTool,
hideTool,
reverseTool,
{
@@ -297,6 +323,7 @@ export class SectionDockPanel implements IBimComponent {
if (type === 'box') {
return [
fillTool,
hideTool,
{
key: 'fit',
@@ -320,6 +347,7 @@ export class SectionDockPanel implements IBimComponent {
}
return [
fillTool,
hideTool,
reverseTool,
{

View File

@@ -15,14 +15,17 @@ export class SectionPlanePanel implements IBimComponent {
private options: SectionPlanePanelOptions;
private isHidden: boolean = false;
private isFilled: boolean = false;
// DOM 引用
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
// 订阅清理
private unsubscribeLocale: (() => void) | null = null;
@@ -31,6 +34,7 @@ export class SectionPlanePanel implements IBimComponent {
constructor(options: SectionPlanePanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
this.element = this.createDom();
}
@@ -43,8 +47,18 @@ export class SectionPlanePanel implements IBimComponent {
return this.isHidden;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
private updateButtonStates(): void {
this.hideBtn?.classList.toggle('active', this.isHidden);
this.fillBtn?.classList.toggle('active', this.isFilled);
}
/**
@@ -88,10 +102,12 @@ export class SectionPlanePanel implements IBimComponent {
this.hideLabelEl.textContent = t('sectionPlane.actions.hide');
this.reverseLabelEl.textContent = t('sectionPlane.actions.reverse');
this.resetLabelEl.textContent = t('sectionPlane.actions.reset');
this.fillLabelEl.textContent = t('sectionPlane.actions.fill');
this.hideBtn.title = t('sectionPlane.actions.hide');
this.reverseBtn.title = t('sectionPlane.actions.reverse');
this.resetBtn.title = t('sectionPlane.actions.reset');
this.fillBtn.title = t('sectionPlane.actions.fill');
}
/**
@@ -116,6 +132,17 @@ export class SectionPlanePanel implements IBimComponent {
const root = document.createElement('div');
root.className = 'section-plane-panel';
// 填充按钮
this.fillBtn = this.createButton(
'fill',
getIcon('填充'),
() => {
this.isFilled = !this.isFilled;
this.updateButtonStates();
this.options.onFillToggle?.(this.isFilled);
}
);
// 隐藏按钮
this.hideBtn = this.createButton(
'hide',
@@ -149,6 +176,7 @@ export class SectionPlanePanel implements IBimComponent {
}
);
root.appendChild(this.fillBtn);
root.appendChild(this.hideBtn);
root.appendChild(this.reverseBtn);
root.appendChild(this.resetBtn);
@@ -159,7 +187,7 @@ export class SectionPlanePanel implements IBimComponent {
/**
* 创建按钮
*/
private createButton(type: 'hide' | 'reverse' | 'reset', iconSvg: string, onClick: () => void): HTMLButtonElement {
private createButton(type: 'hide' | 'reverse' | 'reset' | 'fill', iconSvg: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'section-plane-btn';
@@ -175,11 +203,12 @@ export class SectionPlanePanel implements IBimComponent {
label.className = 'section-plane-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
} else if (type === 'fill') {
this.fillLabelEl = label;
} else {
this.resetLabelEl = label;
}

View File

@@ -7,12 +7,23 @@ export interface SectionPlanePanelOptions {
*/
defaultHidden?: boolean;
/**
* 初始填充状态
*/
defaultFill?: boolean;
/**
* 隐藏状态切换回调
* @param isHidden 是否隐藏
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 填充状态切换回调
* @param isFilled 是否填充
*/
onFillToggle?: (isFilled: boolean) => void;
/**
* 反向按钮回调
*/

File diff suppressed because one or more lines are too long

View File

@@ -135,7 +135,8 @@ export const enUS: TranslationDictionary = {
actions: {
hide: 'Hide',
reverse: 'Reverse',
reset: 'Reset'
reset: 'Reset',
fill: 'Fill'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const enUS: TranslationDictionary = {
reset: 'Reset',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: 'Fill'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const enUS: TranslationDictionary = {
hide: 'Hide',
reverse: 'Reverse',
fitToModel: 'Fit',
reset: 'Reset'
reset: 'Reset',
fill: 'Fill'
},
axes: {
x: 'X',

View File

@@ -147,6 +147,7 @@ export interface TranslationDictionary {
hide: string;
reverse: string;
reset: string;
fill: string;
};
};
sectionAxis: {
@@ -158,6 +159,7 @@ export interface TranslationDictionary {
axisX: string;
axisY: string;
axisZ: string;
fill: string;
};
};
sectionBox: {
@@ -167,6 +169,7 @@ export interface TranslationDictionary {
reverse: string;
fitToModel: string;
reset: string;
fill: string;
};
axes: {
x: string;

View File

@@ -135,7 +135,8 @@ export const zhCN: TranslationDictionary = {
actions: {
hide: '隐藏',
reverse: '反向',
reset: '重置'
reset: '重置',
fill: '填充'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const zhCN: TranslationDictionary = {
reset: '重置',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: '填充'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const zhCN: TranslationDictionary = {
hide: '隐藏',
reverse: '反向',
fitToModel: '适应',
reset: '重置'
reset: '重置',
fill: '填充'
},
axes: {
x: 'X',

View File

@@ -135,7 +135,8 @@ export const zhTW: TranslationDictionary = {
actions: {
hide: '隱藏',
reverse: '反向',
reset: '重設'
reset: '重設',
fill: '填充'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const zhTW: TranslationDictionary = {
reset: '重設',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: '填充'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const zhTW: TranslationDictionary = {
hide: '隱藏',
reverse: '反向',
fitToModel: '適應',
reset: '重設'
reset: '重設',
fill: '填充'
},
axes: {
x: 'X',

View File

@@ -12,7 +12,6 @@ import { createWalkRadialButton } from '../components/radial-toolbar/buttons/wal
export interface RadialToolbarManagerOptions {
items?: RadialMenuItem[];
itemsPerRing?: number;
}
export class RadialToolbarManager extends BaseManager {
@@ -24,7 +23,6 @@ export class RadialToolbarManager extends BaseManager {
this.toolbar = new RadialToolbar({
container,
items: options?.items ?? this.createDefaultItems(),
itemsPerRing: options?.itemsPerRing ?? 4,
mainButtonIcon: getIcon('主视角'),
mainButtonLabel: 'toolbar.home',
onMainButtonClick: () => {
@@ -61,6 +59,10 @@ export class RadialToolbarManager extends BaseManager {
this.toolbar.setItemActive('walk', dock.isOpen('walk'));
}
public addItem(item: RadialMenuItem): void {
this.toolbar?.addItem(item);
}
public destroy(): void {
if (this.unsubscribeDockState) {
this.unsubscribeDockState();

View File

@@ -61,6 +61,7 @@ export class SectionAxisDialogManager extends BaseDialogManager {
this.panel = new SectionAxisPanel({
defaultAxis: 'x',
defaultHidden: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionAxisDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -69,6 +70,9 @@ export class SectionAxisDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onReverse: () => {
this.engineComponent?.reverseSection();
},

View File

@@ -53,6 +53,7 @@ export class SectionBoxDialogManager extends BaseDialogManager {
this.panel = new SectionBoxPanel({
defaultHidden: false,
defaultReversed: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionBoxDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -61,6 +62,9 @@ export class SectionBoxDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onFitToModel: () => {
// 对接底层 scaleBox():缩放剖切盒到场景整体包围盒
this.engineComponent?.scaleSectionBox();
@@ -70,7 +74,7 @@ export class SectionBoxDialogManager extends BaseDialogManager {
// UI 侧会自行将滑块强制恢复到 0-100并将隐藏/反向按钮恢复为关闭状态。
this.engineComponent?.deactivateSection();
this.engineComponent?.activeSection('box');
// 确保剖切可见(避免上一次处于隐藏状态导致看起来没重置
// 确保剖切可见(避免上一次处于隐藏状态导致"看起来没重置"
this.engineComponent?.recoverSection();
},
onRangeChange: (range) => {

View File

@@ -91,6 +91,10 @@ export class SectionDockManager extends BaseManager {
},
onFitToModel: () => {
this.engineComponent?.scaleSectionBox();
},
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
}
});
this.panel.init();

View File

@@ -53,6 +53,7 @@ export class SectionPlaneDialogManager extends BaseDialogManager {
protected createContent(): HTMLElement {
this.panel = new SectionPlanePanel({
defaultHidden: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionPlaneDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -61,6 +62,9 @@ export class SectionPlaneDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onReverse: () => {
this.engineComponent?.reverseSection();
},

View File

@@ -10,6 +10,10 @@ export interface EngineEvents {
'engine:model-loading-completed': {};
'engine:object-clicked': { objectId: string; position: { x: number; y: number; z: number } };
'encoding:start': { data?: any };
'encoding:complete': { data?: any };
'encoding:error': { data?: any };
// 树组件事件
'ui:tree-node-check': { id: string; checked: boolean; node: any };
'ui:tree-node-select': { id: string; selected: boolean; node: any };

View File

@@ -27,6 +27,7 @@ const ICONS: Record<string, string> = {
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><g transform="translate(-84.667 -84.667)"><path d="M87.667,97V87.667H97V85.333H86.733a1.4,1.4,0,0,0-1.4,1.4V97Zm0,23.333H85.333V130.6a1.4,1.4,0,0,0,1.4,1.4H97v-2.333H87.667v-9.333Zm32.667,9.333V132H130.6a1.4,1.4,0,0,0,1.4-1.4V120.333h-2.333v9.333h-9.333ZM129.667,97H132V86.733a1.4,1.4,0,0,0-1.4-1.4H120.333v2.333h9.333Z"/><path d="M270.857,243.5l11.3-6.652V223.387l-11.3,6.361V243.5Zm-1.8,0v-13.75l-11.3-6.361v13.456l11.3,6.652Zm-10.278-21.621,11.177,6.284,11.177-6.284L269.958,215.3l-11.177,6.573Zm11.622-8.426,13.1,7.709a.919.919,0,0,1,.448.793v15.419a.926.926,0,0,1-.448.793l-13.1,7.709a.887.887,0,0,1-.9,0l-13.1-7.709a.905.905,0,0,1-.448-.793V221.954a.919.919,0,0,1,.448-.793l13.1-7.709A.887.887,0,0,1,270.4,213.452Z" transform="translate(-161.292 -120.997)"/></g></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M61.274,76.178l0-.007-.007,0a32.458,32.458,0,0,0-7.014-9.981L51.53,68.91a28.382,28.382,0,0,1,6.124,8.635c-4.472,9.25-10.809,13.636-19.472,13.636A20.615,20.615,0,0,1,30.6,89.838l-2.937,2.933a23.916,23.916,0,0,0,10.514,2.277q15.472,0,23.088-16.116a3.229,3.229,0,0,0,0-2.755ZM57.839,58.351l-2.274-2.277a.429.429,0,0,0-.608,0l-6.265,6.262a23.775,23.775,0,0,0-10.51-2.281q-15.472,0-23.088,16.116v.007a3.236,3.236,0,0,0,0,2.765,32.617,32.617,0,0,0,7.014,9.985L16.7,94.32a.429.429,0,0,0,0,.608l2.274,2.277a.429.429,0,0,0,.608,0L57.839,58.949a.426.426,0,0,0,0-.6ZM32.113,78.915a6.011,6.011,0,0,1,7.22-7.22l-7.223,7.22Zm9.9-9.9A9.452,9.452,0,0,0,29.43,81.6l-4.6,4.6a28.424,28.424,0,0,1-6.124-8.635c4.475-9.25,10.816-13.636,19.472-13.636a20.615,20.615,0,0,1,7.577,1.343Zm-4.046,14.55a5.752,5.752,0,0,1-1.01-.086l-2.744,2.741A9.446,9.446,0,0,0,46.638,73.794L43.9,76.538a6.018,6.018,0,0,1-5.932,7.024Z" transform="translate(-14.182 -52.64)"/></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M32.936,32.658a15.914,15.914,0,0,1-9.6,3.064c-.451-.006-.9-.042-1.342-.077-.182-.018-.357-.047-.539-.077-.352-.047-.7-.089-1.037-.164-.211-.036-.422-.1-.621-.148-.334-.077-.668-.154-.99-.254-.153-.059-.3-.11-.457-.177-.381-.124-.755-.26-1.113-.414-.082-.036-.162-.071-.24-.1-.422-.2-.844-.408-1.248-.627-.017-.012-.035-.018-.054-.03a16.485,16.485,0,0,1-3.732-2.9c-.017-.018-.035-.042-.054-.059-.34-.355-.668-.721-.979-1.118-.065-.083-.123-.16-.194-.254A17.286,17.286,0,0,1,7.149,18.751h3.98L6.318,9.076,0,18.874H3.942a20.809,20.809,0,0,0,3.5,11.57.871.871,0,0,0,.075.14c.225.34.486.648.724.961.1.113.177.232.274.362.354.443.746.869,1.133,1.285.043.043.075.076.108.113a19.778,19.778,0,0,0,4.436,3.446c.043.027.08.043.128.076.467.259.949.5,1.432.719.124.055.242.113.359.164.419.184.847.335,1.277.493.2.076.4.14.611.21.378.113.762.21,1.153.3.257.065.509.129.772.184a2.252,2.252,0,0,0,.316.076c.37.07.735.1,1.1.151.134.027.27.049.4.065.66.065,1.314.11,1.974.11,4.006,0,8.852-1.3,11.85-4.177a1.908,1.908,0,0,0,.333-2.484,2.228,2.228,0,0,0-2.952.023ZM44.053,20.424a20.728,20.728,0,0,0-3.47-11.537c-.032-.055-.054-.113-.08-.164-.284-.405-.574-.778-.869-1.161a1.289,1.289,0,0,1-.1-.14A19.86,19.86,0,0,0,32.168,1.7c-.086-.032-.155-.076-.242-.11-.456-.195-.922-.362-1.394-.53C30.37,1,30.2.937,30.033.883,29.62.754,29.212.651,28.792.548c-.231-.055-.467-.113-.7-.164-.113-.022-.22-.055-.338-.081-.311-.055-.617-.081-.933-.124C26.6.151,26.392.119,26.18.1,25.655.042,25.135.021,24.616.016c-.1,0-.188-.016-.284-.016-.016,0-.032.005-.049.005A19.124,19.124,0,0,0,13.02,3.695a1.682,1.682,0,0,0-.466,2.515,1.777,1.777,0,0,0,2.309.3A15.251,15.251,0,0,1,24.374,3.45c.486.006.971.03,1.441.077.147.012.287.036.432.059a11.3,11.3,0,0,1,1.16.189c.162.03.334.077.493.11.381.089.744.178,1.107.3a2.577,2.577,0,0,1,.34.124c.422.142.832.29,1.23.467.047.012.082.047.123.059a16.492,16.492,0,0,1,6.182,4.8.367.367,0,0,0,.024.036A17.308,17.308,0,0,1,40.629,20.43H36.645l5.048,9.787L48,20.424Zm0,0" transform="translate(0 4.349)"/></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M24 6L10 22h10v18h8V22h10L24 6z" opacity="0.35"/><path d="M8 24h32v4H8z"/></svg>',
// ========== 测量相关图标 (32x32) ==========
: '<svg width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M84.131,193.119a1.056,1.056,0,0,1,1.116.982v7.857a1.056,1.056,0,0,1-1.116.982H54.367a1.056,1.056,0,0,1-1.116-.982V194.1a1.056,1.056,0,0,1,1.116-.982Zm-1.116,1.964H55.483v5.893H83.015Zm1.116-13.749a1.064,1.064,0,0,1,1.114.935,1.032,1.032,0,0,1-1.007,1.025l-.107,0H71.2l-7.858,6.914a1.227,1.227,0,0,1-1.578,0l-8.185-7.2-.018-.016-.032-.031.049.047a1.107,1.107,0,0,1-.092-.092l-.011-.014a.869.869,0,0,1-.182-.857l0-.008L53.31,182l.012-.029.02-.045.019-.035a1.1,1.1,0,0,1,.891-.552h.007q.053,0,.107,0ZM68.043,183.3H57.06l5.492,4.831Z" transform="translate(-53.247 -176.136)"/></svg>',