feat: 迁移BimEngine到radial+dock并完善测量/剖切/漫游面板

This commit is contained in:
yuding
2026-03-30 15:56:18 +08:00
parent 2574a11284
commit 819992f331
26 changed files with 9575 additions and 8579 deletions

View File

@@ -144,13 +144,25 @@ export class BottomDockManager extends BaseManager {
this.register({
id: 'section',
title: '剖切',
createContent: () => this.stack.createPlaceholderContent('剖切面板占位')
createContent: () => {
const sectionPanel = this.registry.sectionDock?.getPanelElement();
if (sectionPanel) {
return sectionPanel;
}
return this.stack.createPlaceholderContent('剖切面板占位');
}
});
this.register({
id: 'walk',
title: '漫游',
createContent: () => this.stack.createPlaceholderContent('漫游面板占位')
createContent: () => {
const walkPanel = this.registry.walkDock?.getPanelElement();
if (walkPanel) {
return walkPanel;
}
return this.stack.createPlaceholderContent('漫游面板占位');
}
});
}
}

View File

@@ -0,0 +1,123 @@
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { SectionDockPanel, type SectionDockAxis, type SectionDockType } from '../components/section-dock-panel';
import { themeManager } from '../services/theme';
import { localeManager } from '../services/locale';
export class SectionDockManager extends BaseManager {
private panel: SectionDockPanel | null = null;
private unsubscribeTheme: (() => void) | null = null;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeDockState: (() => void) | null = null;
constructor(registry: ManagerRegistry) {
super(registry);
}
public init(): void {
if (!this.unsubscribeTheme) {
this.unsubscribeTheme = themeManager.subscribe(() => {
this.applyPresentation();
});
}
if (!this.unsubscribeLocale) {
this.unsubscribeLocale = localeManager.subscribe(() => {
this.applyPresentation();
});
}
if (!this.unsubscribeDockState) {
this.unsubscribeDockState = this.registry.bottomDock?.onStateChange((state) => {
if (state.id !== 'section') {
return;
}
if (!state.open) {
console.log("deactivateSection")
this.engineComponent?.deactivateSection();
return;
}
this.panel?.resetForOpen();
}) ?? null;
}
}
public destroy(): void {
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeDockState) {
this.unsubscribeDockState();
this.unsubscribeDockState = null;
}
this.panel?.destroy();
this.panel = null;
super.destroy();
}
public getPanelElement(): HTMLElement {
if (!this.panel) {
this.panel = new SectionDockPanel({
defaultType: 'face',
defaultAxis: 'x',
defaultHidden: false,
onTypeChange: (type, axis) => {
this.activateByType(type, axis);
},
onAxisChange: (axis) => {
if (this.panel?.getActiveType() === 'axis') {
this.engineComponent?.activeSection(axis);
}
},
onHideToggle: (hidden) => {
if (hidden) {
this.engineComponent?.hideSection();
return;
}
this.engineComponent?.recoverSection();
},
onReverse: () => {
this.engineComponent?.reverseSection();
},
onReset: (type, axis) => {
this.engineComponent?.deactivateSection();
this.activateByType(type, axis);
},
onFitToModel: () => {
this.engineComponent?.scaleSectionBox();
}
});
this.panel.init();
}
this.panel.resetForOpen();
this.applyPresentation();
return this.panel.element;
}
private activateByType(type: SectionDockType, axis: SectionDockAxis): void {
if (type === 'face') {
this.engineComponent?.activeSection('face');
return;
}
if (type === 'box') {
this.engineComponent?.activeSection('box');
return;
}
this.engineComponent?.activeSection(axis);
}
private applyPresentation(): void {
if (!this.panel) {
return;
}
this.panel.setTheme(themeManager.getTheme());
this.panel.setLocales();
}
}

View File

