Files
bim_engine/src/managers/setting-dialog-manager.ts
yuding c3bd82c03a 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
2026-03-04 16:40:35 +08:00

507 lines
18 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 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 320; }
// ================ 本地 UI 状态(不持久化) ================
/** 边线开关状态 */
private edgeLineEnabled: boolean = false;
/** 当前选中的地面类型 */
private groundType: GroundType = 'none';
/** 地面大小 */
private groundSize: number = 100;
/** 对比度0-200100为默认值 */
private contrastValue: number = 100;
/** 饱和度0-200100为默认值 */
private saturationValue: number = 100;
/** 光照强度0-200100为默认值 */
private lightIntensityValue: number = 100;
/** 当前选中的环境背景 */
private environmentType: EnvironmentType = 'default';
constructor(registry: ManagerRegistry) {
super(registry);
}
public init(): void { }
/** 对话框居中显示 */
protected getDialogPosition() {
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)
};
}
// ======================== 主内容构建 ========================
protected createContent(): HTMLElement {
// 注入一次全局滑块样式
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, 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 = 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.registry.engine3d?.setRenderMode(mode.key);
// 重建弹窗以刷新状态
this.hide();
this.show();
});
group.appendChild(btn);
}
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;
}
}