Files
bim_engine/src/managers/setting-dialog-manager.ts

507 lines
18 KiB
TypeScript
Raw Normal View History

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;
}
}