初始化

This commit is contained in:
yuding
2025-12-25 15:47:57 +08:00
parent 04a5e74284
commit 9b6959585d
28 changed files with 6464 additions and 4197 deletions

View File

@@ -0,0 +1,186 @@
/* 漫游控制面板 */
.walk-control-panel {
display: flex;
align-items: center;
gap: 20px;
padding: 8px 16px;
background: var(--bim-walk-control-bg, rgba(0, 0, 0, 0.8));
border-radius: 8px;
user-select: none;
}
/* 分割线 */
.walk-divider {
width: 1px;
height: 40px;
background: var(--bim-divider-color, rgba(255, 255, 255, 0.2));
flex-shrink: 0;
}
/* 左侧按钮区 */
.walk-control-left {
display: flex;
gap: 8px;
}
.walk-icon-btn {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
color: var(--bim-icon-color, #ccc);
}
.walk-icon-btn:hover {
background: var(--bim-walk-btn-hover, rgba(255, 255, 255, 0.15));
}
.walk-icon-btn.active {
background: var(--bim-walk-btn-active, rgba(255, 255, 255, 0.3));
}
.walk-icon-btn svg {
width: 24px;
height: 24px;
}
/* 中间设置区 */
.walk-control-settings {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
}
/* 速度控件 */
.walk-speed-control {
display: flex;
align-items: center;
gap: 12px;
}
.walk-speed-label {
color: var(--bim-text-color, #fff);
font-size: 14px;
white-space: nowrap;
}
.walk-speed-group {
display: flex;
align-items: center;
gap: 8px;
background: var(--bim-speed-group-bg, rgba(255, 255, 255, 0.1));
border-radius: 4px;
padding: 4px;
}
.walk-speed-btn {
width: 32px;
height: 32px;
background: var(--bim-speed-btn-bg, rgba(255, 255, 255, 0.1));
border: none;
border-radius: 4px;
color: var(--bim-text-color, #fff);
font-size: 18px;
cursor: pointer;
transition: background 0.2s;
}
.walk-speed-btn:hover {
background: var(--bim-speed-btn-hover, rgba(255, 255, 255, 0.2));
}
.walk-speed-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.walk-speed-display {
min-width: 40px;
text-align: center;
color: var(--bim-text-color, #fff);
font-size: 14px;
font-weight: bold;
}
/* 复选框 */
.walk-checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.walk-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.walk-checkbox:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.walk-checkbox-label {
color: var(--bim-text-color, #fff);
font-size: 14px;
white-space: nowrap;
}
.walk-checkbox-wrapper input:disabled + .walk-checkbox-label {
opacity: 0.5;
}
/* 下拉框 */
.walk-select-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.walk-select-label {
color: var(--bim-text-color, #fff);
font-size: 14px;
white-space: nowrap;
}
.walk-select {
padding: 6px 12px;
background: var(--bim-select-bg, rgba(255, 255, 255, 0.1));
border: 1px solid var(--bim-select-border, rgba(255, 255, 255, 0.2));
border-radius: 4px;
color: var(--bim-text-color, #fff);
font-size: 14px;
cursor: pointer;
min-width: 120px;
}
.walk-select option {
background: var(--bim-select-option-bg, #333);
color: var(--bim-text-color, #fff);
}
/* 退出按钮 */
.walk-exit-btn {
padding: 10px 24px;
background: var(--bim-primary-color, #1890ff);
border: none;
border-radius: 6px;
color: #fff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.walk-exit-btn:hover {
background: var(--bim-primary-hover, #40a9ff);
}

View File

@@ -0,0 +1,459 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { WalkControlPanelOptions, WalkControlState, WalkControlMode, CharacterModel, WalkMode } from './types';
export class WalkControlPanel implements IBimComponent {
public element!: HTMLElement;
private options: WalkControlPanelOptions;
// 状态
private state: WalkControlState = {
mode: 'none',
isPlanViewActive: false,
speed: 1,
gravity: false,
collision: false,
characterModel: 'construction-worker',
walkMode: 'walk'
};
// DOM 引用 - 左侧按钮
private planViewBtn!: HTMLButtonElement;
private pathModeBtn!: HTMLButtonElement;
private walkModeBtn!: HTMLButtonElement;
// DOM 引用 - 中间设置区
private settingsContainer!: HTMLElement;
private speedControl!: HTMLElement;
private speedDecreaseBtn!: HTMLButtonElement;
private speedIncreaseBtn!: HTMLButtonElement;
private speedDisplay!: HTMLElement;
private gravityCheckbox!: HTMLInputElement;
private gravityLabel!: HTMLElement;
private collisionCheckbox!: HTMLInputElement;
private collisionLabel!: HTMLElement;
private characterModelSelect!: HTMLSelectElement;
private characterModelLabel!: HTMLElement;
private walkModeSelect!: HTMLSelectElement;
private walkModeLabel!: HTMLElement;
// DOM 引用 - 退出按钮
private exitBtn!: HTMLButtonElement;
// 国际化订阅
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: WalkControlPanelOptions = {}) {
this.options = options;
this.state.speed = options.defaultSpeed ?? 1;
this.state.gravity = options.defaultGravity ?? false;
this.state.collision = options.defaultCollision ?? false;
this.state.characterModel = options.defaultCharacterModel ?? 'construction-worker';
this.state.walkMode = options.defaultWalkMode ?? 'walk';
}
public init(): void {
this.element = this.createPanel();
this.updateSettingsView();
// 订阅
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
// --- 公共方法 ---
public setPlanViewActive(active: boolean): void {
this.state.isPlanViewActive = active;
this.updateButtonStates();
}
public setPathModeActive(active: boolean): void {
// 只有当前是路径模式时,取消才设置为 none
// 避免在其他模式下被误设置为 none
if (!active && this.state.mode !== 'path') {
return;
}
const newMode: WalkControlMode = active ? 'path' : 'none';
this.setMode(newMode);
}
public getState(): WalkControlState {
return { ...this.state };
}
// --- 私有方法 ---
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'walk-control-panel';
// 左侧按钮区
const leftButtons = this.createLeftButtons();
// 分割线1
const divider1 = document.createElement('div');
divider1.className = 'walk-divider';
// 中间设置区
this.settingsContainer = this.createSettingsContainer();
// 分割线2
const divider2 = document.createElement('div');
divider2.className = 'walk-divider';
// 右侧退出按钮
const exitBtn = this.createExitButton();
panel.appendChild(leftButtons);
panel.appendChild(divider1);
panel.appendChild(this.settingsContainer);
panel.appendChild(divider2);
panel.appendChild(exitBtn);
return panel;
}
private createLeftButtons(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-control-left';
this.planViewBtn = this.createIconButton('plan-view', () => {
this.state.isPlanViewActive = !this.state.isPlanViewActive;
this.updateButtonStates();
this.options.onPlanViewToggle?.(this.state.isPlanViewActive);
});
this.pathModeBtn = this.createIconButton('path', () => {
const newMode: WalkControlMode = this.state.mode === 'path' ? 'none' : 'path';
this.setMode(newMode);
this.options.onPathModeToggle?.(newMode === 'path');
});
this.walkModeBtn = this.createIconButton('walk', () => {
const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk';
this.setMode(newMode);
this.options.onWalkModeToggle?.(newMode === 'walk');
});
container.appendChild(this.planViewBtn);
container.appendChild(this.pathModeBtn);
container.appendChild(this.walkModeBtn);
return container;
}
private createSettingsContainer(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-control-settings';
// 移动速度控件
this.speedControl = this.createSpeedControl();
// 重力复选框
const gravityWrapper = document.createElement('label');
gravityWrapper.className = 'walk-checkbox-wrapper walk-checkbox-gravity';
this.gravityCheckbox = document.createElement('input');
this.gravityCheckbox.type = 'checkbox';
this.gravityCheckbox.className = 'walk-checkbox';
this.gravityCheckbox.checked = this.state.gravity;
this.gravityCheckbox.addEventListener('change', () => {
this.state.gravity = this.gravityCheckbox.checked;
this.options.onGravityToggle?.(this.state.gravity);
});
this.gravityLabel = document.createElement('span');
this.gravityLabel.className = 'walk-checkbox-label';
gravityWrapper.appendChild(this.gravityCheckbox);
gravityWrapper.appendChild(this.gravityLabel);
// 碰撞复选框
const collisionWrapper = document.createElement('label');
collisionWrapper.className = 'walk-checkbox-wrapper walk-checkbox-collision';
this.collisionCheckbox = document.createElement('input');
this.collisionCheckbox.type = 'checkbox';
this.collisionCheckbox.className = 'walk-checkbox';
this.collisionCheckbox.checked = this.state.collision;
this.collisionCheckbox.addEventListener('change', () => {
this.state.collision = this.collisionCheckbox.checked;
this.options.onCollisionToggle?.(this.state.collision);
});
this.collisionLabel = document.createElement('span');
this.collisionLabel.className = 'walk-checkbox-label';
collisionWrapper.appendChild(this.collisionCheckbox);
collisionWrapper.appendChild(this.collisionLabel);
// 角色模型选择
const characterWrapper = document.createElement('div');
characterWrapper.className = 'walk-select-wrapper walk-select-wrapper-character-model';
this.characterModelLabel = document.createElement('label');
this.characterModelLabel.className = 'walk-select-label';
this.characterModelSelect = document.createElement('select');
this.characterModelSelect.className = 'walk-select walk-select-character-model';
this.characterModelSelect.addEventListener('change', () => {
this.state.characterModel = this.characterModelSelect.value as CharacterModel;
this.options.onCharacterModelChange?.(this.state.characterModel);
});
characterWrapper.appendChild(this.characterModelLabel);
characterWrapper.appendChild(this.characterModelSelect);
// 行走模式选择
const walkModeWrapper = document.createElement('div');
walkModeWrapper.className = 'walk-select-wrapper walk-select-wrapper-walk-mode';
this.walkModeLabel = document.createElement('label');
this.walkModeLabel.className = 'walk-select-label';
this.walkModeSelect = document.createElement('select');
this.walkModeSelect.className = 'walk-select walk-select-walk-mode';
this.walkModeSelect.addEventListener('change', () => {
this.state.walkMode = this.walkModeSelect.value as WalkMode;
this.options.onWalkModeChange?.(this.state.walkMode);
});
walkModeWrapper.appendChild(this.walkModeLabel);
walkModeWrapper.appendChild(this.walkModeSelect);
// 添加所有控件
// 注意:顺序为 速度、角色模型、行走模式、重力、碰撞
// 这样在漫游模式下显示的顺序就是:角色模型、行走模式、重力、碰撞
container.appendChild(this.speedControl);
container.appendChild(characterWrapper);
container.appendChild(walkModeWrapper);
container.appendChild(gravityWrapper);
container.appendChild(collisionWrapper);
return container;
}
private createSpeedControl(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-speed-control';
const label = document.createElement('label');
label.className = 'walk-speed-label';
label.textContent = t('walkControl.speed');
const controlGroup = document.createElement('div');
controlGroup.className = 'walk-speed-group';
// 减速按钮
this.speedDecreaseBtn = document.createElement('button');
this.speedDecreaseBtn.className = 'walk-speed-btn';
this.speedDecreaseBtn.textContent = '-';
this.speedDecreaseBtn.addEventListener('click', () => {
if (this.state.speed > 1) {
this.state.speed--;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
// 速度显示
this.speedDisplay = document.createElement('div');
this.speedDisplay.className = 'walk-speed-display';
this.speedDisplay.textContent = `${this.state.speed}X`;
// 加速按钮
this.speedIncreaseBtn = document.createElement('button');
this.speedIncreaseBtn.className = 'walk-speed-btn';
this.speedIncreaseBtn.textContent = '+';
this.speedIncreaseBtn.addEventListener('click', () => {
if (this.state.speed < 10) {
this.state.speed++;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
controlGroup.appendChild(this.speedDecreaseBtn);
controlGroup.appendChild(this.speedDisplay);
controlGroup.appendChild(this.speedIncreaseBtn);
container.appendChild(label);
container.appendChild(controlGroup);
return container;
}
private createIconButton(type: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = `walk-icon-btn walk-icon-btn-${type}`;
btn.innerHTML = this.getIconSVG(type);
btn.addEventListener('click', onClick);
return btn;
}
private createExitButton(): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = 'walk-exit-btn';
btn.addEventListener('click', () => {
this.options.onExit?.();
});
this.exitBtn = btn;
return btn;
}
private setMode(mode: WalkControlMode): void {
const oldMode = this.state.mode;
// 如果从walk模式切换到其他模式触发walk关闭事件
if (oldMode === 'walk' && mode !== 'walk') {
this.options.onWalkModeToggle?.(false);
}
// 如果从path模式切换到其他模式触发path关闭事件
if (oldMode === 'path' && mode !== 'path') {
this.options.onPathModeToggle?.(false);
}
this.state.mode = mode;
// 路径模式:禁用重力和碰撞
if (mode === 'path') {
this.state.gravity = false;
this.state.collision = false;
this.gravityCheckbox.checked = false;
this.gravityCheckbox.disabled = true;
this.collisionCheckbox.checked = false;
this.collisionCheckbox.disabled = true;
} else {
this.gravityCheckbox.disabled = false;
this.collisionCheckbox.disabled = false;
}
this.updateButtonStates();
this.updateSettingsView();
this.updateSpeedButtonStates();
}
private updateButtonStates(): void {
// 平面图按钮
this.planViewBtn.classList.toggle('active', this.state.isPlanViewActive);
// 路径漫游按钮
this.pathModeBtn.classList.toggle('active', this.state.mode === 'path');
// 漫游按钮
this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk');
}
private updateSettingsView(): void {
// 根据模式显示/隐藏不同的控件
const speedWrapper = this.speedControl;
const gravityWrapper = this.gravityCheckbox.parentElement!;
const collisionWrapper = this.collisionCheckbox.parentElement!;
const characterWrapper = this.characterModelSelect.parentElement!;
const walkModeWrapper = this.walkModeSelect.parentElement!;
if (this.state.mode === 'walk') {
// 漫游模式:隐藏速度,显示模型、行走模式、重力、碰撞
speedWrapper.style.display = 'none';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'flex';
walkModeWrapper.style.display = 'flex';
} else {
// 默认或路径模式:显示速度、重力、碰撞,隐藏模型和行走模式
speedWrapper.style.display = 'flex';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'none';
walkModeWrapper.style.display = 'none';
}
}
private updateSpeedDisplay(): void {
this.speedDisplay.textContent = `${this.state.speed}X`;
this.updateSpeedButtonStates();
}
private updateSpeedButtonStates(): void {
this.speedDecreaseBtn.disabled = this.state.speed <= 1;
this.speedIncreaseBtn.disabled = this.state.speed >= 10;
}
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
'plan-view': '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/></svg>',
'path': '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>',
'walk': '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>'
};
return icons[type] || '';
}
public setLocales(): void {
// 更新速度标签
const speedLabel = this.speedControl.querySelector('.walk-speed-label');
if (speedLabel) {
speedLabel.textContent = t('walkControl.speed');
}
// 更新复选框标签
this.gravityLabel.textContent = t('walkControl.gravity');
this.collisionLabel.textContent = t('walkControl.collision');
// 更新角色模型下拉框
this.characterModelLabel.textContent = t('walkControl.characterModel.label');
this.characterModelSelect.innerHTML = '';
const constructionWorkerOption = document.createElement('option');
constructionWorkerOption.value = 'construction-worker';
constructionWorkerOption.textContent = t('walkControl.characterModel.constructionWorker');
constructionWorkerOption.selected = this.state.characterModel === 'construction-worker';
this.characterModelSelect.appendChild(constructionWorkerOption);
const officeMaleOption = document.createElement('option');
officeMaleOption.value = 'office-male';
officeMaleOption.textContent = t('walkControl.characterModel.officeMale');
officeMaleOption.selected = this.state.characterModel === 'office-male';
this.characterModelSelect.appendChild(officeMaleOption);
// 更新行走模式下拉框
this.walkModeLabel.textContent = t('walkControl.walkMode.label');
this.walkModeSelect.innerHTML = '';
const walkOption = document.createElement('option');
walkOption.value = 'walk';
walkOption.textContent = t('walkControl.walkMode.walk');
walkOption.selected = this.state.walkMode === 'walk';
this.walkModeSelect.appendChild(walkOption);
const runOption = document.createElement('option');
runOption.value = 'run';
runOption.textContent = t('walkControl.walkMode.run');
runOption.selected = this.state.walkMode === 'run';
this.walkModeSelect.appendChild(runOption);
// 更新退出按钮
this.exitBtn.textContent = t('walkControl.exit');
}
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-walk-control-bg', theme.panelBackground ?? 'rgba(0, 0, 0, 0.8)');
style.setProperty('--bim-walk-btn-hover', theme.componentHover ?? 'rgba(255, 255, 255, 0.15)');
style.setProperty('--bim-walk-btn-active', theme.componentActive ?? 'rgba(255, 255, 255, 0.3)');
style.setProperty('--bim-primary-color', theme.primary ?? '#1890ff');
style.setProperty('--bim-primary-hover', theme.primaryHover ?? '#40a9ff');
style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
style.setProperty('--bim-text-color', theme.textPrimary ?? '#fff');
style.setProperty('--bim-divider-color', theme.border ?? 'rgba(255, 255, 255, 0.2)');
style.setProperty('--bim-speed-group-bg', theme.componentHover ?? 'rgba(255, 255, 255, 0.1)');
style.setProperty('--bim-speed-btn-bg', theme.componentHover ?? 'rgba(255, 255, 255, 0.1)');
style.setProperty('--bim-speed-btn-hover', theme.componentActive ?? 'rgba(255, 255, 255, 0.2)');
style.setProperty('--bim-select-bg', theme.componentHover ?? 'rgba(255, 255, 255, 0.1)');
style.setProperty('--bim-select-border', theme.border ?? 'rgba(255, 255, 255, 0.2)');
style.setProperty('--bim-select-option-bg', theme.panelBackground ?? '#333');
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* 漫游控制模式
*/
export type WalkControlMode = 'none' | 'path' | 'walk';
/**
* 角色模型类型
*/
export type CharacterModel = 'office-male' | 'construction-worker';
/**
* 行走模式类型
*/
export type WalkMode = 'walk' | 'run';
/**
* 漫游控制面板配置选项
*/
export interface WalkControlPanelOptions {
/** 平面图切换回调 */
onPlanViewToggle?: (isActive: boolean) => void;
/** 路径漫游模式切换回调 */
onPathModeToggle?: (isActive: boolean) => void;
/** 漫游模式切换回调 */
onWalkModeToggle?: (isActive: boolean) => void;
/** 速度变化回调 */
onSpeedChange?: (speed: number) => void;
/** 重力切换回调 */
onGravityToggle?: (enabled: boolean) => void;
/** 碰撞切换回调 */
onCollisionToggle?: (enabled: boolean) => void;
/** 角色模型变化回调 */
onCharacterModelChange?: (model: CharacterModel) => void;
/** 行走模式变化回调 */
onWalkModeChange?: (mode: WalkMode) => void;
/** 退出回调 */
onExit?: () => void;
/** 默认速度 (0-100) */
defaultSpeed?: number;
/** 默认重力状态 */
defaultGravity?: boolean;
/** 默认碰撞状态 */
defaultCollision?: boolean;
/** 默认角色模型 */
defaultCharacterModel?: CharacterModel;
/** 默认行走模式 */
defaultWalkMode?: WalkMode;
}
/**
* 漫游控制状态
*/
export interface WalkControlState {
/** 当前模式 */
mode: WalkControlMode;
/** 平面图是否激活 */
isPlanViewActive: boolean;
/** 移动速度 (0-100) */
speed: number;
/** 重力是否启用 */
gravity: boolean;
/** 碰撞是否启用 */
collision: boolean;
/** 当前角色模型 */
characterModel: CharacterModel;
/** 当前行走模式 */
walkMode: WalkMode;
}