feat: 迁移BimEngine到radial+dock并完善测量/剖切/漫游面板
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
declare const __APP_VERSION__: string;
|
||||
import './bim-engine.css';
|
||||
import { ToolbarManager } from './managers/toolbar-manager';
|
||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
||||
import { DialogManager } from './managers/dialog-manager';
|
||||
import { EngineManager } from './managers/engine-manager';
|
||||
import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
||||
import { RadialToolbarManager } from './managers/radial-toolbar-manager';
|
||||
import { BottomDockManager } from './managers/bottom-dock-manager';
|
||||
import { MeasureDockManager } from './managers/measure-dock-manager';
|
||||
import { SectionDockManager } from './managers/section-dock-manager';
|
||||
import { WalkDockManager } from './managers/walk-dock-manager';
|
||||
|
||||
import { MeasureDialogManager } from './managers/measure-dialog-manager';
|
||||
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
|
||||
@@ -16,6 +18,7 @@ import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
|
||||
import { SettingDialogManager } from './managers/setting-dialog-manager';
|
||||
import { ComponentDetailManager } from './managers/component-detail-manager';
|
||||
import { AiChatManager } from './managers/ai-chat-manager';
|
||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
@@ -35,12 +38,14 @@ export class BimEngine {
|
||||
private lastSyncedHeight = -1;
|
||||
private registry: ManagerRegistry;
|
||||
|
||||
public toolbar: ToolbarManager | null = null;
|
||||
public constructTreeBtn: ConstructTreeManagerBtn | null = null;
|
||||
public buttonGroup: ButtonGroupManager | null = null;
|
||||
public dialog: DialogManager | null = null;
|
||||
public engine: EngineManager | null = null;
|
||||
public rightKey: RightKeyManager | null = null;
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
public measure: MeasureDialogManager | null = null;
|
||||
public sectionPlane: SectionPlaneDialogManager | null = null;
|
||||
@@ -51,6 +56,7 @@ export class BimEngine {
|
||||
public componentDetail: ComponentDetailManager | null = null;
|
||||
public aiChat: AiChatManager | null = null;
|
||||
public setting: SettingDialogManager | null = null;
|
||||
public constructTreeBtn: ConstructTreeManagerBtn | null = null;
|
||||
|
||||
private readonly handleWindowResize = () => {
|
||||
this.updateClientSizeDisplay();
|
||||
@@ -133,10 +139,23 @@ export class BimEngine {
|
||||
|
||||
this.engine = new EngineManager(this.wrapper, this.registry);
|
||||
this.dialog = new DialogManager(this.wrapper, this.registry);
|
||||
this.toolbar = new ToolbarManager(this.wrapper, this.registry);
|
||||
this.buttonGroup = new ButtonGroupManager(this.wrapper, this.registry);
|
||||
this.rightKey = new RightKeyManager(this.wrapper, this.registry);
|
||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this.wrapper, this.registry);
|
||||
this.bottomDock = new BottomDockManager(this.wrapper, this.registry);
|
||||
this.registry.bottomDock = this.bottomDock;
|
||||
|
||||
this.measureDock = new MeasureDockManager(this.registry);
|
||||
this.registry.measureDock = this.measureDock;
|
||||
this.measureDock.init();
|
||||
|
||||
this.sectionDock = new SectionDockManager(this.registry);
|
||||
this.registry.sectionDock = this.sectionDock;
|
||||
this.sectionDock.init();
|
||||
|
||||
this.walkDock = new WalkDockManager(this.registry);
|
||||
this.registry.walkDock = this.walkDock;
|
||||
this.walkDock.init();
|
||||
|
||||
this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry);
|
||||
|
||||
this.measure = new MeasureDialogManager(this.registry);
|
||||
this.sectionPlane = new SectionPlaneDialogManager(this.registry);
|
||||
@@ -149,10 +168,8 @@ export class BimEngine {
|
||||
|
||||
this.registry.engine3d = this.engine;
|
||||
this.registry.dialog = this.dialog;
|
||||
this.registry.toolbar = this.toolbar;
|
||||
this.registry.buttonGroup = this.buttonGroup;
|
||||
this.registry.rightKey = this.rightKey;
|
||||
this.registry.constructTree = this.constructTreeBtn;
|
||||
this.registry.radialToolbar = this.radialToolbar;
|
||||
|
||||
this.registry.measure = this.measure;
|
||||
this.registry.sectionPlane = this.sectionPlane;
|
||||
@@ -170,6 +187,9 @@ export class BimEngine {
|
||||
this.registry.aiChat = this.aiChat;
|
||||
this.aiChat.init();
|
||||
|
||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this.wrapper, this.registry);
|
||||
this.registry.constructTree = this.constructTreeBtn;
|
||||
|
||||
this.setting = new SettingDialogManager(this.registry);
|
||||
this.registry.setting = this.setting;
|
||||
this.setting.init();
|
||||
@@ -236,8 +256,11 @@ export class BimEngine {
|
||||
public destroy() {
|
||||
this.unbindSizeObserver();
|
||||
|
||||
this.toolbar?.destroy();
|
||||
this.buttonGroup?.destroy();
|
||||
this.radialToolbar?.destroy();
|
||||
this.measureDock?.destroy();
|
||||
this.sectionDock?.destroy();
|
||||
this.walkDock?.destroy();
|
||||
this.bottomDock?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.dialog?.destroy();
|
||||
this.rightKey?.destroy();
|
||||
@@ -247,6 +270,7 @@ export class BimEngine {
|
||||
this.sectionAxis?.destroy();
|
||||
this.sectionBox?.destroy();
|
||||
this.walkControl?.destroy();
|
||||
this.constructTreeBtn?.destroy();
|
||||
this.aiChat?.destroy();
|
||||
this.setting?.destroy();
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
box-shadow: var(--bd-shadow, 0 2px 8px rgba(15, 23, 42, 0.1));
|
||||
transition: transform 220ms ease, opacity 200ms ease;
|
||||
overflow: visible;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bottom-dock-panel:hover,
|
||||
.bottom-dock-panel:focus-within {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bottom-dock-panel.is-entering {
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface EngineSettingPreset {
|
||||
settings: EngineSettings;
|
||||
readonly?: boolean;
|
||||
source?: 'sdk-default' | 'external' | 'user';
|
||||
allowModify?: boolean;
|
||||
}
|
||||
|
||||
export interface PresetListItem {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--bim-bg-elevated, #e8ecf2) 92%, #ffffff 8%);
|
||||
box-sizing: border-box;
|
||||
color: var(--bim-text-secondary, #475569);
|
||||
font-size: 13px;
|
||||
@@ -187,7 +186,6 @@
|
||||
|
||||
.measure-dock-panel-mode-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bim-primary, #4f88ff) 35%, transparent 65%);
|
||||
}
|
||||
@@ -332,4 +330,4 @@
|
||||
.measure-dock-panel-action-expand.is-expanded {
|
||||
height: 74px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,23 +400,21 @@ export class RadialToolbar implements IBimComponent {
|
||||
style.setProperty('--bim-icon-inverse', theme.iconInverse);
|
||||
style.setProperty('--bim-shadow-glow', theme.shadowGlow);
|
||||
|
||||
const isDark = theme.name === 'dark';
|
||||
style.setProperty('--rt-main-bg', theme.bgBase);
|
||||
style.setProperty('--rt-main-bg-hover', theme.bgBase);
|
||||
style.setProperty('--rt-main-border', theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-main-shadow', theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-main-shadow-hover', theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-main-icon', theme.floatingIconColor);
|
||||
style.setProperty('--rt-main-icon-hover', theme.floatingIconColorHover);
|
||||
|
||||
style.setProperty('--rt-main-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
||||
style.setProperty('--rt-main-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
||||
style.setProperty('--rt-main-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-main-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-main-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-main-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
||||
style.setProperty('--rt-main-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
||||
|
||||
style.setProperty('--rt-sub-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
||||
style.setProperty('--rt-sub-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
||||
style.setProperty('--rt-sub-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-sub-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-sub-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-sub-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
||||
style.setProperty('--rt-sub-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
||||
style.setProperty('--rt-sub-bg', theme.bgBase);
|
||||
style.setProperty('--rt-sub-bg-hover', theme.primaryHover);
|
||||
style.setProperty('--rt-sub-border', theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-sub-shadow', theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-sub-shadow-hover', theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-sub-icon', theme.floatingIconColor);
|
||||
style.setProperty('--rt-sub-icon-hover', theme.iconHover);
|
||||
}
|
||||
|
||||
public setItemActive(id: string, active: boolean): void {
|
||||
|
||||
142
src/components/section-dock-panel/index.css
Normal file
142
src/components/section-dock-panel/index.css
Normal file
@@ -0,0 +1,142 @@
|
||||
.section-dock-panel {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--bim-text-secondary, #475569);
|
||||
}
|
||||
|
||||
.section-dock-axis-panel {
|
||||
display: none;
|
||||
width: fit-content;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-dock-axis-panel.is-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.section-dock-axis-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.section-dock-axis-btn:hover {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.section-dock-axis-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.section-dock-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section-dock-types,
|
||||
.section-dock-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-dock-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: color-mix(in srgb, var(--bim-border-default, #cbd5e1) 84%, transparent 16%);
|
||||
}
|
||||
|
||||
.section-dock-type-btn,
|
||||
.section-dock-tool-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.section-dock-type-btn:hover,
|
||||
.section-dock-tool-btn:hover {
|
||||
border-color: var(--bim-border-strong, rgba(100, 116, 139, 0.6));
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.section-dock-type-btn.is-active,
|
||||
.section-dock-tool-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.section-dock-type-icon,
|
||||
.section-dock-tool-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-dock-type-icon svg,
|
||||
.section-dock-tool-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 6px);
|
||||
transform: translateX(-50%);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: 8;
|
||||
transition: opacity 60ms ease;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip]:hover::after,
|
||||
.section-dock-panel [data-tooltip]:focus-visible::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
330
src/components/section-dock-panel/index.ts
Normal file
330
src/components/section-dock-panel/index.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import './index.css';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { t } from '../../services/locale';
|
||||
import { getIcon } from '../../utils/icon-manager';
|
||||
|
||||
export type SectionDockType = 'face' | 'axis' | 'box';
|
||||
export type SectionDockAxis = 'x' | 'y' | 'z';
|
||||
|
||||
export interface SectionDockPanelOptions {
|
||||
defaultType?: SectionDockType;
|
||||
defaultAxis?: SectionDockAxis;
|
||||
defaultHidden?: boolean;
|
||||
onTypeChange?: (type: SectionDockType, axis: SectionDockAxis) => void;
|
||||
onAxisChange?: (axis: SectionDockAxis) => void;
|
||||
onHideToggle?: (hidden: boolean) => void;
|
||||
onReverse?: () => void;
|
||||
onReset?: (type: SectionDockType, axis: SectionDockAxis) => void;
|
||||
onFitToModel?: () => void;
|
||||
}
|
||||
|
||||
interface ToolDefinition {
|
||||
key: 'hide' | 'reverse' | 'reset' | 'fit';
|
||||
iconName: string;
|
||||
textKey: string;
|
||||
onClick: () => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class SectionDockPanel implements IBimComponent {
|
||||
public readonly element: HTMLElement;
|
||||
private readonly options: SectionDockPanelOptions;
|
||||
|
||||
private readonly typeButtons: Map<SectionDockType, HTMLButtonElement> = new Map();
|
||||
private readonly axisButtons: Map<SectionDockAxis, HTMLButtonElement> = new Map();
|
||||
private readonly toolContainer: HTMLElement;
|
||||
private readonly axisPanel: HTMLElement;
|
||||
|
||||
private activeType: SectionDockType;
|
||||
private activeAxis: SectionDockAxis;
|
||||
private isHidden: boolean;
|
||||
|
||||
constructor(options: SectionDockPanelOptions = {}) {
|
||||
this.options = options;
|
||||
this.activeType = options.defaultType ?? 'face';
|
||||
this.activeAxis = options.defaultAxis ?? 'x';
|
||||
this.isHidden = options.defaultHidden ?? false;
|
||||
|
||||
const { root, toolContainer, axisPanel } = this.createDom();
|
||||
this.element = root;
|
||||
this.toolContainer = toolContainer;
|
||||
this.axisPanel = axisPanel;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.applyTypeState();
|
||||
this.applyAxisState();
|
||||
this.renderTools();
|
||||
this.setLocales();
|
||||
}
|
||||
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
const style = this.element.style;
|
||||
style.setProperty('--bim-text-primary', theme.textPrimary);
|
||||
style.setProperty('--bim-text-secondary', theme.textSecondary);
|
||||
style.setProperty('--bim-border-default', theme.borderDefault);
|
||||
style.setProperty('--bim-border-strong', theme.borderStrong);
|
||||
style.setProperty('--bim-bg-inset', theme.bgInset);
|
||||
style.setProperty('--bim-bg-elevated', theme.bgElevated);
|
||||
style.setProperty('--bim-primary', theme.primary);
|
||||
style.setProperty('--bim-primary-subtle', theme.primarySubtle);
|
||||
style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
this.typeButtons.get('face')!.dataset.tooltip = t('toolbar.sectionPlane');
|
||||
this.typeButtons.get('axis')!.dataset.tooltip = t('toolbar.sectionAxis');
|
||||
this.typeButtons.get('box')!.dataset.tooltip = t('toolbar.sectionBox');
|
||||
|
||||
this.axisButtons.get('x')!.dataset.label = 'X';
|
||||
this.axisButtons.get('y')!.dataset.label = 'Y';
|
||||
this.axisButtons.get('z')!.dataset.label = 'Z';
|
||||
|
||||
this.typeButtons.forEach((button) => {
|
||||
button.setAttribute('aria-label', button.dataset.tooltip ?? '');
|
||||
});
|
||||
this.axisButtons.forEach((button) => {
|
||||
button.setAttribute('aria-label', button.dataset.label ?? '');
|
||||
});
|
||||
|
||||
this.renderTools();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
public resetForOpen(): void {
|
||||
this.activeType = 'face';
|
||||
this.isHidden = false;
|
||||
this.applyTypeState();
|
||||
this.renderTools();
|
||||
this.options.onTypeChange?.(this.activeType, this.activeAxis);
|
||||
this.options.onHideToggle?.(false);
|
||||
}
|
||||
|
||||
public getActiveType(): SectionDockType {
|
||||
return this.activeType;
|
||||
}
|
||||
|
||||
public getActiveAxis(): SectionDockAxis {
|
||||
return this.activeAxis;
|
||||
}
|
||||
|
||||
private createDom(): { root: HTMLElement; toolContainer: HTMLElement; axisPanel: HTMLElement } {
|
||||
const root = document.createElement('div');
|
||||
root.className = 'section-dock-panel';
|
||||
|
||||
const axisPanel = document.createElement('div');
|
||||
axisPanel.className = 'section-dock-axis-panel';
|
||||
|
||||
(['x', 'y', 'z'] as SectionDockAxis[]).forEach((axis) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-axis-btn';
|
||||
button.textContent = axis.toUpperCase();
|
||||
button.addEventListener('click', () => {
|
||||
this.handleAxisChange(axis);
|
||||
});
|
||||
this.axisButtons.set(axis, button);
|
||||
axisPanel.appendChild(button);
|
||||
});
|
||||
|
||||
const main = document.createElement('div');
|
||||
main.className = 'section-dock-main';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'section-dock-types';
|
||||
|
||||
const faceBtn = this.createTypeButton('face', getIcon('拾曲面剖切'));
|
||||
const axisBtn = this.createTypeButton('axis', getIcon('轴向剖切'));
|
||||
const boxBtn = this.createTypeButton('box', getIcon('剖切盒'));
|
||||
|
||||
left.appendChild(faceBtn);
|
||||
left.appendChild(axisBtn);
|
||||
left.appendChild(boxBtn);
|
||||
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'section-dock-divider';
|
||||
|
||||
const toolContainer = document.createElement('div');
|
||||
toolContainer.className = 'section-dock-tools';
|
||||
|
||||
main.appendChild(left);
|
||||
main.appendChild(divider);
|
||||
main.appendChild(toolContainer);
|
||||
|
||||
root.appendChild(axisPanel);
|
||||
root.appendChild(main);
|
||||
|
||||
return { root, toolContainer, axisPanel };
|
||||
}
|
||||
|
||||
private createTypeButton(type: SectionDockType, iconSvg: string): HTMLButtonElement {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-type-btn';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'section-dock-type-icon';
|
||||
icon.innerHTML = iconSvg;
|
||||
|
||||
button.appendChild(icon);
|
||||
button.addEventListener('click', () => {
|
||||
this.handleTypeChange(type);
|
||||
});
|
||||
|
||||
this.typeButtons.set(type, button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private handleTypeChange(type: SectionDockType): void {
|
||||
if (this.activeType === type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeType = type;
|
||||
this.applyTypeState();
|
||||
this.renderTools();
|
||||
|
||||
if (this.isHidden) {
|
||||
this.isHidden = false;
|
||||
this.options.onHideToggle?.(false);
|
||||
}
|
||||
|
||||
this.options.onTypeChange?.(type, this.activeAxis);
|
||||
}
|
||||
|
||||
private handleAxisChange(axis: SectionDockAxis): void {
|
||||
if (this.activeAxis === axis) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeAxis = axis;
|
||||
this.applyAxisState();
|
||||
|
||||
if (this.activeType === 'axis') {
|
||||
this.options.onAxisChange?.(axis);
|
||||
}
|
||||
}
|
||||
|
||||
private applyTypeState(): void {
|
||||
this.typeButtons.forEach((button, type) => {
|
||||
button.classList.toggle('is-active', type === this.activeType);
|
||||
});
|
||||
|
||||
const axisVisible = this.activeType === 'axis';
|
||||
this.axisPanel.classList.toggle('is-visible', axisVisible);
|
||||
}
|
||||
|
||||
private applyAxisState(): void {
|
||||
this.axisButtons.forEach((button, axis) => {
|
||||
button.classList.toggle('is-active', axis === this.activeAxis);
|
||||
});
|
||||
}
|
||||
|
||||
private renderTools(): void {
|
||||
this.toolContainer.innerHTML = '';
|
||||
|
||||
this.getToolsForType(this.activeType).forEach((tool) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-tool-btn';
|
||||
if (tool.isActive) {
|
||||
button.classList.add('is-active');
|
||||
}
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'section-dock-tool-icon';
|
||||
icon.innerHTML = getIcon(tool.iconName);
|
||||
|
||||
button.appendChild(icon);
|
||||
const text = t(tool.textKey);
|
||||
button.dataset.tooltip = text;
|
||||
button.setAttribute('aria-label', text);
|
||||
button.addEventListener('click', tool.onClick);
|
||||
|
||||
this.toolContainer.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
private getToolsForType(type: SectionDockType): ToolDefinition[] {
|
||||
const hideTool: ToolDefinition = {
|
||||
key: 'hide',
|
||||
iconName: '隐藏',
|
||||
textKey: type === 'box' ? 'sectionBox.actions.hide' : type === 'axis' ? 'sectionAxis.actions.hide' : 'sectionPlane.actions.hide',
|
||||
isActive: this.isHidden,
|
||||
onClick: () => {
|
||||
this.isHidden = !this.isHidden;
|
||||
this.options.onHideToggle?.(this.isHidden);
|
||||
this.renderTools();
|
||||
}
|
||||
};
|
||||
|
||||
const reverseTool: ToolDefinition = {
|
||||
key: 'reverse',
|
||||
iconName: '反向',
|
||||
textKey: type === 'axis' ? 'sectionAxis.actions.reverse' : type === 'box' ? 'sectionBox.actions.reverse' : 'sectionPlane.actions.reverse',
|
||||
onClick: () => {
|
||||
this.options.onReverse?.();
|
||||
}
|
||||
};
|
||||
|
||||
if (type === 'axis') {
|
||||
return [
|
||||
hideTool,
|
||||
reverseTool,
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionAxis.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (type === 'box') {
|
||||
return [
|
||||
hideTool,
|
||||
{
|
||||
key: 'fit',
|
||||
iconName: '适应到模型',
|
||||
textKey: 'sectionBox.actions.fitToModel',
|
||||
onClick: () => {
|
||||
this.options.onFitToModel?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionBox.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
hideTool,
|
||||
reverseTool,
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionPlane.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
107
src/components/walk-dock-panel/index.css
Normal file
107
src/components/walk-dock-panel/index.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.walk-control-panel.walk-dock-panel {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
color: var(--bim-text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-divider {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-control-left,
|
||||
.walk-control-panel.walk-dock-panel .walk-control-settings {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn:hover {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn.active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn.active svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-control {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-label,
|
||||
.walk-control-panel.walk-dock-panel .walk-checkbox-label,
|
||||
.walk-control-panel.walk-dock-panel .walk-select-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-group {
|
||||
padding: 2px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-elevated, #f8fafc) 88%, #ffffff 12%);
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-display {
|
||||
min-width: 30px;
|
||||
font-size: 12px;
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--bim-primary, #3b82f6);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-select {
|
||||
min-width: 90px;
|
||||
height: 24px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-exit-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
39
src/components/walk-dock-panel/index.ts
Normal file
39
src/components/walk-dock-panel/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import './index.css';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { WalkControlPanel } from '../walk-control-panel';
|
||||
import type { WalkControlPanelOptions, WalkControlState } from '../walk-control-panel/types';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
|
||||
export class WalkDockPanel implements IBimComponent {
|
||||
public readonly element: HTMLElement;
|
||||
private readonly panel: WalkControlPanel;
|
||||
|
||||
constructor(options: WalkControlPanelOptions = {}) {
|
||||
this.panel = new WalkControlPanel(options);
|
||||
this.panel.init();
|
||||
this.element = this.panel.element;
|
||||
this.element.classList.add('walk-dock-panel');
|
||||
}
|
||||
|
||||
public init(): void {}
|
||||
|
||||
public setPlanViewActive(active: boolean): void {
|
||||
this.panel.setPlanViewActive(active);
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
this.panel.setLocales();
|
||||
}
|
||||
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
this.panel.setTheme(theme);
|
||||
}
|
||||
|
||||
public getState(): WalkControlState {
|
||||
return this.panel.getState();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.panel.destroy();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
*/
|
||||
private render(): void {
|
||||
this.element.innerHTML = '';
|
||||
|
||||
|
||||
// 渲染路径设置区域
|
||||
const settings = this.createSettingsSection();
|
||||
this.element.appendChild(settings);
|
||||
@@ -104,13 +104,13 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// ===== 漫游时间 =====
|
||||
const durationGroup = document.createElement('div');
|
||||
durationGroup.className = 'walk-path-form-group';
|
||||
|
||||
|
||||
const durationLabel = document.createElement('label');
|
||||
durationLabel.textContent = t('walkControl.path.duration');
|
||||
|
||||
|
||||
const durationWrapper = document.createElement('div');
|
||||
durationWrapper.className = 'walk-path-input-wrapper';
|
||||
|
||||
|
||||
const durationInput = document.createElement('input');
|
||||
durationInput.type = 'number';
|
||||
durationInput.className = 'walk-path-input';
|
||||
@@ -121,11 +121,11 @@ export class WalkPathPanel implements IBimComponent {
|
||||
const val = parseInt((e.target as HTMLInputElement).value) || 1;
|
||||
this.duration = val * 1000;
|
||||
};
|
||||
|
||||
|
||||
const durationUnit = document.createElement('span');
|
||||
durationUnit.className = 'walk-path-unit';
|
||||
durationUnit.textContent = t('walkControl.path.durationUnit');
|
||||
|
||||
|
||||
durationWrapper.appendChild(durationInput);
|
||||
durationWrapper.appendChild(durationUnit);
|
||||
durationGroup.appendChild(durationLabel);
|
||||
@@ -134,7 +134,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// ===== 循环播放 =====
|
||||
const loopGroup = document.createElement('div');
|
||||
loopGroup.className = 'walk-path-form-group walk-path-form-group-inline';
|
||||
|
||||
|
||||
const loopCheckbox = document.createElement('input');
|
||||
loopCheckbox.type = 'checkbox';
|
||||
loopCheckbox.id = 'walk-path-loop-checkbox';
|
||||
@@ -144,11 +144,11 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// 更新循环播放状态
|
||||
this.loop = (e.target as HTMLInputElement).checked;
|
||||
};
|
||||
|
||||
|
||||
const loopLabel = document.createElement('label');
|
||||
loopLabel.htmlFor = 'walk-path-loop-checkbox';
|
||||
loopLabel.textContent = t('walkControl.path.loop');
|
||||
|
||||
|
||||
loopGroup.appendChild(loopCheckbox);
|
||||
loopGroup.appendChild(loopLabel);
|
||||
|
||||
@@ -390,7 +390,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
if (!this.element) return;
|
||||
// 设置 CSS 变量
|
||||
this.element.style.setProperty('--bim-text-primary', theme.textPrimary ?? '#fff');
|
||||
this.element.style.setProperty('--bim-text-secondary', theme.textSecondary ?? '#94a3b8');
|
||||
this.element.style.setProperty('--bim-text-secondary', theme.textPrimary ?? '#fff');
|
||||
this.element.style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
|
||||
this.element.style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
|
||||
this.element.style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
|
||||
|
||||
@@ -26,6 +26,8 @@ import type { AiChatManager } from '../managers/ai-chat-manager';
|
||||
import type { RadialToolbarManager } from '../managers/radial-toolbar-manager';
|
||||
import type { BottomDockManager } from '../managers/bottom-dock-manager';
|
||||
import type { MeasureDockManager } from '../managers/measure-dock-manager';
|
||||
import type { SectionDockManager } from '../managers/section-dock-manager';
|
||||
import type { WalkDockManager } from '../managers/walk-dock-manager';
|
||||
|
||||
/**
|
||||
* Manager 注册表 - 实例模式
|
||||
@@ -79,6 +81,8 @@ export class ManagerRegistry {
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -107,6 +111,8 @@ export class ManagerRegistry {
|
||||
this.radialToolbar = null;
|
||||
this.bottomDock = null;
|
||||
this.measureDock = null;
|
||||
this.sectionDock = null;
|
||||
this.walkDock = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { RadialToolbarManager } from './managers/radial-toolbar-manager';
|
||||
import { BottomDockManager } from './managers/bottom-dock-manager';
|
||||
import { MeasureDockManager } from './managers/measure-dock-manager';
|
||||
import { SectionDockManager } from './managers/section-dock-manager';
|
||||
import { WalkDockManager } from './managers/walk-dock-manager';
|
||||
|
||||
import { MeasureDialogManager } from './managers/measure-dialog-manager';
|
||||
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
|
||||
@@ -46,6 +48,8 @@ export class CusBimEngine {
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
public measure: MeasureDialogManager | null = null;
|
||||
public sectionPlane: SectionPlaneDialogManager | null = null;
|
||||
@@ -150,6 +154,14 @@ export class CusBimEngine {
|
||||
this.registry.measureDock = this.measureDock;
|
||||
this.measureDock.init();
|
||||
|
||||
this.sectionDock = new SectionDockManager(this.registry);
|
||||
this.registry.sectionDock = this.sectionDock;
|
||||
this.sectionDock.init();
|
||||
|
||||
this.walkDock = new WalkDockManager(this.registry);
|
||||
this.registry.walkDock = this.walkDock;
|
||||
this.walkDock.init();
|
||||
|
||||
this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry);
|
||||
|
||||
this.measure = new MeasureDialogManager(this.registry);
|
||||
@@ -252,6 +264,8 @@ export class CusBimEngine {
|
||||
|
||||
this.radialToolbar?.destroy();
|
||||
this.measureDock?.destroy();
|
||||
this.sectionDock?.destroy();
|
||||
this.walkDock?.destroy();
|
||||
this.bottomDock?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.dialog?.destroy();
|
||||
|
||||
@@ -143,6 +143,7 @@ export const enUS: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: 'Hide',
|
||||
reverse: 'Reverse',
|
||||
reset: 'Reset',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -154,6 +154,7 @@ export interface TranslationDictionary {
|
||||
actions: {
|
||||
hide: string;
|
||||
reverse: string;
|
||||
reset: string;
|
||||
axisX: string;
|
||||
axisY: string;
|
||||
axisZ: string;
|
||||
|
||||
@@ -143,6 +143,7 @@ export const zhCN: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: '隐藏',
|
||||
reverse: '反向',
|
||||
reset: '重置',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -143,6 +143,7 @@ export const zhTW: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: '隱藏',
|
||||
reverse: '反向',
|
||||
reset: '重設',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -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('漫游面板占位');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
123
src/managers/section-dock-manager.ts
Normal file
123
src/managers/section-dock-manager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
125
src/managers/walk-dock-manager.ts
Normal file
125
src/managers/walk-dock-manager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user