feat: 新增底部Dock测量面板与回调联动

This commit is contained in:
yuding
2026-03-30 10:53:39 +08:00
parent c11140f967
commit 2574a11284
42 changed files with 18388 additions and 11404 deletions

View File

@@ -0,0 +1,156 @@
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { BottomDockStack } from '../components/bottom-dock-stack';
import { themeManager } from '../services/theme';
export interface BottomDockPanelDefinition {
id: string;
title: string;
closable?: boolean;
createContent?: () => HTMLElement;
}
export interface BottomDockStateChange {
id: string;
open: boolean;
}
type BottomDockStateListener = (state: BottomDockStateChange) => void;
export class BottomDockManager extends BaseManager {
private stack: BottomDockStack;
private definitions: Map<string, BottomDockPanelDefinition> = new Map();
private openStates: Map<string, boolean> = new Map();
private listeners: Set<BottomDockStateListener> = new Set();
private unsubscribeTheme: (() => void) | null = null;
constructor(container: HTMLElement, registry: ManagerRegistry) {
super(registry);
this.stack = new BottomDockStack(container);
this.stack.setTheme(themeManager.getTheme());
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.stack.setTheme(theme);
});
this.registerDefaultPanels();
}
public register(definition: BottomDockPanelDefinition): void {
this.definitions.set(definition.id, definition);
if (!this.openStates.has(definition.id)) {
this.openStates.set(definition.id, false);
}
}
public unregister(id: string): void {
if (this.isOpen(id)) {
this.close(id);
}
this.definitions.delete(id);
this.openStates.delete(id);
}
public toggle(id: string): void {
if (this.isOpen(id)) {
this.close(id);
return;
}
this.open(id);
}
public open(id: string): void {
const definition = this.definitions.get(id);
if (!definition) {
console.warn(`[BottomDock] Unknown panel id: ${id}`);
return;
}
if (this.isOpen(id)) {
return;
}
const content = definition.createContent
? definition.createContent()
: this.stack.createPlaceholderContent(`${definition.title} 面板内容占位`);
this.stack.addPanel({
id,
content,
closable: definition.closable !== false,
onClose: () => {
this.close(id);
}
});
this.openStates.set(id, true);
this.emitState({ id, open: true });
}
public close(id: string): void {
if (!this.isOpen(id)) {
return;
}
this.stack.removePanel(id);
this.openStates.set(id, false);
this.emitState({ id, open: false });
}
public isOpen(id: string): boolean {
return this.openStates.get(id) ?? false;
}
public createPlaceholderContent(text: string): HTMLElement {
return this.stack.createPlaceholderContent(text);
}
public onStateChange(listener: BottomDockStateListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
public destroy(): void {
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.listeners.clear();
this.definitions.clear();
this.openStates.clear();
this.stack.destroy();
super.destroy();
}
private emitState(state: BottomDockStateChange): void {
this.listeners.forEach((listener) => {
listener(state);
});
}
private registerDefaultPanels(): void {
this.register({
id: 'measure',
title: '测量',
createContent: () => {
const measurePanel = this.registry.measureDock?.getPanelElement();
if (measurePanel) {
return measurePanel;
}
return this.stack.createPlaceholderContent('测量面板占位');
}
});
this.register({
id: 'section',
title: '剖切',
createContent: () => this.stack.createPlaceholderContent('剖切面板占位')
});
this.register({
id: 'walk',
title: '漫游',
createContent: () => this.stack.createPlaceholderContent('漫游面板占位')
});
}
}

View File

