2026-02-28 11:26:59 +08:00
|
|
|
|
import { BaseDialogManager } from '../core/base-dialog-manager';
|
|
|
|
|
|
import { ManagerRegistry } from '../core/manager-registry';
|
|
|
|
|
|
import { t } from '../services/locale';
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
/** 渲染模式类型 */
|
2026-02-28 11:26:59 +08:00
|
|
|
|
type RenderMode = 'simple' | 'balance' | 'advanced';
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
/** 设置项列表项(环境背景/地面类型由底层引擎动态返回) */
|
|
|
|
|
|
type SettingListItem = { name: string; id: string };
|
2026-03-04 16:40:35 +08:00
|
|
|
|
|
|
|
|
|
|
// ======================== 通用样式常量 ========================
|
|
|
|
|
|
// 全部使用项目主题 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))';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置弹窗管理器
|
2026-03-05 11:15:57 +08:00
|
|
|
|
* 管理全局设置面板,包含渲染模式、边线、光照/对比度/饱和度、环境背景、地面等设置项
|
2026-03-04 16:40:35 +08:00
|
|
|
|
*/
|
2026-02-28 11:26:59 +08:00
|
|
|
|
export class SettingDialogManager extends BaseDialogManager {
|
|
|
|
|
|
protected get dialogId() { return 'setting-dialog'; }
|
|
|
|
|
|
protected get dialogTitle() { return 'setting.dialogTitle'; }
|
2026-03-04 16:40:35 +08:00
|
|
|
|
protected get dialogWidth() { return 320; }
|
|
|
|
|
|
|
|
|
|
|
|
// ================ 本地 UI 状态(不持久化) ================
|
|
|
|
|
|
|
|
|
|
|
|
/** 边线开关状态 */
|
|
|
|
|
|
private edgeLineEnabled: boolean = false;
|
2026-03-05 11:15:57 +08:00
|
|
|
|
/** 对比度(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;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
|
|
|
|
|
constructor(registry: ManagerRegistry) {
|
|
|
|
|
|
super(registry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
public init(): void { }
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
/** 对话框位置:若有保存的位置则恢复,否则居中 */
|
2026-02-28 11:26:59 +08:00
|
|
|
|
protected getDialogPosition() {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 若上次 hide/show 保存了位置,则恢复到原位
|
|
|
|
|
|
if (this.savedPosition) {
|
|
|
|
|
|
const pos = this.savedPosition;
|
|
|
|
|
|
this.savedPosition = null;
|
|
|
|
|
|
return pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 11:26:59 +08:00
|
|
|
|
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,
|
2026-03-04 16:40:35 +08:00
|
|
|
|
y: Math.max(20, (containerHeight - 520) / 2)
|
2026-02-28 11:26:59 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从底层引擎加载当前状态(回显)
|
|
|
|
|
|
* 在 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
// ======================== 主内容构建 ========================
|
|
|
|
|
|
|
2026-02-28 11:26:59 +08:00
|
|
|
|
protected createContent(): HTMLElement {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 从引擎加载当前状态(回显)
|
|
|
|
|
|
this.loadEngineState();
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
// 注入一次全局滑块样式
|
|
|
|
|
|
this.ensureSliderStyle();
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
|
|
|
|
|
const content = document.createElement('div');
|
2026-03-04 16:40:35 +08:00
|
|
|
|
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,
|
2026-03-05 11:15:57 +08:00
|
|
|
|
0, 100,
|
|
|
|
|
|
(v) => {
|
|
|
|
|
|
this.lightIntensityValue = v;
|
|
|
|
|
|
this.engineComponent?.setAmbientLightIntensity(v);
|
|
|
|
|
|
}
|
2026-03-04 16:40:35 +08:00
|
|
|
|
));
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
// 4. 对比度
|
|
|
|
|
|
content.appendChild(this.createSliderSection(
|
|
|
|
|
|
t('setting.contrast'),
|
|
|
|
|
|
this.contrastValue,
|
2026-03-05 11:15:57 +08:00
|
|
|
|
0, 100,
|
|
|
|
|
|
(v) => {
|
|
|
|
|
|
this.contrastValue = v;
|
|
|
|
|
|
this.engineComponent?.setSceneContrast(v);
|
|
|
|
|
|
}
|
2026-03-04 16:40:35 +08:00
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 饱和度
|
|
|
|
|
|
content.appendChild(this.createSliderSection(
|
|
|
|
|
|
t('setting.saturation'),
|
|
|
|
|
|
this.saturationValue,
|
2026-03-05 11:15:57 +08:00
|
|
|
|
0, 100,
|
|
|
|
|
|
(v) => {
|
|
|
|
|
|
this.saturationValue = v;
|
|
|
|
|
|
this.engineComponent?.setSceneSaturation(v);
|
|
|
|
|
|
}
|
2026-03-04 16:40:35 +08:00
|
|
|
|
));
|
|
|
|
|
|
content.appendChild(this.createDivider());
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 6. 环境背景(动态列表 + 显示开关)
|
2026-03-04 16:40:35 +08:00
|
|
|
|
content.appendChild(this.createEnvironmentSection());
|
|
|
|
|
|
content.appendChild(this.createDivider());
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 7. 地面(动态列表 + 地面高度)
|
2026-03-04 16:40:35 +08:00
|
|
|
|
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 {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
const currentMode = (this.engineComponent?.getRenderMode() ?? 'balance') as RenderMode;
|
2026-03-04 16:40:35 +08:00
|
|
|
|
|
|
|
|
|
|
const section = document.createElement('div');
|
|
|
|
|
|
|
|
|
|
|
|
// 标签
|
2026-02-28 11:26:59 +08:00
|
|
|
|
const label = document.createElement('div');
|
2026-03-04 16:40:35 +08:00
|
|
|
|
label.style.cssText = SECTION_LABEL_STYLE;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
label.textContent = t('setting.renderMode');
|
2026-03-04 16:40:35 +08:00
|
|
|
|
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};
|
|
|
|
|
|
`;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
|
|
|
|
|
const modes: { key: RenderMode; labelKey: string }[] = [
|
|
|
|
|
|
{ key: 'simple', labelKey: 'setting.modes.simple' },
|
|
|
|
|
|
{ key: 'balance', labelKey: 'setting.modes.balance' },
|
|
|
|
|
|
{ key: 'advanced', labelKey: 'setting.modes.advanced' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
for (let i = 0; i < modes.length; i++) {
|
|
|
|
|
|
const mode = modes[i];
|
|
|
|
|
|
const isActive = mode.key === currentMode;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
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};` : ''}
|
2026-02-28 11:26:59 +08:00
|
|
|
|
`;
|
2026-03-04 16:40:35 +08:00
|
|
|
|
btn.textContent = t(mode.labelKey);
|
2026-02-28 11:26:59 +08:00
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
// 悬停效果
|
|
|
|
|
|
btn.addEventListener('mouseenter', () => {
|
|
|
|
|
|
if (!isActive) btn.style.background = HOVER_BG;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
});
|
2026-03-04 16:40:35 +08:00
|
|
|
|
btn.addEventListener('mouseleave', () => {
|
|
|
|
|
|
if (!isActive) btn.style.background = 'transparent';
|
2026-02-28 11:26:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
btn.addEventListener('click', () => {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
this.engineComponent?.setRenderMode(mode.key);
|
|
|
|
|
|
// 保存当前对话框位置,重建后恢复
|
|
|
|
|
|
const el = document.getElementById(this.dialogId);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
this.savedPosition = { x: el.offsetLeft, y: el.offsetTop };
|
|
|
|
|
|
}
|
2026-02-28 11:26:59 +08:00
|
|
|
|
this.hide();
|
|
|
|
|
|
this.show();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
group.appendChild(btn);
|
2026-02-28 11:26:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
section.appendChild(group);
|
|
|
|
|
|
return section;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ======================== 2. 边线开关 ========================
|
|
|
|
|
|
|
|
|
|
|
|
/** 创建边线开关行 */
|
|
|
|
|
|
private createEdgeLineSection(): HTMLElement {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
return this.createToggleRow(
|
|
|
|
|
|
t('setting.edgeLine'),
|
|
|
|
|
|
this.edgeLineEnabled,
|
|
|
|
|
|
(enabled) => {
|
|
|
|
|
|
this.edgeLineEnabled = enabled;
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
this.engineComponent?.activeModelEdge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.engineComponent?.disActiveModelEdge();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-03-04 16:40:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ======================== 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// ======================== 6. 环境背景(动态列表 + 显示开关) ========================
|
2026-03-04 16:40:35 +08:00
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
/** 创建环境背景区 */
|
2026-03-04 16:40:35 +08:00
|
|
|
|
private createEnvironmentSection(): HTMLElement {
|
|
|
|
|
|
const section = document.createElement('div');
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 标签
|
2026-03-04 16:40:35 +08:00
|
|
|
|
const label = document.createElement('div');
|
|
|
|
|
|
label.style.cssText = SECTION_LABEL_STYLE;
|
|
|
|
|
|
label.textContent = t('setting.environment');
|
|
|
|
|
|
section.appendChild(label);
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 下拉列表(从底层引擎动态获取)
|
2026-03-04 16:40:35 +08:00
|
|
|
|
const select = this.createSelect(
|
2026-03-05 11:15:57 +08:00
|
|
|
|
this.environmentList.map(item => ({ value: item.id, label: item.name })),
|
|
|
|
|
|
this.environmentId,
|
|
|
|
|
|
(val) => {
|
|
|
|
|
|
this.environmentId = val;
|
|
|
|
|
|
this.engineComponent?.setHDRBackgroundId(val);
|
|
|
|
|
|
}
|
2026-03-04 16:40:35 +08:00
|
|
|
|
);
|
|
|
|
|
|
section.appendChild(select);
|
2026-03-05 11:15:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 显示背景开关
|
|
|
|
|
|
const toggleRow = this.createToggleRow(
|
|
|
|
|
|
t('setting.backgroundVisible'),
|
|
|
|
|
|
this.backgroundVisible,
|
|
|
|
|
|
(enabled) => {
|
|
|
|
|
|
this.backgroundVisible = enabled;
|
|
|
|
|
|
this.engineComponent?.setHDRBackgroundVisibility(enabled);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
toggleRow.style.marginTop = '10px';
|
|
|
|
|
|
section.appendChild(toggleRow);
|
|
|
|
|
|
|
2026-03-04 16:40:35 +08:00
|
|
|
|
return section;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// ======================== 7. 地面(动态列表 + 地面高度) ========================
|
2026-03-04 16:40:35 +08:00
|
|
|
|
|
|
|
|
|
|
/** 创建地面设置区 */
|
|
|
|
|
|
private createGroundSection(): HTMLElement {
|
|
|
|
|
|
const section = document.createElement('div');
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 标签
|
2026-03-04 16:40:35 +08:00
|
|
|
|
const label = document.createElement('div');
|
|
|
|
|
|
label.style.cssText = SECTION_LABEL_STYLE;
|
|
|
|
|
|
label.textContent = t('setting.ground');
|
|
|
|
|
|
section.appendChild(label);
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 下拉列表(从底层引擎动态获取)
|
2026-03-04 16:40:35 +08:00
|
|
|
|
const select = this.createSelect(
|
2026-03-05 11:15:57 +08:00
|
|
|
|
this.groundList.map(item => ({ value: item.id, label: item.name })),
|
|
|
|
|
|
this.groundId,
|
2026-03-04 16:40:35 +08:00
|
|
|
|
(val) => {
|
2026-03-05 11:15:57 +08:00
|
|
|
|
this.groundId = val;
|
|
|
|
|
|
this.engineComponent?.setGroundId(val);
|
2026-03-04 16:40:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
section.appendChild(select);
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 地面高度输入行
|
|
|
|
|
|
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;
|
2026-03-04 16:40:35 +08:00
|
|
|
|
border: 1px solid ${BORDER_COLOR};
|
|
|
|
|
|
background: ${INPUT_BG};
|
|
|
|
|
|
color: ${TEXT_PRIMARY};
|
|
|
|
|
|
font-size: 13px; outline: none; text-align: right;
|
|
|
|
|
|
`;
|
2026-03-05 11:15:57 +08:00
|
|
|
|
elevationInput.addEventListener('input', () => {
|
|
|
|
|
|
const v = Number(elevationInput.value);
|
|
|
|
|
|
if (!isNaN(v)) {
|
|
|
|
|
|
this.groundElevation = v;
|
|
|
|
|
|
this.engineComponent?.setGroundElevation(v);
|
2026-03-04 16:40:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-05 11:15:57 +08:00
|
|
|
|
elevationInput.addEventListener('focus', () => {
|
|
|
|
|
|
elevationInput.style.borderColor = PRIMARY;
|
2026-03-04 16:40:35 +08:00
|
|
|
|
});
|
2026-03-05 11:15:57 +08:00
|
|
|
|
elevationInput.addEventListener('blur', () => {
|
|
|
|
|
|
elevationInput.style.borderColor = BORDER_COLOR;
|
2026-03-04 16:40:35 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
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);
|
2026-03-04 16:40:35 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-05 11:15:57 +08:00
|
|
|
|
// 聚焦<E8819A><E784A6>亮
|
2026-03-04 16:40:35 +08:00
|
|
|
|
select.addEventListener('focus', () => {
|
|
|
|
|
|
select.style.borderColor = PRIMARY;
|
|
|
|
|
|
});
|
|
|
|
|
|
select.addEventListener('blur', () => {
|
|
|
|
|
|
select.style.borderColor = BORDER_COLOR;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return select;
|
2026-02-28 11:26:59 +08:00
|
|
|
|
}
|
2026-03-05 11:15:57 +08:00
|
|
|
|
|
|
|
|
|
|
// ======================== 通用开关行 ========================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建一个标签 + 开关的行
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
2026-02-28 11:26:59 +08:00
|
|
|
|
}
|