feat: 新增底部Dock测量面板与回调联动
This commit is contained in:
156
src/managers/bottom-dock-manager.ts
Normal file
156
src/managers/bottom-dock-manager.ts
Normal 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('漫游面板占位')
|
||||
});
|
||||
}
|
||||
}
|
||||
178
src/managers/measure-dock-manager.ts
Normal file
178
src/managers/measure-dock-manager.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
73
src/managers/radial-toolbar-manager.ts
Normal file
73
src/managers/radial-toolbar-manager.ts
Normal 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
@@ -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] 打开漫游面板,激活第一人称模式');
|
||||
|
||||
Reference in New Issue
Block a user