@@ -100,8 +100,9 @@ export class SettingDialogManager extends BaseDialogManager {
}
private ensurePresetListWithInternalDefault(): void {
const builtin = this.createInternalDefaultPreset();
const withoutInternal = this.presetList.filter((item) => item.id !== SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID);
const hasCustomDefault = withoutInternal.some((item) => item.isDefault);
const builtin = this.createInternalDefaultPreset(!hasCustomDefault);
this.presetList = [...withoutInternal, builtin];
if (!this.selectedPresetId || !this.presetList.some((item) => item.id === this.selectedPresetId)) {
this.selectedPresetId = builtin.id;
@@ -118,21 +119,24 @@ export class SettingDialogManager extends BaseDialogManager {
this.ensurePresetListWithInternalDefault();
}
private createInternalDefaultPreset(): EngineSettingPreset {
private createInternalDefaultPreset(isDefault: boolean = true): EngineSettingPreset {
return {
id: SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID,
presetName: t('setting.defaultPresetLabel'),
isDefault: true,
isDefault,
settings: structuredClone(DEFAULT_SETTINGS),
source: 'sdk-default',
readonly: false,
allowModify: false,
};
}
public setPresetList(presets: EngineSettingPreset[]): void {
this.presetList = this.normalizePresetList(presets);
this.ensurePresetListWithInternalDefault();
this.selectedPresetId = this.resolveDefaultPresetId(this.presetList);
if (!this.selectedPresetId && this.presetList.length > 0) {
this.selectedPresetId = this.presetList[0].id;
}
@@ -169,11 +173,11 @@ export class SettingDialogManager extends BaseDialogManager {
this.ensureSliderStyle();
const content = document.createElement('div');
content.style.cssText = 'max-height: 520px; display: flex; flex-direction: column;';
content.style.cssText = 'height: 600px; display: flex; flex-direction: column;';
const scrollBody = document.createElement('div');
scrollBody.className = 'setting-scroll-body';
scrollBody.style.cssText = 'padding: 14px 16px 12px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px;';
scrollBody.style.cssText = 'padding: 14px 16px 12px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 0;';
scrollBody.appendChild(this.createRenderModeSection());
scrollBody.appendChild(this.createSliderSection(
@@ -228,7 +232,11 @@ export class SettingDialogManager extends BaseDialogManager {
const engine = this.engineComponent;
if (!engine) return;
const savedSelectedId = this.selectedPresetId;
this.ensurePresetListWithInternalDefault();
if (savedSelectedId && this.presetList.some((item) => item.id === savedSelectedId)) {
this.selectedPresetId = savedSelectedId;
}
this.presetLists = engine.getPresetLists();
this.settings = engine.getSettings();
this.isDirty = this.getCurrentSettingsDirtyState(this.settings);
@@ -314,23 +322,32 @@ export class SettingDialogManager extends BaseDialogManager {
const actions = document.createElement('div');
const canDelete = this.isSelectedPresetDeletable();
actions.style.cssText = canDelete
? 'display: grid; grid-template-columns: minmax(0, 1.55fr) 40px minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;'
: 'display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;';
const allowModify = this.isSelectedPresetAllowModify();
actions.style.cssText = 'display: grid; grid-template-columns: minmax(0, 1.55fr) 40px minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;';
actions.appendChild(this.createBottomPresetSelect());
if (canDelete) {
if (allowModify && canDelete) {
actions.appendChild(this.createDeleteIconButton());
} else {
const placeholder = document.createElement('div');
placeholder.style.cssText = 'width: 40px; height: 36px;';
actions.appendChild(placeholder);
}
actions.appendChild(this.createFooterActionButton(t('setting.savePreset'), {
background: PRIMARY,
color: TEXT_INVERSE,
border: 'none',
onClick: () => this.handleSaveCurrentPreset(),
compact: true,
}));
if (allowModify) {
actions.appendChild(this.createFooterActionButton(t('setting.savePreset'), {
background: PRIMARY,
color: TEXT_INVERSE,
border: 'none',
onClick: () => this.handleSaveCurrentPreset(),
compact: true,
}));
} else {
const placeholder = document.createElement('div');
actions.appendChild(placeholder);
}
actions.appendChild(this.createFooterActionButton(t('setting.saveAsNewPreset'), {
background: PRIMARY,
@@ -371,9 +388,17 @@ export class SettingDialogManager extends BaseDialogManager {
private isSelectedPresetDeletable(): boolean {
if (!this.selectedPresetId) return false;
if (this.selectedPresetId === SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID) return false;
const preset = this.presetList.find((item) => item.id === this.selectedPresetId);
if (preset?.allowModify === false) return false;
return this.presetList.some((item) => item.id === this.selectedPresetId);
}
private isSelectedPresetAllowModify(): boolean {
if (!this.selectedPresetId) return true;
const preset = this.presetList.find((item) => item.id === this.selectedPresetId);
return preset?.allowModify !== false;
}
private createBottomPresetSelect(): HTMLElement {
const options: SelectOption[] = [];
if (this.presetList.length === 0) {
@@ -875,6 +900,9 @@ export class SettingDialogManager extends BaseDialogManager {
}
private handleSaveCurrentPreset(): void {
if (!this.isSelectedPresetAllowModify()) {
return;
}
if (!this.selectedPresetId) {
window.alert(t('setting.selectPresetFirst'));
return;
@@ -886,14 +914,20 @@ export class SettingDialogManager extends BaseDialogManager {
return;
}
const savedPresetId = this.selectedPresetId;
const currentSettings = this.engineComponent?.getSettings() ?? structuredClone(this.settings);
const updatedPreset: EngineSettingPreset = {
...this.presetList[index],
settings: structuredClone(currentSettings),
isDefault: true,
};
this.presetList = this.presetList.map((item, i) => (i === index ? updatedPreset : item));
this.presetList = this.presetList.map((item, i) => ({
...(i === index ? updatedPreset : item),
isDefault: i === index,
}));
this.ensurePresetListWithInternalDefault();
this.selectedPresetId = savedPresetId;
this.settings = structuredClone(currentSettings);
this.isDirty = false;
this.updateUndoRestoreVisibility();
@@ -922,14 +956,14 @@ export class SettingDialogManager extends BaseDialogManager {
const preset: EngineSettingPreset = {
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
presetName: trimmedName,
isDefault: false,
isDefault: true,
settings: structuredClone(currentSettings),
source: 'user',
readonly: false,
};
const withoutInternal = this.presetList.filter((item) => item.id !== SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID);
this.presetList = [...withoutInternal, preset];
this.presetList = [...withoutInternal.map((item) => ({ ...item, isDefault: false })), preset];
this.ensurePresetListWithInternalDefault();
this.selectedPresetId = preset.id;
this.settings = structuredClone(currentSettings);
@@ -945,7 +979,7 @@ export class SettingDialogManager extends BaseDialogManager {
}
private openDeletePresetConfirmModal(): void {
if (!this.isSelectedPresetDeletable()) {
if (!this.isSelectedPresetAllowModify() || !this.isSelectedPresetDeletable()) {
return;
}
this.closeDeletePresetConfirmModal();

View File

@@ -0,0 +1,125 @@
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { WalkDockPanel } from '../components/walk-dock-panel';
import { WalkPathDialogManager } from './walk-path-dialog-manager';
export class WalkDockManager extends BaseManager {
private panel: WalkDockPanel | null = null;
private pathManager: WalkPathDialogManager | null = null;
private unsubscribeDockState: (() => void) | null = null;
constructor(registry: ManagerRegistry) {
super(registry);
}
public init(): void {
this.pathManager = new WalkPathDialogManager(this.registry);
this.pathManager.init();
if (!this.unsubscribeDockState) {
this.unsubscribeDockState = this.registry.bottomDock?.onStateChange((state) => {
if (state.id !== 'walk') {
return;
}
if (state.open) {
this.onOpen();
return;
}
this.onClose();
}) ?? null;
}
}
public destroy(): void {
if (this.unsubscribeDockState) {
this.unsubscribeDockState();
this.unsubscribeDockState = null;
}
this.onClose();
this.pathManager?.destroy();
this.pathManager = null;
super.destroy();
}
public getPanelElement(): HTMLElement {
if (!this.panel) {
this.panel = new WalkDockPanel({
onPlanViewToggle: (isActive) => {
this.engineComponent?.toggleMiniMap();
this.registry.toolbar?.setBtnActive('map', isActive);
this.emit('walk:plan-view-toggle', { isActive });
},
onPathModeToggle: (isActive) => {
if (isActive) {
this.pathManager?.show();
} else {
this.pathManager?.hide();
}
this.emit('walk:path-mode-toggle', { isActive });
},
onWalkModeToggle: (isActive) => {
if (isActive) {
this.pathManager?.hide();
}
this.emit('walk:walk-mode-toggle', { isActive });
},
onSpeedChange: (speed) => {
const engineSpeed = speed * 0.1;
this.engineComponent?.setWalkSpeed(engineSpeed);
this.emit('walk:speed-change', { speed });
},
onGravityToggle: (enabled) => {
this.engineComponent?.setWalkGravity(enabled);
this.emit('walk:gravity-toggle', { enabled });
},
onCollisionToggle: (enabled) => {
this.engineComponent?.setWalkCollision(enabled);
this.emit('walk:collision-toggle', { enabled });
},
onCharacterModelChange: (model) => {
void model;
},
onWalkModeChange: (mode) => {
void mode;
},
onExit: () => {
this.registry.bottomDock?.close('walk');
}
});
this.panel.init();
}
this.syncMapState();
return this.panel.element;
}
private onOpen(): void {
this.engineComponent?.activateFirstPersonMode();
this.syncMapState();
}
private onClose(): void {
this.pathManager?.hide();
if (this.engineComponent?.getMiniMapState()) {
this.engineComponent.toggleMiniMap();
}
this.engineComponent?.deactivateFirstPersonMode();
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
this.registry.toolbar?.setBtnActive('map', false);
}
private syncMapState(): void {
const mapState = this.engineComponent?.getMiniMapState() ?? false;
this.panel?.setPlanViewActive(mapState);
}
}