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-100,50为默认值) */ private contrastValue: number = 50; /** 饱和度(0-100,50为默认值) */ private saturationValue: number = 50; /** 光照强度(0-100,50为默认值) */ 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; } // ======================== 通用下拉框 ======================== /** * 创建一个自定义样式的