@@ -0,0 +1,178 @@
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { MeasureDockPanel } from '../components/measure-dock-panel';
import { themeManager } from '../services/theme';
import { localeManager } from '../services/locale';
import { getModeBycallBackType, type CallBackType } from '../types/measure';
interface EngineMeasureData {
type: CallBackType;
}
export class MeasureDockManager extends BaseManager {
private panel: MeasureDockPanel | null = null;
private unsubscribeTheme: (() => void) | null = null;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeDockState: (() => void) | null = null;
private unsubscribeMeasureEvents: (() => 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 !== 'measure' || state.open) {
if (state.id === 'measure' && state.open) {
this.ensureMeasureEventSubscription();
}
return;
}
this.engineComponent?.deactivateMeasure();
}) ?? null;
}
this.ensureMeasureEventSubscription();
}
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;
}
if (this.unsubscribeMeasureEvents) {
this.unsubscribeMeasureEvents();
this.unsubscribeMeasureEvents = null;
}
this.panel?.destroy();
this.panel = null;
super.destroy();
}
public getPanelElement(): HTMLElement {
this.ensureMeasureEventSubscription();
if (!this.panel) {
this.panel = new MeasureDockPanel({
defaultMode: 'distance',
defaultExpanded: false,
defaultClearHeightDirection: 1,
defaultClearHeightSelectType: 'point',
onModeChange: (mode) => {
this.engineComponent?.activateMeasure(mode);
},
onClearAll: () => {
this.engineComponent?.clearAllMeasures();
},
onConfigSave: (config) => {
this.engineComponent?.saveMeasureSetting({
unit: config.unit,
precision: config.precision
});
},
onClearHeightDirectionChange: (direction) => {
this.engineComponent?.setClearHeightDirection(direction);
},
onClearHeightSelectTypeChange: (selectType) => {
this.engineComponent?.setClearHeightSelectType(selectType);
}
});
this.panel.init();
this.panel.switchMode('distance');
this.engineComponent?.setClearHeightDirection(1);
this.engineComponent?.setClearHeightSelectType('point');
const config = this.panel.getConfig();
this.engineComponent?.saveMeasureSetting({
unit: config.unit,
precision: config.precision
});
} else {
this.panel.switchMode('distance');
}
this.applyPresentation();
return this.panel.element;
}
private ensureMeasureEventSubscription(): void {
if (this.unsubscribeMeasureEvents) {
return;
}
const ec = this.engineComponent;
if (!ec) {
console.warn('[MeasureDockManager] skip callback binding: engine component not ready yet');
return;
}
const changedHandler = (data: EngineMeasureData) => {
this.handleMeasureCallback('measure-changed', data);
};
const clickHandler = (data: EngineMeasureData) => {
this.handleMeasureCallback('measure-click', data);
};
ec.onRawEvent('measure-changed', changedHandler);
ec.onRawEvent('measure-click', clickHandler);
this.unsubscribeMeasureEvents = () => {
ec.offRawEvent('measure-changed', changedHandler);
ec.offRawEvent('measure-click', clickHandler);
};
console.log('[MeasureDockManager] raw event callbacks bound');
}
private applyPresentation(): void {
if (!this.panel) {
return;
}
this.panel.setTheme(themeManager.getTheme());
this.panel.setLocales();
}
private handleMeasureCallback(eventName: 'measure-changed' | 'measure-click', data: EngineMeasureData): void {
const isOpen = this.registry.bottomDock?.isOpen('measure') ?? false;
const mode = getModeBycallBackType(data.type);
console.log('[MeasureDockManager] callback received', {
event: eventName,
type: data.type,
mode,
dockOpen: isOpen,
hasPanel: Boolean(this.panel)
});
if (!isOpen) {
console.log('[MeasureDockManager] skip mode switch: measure dock is closed');
return;
}
if (!mode || !this.panel) {
console.log('[MeasureDockManager] skip mode switch: invalid mode or panel not ready');
return;
}
this.panel.switchMode(mode, false);
this.engineComponent?.activateMeasure(mode);
console.log('[MeasureDockManager] switched mode by callback', { event: eventName, mode });
}
}

View File

@@ -0,0 +1,73 @@
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { RadialToolbar } from '../components/radial-toolbar';
import type { RadialMenuItem } from '../components/radial-toolbar/types';
import { themeManager } from '../services/theme';
import { getIcon } from '../utils/icon-manager';
import type { BottomDockStateChange } from './bottom-dock-manager';
import { createMeasureRadialButton } from '../components/radial-toolbar/buttons/measure';
import { createSectionRadialButton } from '../components/radial-toolbar/buttons/section';
import { createSettingRadialButton } from '../components/radial-toolbar/buttons/setting';
import { createWalkRadialButton } from '../components/radial-toolbar/buttons/walk';
export interface RadialToolbarManagerOptions {
items?: RadialMenuItem[];
itemsPerRing?: number;
}
export class RadialToolbarManager extends BaseManager {
private toolbar: RadialToolbar | null = null;
private unsubscribeDockState: (() => void) | null = null;
constructor(container: HTMLElement, registry: ManagerRegistry, options?: RadialToolbarManagerOptions) {
super(registry);
this.toolbar = new RadialToolbar({
container,
items: options?.items ?? this.createDefaultItems(),
itemsPerRing: options?.itemsPerRing ?? 4,
mainButtonIcon: getIcon('主视角'),
mainButtonLabel: 'toolbar.home',
onMainButtonClick: () => {
console.log('[RadialToolbar] main: home');
this.registry.engine3d?.getEngineComponent()?.CameraGoHome();
}
});
this.toolbar.setTheme(themeManager.getTheme());
this.unsubscribeDockState = this.registry.bottomDock?.onStateChange(({ id, open }: BottomDockStateChange) => {
this.toolbar?.setItemActive(id, open);
}) ?? null;
this.syncInitialToggleStates();
}
private createDefaultItems(): RadialMenuItem[] {
return [
createSettingRadialButton(this.registry),
createMeasureRadialButton(this.registry),
createSectionRadialButton(this.registry),
createWalkRadialButton(this.registry),
];
}
private syncInitialToggleStates(): void {
const dock = this.registry.bottomDock;
if (!dock || !this.toolbar) {
return;
}
this.toolbar.setItemActive('measure', dock.isOpen('measure'));
this.toolbar.setItemActive('section', dock.isOpen('section'));
this.toolbar.setItemActive('walk', dock.isOpen('walk'));
}
public destroy(): void {
if (this.unsubscribeDockState) {
this.unsubscribeDockState();
this.unsubscribeDockState = null;
}
this.toolbar?.destroy();
this.toolbar = null;
super.destroy();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,12 +29,7 @@ export class WalkControlManager extends BaseManager {
/** 显示漫游控制面板 */
public show(): void {
if (!this.registry.toolbar) {
console.warn('Toolbar not initialized');
return;
}
this.registry.toolbar.hide();
this.registry.toolbar?.hide();
// 打开漫游面板时,默认激活第一人称模式
console.log('[WalkControl] 打开漫游面板,激活第一人称模式');