feat: upgrade to v1.3.2 with settings panel overhaul, clear height enhancements and bug fixes
- Overhaul settings dialog: add edge line toggle, contrast/saturation/light intensity sliders, environment and ground type selectors - Add clear height measurement options: direction (up/down) and select type (point/element) with radio button UI - Fix right-click context menu triggering during model drag rotation (add move threshold) - Fix measure dialog event listener leak (on → off for cleanup) - Update mini map API to use engine.minMap.toggle() - Replace text-based measure icons with proper SVG assets (净高/净距/坐标/面积) - Add i18n keys for all new settings and clear height options (zh-CN / en-US) - Bump iflow-engine-base dependency to ^2.0.5 - Rebuild dist and sync demo libs
This commit is contained in:
1
src/assets/净距.svg
Normal file
1
src/assets/净距.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
1
src/assets/净高.svg
Normal file
1
src/assets/净高.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M19.0032 32.1184C18.712 32.1184 18.4032 32.1184 18.112 32.4096C17.512 32.6992 17.8224 33.5904 18.112 33.8816L23.1472 38.6256C23.7472 38.9152 24.6192 38.9152 24.928 38.6256L29.9632 33.8816C30.2528 33.5904 30.2528 33.2816 30.2528 32.9904C30.2528 32.3904 29.6528 32.1008 29.072 32.1008L25.7856 32.1008L25.7856 16.2192L29.0256 16.2192C29.3152 16.2192 29.6256 16.2192 29.9152 15.928C30.5152 15.6368 30.2064 14.7472 29.9152 14.456L24.8816 9.7072C24.2816 9.4176 23.4096 9.4176 23.0992 9.7072L18.0656 14.4512C17.7744 14.7424 17.7744 15.0512 17.7744 15.3424C17.7744 15.9424 18.3744 16.232 18.9568 16.232L22.24 16.232L22.24 32.1328L19.0016 32.1328L19.0016 32.1184L19.0032 32.1184ZM4.75842 6.80639L43.2384 6.80639C44.128 6.80639 44.7104 6.20639 44.7104 5.33442C44.7104 4.1536 44.1088 3.2624 43.2384 3.2624L4.75678 3.2624C4.1568 3.2624 3.5744 3.8624 3.2848 4.73442C3.2848 5.62561 3.8848 6.5152 4.75678 6.80639L4.75842 6.80639ZM43.256 41.1936L4.75842 41.1936C4.1584 41.1936 3.576 41.7936 3.2864 42.6656C3.2864 43.5568 3.8864 44.4464 4.75842 44.7376L43.2384 44.7376C44.128 44.7376 44.7104 44.1376 44.7104 43.2656C44.728 42.08 44.1328 41.1936 43.256 41.1936Z" fill="#404040" ></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/坐标.svg
Normal file
1
src/assets/坐标.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M40.1698 18.0929C40.1428 20.5301 39.3645 22.7586 38.3131 24.9092C36.7378 28.1319 34.6461 31.0118 32.3727 33.7664C30.2564 36.3308 27.9853 38.7519 25.5613 41.0284C24.7143 41.8237 23.9413 41.8481 23.073 41.0687C18.5767 37.0334 14.5359 32.6092 11.4515 27.3755C10.9495 26.5238 10.4945 25.6458 10.1025 24.7372C9.73107 23.8761 10.0514 22.9753 10.8534 22.6034C11.6577 22.2304 12.582 22.5576 12.9732 23.4086C14.8167 27.4176 17.5132 30.8241 20.4598 34.0548C21.596 35.3006 22.7863 36.4939 24.0184 37.6454C24.1708 37.7877 24.2667 37.9033 24.4831 37.6853C28.5664 33.5709 32.3892 29.2509 35.1723 24.1156C35.9836 22.6186 36.66 21.0543 36.8866 19.3498C37.2281 16.7802 36.712 14.3356 35.493 12.0807C33.1715 7.78618 29.5679 5.33714 24.6496 5.16445C19.0467 4.96786 14.0685 8.58606 12.3051 13.9294C11.8621 15.2717 11.6275 16.6474 11.6092 18.061C11.5962 19.0717 10.9335 19.7853 10.0201 19.783C9.10383 19.7808 8.43479 19.0505 8.43746 18.056C8.45818 10.5393 13.6357 4.02759 20.9324 2.34148C26.9693 0.946299 33.4364 3.45514 37.0904 8.60424C39.1088 11.4483 40.1115 14.6187 40.1698 18.0929ZM6.42075 43.1971C18.363 43.1971 30.2366 43.1971 42.1204 43.1971C42.1186 42.9919 41.9834 42.8579 41.8977 42.7076C40.3484 39.9866 38.7956 37.2673 37.2401 34.5498C36.9062 33.9668 36.8856 33.3861 37.257 32.8222C37.5915 32.314 38.0819 32.0681 38.6918 32.1099C39.2399 32.1477 39.6712 32.4169 39.9425 32.8899C42.0656 36.5932 44.1915 40.2948 46.2926 44.0105C46.9411 45.1576 46.1392 46.3818 44.7752 46.3821C37.6695 46.3845 30.564 46.3832 23.4585 46.3832C16.9643 46.3832 10.4699 46.3507 3.97625 46.4082C2.51885 46.4212 1.62531 45.0904 2.42647 43.7424C4.52293 40.2152 6.51577 36.6264 8.55216 33.0634C8.99072 32.2962 9.67627 31.9592 10.3942 32.1457C11.456 32.4219 11.9181 33.5491 11.3553 34.5444C10.2685 36.467 9.16594 38.3807 8.07099 40.2987C7.53258 41.2418 6.99643 42.1859 6.42075 43.1971ZM30.6461 17.8271C30.6465 21.3322 27.7864 24.1813 24.2754 24.1734C20.7946 24.1656 17.9554 21.3147 17.9555 17.8276C17.9557 14.3185 20.8087 11.4763 24.3244 11.4829C27.8051 11.4893 30.6458 14.3402 30.6461 17.8271ZM24.3201 14.6556C22.5583 14.656 21.1314 16.0714 21.1285 17.8213C21.1256 19.591 22.5741 21.0173 24.3575 21.0005C26.0699 20.9844 27.4807 19.5391 27.4733 17.8085C27.4659 16.0808 26.0403 14.6552 24.3201 14.6556Z" fill="#404040" ></path></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
src/assets/面积.svg
Normal file
1
src/assets/面积.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M28.7725 16.8582L16.6882 28.9801C16.0335 29.6368 16.0335 30.7313 16.6882 31.3881C17.0156 31.7164 17.452 31.8806 17.8885 31.8806C18.3249 31.8806 18.7614 31.7164 19.0887 31.3881L31.173 19.2662C31.8277 18.6094 31.8277 17.5149 31.173 16.8582C30.5183 16.2015 29.4272 16.2015 28.7725 16.8582Z" fill="#404040" ></path><path d="M44.5667 11.9055C45.385 11.9055 46.0397 11.2488 46.0397 10.4552L46.0397 3.47761C46.0397 2.65672 45.385 2 44.5667 2L37.6107 2C36.7924 2 36.1377 2.65672 36.1377 3.47761L36.1377 5.06468L11.9418 5.06468L11.9418 3.47761C11.9418 2.65672 11.2871 2 10.4688 2L3.51277 2C2.69442 2 2.03974 2.65672 2.03974 3.47761L2.03974 10.4279C2.03974 11.2488 2.69442 11.9055 3.51277 11.9055L5.20403 11.9055L5.20403 36.0671L3.51277 36.0671C2.69442 36.0671 2.03974 36.7239 2.03974 37.5448L2.03974 44.5224C2.03974 45.3433 2.69442 46 3.51277 46L10.4688 46C11.2598 46 11.9145 45.3433 11.9418 44.5224L11.9418 42.7438L36.1377 42.7438L36.1377 44.5224C36.1377 45.3433 36.7924 46 37.6107 46L44.5667 46C45.385 46 46.0397 45.3433 46.0397 44.5224L46.0397 37.5448C46.0397 36.7239 45.385 36.0671 44.5667 36.0671L42.7936 36.0671L42.7936 11.9055L44.5667 11.9055ZM39.0837 4.92786L43.0937 4.92786L43.0937 8.95024L39.0837 8.95024L39.0837 4.92786ZM4.9858 4.92786L8.99572 4.92786L8.99572 8.95024L4.9858 8.95024L4.9858 4.92786ZM8.99572 43.0448L4.9858 43.0448L4.9858 39.0224L8.99572 39.0224L8.99572 43.0448ZM43.0937 43.0448L39.0837 43.0448L39.0837 39.0224L43.0937 39.0224L43.0937 43.0448ZM39.3838 36.0671L37.6107 36.0671C36.7924 36.0671 36.1377 36.7239 36.1377 37.5448L36.1377 39.3234L11.9418 39.3234L11.9418 37.5448C11.9418 36.7239 11.2871 36.0671 10.4688 36.0671L8.61382 36.0671L8.61382 11.9055L10.4688 11.9055C11.2598 11.9055 11.9145 11.2488 11.9418 10.4552L11.9418 8.48507L36.1377 8.48507L36.1377 10.4279C36.1377 11.2488 36.7924 11.9055 37.6107 11.9055L39.3838 11.9055L39.3838 36.0671Z" fill="#404040" ></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -2,13 +2,13 @@ import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { themeManager } from '../../services/theme';
|
||||
import type { EngineOptions, ModelLoadOptions, EngineInfo } from './types';
|
||||
import { type MeasureMode } from '../../types/measure';
|
||||
import { type MeasureMode, type ClearHeightDirection, type ClearHeightSelectType } from '../../types/measure';
|
||||
import type { MeasureUnit, MeasurePrecision } from '../measure-panel/types';
|
||||
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 { 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, ModelLoadOptions, EngineInfo };
|
||||
@@ -369,6 +369,30 @@ export class Engine implements IBimComponent {
|
||||
this.engine.measure.saveSetting?.(setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置净高测量朝向
|
||||
* @param direction 0=朝下,1=朝上
|
||||
*/
|
||||
public setClearHeightDirection(direction: ClearHeightDirection): void {
|
||||
if (!this._isInitialized || !this.engine?.measure) {
|
||||
return;
|
||||
}
|
||||
console.log(`[Engine] Setting clearHeight direction: ${direction}`);
|
||||
this.engine.measure.clearHeightMeasure.setDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置净高测量选择对象类型
|
||||
* @param selectType 'point'=选择点,'element'=选择构件
|
||||
*/
|
||||
public setClearHeightSelectType(selectType: ClearHeightSelectType): void {
|
||||
if (!this._isInitialized || !this.engine?.measure) {
|
||||
return;
|
||||
}
|
||||
console.log(`[Engine] Setting clearHeight selectType: ${selectType}`);
|
||||
this.engine.measure.clearHeightMeasure.setSelectType(selectType);
|
||||
}
|
||||
|
||||
// ==================== 结束:测量功能 ====================
|
||||
|
||||
// ==================== 剖切功能(统一 API) ====================
|
||||
@@ -696,11 +720,22 @@ export class Engine implements IBimComponent {
|
||||
* 切换小地图显示状态
|
||||
*/
|
||||
public toggleMiniMap(): void {
|
||||
if (!this._isInitialized || !this.engine?.controlModule) {
|
||||
if (!this._isInitialized || !this.engine) {
|
||||
console.error('[Engine] Cannot toggle mini map: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.controlModule.toggleMinMap();
|
||||
this.engine.minMap?.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小地图显示状态
|
||||
* @returns true=显示,false=隐藏
|
||||
*/
|
||||
public getMiniMapState(): boolean {
|
||||
if (!this._isInitialized || !this.engine) {
|
||||
return false;
|
||||
}
|
||||
return this.engine.minMap?.getState() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1004,7 +1039,7 @@ export class Engine implements IBimComponent {
|
||||
const normalizeUrl = (url: string): string => url.replace(/\/+$/, '');
|
||||
|
||||
const loadedModels = Array.isArray((this.engine as any)?.models)
|
||||
? ((this.engine as any).models as Array<{ url?: string; nodesMap?: Map<number, { indexes?: unknown[] }> }> )
|
||||
? ((this.engine as any).models as Array<{ url?: string; nodesMap?: Map<number, { indexes?: unknown[] }> }>)
|
||||
: [];
|
||||
const modelMap = new Map<string, { nodesMap?: Map<number, { indexes?: unknown[] }> }>();
|
||||
for (const loadedModel of loadedModels) {
|
||||
|
||||
@@ -208,6 +208,66 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ========== 模式选项区(净高朝向/选择对象) ========== */
|
||||
.bim-measure-mode-options {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--bim-divider);
|
||||
}
|
||||
|
||||
.bim-measure-mode-options.is-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bim-measure-radio-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bim-measure-radio-group .bim-measure-radio-label {
|
||||
color: var(--bim-text-secondary);
|
||||
min-width: 58px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.bim-measure-radio-group .bim-measure-radio-items {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bim-measure-radio-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bim-border-default);
|
||||
background: var(--bim-bg-inset);
|
||||
color: var(--bim-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bim-measure-radio-item:hover {
|
||||
border-color: var(--bim-border-strong);
|
||||
color: var(--bim-text-primary);
|
||||
}
|
||||
|
||||
.bim-measure-radio-item.is-active {
|
||||
background: var(--bim-primary-subtle);
|
||||
border-color: var(--bim-primary);
|
||||
color: var(--bim-primary);
|
||||
}
|
||||
|
||||
.bim-measure-result {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ThemeConfig } from '../../themes/types';
|
||||
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 type { MeasureConfig, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit, ClearHeightDirection, ClearHeightSelectType } from './types';
|
||||
import { MEASURE_TYPES, MEASURE_MODES_ORDERED, getValueType, type MeasureMode, type MeasureValueType } from '../../types/measure';
|
||||
|
||||
/**
|
||||
@@ -27,6 +27,10 @@ export class MeasurePanel implements IBimComponent {
|
||||
private isExpanded: boolean;
|
||||
private result: MeasureResult | null = null;
|
||||
|
||||
// 净高模式专属状态
|
||||
private clearHeightDirection: ClearHeightDirection = 0; // 默认朝下
|
||||
private clearHeightSelectType: ClearHeightSelectType = 'point'; // 默认选择点
|
||||
|
||||
/**
|
||||
* 测量配置(单位/精度)
|
||||
* 说明:
|
||||
@@ -67,6 +71,11 @@ export class MeasurePanel implements IBimComponent {
|
||||
private clearBtn!: HTMLButtonElement;
|
||||
private settingsBtn!: HTMLButtonElement;
|
||||
|
||||
// 净高模式选项 DOM
|
||||
private modeOptionsEl!: HTMLElement;
|
||||
private directionBtns: Map<ClearHeightDirection, HTMLButtonElement> = new Map();
|
||||
private selectTypeBtns: Map<ClearHeightSelectType, HTMLButtonElement> = new Map();
|
||||
|
||||
// Settings DOM
|
||||
private mainViewEl!: HTMLElement;
|
||||
private settingsViewEl!: HTMLElement;
|
||||
@@ -115,6 +124,7 @@ export class MeasurePanel implements IBimComponent {
|
||||
// 初始渲染状态(按钮显隐、选中态、结果区)
|
||||
this.applyExpandedState();
|
||||
this.applyActiveModeState();
|
||||
this.applyClearHeightOptionsState();
|
||||
this.applyViewState();
|
||||
this.renderResult();
|
||||
|
||||
@@ -233,6 +243,7 @@ export class MeasurePanel implements IBimComponent {
|
||||
if (this.activeMode === mode) return;
|
||||
this.activeMode = mode;
|
||||
this.applyActiveModeState();
|
||||
this.applyClearHeightOptionsState();
|
||||
|
||||
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
|
||||
|
||||
@@ -422,6 +433,10 @@ export class MeasurePanel implements IBimComponent {
|
||||
toolsBox.appendChild(toggleBox);
|
||||
this.mainViewEl.appendChild(toolsBox);
|
||||
|
||||
// 净高模式选项区(朝向 + 选择对象)
|
||||
this.modeOptionsEl = this.createModeOptionsDom();
|
||||
this.mainViewEl.appendChild(this.modeOptionsEl);
|
||||
|
||||
// 中部:结果区
|
||||
const resultBox = document.createElement('div');
|
||||
resultBox.className = 'bim-measure-result';
|
||||
@@ -593,6 +608,118 @@ export class MeasurePanel implements IBimComponent {
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建净高模式选项区 DOM(朝向 + 选择对象)
|
||||
*/
|
||||
private createModeOptionsDom(): HTMLElement {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'bim-measure-mode-options';
|
||||
|
||||
// 朝向单选组
|
||||
const dirGroup = document.createElement('div');
|
||||
dirGroup.className = 'bim-measure-radio-group';
|
||||
const dirLabel = document.createElement('span');
|
||||
dirLabel.className = 'bim-measure-radio-label';
|
||||
dirLabel.dataset.i18nKey = 'measure.clearHeight.direction';
|
||||
const dirItems = document.createElement('div');
|
||||
dirItems.className = 'bim-measure-radio-items';
|
||||
|
||||
const dirDown = this.createRadioItem('measure.clearHeight.directionDown', () => {
|
||||
this.setClearHeightDirection(0);
|
||||
});
|
||||
const dirUp = this.createRadioItem('measure.clearHeight.directionUp', () => {
|
||||
this.setClearHeightDirection(1);
|
||||
});
|
||||
this.directionBtns.set(0, dirDown);
|
||||
this.directionBtns.set(1, dirUp);
|
||||
|
||||
dirItems.appendChild(dirDown);
|
||||
dirItems.appendChild(dirUp);
|
||||
dirGroup.appendChild(dirLabel);
|
||||
dirGroup.appendChild(dirItems);
|
||||
box.appendChild(dirGroup);
|
||||
|
||||
// 选择对象单选组
|
||||
const selGroup = document.createElement('div');
|
||||
selGroup.className = 'bim-measure-radio-group';
|
||||
const selLabel = document.createElement('span');
|
||||
selLabel.className = 'bim-measure-radio-label';
|
||||
selLabel.dataset.i18nKey = 'measure.clearHeight.selectType';
|
||||
const selItems = document.createElement('div');
|
||||
selItems.className = 'bim-measure-radio-items';
|
||||
|
||||
const selPoint = this.createRadioItem('measure.clearHeight.selectPoint', () => {
|
||||
this.setClearHeightSelectType('point');
|
||||
});
|
||||
const selElement = this.createRadioItem('measure.clearHeight.selectElement', () => {
|
||||
this.setClearHeightSelectType('element');
|
||||
});
|
||||
this.selectTypeBtns.set('point', selPoint);
|
||||
this.selectTypeBtns.set('element', selElement);
|
||||
|
||||
selItems.appendChild(selPoint);
|
||||
selItems.appendChild(selElement);
|
||||
selGroup.appendChild(selLabel);
|
||||
selGroup.appendChild(selItems);
|
||||
box.appendChild(selGroup);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
private createRadioItem(i18nKey: string, onClick: () => void): HTMLButtonElement {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'bim-measure-radio-item';
|
||||
btn.dataset.i18nKey = i18nKey;
|
||||
btn.textContent = t(i18nKey);
|
||||
btn.addEventListener('click', onClick);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private setClearHeightDirection(direction: ClearHeightDirection): void {
|
||||
if (this.clearHeightDirection === direction) return;
|
||||
this.clearHeightDirection = direction;
|
||||
this.applyClearHeightOptionsState();
|
||||
this.options.onClearHeightDirectionChange?.(direction);
|
||||
}
|
||||
|
||||
private setClearHeightSelectType(selectType: ClearHeightSelectType): void {
|
||||
if (this.clearHeightSelectType === selectType) return;
|
||||
this.clearHeightSelectType = selectType;
|
||||
this.applyClearHeightOptionsState();
|
||||
this.options.onClearHeightSelectTypeChange?.(selectType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用净高选项状态(显隐 + 选中态)
|
||||
*/
|
||||
private applyClearHeightOptionsState(): void {
|
||||
// 显隐
|
||||
if (this.activeMode === 'clearHeight') {
|
||||
this.modeOptionsEl.classList.add('is-visible');
|
||||
} else {
|
||||
this.modeOptionsEl.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
// 朝向按钮选中态
|
||||
for (const [dir, btn] of this.directionBtns.entries()) {
|
||||
if (dir === this.clearHeightDirection) {
|
||||
btn.classList.add('is-active');
|
||||
} else {
|
||||
btn.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
// 选择对象按钮选中态
|
||||
for (const [type, btn] of this.selectTypeBtns.entries()) {
|
||||
if (type === this.clearHeightSelectType) {
|
||||
btn.classList.add('is-active');
|
||||
} else {
|
||||
btn.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private makeOption(unit: MeasureUnit): HTMLOptionElement {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = unit;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
// 从通用 types 目录导入 MeasureMode,避免组件间耦合
|
||||
import type { MeasureMode } from '../../types/measure';
|
||||
import type { MeasureMode, ClearHeightDirection, ClearHeightSelectType } from '../../types/measure';
|
||||
// 同时重新导出,保持向后兼容
|
||||
export type { MeasureMode };
|
||||
export type { MeasureMode, ClearHeightDirection, ClearHeightSelectType };
|
||||
|
||||
/**
|
||||
* 距离/标高等“长度类”单位
|
||||
@@ -105,6 +105,18 @@ export interface MeasurePanelOptions {
|
||||
* 说明:用户点击"保存设置"时触发,用于同步到引擎
|
||||
*/
|
||||
onConfigSave?: (config: MeasureConfig) => void;
|
||||
|
||||
/**
|
||||
* 净高朝向变更回调
|
||||
* @param direction 0=朝下,1=朝上
|
||||
*/
|
||||
onClearHeightDirectionChange?: (direction: ClearHeightDirection) => void;
|
||||
|
||||
/**
|
||||
* 净高选择对象变更回调
|
||||
* @param selectType 'point'=选择点,'element'=选择构件
|
||||
*/
|
||||
onClearHeightSelectTypeChange?: (selectType: ClearHeightSelectType) => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@ export class BimRightKey implements IBimComponent {
|
||||
private options?: RightKeyOptions;
|
||||
|
||||
private mouseDownTime: number = 0;
|
||||
private mouseDownX: number = 0;
|
||||
private mouseDownY: number = 0;
|
||||
private readonly CLICK_THRESHOLD: number = 200; // ms
|
||||
private readonly MOVE_THRESHOLD: number = 5; // px
|
||||
|
||||
constructor(options?: RightKeyOptions) {
|
||||
this.options = options;
|
||||
@@ -75,9 +78,11 @@ export class BimRightKey implements IBimComponent {
|
||||
}
|
||||
|
||||
private handleContainerMouseDown = (e: MouseEvent): void => {
|
||||
// 记录右键按下时间 (button 2 是右键)
|
||||
// 记录右键按下时间和位置 (button 2 是右键)
|
||||
if (e.button === 2) {
|
||||
this.mouseDownTime = Date.now();
|
||||
this.mouseDownX = e.clientX;
|
||||
this.mouseDownY = e.clientY;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,6 +96,13 @@ export class BimRightKey implements IBimComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查移动距离,如果鼠标移动超过阈值(用户在拖拽旋转模型),则不触发回调
|
||||
const dx = e.clientX - this.mouseDownX;
|
||||
const dy = e.clientY - this.mouseDownY;
|
||||
if (dx * dx + dy * dy > this.MOVE_THRESHOLD * this.MOVE_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发有效右键回调
|
||||
if (this.options?.onContext) {
|
||||
this.options.onContext(e);
|
||||
|
||||
@@ -119,6 +119,14 @@ export const enUS: TranslationDictionary = {
|
||||
hint: 'Distance, clear distance, clear height and elevation use this unit by default; angle and area use their own units.',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
clearHeight: {
|
||||
direction: 'Direction:',
|
||||
directionDown: 'Down',
|
||||
directionUp: 'Up',
|
||||
selectType: 'Select:',
|
||||
selectPoint: 'Point',
|
||||
selectElement: 'Element',
|
||||
}
|
||||
},
|
||||
sectionPlane: {
|
||||
@@ -217,6 +225,29 @@ export const enUS: TranslationDictionary = {
|
||||
simple: 'Performance',
|
||||
balance: 'Balanced',
|
||||
advanced: 'Quality',
|
||||
}
|
||||
},
|
||||
edgeLine: 'Edge Lines',
|
||||
ground: 'Ground',
|
||||
groundSize: 'Size',
|
||||
groundTypes: {
|
||||
none: 'None',
|
||||
concrete: 'Concrete',
|
||||
grass: 'Grass',
|
||||
tile: 'Tile',
|
||||
water: 'Water',
|
||||
wood: 'Wood',
|
||||
},
|
||||
contrast: 'Contrast',
|
||||
saturation: 'Saturation',
|
||||
lightIntensity: 'Light Intensity',
|
||||
environment: 'Environment',
|
||||
environments: {
|
||||
default: 'Default',
|
||||
outdoor: 'Outdoor',
|
||||
indoor: 'Indoor',
|
||||
night: 'Night',
|
||||
overcast: 'Overcast',
|
||||
studio: 'Studio',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,6 +131,14 @@ export interface TranslationDictionary {
|
||||
save: string;
|
||||
cancel: string;
|
||||
};
|
||||
clearHeight: {
|
||||
direction: string;
|
||||
directionDown: string;
|
||||
directionUp: string;
|
||||
selectType: string;
|
||||
selectPoint: string;
|
||||
selectElement: string;
|
||||
};
|
||||
};
|
||||
sectionPlane: {
|
||||
dialogTitle: string;
|
||||
@@ -233,12 +241,42 @@ export interface TranslationDictionary {
|
||||
};
|
||||
setting: {
|
||||
dialogTitle: string;
|
||||
/** 渲染模式 */
|
||||
renderMode: string;
|
||||
modes: {
|
||||
simple: string;
|
||||
balance: string;
|
||||
advanced: string;
|
||||
};
|
||||
/** 边线 */
|
||||
edgeLine: string;
|
||||
/** 显示地面 */
|
||||
ground: string;
|
||||
groundSize: string;
|
||||
groundTypes: {
|
||||
none: string;
|
||||
concrete: string;
|
||||
grass: string;
|
||||
tile: string;
|
||||
water: string;
|
||||
wood: string;
|
||||
};
|
||||
/** 对比度 */
|
||||
contrast: string;
|
||||
/** 饱和度 */
|
||||
saturation: string;
|
||||
/** 光照强度 */
|
||||
lightIntensity: string;
|
||||
/** 环境背景 */
|
||||
environment: string;
|
||||
environments: {
|
||||
default: string;
|
||||
outdoor: string;
|
||||
indoor: string;
|
||||
night: string;
|
||||
overcast: string;
|
||||
studio: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,14 @@ export const zhCN: TranslationDictionary = {
|
||||
hint: '距离、净距、净高和标高默认使用该单位;角度和面积有各自默认单位。',
|
||||
save: '保存设置',
|
||||
cancel: '取消',
|
||||
},
|
||||
clearHeight: {
|
||||
direction: '朝向:',
|
||||
directionDown: '朝下',
|
||||
directionUp: '朝上',
|
||||
selectType: '选择对象:',
|
||||
selectPoint: '选择点',
|
||||
selectElement: '选择构件',
|
||||
}
|
||||
},
|
||||
sectionPlane: {
|
||||
@@ -217,6 +225,29 @@ export const zhCN: TranslationDictionary = {
|
||||
simple: '性能模式',
|
||||
balance: '平衡模式',
|
||||
advanced: '效果模式',
|
||||
}
|
||||
},
|
||||
edgeLine: '边线',
|
||||
ground: '显示地面',
|
||||
groundSize: '大小',
|
||||
groundTypes: {
|
||||
none: '无',
|
||||
concrete: '混凝土',
|
||||
grass: '草地',
|
||||
tile: '瓷砖',
|
||||
water: '水面',
|
||||
wood: '木地板',
|
||||
},
|
||||
contrast: '对比度',
|
||||
saturation: '饱和度',
|
||||
lightIntensity: '光照强度',
|
||||
environment: '环境背景',
|
||||
environments: {
|
||||
default: '默认',
|
||||
outdoor: '室外',
|
||||
indoor: '室内',
|
||||
night: '夜晚',
|
||||
overcast: '阴天',
|
||||
studio: '工作室',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Engine, type EngineOptions, type ModelLoadOptions, type EngineInfo } from '../components/engine';
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { RightKeyManager } from './right-key-manager';
|
||||
import type { MeasureMode } from '../types/measure';
|
||||
import type { MeasureMode, ClearHeightDirection, ClearHeightSelectType } from '../types/measure';
|
||||
import type { MeasureUnit, MeasurePrecision } from '../components/measure-panel/types';
|
||||
import type { SectionBoxRange } from '../components/section-box-panel/types';
|
||||
import type { MenuItemConfig } from '../components/menu/item';
|
||||
@@ -329,6 +329,28 @@ export class EngineManager extends BaseManager {
|
||||
this.engineInstance.saveMeasureSetting(setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置净高测量朝向
|
||||
* @param direction 0=朝下,1=朝上
|
||||
*/
|
||||
public setClearHeightDirection(direction: ClearHeightDirection): void {
|
||||
if (!this.engineInstance) {
|
||||
return;
|
||||
}
|
||||
this.engineInstance.setClearHeightDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置净高测量选择对象类型
|
||||
* @param selectType 'point'=选择点,'element'=选择构件
|
||||
*/
|
||||
public setClearHeightSelectType(selectType: ClearHeightSelectType): void {
|
||||
if (!this.engineInstance) {
|
||||
return;
|
||||
}
|
||||
this.engineInstance.setClearHeightSelectType(selectType);
|
||||
}
|
||||
|
||||
public activeSection(mode: 'x' | 'y' | 'z' | 'box' | 'face'): void {
|
||||
if (!this.engineInstance) {
|
||||
console.warn('[EngineManager] 3D Engine not initialized.');
|
||||
@@ -467,6 +489,14 @@ export class EngineManager extends BaseManager {
|
||||
this.engineInstance?.toggleMiniMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小地图显示状态
|
||||
* @returns true=显示,false=隐藏
|
||||
*/
|
||||
public getMiniMapState(): boolean {
|
||||
return this.engineInstance?.getMiniMapState() ?? false;
|
||||
}
|
||||
|
||||
public isFirstPersonModeActive(): boolean {
|
||||
return this.engineInstance?.isFirstPersonModeActive() ?? false;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,15 @@ export class MeasureDialogManager extends BaseDialogManager {
|
||||
unit: config.unit,
|
||||
precision: config.precision
|
||||
});
|
||||
}
|
||||
},
|
||||
onClearHeightDirectionChange: (direction) => {
|
||||
console.log('[MeasureDialogManager] 净高朝向切换:', direction);
|
||||
this.registry.engine3d?.setClearHeightDirection(direction);
|
||||
},
|
||||
onClearHeightSelectTypeChange: (selectType) => {
|
||||
console.log('[MeasureDialogManager] 净高选择对象切换:', selectType);
|
||||
this.registry.engine3d?.setClearHeightSelectType(selectType);
|
||||
},
|
||||
});
|
||||
this.panel.init();
|
||||
|
||||
@@ -83,6 +91,10 @@ export class MeasureDialogManager extends BaseDialogManager {
|
||||
protected onDialogCreated(): void {
|
||||
this.dialog?.fitHeight(false);
|
||||
|
||||
// 同步净高默认值到引擎(朝下=0,选择点='point')
|
||||
this.registry.engine3d?.setClearHeightDirection(0);
|
||||
this.registry.engine3d?.setClearHeightSelectType('point');
|
||||
|
||||
const engine = this.registry.engine3d?.getEngine();
|
||||
if (engine?.events) {
|
||||
const handler = (data: EngineMeasureData) => {
|
||||
@@ -95,7 +107,7 @@ export class MeasureDialogManager extends BaseDialogManager {
|
||||
engine.events.on('measure-click', handler);
|
||||
this.unsubscribeMeasureChanged = () => {
|
||||
engine.events.off('measure-changed', handler);
|
||||
engine.events.on('measure-click', handler);
|
||||
engine.events.off('measure-click', handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,81 @@ import { BaseDialogManager } from '../core/base-dialog-manager';
|
||||
import { ManagerRegistry } from '../core/manager-registry';
|
||||
import { t } from '../services/locale';
|
||||
|
||||
/** 渲染模式类型 */
|
||||
type RenderMode = 'simple' | 'balance' | 'advanced';
|
||||
|
||||
/** 地面类型 */
|
||||
type GroundType = 'none' | 'concrete' | 'grass' | 'tile' | 'water' | 'wood';
|
||||
|
||||
/** 环境背景类型 */
|
||||
type EnvironmentType = 'default' | 'outdoor' | 'indoor' | 'night' | 'overcast' | 'studio';
|
||||
|
||||
// ======================== 通用样式常量 ========================
|
||||
// 全部使用项目主题 CSS 变量(--bim-*),同时提供浅色/深色皆可见的 fallback
|
||||
|
||||
/** 分节标签样式 */
|
||||
const SECTION_LABEL_STYLE = 'font-size: 13px; color: var(--bim-text-secondary, #475569); margin-bottom: 8px;';
|
||||
|
||||
/** 分隔线样式 —— 使用 --bim-divider,浅色=#e2e8f0,深色=#334155 */
|
||||
const DIVIDER_STYLE = 'height: 1px; background: var(--bim-divider, #e2e8f0); margin: 14px 0;';
|
||||
|
||||
/** 设置行样式(标签 + 控件水平排列) */
|
||||
const ROW_STYLE = 'display: flex; align-items: center; justify-content: space-between; gap: 12px;';
|
||||
|
||||
/** 滑块输入框右侧数值样式 */
|
||||
const SLIDER_VALUE_STYLE = 'font-size: 12px; color: var(--bim-text-secondary, #475569); min-width: 32px; text-align: right;';
|
||||
|
||||
/** 边框颜色 —— 使用 --bim-border-default */
|
||||
const BORDER_COLOR = 'var(--bim-border-default, #e2e8f0)';
|
||||
|
||||
/** 输入框/下拉背景 —— 使用 --bim-bg-inset */
|
||||
const INPUT_BG = 'var(--bim-bg-inset, #f1f5f9)';
|
||||
|
||||
/** 主色 */
|
||||
const PRIMARY = 'var(--bim-primary, #3b82f6)';
|
||||
|
||||
/** 文字主色 */
|
||||
const TEXT_PRIMARY = 'var(--bim-text-primary, #0f172a)';
|
||||
|
||||
/** 反色文字 */
|
||||
const TEXT_INVERSE = 'var(--bim-text-inverse, #ffffff)';
|
||||
|
||||
/** 悬停背景 */
|
||||
const HOVER_BG = 'var(--bim-component-bg-hover, rgba(0,0,0,0.04))';
|
||||
|
||||
/**
|
||||
* 设置弹窗管理器
|
||||
* 管理全局设置面板,包含渲染模式、边线、地面、对比度/饱和度、光照强度、环境背景等设置项
|
||||
*/
|
||||
export class SettingDialogManager extends BaseDialogManager {
|
||||
protected get dialogId() { return 'setting-dialog'; }
|
||||
protected get dialogTitle() { return 'setting.dialogTitle'; }
|
||||
protected get dialogWidth() { return 280; }
|
||||
protected get dialogWidth() { return 320; }
|
||||
|
||||
// ================ 本地 UI 状态(不持久化) ================
|
||||
|
||||
/** 边线开关状态 */
|
||||
private edgeLineEnabled: boolean = false;
|
||||
/** 当前选中的地面类型 */
|
||||
private groundType: GroundType = 'none';
|
||||
/** 地面大小 */
|
||||
private groundSize: number = 100;
|
||||
/** 对比度(0-200,100为默认值) */
|
||||
private contrastValue: number = 100;
|
||||
/** 饱和度(0-200,100为默认值) */
|
||||
private saturationValue: number = 100;
|
||||
/** 光照强度(0-200,100为默认值) */
|
||||
private lightIntensityValue: number = 100;
|
||||
/** 当前选中的环境背景 */
|
||||
private environmentType: EnvironmentType = 'default';
|
||||
|
||||
constructor(registry: ManagerRegistry) {
|
||||
super(registry);
|
||||
}
|
||||
|
||||
public init(): void {}
|
||||
public init(): void { }
|
||||
|
||||
/** 对话框居中显示 */
|
||||
protected getDialogPosition() {
|
||||
const container = this.registry.container;
|
||||
if (!container) return { x: 100, y: 100 };
|
||||
@@ -24,84 +86,421 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
|
||||
return {
|
||||
x: (containerWidth - this.dialogWidth) / 2,
|
||||
y: (containerHeight - 200) / 2
|
||||
y: Math.max(20, (containerHeight - 520) / 2)
|
||||
};
|
||||
}
|
||||
|
||||
// ======================== 主内容构建 ========================
|
||||
|
||||
protected createContent(): HTMLElement {
|
||||
const currentMode = this.registry.engine3d?.getRenderMode() ?? 'balance';
|
||||
// 注入一次全局滑块样式
|
||||
this.ensureSliderStyle();
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = 'padding: 16px;';
|
||||
content.style.cssText = 'padding: 16px; max-height: 480px; overflow-y: auto;';
|
||||
|
||||
// 渲染模式标题
|
||||
// 1. 渲染模式(按钮组)
|
||||
content.appendChild(this.createRenderModeSection());
|
||||
content.appendChild(this.createDivider());
|
||||
|
||||
// 2. 边线开关
|
||||
content.appendChild(this.createEdgeLineSection());
|
||||
content.appendChild(this.createDivider());
|
||||
|
||||
// 3. 光照强度
|
||||
content.appendChild(this.createSliderSection(
|
||||
t('setting.lightIntensity'),
|
||||
this.lightIntensityValue,
|
||||
0, 200,
|
||||
(v) => { this.lightIntensityValue = v; }
|
||||
));
|
||||
|
||||
// 4. 对比度
|
||||
content.appendChild(this.createSliderSection(
|
||||
t('setting.contrast'),
|
||||
this.contrastValue,
|
||||
0, 200,
|
||||
(v) => { this.contrastValue = v; }
|
||||
));
|
||||
|
||||
// 5. 饱和度
|
||||
content.appendChild(this.createSliderSection(
|
||||
t('setting.saturation'),
|
||||
this.saturationValue,
|
||||
0, 200,
|
||||
(v) => { this.saturationValue = v; }
|
||||
));
|
||||
content.appendChild(this.createDivider());
|
||||
|
||||
// 6. 环境背景(下拉)
|
||||
content.appendChild(this.createEnvironmentSection());
|
||||
content.appendChild(this.createDivider());
|
||||
|
||||
// 7. 显示地面(下拉 + 大小输入)
|
||||
content.appendChild(this.createGroundSection());
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// ======================== 分隔线 ========================
|
||||
|
||||
/** 创建分隔线 */
|
||||
private createDivider(): HTMLElement {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = DIVIDER_STYLE;
|
||||
return div;
|
||||
}
|
||||
|
||||
// ======================== 1. 渲染模式(按钮组) ========================
|
||||
|
||||
/** 创建渲染模式按钮组 */
|
||||
private createRenderModeSection(): HTMLElement {
|
||||
const currentMode = (this.registry.engine3d?.getRenderMode() ?? 'balance') as RenderMode;
|
||||
|
||||
const section = document.createElement('div');
|
||||
|
||||
// 标签
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #94a3b8); margin-bottom: 12px;';
|
||||
label.style.cssText = SECTION_LABEL_STYLE;
|
||||
label.textContent = t('setting.renderMode');
|
||||
content.appendChild(label);
|
||||
section.appendChild(label);
|
||||
|
||||
// 按钮组容器
|
||||
const group = document.createElement('div');
|
||||
group.style.cssText = `
|
||||
display: flex; gap: 0; border-radius: 6px; overflow: hidden;
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
`;
|
||||
|
||||
// 三个模式选项
|
||||
const modes: { key: RenderMode; labelKey: string }[] = [
|
||||
{ key: 'simple', labelKey: 'setting.modes.simple' },
|
||||
{ key: 'balance', labelKey: 'setting.modes.balance' },
|
||||
{ key: 'advanced', labelKey: 'setting.modes.advanced' },
|
||||
];
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.style.cssText = 'display: flex; flex-direction: column; gap: 4px;';
|
||||
for (let i = 0; i < modes.length; i++) {
|
||||
const mode = modes[i];
|
||||
const isActive = mode.key === currentMode;
|
||||
|
||||
for (const mode of modes) {
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = `
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; border-radius: 6px; cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
background: ${mode.key === currentMode ? 'var(--bim-primary, #3b82f6)' : 'transparent'};
|
||||
color: ${mode.key === currentMode ? 'var(--bim-text-inverse, #fff)' : 'var(--bim-text-primary, #fff)'};
|
||||
const btn = document.createElement('div');
|
||||
btn.style.cssText = `
|
||||
flex: 1; text-align: center; padding: 7px 0; cursor: pointer;
|
||||
font-size: 13px; font-weight: 500; transition: all 0.15s;
|
||||
background: ${isActive ? PRIMARY : 'transparent'};
|
||||
color: ${isActive ? TEXT_INVERSE : TEXT_PRIMARY};
|
||||
${i < modes.length - 1 ? `border-right: 1px solid ${BORDER_COLOR};` : ''}
|
||||
`;
|
||||
btn.textContent = t(mode.labelKey);
|
||||
|
||||
// 圆点指示器
|
||||
const dot = document.createElement('div');
|
||||
dot.style.cssText = `
|
||||
width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
|
||||
border: 2px solid ${mode.key === currentMode ? 'var(--bim-text-inverse, #fff)' : 'var(--bim-text-tertiary, #64748b)'};
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
`;
|
||||
if (mode.key === currentMode) {
|
||||
const inner = document.createElement('div');
|
||||
inner.style.cssText = 'width: 8px; height: 8px; border-radius: 50%; background: var(--bim-text-inverse, #fff);';
|
||||
dot.appendChild(inner);
|
||||
}
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.style.cssText = 'font-size: 14px; font-weight: 500;';
|
||||
text.textContent = t(mode.labelKey);
|
||||
|
||||
item.appendChild(dot);
|
||||
item.appendChild(text);
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
if (mode.key !== currentMode) {
|
||||
item.style.background = 'var(--bim-floating-btn-bg, rgba(255,255,255,0.08))';
|
||||
}
|
||||
// 悬停效果
|
||||
btn.addEventListener('mouseenter', () => {
|
||||
if (!isActive) btn.style.background = HOVER_BG;
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
if (mode.key !== currentMode) {
|
||||
item.style.background = 'transparent';
|
||||
}
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
if (!isActive) btn.style.background = 'transparent';
|
||||
});
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.registry.engine3d?.setRenderMode(mode.key);
|
||||
// 重新渲染弹窗内容
|
||||
// 重建弹窗以刷新状态
|
||||
this.hide();
|
||||
this.show();
|
||||
});
|
||||
|
||||
group.appendChild(item);
|
||||
group.appendChild(btn);
|
||||
}
|
||||
|
||||
content.appendChild(group);
|
||||
return content;
|
||||
section.appendChild(group);
|
||||
return section;
|
||||
}
|
||||
|
||||
// ======================== 2. 边线开关 ========================
|
||||
|
||||
/** 创建边线开关行 */
|
||||
private createEdgeLineSection(): HTMLElement {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = ROW_STYLE;
|
||||
|
||||
// 标签
|
||||
const label = document.createElement('span');
|
||||
label.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);';
|
||||
label.textContent = t('setting.edgeLine');
|
||||
row.appendChild(label);
|
||||
|
||||
// 开关容器 —— 关闭态使用 --bim-border-strong 保证浅色主题可见
|
||||
const toggleOffBg = 'var(--bim-border-strong, #cbd5e1)';
|
||||
const toggleOnBg = PRIMARY;
|
||||
|
||||
const toggle = document.createElement('div');
|
||||
toggle.style.cssText = `
|
||||
width: 40px; height: 22px; border-radius: 11px; cursor: pointer;
|
||||
position: relative; transition: background 0.2s;
|
||||
background: ${this.edgeLineEnabled ? toggleOnBg : toggleOffBg};
|
||||
`;
|
||||
|
||||
// 滑块圆点
|
||||
const knob = document.createElement('div');
|
||||
knob.style.cssText = `
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
background: #fff; position: absolute; top: 2px;
|
||||
transition: left 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
left: ${this.edgeLineEnabled ? '20px' : '2px'};
|
||||
`;
|
||||
toggle.appendChild(knob);
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
this.edgeLineEnabled = !this.edgeLineEnabled;
|
||||
toggle.style.background = this.edgeLineEnabled ? toggleOnBg : toggleOffBg;
|
||||
knob.style.left = this.edgeLineEnabled ? '20px' : '2px';
|
||||
});
|
||||
|
||||
row.appendChild(toggle);
|
||||
return row;
|
||||
}
|
||||
|
||||
// ======================== 3/4/5. 通用滑块 ========================
|
||||
|
||||
/**
|
||||
* 创建一个滑块设置区块(标签 + 滑块 + 数值)
|
||||
* @param labelText 标签文本
|
||||
* @param value 当前值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @param onChange 值变化回调
|
||||
*/
|
||||
private createSliderSection(
|
||||
labelText: string,
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
onChange: (val: number) => void
|
||||
): HTMLElement {
|
||||
const section = document.createElement('div');
|
||||
section.style.cssText = 'margin-bottom: 4px;';
|
||||
|
||||
// 上部:标签 + 数值
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = ROW_STYLE + ' margin-bottom: 6px;';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);';
|
||||
label.textContent = labelText;
|
||||
header.appendChild(label);
|
||||
|
||||
const valueDisplay = document.createElement('span');
|
||||
valueDisplay.style.cssText = SLIDER_VALUE_STYLE;
|
||||
valueDisplay.textContent = String(value);
|
||||
header.appendChild(valueDisplay);
|
||||
|
||||
section.appendChild(header);
|
||||
|
||||
// 滑块 —— 轨道使用 --bim-border-strong 保证在浅色/深色下都可见
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = String(min);
|
||||
slider.max = String(max);
|
||||
slider.value = String(value);
|
||||
slider.className = 'bim-setting-slider';
|
||||
slider.style.cssText = `
|
||||
width: 100%; height: 4px; -webkit-appearance: none; appearance: none;
|
||||
background: var(--bim-border-strong, #cbd5e1); border-radius: 2px;
|
||||
outline: none; cursor: pointer;
|
||||
`;
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
const v = Number(slider.value);
|
||||
valueDisplay.textContent = String(v);
|
||||
onChange(v);
|
||||
});
|
||||
|
||||
section.appendChild(slider);
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入全局滑块拇指样式(只注入一次)
|
||||
* 使用 ::-webkit-slider-thumb / ::-moz-range-thumb 伪元素
|
||||
*/
|
||||
private ensureSliderStyle(): void {
|
||||
if (document.querySelector('#bim-setting-slider-style')) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'bim-setting-slider-style';
|
||||
style.textContent = `
|
||||
.bim-setting-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; appearance: none;
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: var(--bim-primary, #3b82f6);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--bim-bg-elevated, #fff);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
.bim-setting-slider::-moz-range-thumb {
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: var(--bim-primary, #3b82f6);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--bim-bg-elevated, #fff);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
.bim-setting-slider::-moz-range-track {
|
||||
background: var(--bim-border-strong, #cbd5e1);
|
||||
height: 4px; border-radius: 2px; border: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// ======================== 6. 环境背景(下拉) ========================
|
||||
|
||||
/** 创建环境背景下拉选择区 */
|
||||
private createEnvironmentSection(): HTMLElement {
|
||||
const section = document.createElement('div');
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = SECTION_LABEL_STYLE;
|
||||
label.textContent = t('setting.environment');
|
||||
section.appendChild(label);
|
||||
|
||||
const envKeys: EnvironmentType[] = ['default', 'outdoor', 'indoor', 'night', 'overcast', 'studio'];
|
||||
|
||||
const select = this.createSelect(
|
||||
envKeys.map(key => ({
|
||||
value: key,
|
||||
label: t(`setting.environments.${key}` as any)
|
||||
})),
|
||||
this.environmentType,
|
||||
(val) => { this.environmentType = val as EnvironmentType; }
|
||||
);
|
||||
|
||||
section.appendChild(select);
|
||||
return section;
|
||||
}
|
||||
|
||||
// ======================== 7. 显示地面(下拉 + 大小) ========================
|
||||
|
||||
/** 创建地面设置区 */
|
||||
private createGroundSection(): HTMLElement {
|
||||
const section = document.createElement('div');
|
||||
|
||||
// 地面类型标签 + 下拉
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = SECTION_LABEL_STYLE;
|
||||
label.textContent = t('setting.ground');
|
||||
section.appendChild(label);
|
||||
|
||||
const groundKeys: GroundType[] = ['none', 'concrete', 'grass', 'tile', 'water', 'wood'];
|
||||
|
||||
const select = this.createSelect(
|
||||
groundKeys.map(key => ({
|
||||
value: key,
|
||||
label: t(`setting.groundTypes.${key}` as any)
|
||||
})),
|
||||
this.groundType,
|
||||
(val) => {
|
||||
this.groundType = val as GroundType;
|
||||
// 显示/隐藏大小输入
|
||||
sizeRow.style.display = val === 'none' ? 'none' : 'flex';
|
||||
}
|
||||
);
|
||||
section.appendChild(select);
|
||||
|
||||
// 大小输入行
|
||||
const sizeRow = document.createElement('div');
|
||||
sizeRow.style.cssText = ROW_STYLE + ' margin-top: 10px;';
|
||||
sizeRow.style.display = this.groundType === 'none' ? 'none' : 'flex';
|
||||
|
||||
const sizeLabel = document.createElement('span');
|
||||
sizeLabel.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);';
|
||||
sizeLabel.textContent = t('setting.groundSize');
|
||||
sizeRow.appendChild(sizeLabel);
|
||||
|
||||
const sizeInput = document.createElement('input');
|
||||
sizeInput.type = 'number';
|
||||
sizeInput.value = String(this.groundSize);
|
||||
sizeInput.min = '1';
|
||||
sizeInput.max = '10000';
|
||||
sizeInput.style.cssText = `
|
||||
width: 80px; padding: 4px 8px; border-radius: 4px;
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
background: ${INPUT_BG};
|
||||
color: ${TEXT_PRIMARY};
|
||||
font-size: 13px; outline: none; text-align: right;
|
||||
`;
|
||||
sizeInput.addEventListener('input', () => {
|
||||
const v = Number(sizeInput.value);
|
||||
if (!isNaN(v) && v > 0) {
|
||||
this.groundSize = v;
|
||||
}
|
||||
});
|
||||
// 聚焦时高亮边框
|
||||
sizeInput.addEventListener('focus', () => {
|
||||
sizeInput.style.borderColor = PRIMARY;
|
||||
});
|
||||
sizeInput.addEventListener('blur', () => {
|
||||
sizeInput.style.borderColor = BORDER_COLOR;
|
||||
});
|
||||
|
||||
sizeRow.appendChild(sizeInput);
|
||||
section.appendChild(sizeRow);
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
// ======================== 通用下拉框 ========================
|
||||
|
||||
/**
|
||||
* 创建一个自定义样式的 <select> 下拉框
|
||||
* @param options 选项列表
|
||||
* @param currentValue 当前选中值
|
||||
* @param onChange 选中变化回调
|
||||
*/
|
||||
private createSelect(
|
||||
options: { value: string; label: string }[],
|
||||
currentValue: string,
|
||||
onChange: (val: string) => void
|
||||
): HTMLElement {
|
||||
const select = document.createElement('select');
|
||||
select.style.cssText = `
|
||||
width: 100%; padding: 7px 10px; border-radius: 6px;
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
background: ${INPUT_BG};
|
||||
color: ${TEXT_PRIMARY};
|
||||
font-size: 13px; outline: none; cursor: pointer;
|
||||
-webkit-appearance: none; appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
padding-right: 28px;
|
||||
`;
|
||||
|
||||
for (const opt of options) {
|
||||
const option = document.createElement('option');
|
||||
option.value = opt.value;
|
||||
option.textContent = opt.label;
|
||||
// option 背景适配:使用 --bim-bg-elevated 保证深浅主题均可读
|
||||
option.style.cssText = `
|
||||
background: var(--bim-bg-elevated, #ffffff);
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
`;
|
||||
if (opt.value === currentValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
onChange(select.value);
|
||||
});
|
||||
|
||||
// 聚焦高亮
|
||||
select.addEventListener('focus', () => {
|
||||
select.style.borderColor = PRIMARY;
|
||||
});
|
||||
select.addEventListener('blur', () => {
|
||||
select.style.borderColor = BORDER_COLOR;
|
||||
});
|
||||
|
||||
return select;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user