Files
bim_engine/src/managers/setting-dialog-manager.ts
yuding b96e5f3262 refactor: slim down EngineManager from 861 to 290 lines by removing passthrough proxy pattern
- EngineManager now only exposes public SDK API (initialize, loadModel, pause/resumeRendering, getEngineComponent, destroy)
- Internal managers access Engine component directly via this.engineComponent getter on BaseManager
- Non-manager components use registry.engine3d.getEngineComponent() for direct Engine access
- Replaced getEngine() with onRawEvent()/offRawEvent() for raw engine event access
- Migrated 62 call sites across 13 files (9 managers, 1 panel, 3 toolbar buttons)
- Updated all architecture docs, API docs, and README to reflect new patterns
2026-03-05 11:15:57 +08:00

606 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BaseDialogManager } from '../core/base-dialog-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { t } from '../services/locale';
/** 渲染模式类型 */
type RenderMode = 'simple' | 'balance' | 'advanced';
/** 设置项列表项(环境背景/地面类型由底层引擎动态返回) */
type SettingListItem = { name: string; id: string };
// ======================== 通用样式常量 ========================
// 全部使用项目主题 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 320; }
// ================ 本地 UI 状态(不持久化) ================
/** 边线开关状态 */
private edgeLineEnabled: boolean = false;
/** 对比度0-10050为默认值 */
private contrastValue: number = 50;
/** 饱和度0-10050为默认值 */
private saturationValue: number = 50;
/** 光照强度0-10050为默认值 */
private lightIntensityValue: number = 50;
/** 环境背景列表(从底层引擎动态获取) */
private environmentList: SettingListItem[] = [];
/** 当前选中的环境背景 ID */
private environmentId: string = '';
/** 是否显示背景 */
private backgroundVisible: boolean = true;
/** 地面列表(从底层引擎动态获取) */
private groundList: SettingListItem[] = [];
/** 当前选中的地面 ID */
private groundId: string = '';
/** 地面高度单位m */
private groundElevation: number = 0;
/** 保存的对话框位置(用于 hide/show 时保持位置不变) */
private savedPosition: { x: number; y: number } | null = null;
constructor(registry: ManagerRegistry) {
super(registry);
}
public init(): void { }
/** 对话框位置:若有保存的位置则恢复,否则居中 */
protected getDialogPosition() {
// 若上次 hide/show 保存了位置,则恢复到原位
if (this.savedPosition) {
const pos = this.savedPosition;
this.savedPosition = null;
return pos;
}
const container = this.registry.container;
if (!container) return { x: 100, y: 100 };
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
return {
x: (containerWidth - this.dialogWidth) / 2,
y: Math.max(20, (containerHeight - 520) / 2)
};
}
/**
* 从底层引擎加载当前状态(回显)
* 在 createContent() 前调用,保证 UI 和引擎状态同步
*/
private loadEngineState(): void {
const engine = this.engineComponent;
if (!engine) return;
// 边线
this.edgeLineEnabled = engine.getModelEdgeActive();
// 光照 / 对比度 / 饱和度
this.lightIntensityValue = engine.getAmbientLightIntensity();
this.contrastValue = engine.getSceneContrast();
this.saturationValue = engine.getSceneSaturation();
// 环境背景
this.environmentList = engine.getHDRBackgroundList();
this.environmentId = engine.getHDRBackgroundId();
this.backgroundVisible = engine.getHDRBackgroundVisibility();
// 地面
this.groundList = engine.getGroundList();
this.groundId = engine.getGroundId();
this.groundElevation = engine.getGroundElevation();
}
// ======================== 主内容构建 ========================
protected createContent(): HTMLElement {
// 从引擎加载当前状态(回显)
this.loadEngineState();
// 注入一次全局滑块样式
this.ensureSliderStyle();
const content = document.createElement('div');
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, 100,
(v) => {
this.lightIntensityValue = v;
this.engineComponent?.setAmbientLightIntensity(v);
}
));
// 4. 对比度
content.appendChild(this.createSliderSection(
t('setting.contrast'),
this.contrastValue,
0, 100,
(v) => {
this.contrastValue = v;
this.engineComponent?.setSceneContrast(v);
}
));
// 5. 饱和度
content.appendChild(this.createSliderSection(
t('setting.saturation'),
this.saturationValue,
0, 100,
(v) => {
this.saturationValue = v;
this.engineComponent?.setSceneSaturation(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.engineComponent?.getRenderMode() ?? 'balance') as RenderMode;
const section = document.createElement('div');
// 标签
const label = document.createElement('div');
label.style.cssText = SECTION_LABEL_STYLE;
label.textContent = t('setting.renderMode');
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' },
];
for (let i = 0; i < modes.length; i++) {
const mode = modes[i];
const isActive = mode.key === currentMode;
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);
// 悬停效果
btn.addEventListener('mouseenter', () => {
if (!isActive) btn.style.background = HOVER_BG;
});
btn.addEventListener('mouseleave', () => {
if (!isActive) btn.style.background = 'transparent';
});
btn.addEventListener('click', () => {
this.engineComponent?.setRenderMode(mode.key);
// 保存当前对话框位置,重建后恢复
const el = document.getElementById(this.dialogId);
if (el) {
this.savedPosition = { x: el.offsetLeft, y: el.offsetTop };
}
this.hide();
this.show();
});
group.appendChild(btn);
}
section.appendChild(group);
return section;
}
// ======================== 2. 边线开关 ========================
/** 创建边线开关行 */
private createEdgeLineSection(): HTMLElement {
return this.createToggleRow(
t('setting.edgeLine'),
this.edgeLineEnabled,
(enabled) => {
this.edgeLineEnabled = enabled;
if (enabled) {
this.engineComponent?.activeModelEdge();
} else {
this.engineComponent?.disActiveModelEdge();
}
}
);
}
// ======================== 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 select = this.createSelect(
this.environmentList.map(item => ({ value: item.id, label: item.name })),
this.environmentId,
(val) => {
this.environmentId = val;
this.engineComponent?.setHDRBackgroundId(val);
}
);
section.appendChild(select);
// 显示背景开关
const toggleRow = this.createToggleRow(
t('setting.backgroundVisible'),
this.backgroundVisible,
(enabled) => {
this.backgroundVisible = enabled;
this.engineComponent?.setHDRBackgroundVisibility(enabled);
}
);
toggleRow.style.marginTop = '10px';
section.appendChild(toggleRow);
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 select = this.createSelect(
this.groundList.map(item => ({ value: item.id, label: item.name })),
this.groundId,
(val) => {
this.groundId = val;
this.engineComponent?.setGroundId(val);
}
);
section.appendChild(select);
// 地面高度输入行
const elevationRow = document.createElement('div');
elevationRow.style.cssText = ROW_STYLE + ' margin-top: 10px;';
const elevationLabel = document.createElement('span');
elevationLabel.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);';
elevationLabel.textContent = t('setting.groundElevation');
elevationRow.appendChild(elevationLabel);
// 输入框 + 单位容器
const inputWrapper = document.createElement('div');
inputWrapper.style.cssText = 'display: flex; align-items: center; gap: 4px;';
const elevationInput = document.createElement('input');
elevationInput.type = 'number';
elevationInput.value = String(this.groundElevation);
elevationInput.style.cssText = `
width: 72px; 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;
`;
elevationInput.addEventListener('input', () => {
const v = Number(elevationInput.value);
if (!isNaN(v)) {
this.groundElevation = v;
this.engineComponent?.setGroundElevation(v);
}
});
elevationInput.addEventListener('focus', () => {
elevationInput.style.borderColor = PRIMARY;
});
elevationInput.addEventListener('blur', () => {
elevationInput.style.borderColor = BORDER_COLOR;
});
const unitLabel = document.createElement('span');
unitLabel.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);';
unitLabel.textContent = t('setting.groundElevationUnit');
inputWrapper.appendChild(elevationInput);
inputWrapper.appendChild(unitLabel);
elevationRow.appendChild(inputWrapper);
section.appendChild(elevationRow);
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);
});
// 聚焦<E8819A><E784A6>
select.addEventListener('focus', () => {
select.style.borderColor = PRIMARY;
});
select.addEventListener('blur', () => {
select.style.borderColor = BORDER_COLOR;
});
return select;
}
// ======================== 通用开关行 ========================
/**
* 创建一个标签 + 开关的行
* @param labelText 标签文本
* @param enabled 当前开关状态
* @param onChange 状态变化回调
*/
private createToggleRow(
labelText: string,
enabled: boolean,
onChange: (enabled: boolean) => void
): 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 = labelText;
row.appendChild(label);
// 开关容器
const toggleOffBg = 'var(--bim-border-strong, #cbd5e1)';
const toggleOnBg = PRIMARY;
let currentState = enabled;
const toggle = document.createElement('div');
toggle.style.cssText = `
width: 40px; height: 22px; border-radius: 11px; cursor: pointer;
position: relative; transition: background 0.2s;
background: ${currentState ? 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: ${currentState ? '20px' : '2px'};
`;
toggle.appendChild(knob);
toggle.addEventListener('click', () => {
currentState = !currentState;
toggle.style.background = currentState ? toggleOnBg : toggleOffBg;
knob.style.left = currentState ? '20px' : '2px';
onChange(currentState);
});
row.appendChild(toggle);
return row;
}
}