fix(menu): refactor menu system to use pure config objects and fix submenu click events

This commit is contained in:
yuding
2025-12-09 18:34:43 +08:00
parent c112c87dad
commit 9ae1d9d809
38 changed files with 6756 additions and 2575 deletions

0
.idea/.gitignore generated vendored Normal file
View File

6
.idea/MarsCodeWorkspaceAppSettings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="progress" value="1.0" />
</component>
</project>

9
.idea/engine.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/engine.iml" filepath="$PROJECT_DIR$/.idea/engine.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

104
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="添加测试信息" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 7
}</component>
<component name="ProjectId" id="36NEeHE9A1qKqNSOk1qNAyy95Qz" />
<component name="ProjectViewState">
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/yuding/WORK/LYZ/project/bimEngine/engine&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/yuding/WORK/LYZ/project/bimEngine/engine/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.27397.103" />
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-IU-252.27397.103" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="" />
<created>1764838723949</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1764838723949</updated>
<workItem from="1764838724952" duration="1396000" />
<workItem from="1764840544897" duration="4283000" />
<workItem from="1764919374022" duration="3896000" />
<workItem from="1764923867307" duration="53000" />
<workItem from="1764923944573" duration="598000" />
<workItem from="1765159215556" duration="215000" />
</task>
<task id="LOCAL-00001" summary="添加测试信息">
<option name="closed" value="true" />
<created>1764844749103</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1764844749103</updated>
</task>
<task id="LOCAL-00002" summary="添加测试信息">
<option name="closed" value="true" />
<created>1764844873205</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1764844873205</updated>
</task>
<task id="LOCAL-00003" summary="添加测试信息">
<option name="closed" value="true" />
<created>1765159346641</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1765159346641</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="添加测试信息" />
<option name="LAST_COMMIT_MESSAGE" value="添加测试信息" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>

View File

@@ -70,6 +70,7 @@ npm run build
**运行 HTML Demo (纯 JS):** **运行 HTML Demo (纯 JS):**
```bash ```bash
npm run dev:demo npm run dev:demo
# 自动执行:构建 SDK -> 复制 SDK 到 demo/lib -> 启动服务器
# 开发服务器运行在 http://localhost:3000 # 开发服务器运行在 http://localhost:3000
# 自动打开 /demo/index.html # 自动打开 /demo/index.html
``` ```
@@ -77,6 +78,7 @@ npm run dev:demo
**运行 Vue Demo:** **运行 Vue Demo:**
```bash ```bash
npm run dev:demo-vue npm run dev:demo-vue
# 自动执行:构建 SDK -> 复制 SDK 到 demo-vue/public/lib -> 启动服务器
# 开发服务器运行在 http://localhost:3000 # 开发服务器运行在 http://localhost:3000
# 自动打开 Vue 示例页面 # 自动打开 Vue 示例页面
``` ```
@@ -88,8 +90,8 @@ npm run dev:all
``` ```
**注意**: **注意**:
- 运行 Demo 前,建议先执行 `npm run build` 构建 SDK确保 Demo 使用的是最新构建的 SDK - 现在的 `npm run dev:demo``npm run dev:demo-vue` 命令已包含自动化构建流程,无需手动运行 `npm run build`
- Demo 会自动从 `dist/` 目录或通过开发服务器加载 SDK - Demo 会自动从本地复制的 SDK 副本加载。
### 1.5 发布配置 ### 1.5 发布配置
@@ -534,6 +536,7 @@ engine.toolbar.setButtonVisibility('my-button', false);
| `ToolbarManager` | `src/managers/toolbar-manager.ts` | 管理底部工具栏 | `BimComponent` | | `ToolbarManager` | `src/managers/toolbar-manager.ts` | 管理底部工具栏 | `BimComponent` |
| `ButtonGroupManager` | `src/managers/button-group-manager.ts` | 管理通用按钮组 | `BimComponent` | | `ButtonGroupManager` | `src/managers/button-group-manager.ts` | 管理通用按钮组 | `BimComponent` |
| `EngineManager` | `src/managers/engine-manager.ts` | 管理 3D 引擎 | `BimComponent` | | `EngineManager` | `src/managers/engine-manager.ts` | 管理 3D 引擎 | `BimComponent` |
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu)。直接使用 `MenuItemConfig` 接口配置 | `BimComponent` |
### 4.2 组件类清单 ### 4.2 组件类清单
@@ -544,6 +547,8 @@ engine.toolbar.setButtonVisibility('my-button', false);
| `BimButtonGroup` | `src/components/button-group/index.ts` | 通用按钮组组件 | `IBimComponent` | | `BimButtonGroup` | `src/components/button-group/index.ts` | 通用按钮组组件 | `IBimComponent` |
| `Toolbar` | `src/components/button-group/toolbar/index.ts` | 底部工具栏组件 | 继承 `BimButtonGroup` | | `Toolbar` | `src/components/button-group/toolbar/index.ts` | 底部工具栏组件 | 继承 `BimButtonGroup` |
| `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` | | `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` |
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
### 4.3 服务类清单 ### 4.3 服务类清单
@@ -960,7 +965,7 @@ export class MyDialog implements IBimComponent {
#### 语言要求(强制) #### 语言要求(强制)
- **所有输出必须使用中文**,包括: - **所有输出必须使用中文**,包括:
- 代码注释 - 代码注释 (**强制:所有代码注释必须使用中文,解释清晰详细**)
- 文档说明 - 文档说明
- 与用户交流 - 与用户交流
- 错误信息 - 错误信息

View File

@@ -89,6 +89,30 @@ onMounted(() => {
engine.value = new Engine(appContainer.value, { locale: 'zh-CN' }); engine.value = new Engine(appContainer.value, { locale: 'zh-CN' });
initEngine(); initEngine();
console.log('Engine initialized:', engine.value); console.log('Engine initialized:', engine.value);
// --- 新增:注册右键菜单 ---
engine.value.rightKey.registerHandler((event: any) => {
const { x, y } = event;
return [
{
id: 'home',
label: 'toolbar.home', // 使用国际化 Key
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>',
onClick: () => {
console.log('Go Home');
}
},
{
id: 'log-pos',
label: '打印坐标',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
onClick: () => {
console.log(`Clicked at: ${x}, ${y}`);
alert(`Right clicked at: ${x}, ${y}`);
}
}
];
});
} }
} catch (err) { } catch (err) {
console.error('Init failed:', err); console.error('Init failed:', err);

View File

@@ -406,4 +406,4 @@
</script> </script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

201
dist/index.d.ts vendored
View File

@@ -146,6 +146,7 @@ export declare class BimEngine extends EventEmitter {
buttonGroup: ButtonGroupManager | null; buttonGroup: ButtonGroupManager | null;
dialog: DialogManager | null; dialog: DialogManager | null;
engine: EngineManager | null; engine: EngineManager | null;
rightKey: RightKeyManager | null;
get localeManager(): LocaleManager; get localeManager(): LocaleManager;
get themeManager(): ThemeManager; get themeManager(): ThemeManager;
constructor(container: HTMLElement | string, options?: { constructor(container: HTMLElement | string, options?: {
@@ -169,6 +170,111 @@ export declare class BimEngine extends EventEmitter {
destroy(): void; destroy(): void;
} }
/**
* 通用菜单列表组件
* 负责渲染一组菜单项,支持分组、排序、图标、快捷键提示和递归多级子菜单。
* 它不包含定位逻辑,仅负责内容渲染。
*/
export declare class BimMenu implements IBimComponent {
element: HTMLElement;
private options;
private unsubscribeLocale;
private unsubscribeTheme;
private activeSubMenu;
constructor(options: MenuOptions);
/**
* 初始化组件
* 渲染 DOM 结构并订阅语言变更
*/
init(): void;
/**
* 设置主题
* @param theme 全局主题配置
*/
setTheme(theme: ThemeConfig): void;
/**
* 响应语言变更
* 重新渲染整个菜单以更新文本
*/
setLocales(): void;
/**
* 销毁组件
* 清理事件监听、子菜单和 DOM 元素
*/
destroy(): void;
/**
* 获取组件根元素
* 实现 IRightKeyContent 接口,允许被 RightKey 容器挂载
*/
getElement(): HTMLElement;
/**
* 核心渲染逻辑
* 处理分组、排序和 DOM 生成
*/
private render;
/**
* 创建单个菜单项的 DOM 元素
*/
private createItemElement;
/**
* 打开子菜单
* @param item 当前菜单项
* @param parentLi 触发的 DOM 元素(用于定位)
*/
private openSubMenu;
/**
* 关闭当前激活的子菜单
*/
private closeSubMenu;
}
/**
* 右键浮层容器组件 (RightKey)
* 这是一个纯粹的定位容器,负责在屏幕指定位置显示内容。
* 它不关心具体内容是什么,只处理定位、边界检测和关闭逻辑。
*/
export declare class BimRightKey implements IBimComponent {
private element;
private content;
private isVisible;
private onCloseCallback?;
constructor(options?: RightKeyOptions);
init(): void;
setTheme(_theme: ThemeConfig): void;
setLocales(): void;
destroy(): void;
/**
* 设置关闭时的回调函数
* 通常用于通知 Manager 状态变更
*/
setOnClose(callback: () => void): void;
/**
* 挂载内容组件
* @param content 实现了 IRightKeyContent 接口的组件实例
*/
mount(content: IRightKeyContent): void;
/**
* 卸载当前内容
*/
unmountContent(): void;
/**
* 在指定位置显示容器
* 包含智能边界检测逻辑,防止溢出屏幕
* @param x 目标 X 坐标 (通常是鼠标点击位置)
* @param y 目标 Y 坐标
*/
show(x: number, y: number): void;
/**
* 隐藏容器
*/
hide(): void;
/**
* 处理全局点击事件
* 用于检测是否点击了容器外部
*/
private handleGlobalClick;
}
/** 按钮内部文字图标排列 */ /** 按钮内部文字图标排列 */
declare type ButtonAlign = 'vertical' | 'horizontal'; declare type ButtonAlign = 'vertical' | 'horizontal';
@@ -249,11 +355,11 @@ export declare function createEngine(r) {
const e = r.version || "v1"; const e = r.version || "v1";
switch (e) { switch (e) {
case "v2": case "v2":
return new Nc(r); return new Bc(r);
case "v1": case "v1":
return new I_(r); return new N_(r);
: :
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Nc(r); return console.warn(`Version '${e}' not found. Falling back to v2.`), new Bc(r);
} }
} }
@@ -483,6 +589,17 @@ declare interface IBimComponent {
destroy(): void; destroy(): void;
} }
declare interface IRightKeyContent {
/**
* 获取组件的根 DOM 元素
*/
getElement(): HTMLElement;
/**
* 销毁组件
*/
destroy(): void;
}
declare type Listener<T = any> = (payload: T) => void; declare type Listener<T = any> = (payload: T) => void;
declare type LocaleChangeListener = (locale: LocaleType) => void; declare type LocaleChangeListener = (locale: LocaleType) => void;
@@ -519,6 +636,39 @@ declare class LocaleManager {
*/ */
declare type LocaleType = 'zh-CN' | 'en-US'; declare type LocaleType = 'zh-CN' | 'en-US';
/**
* 菜单项配置接口 (用于简化的对象配置)
*/
export declare interface MenuItemConfig {
id: string;
label: string;
onClick?: () => void;
icon?: string;
group?: string;
order?: number;
children?: MenuItemConfig[];
disabled?: boolean;
visible?: boolean;
}
/**
* <20><><EFBFBD>单组件配置选项
*/
declare interface MenuOptions {
/**
* 菜单项列表
* 可以是扁平数组,组件会根据 group 字段自动分组
*/
items: MenuItemConfig[];
/**
* 分组显示顺序
* 包含组 ID 的字符串数组。
* 例如: ['view', 'edit', 'tools']
* 未在此数组中定义的组将按默认顺序排在最后。
*/
groupOrder?: string[];
}
/** /**
* 模型加载选项 * 模型加载选项
* 用于配置模型的位置、旋转和缩放 * 用于配置模型的位置、旋转和缩放
@@ -538,6 +688,51 @@ export declare interface OptButton extends ButtonConfig {
children?: OptButton[]; children?: OptButton[];
} }
/**
* 右键菜单管理器 (RightKeyManager)
* 负责协调右键交互流程:
* 1. 监听 Canvas/容器的 contextmenu 事件
* 2. 通过注册的处理器 (Handler) 获取需要显示的菜单项
* 3. 实例化 Menu 组件并装载到 RightKey 容器中显示
*/
declare class RightKeyManager extends BimComponent {
private container;
private rightKeyPanel;
private contextHandlers;
constructor(engine: BimEngine, container: HTMLElement);
private initEventListeners;
destroy(): void;
/**
* 注册上下文菜单处理器
* @param handler 处理函数,接收鼠标事件,返回菜单项数组
*/
registerHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void;
/**
* 手动显示菜单
* 允许外部直接调用以显示特定的菜单,不一定依赖右键事件
* @param x 屏幕 X 坐标
* @param y 屏幕 Y 坐标
* @param items 菜单项列表
* @param groupOrder 可<><E58FAF>的分组顺序
*/
showMenu(x: number, y: number, items: MenuItemConfig[], groupOrder?: string[]): void;
/**
* 隐藏右键菜单
*/
hide(): void;
/**
* 处理右键点击事件
*/
private handleContextMenu;
}
declare interface RightKeyOptions {
/** 自定义 CSS 类名 */
className?: string;
/** 层级 (z-index) */
zIndex?: number;
}
declare type ThemeChangeListener = (theme: ThemeConfig) => void; declare type ThemeChangeListener = (theme: ThemeConfig) => void;
/** /**

File diff suppressed because it is too large Load Diff

882
docs/components/dialog.md Normal file
View File

@@ -0,0 +1,882 @@
# Dialog 组件详细文档
> 本文档详细描述 Dialog 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `BimDialog`
- **文件路径**: `src/components/dialog/index.ts`
- **类型定义**: `src/components/dialog/index.type.ts`
- **样式文件**: `src/components/dialog/index.css`
- **实现接口**: `IBimComponent`
- **用途**: 提供可拖拽、可缩放的通用弹窗组件,支持自定义内容和样式
### 1.2 在 SDK 中的位置
- Dialog 组件是独立的 UI 组件
- 必须通过 `DialogManager` 使用,不允许直接使用
- `DialogManager` 位于 `src/managers/dialog-manager.ts`
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: DialogOptions)
```
**参数**:
- `options`: `DialogOptions` - 弹窗配置选项(详见类型定义)
**默认配置**:
```typescript
{
title: 'Dialog',
width: 300,
height: 'auto',
position: 'center',
draggable: true,
resizable: false,
minWidth: 200,
minHeight: 100
}
```
**行为**:
- 合并用户配置和默认配置
- 创建 DOM 结构
- 自动调用 `init()` 方法
### 2.2 公共方法
#### `init(): void`
初始化组件功能(实现 `IBimComponent` 接口)
**功能**:
- 将弹窗元素添加到容器
- 初始化位置
- 如果 `draggable` 为 true初始化拖拽功能
- 如果 `resizable` 为 true初始化缩放功能
- 订阅主题和语言变更
- 调用 `onOpen` 回调
**调用时机**: 构造函数中自动调用,也可手动调用
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 将主题颜色映射到 CSS 变量
- 如果用户未自定义颜色,使用主题颜色
- 如果用户已自定义颜色,保持用户自定义
**CSS 变量映射**:
- `--bim-dialog-bg``theme.panelBackground` (如果未自定义 `backgroundColor`)
- `--bim-dialog-header-bg``theme.componentHover` (如果未自定义 `headerBackgroundColor`)
- `--bim-dialog-title-color``theme.textPrimary` (如果未自定义 `titleColor`)
- `--bim-dialog-text-color``theme.textPrimary` (如果未自定义 `textColor`)
- `--bim-dialog-border-color``theme.border` (如果未自定义 `borderColor`)
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 更新标题文本(如果标题是翻译键)
- 使用 `t()` 函数获取翻译后的文本
**行为**:
- 查找 `.bim-dialog-title` 元素
- 如果 `options.title` 存在,使用 `t(options.title)` 更新文本
#### `setContent(content: HTMLElement | string): void`
动态设置弹窗内容
**参数**:
- `content`: `HTMLElement | string` - 内容元素或 HTML 字符串
**功能**:
- 清空当前内容
- 设置新内容(支持 HTML 字符串或 DOM 元素)
#### `close(): void`
关闭弹窗并销毁
**功能**:
- 清理 `requestAnimationFrame`(如果存在)
- 取消主题和语言订阅
- 从 DOM 中移除元素
- 标记为已销毁
- 调用 `onClose` 回调
#### `destroy(): void`
销毁组件(实现 `IBimComponent` 接口)
**功能**:
- 调用 `close()` 方法
### 2.3 私有方法(供理解实现细节)
#### `createDom(): HTMLElement`
创建弹窗的 DOM 结构
**返回**: 创建的弹窗根元素
**DOM 结构**:
```html
<div class="bim-dialog" [id="..."]>
<div class="bim-dialog-header" [class="draggable"]>
<span class="bim-dialog-title">标题文本</span>
<span class="bim-dialog-close">&times;</span>
</div>
<div class="bim-dialog-content">
<!-- 用户内容 -->
</div>
[<div class="bim-dialog-resize-handle"></div>] <!-- 如果 resizable -->
</div>
```
**关键实现**:
1. 创建根元素,应用 CSS 变量和尺寸
2. 创建标题栏,包含标题和关闭按钮
3. 创建内容区域,支持 HTML 字符串或 DOM 元素
4. 如果 `resizable` 为 true创建缩放手柄
5. **事件拦截**: 绑定所有鼠标/触摸事件,使用 `stopPropagation()` 阻止冒泡,防止传递给 3D 引擎
**事件拦截列表**:
```typescript
['click', 'dblclick', 'contextmenu', 'wheel',
'mousedown', 'mouseup', 'mousemove',
'touchstart', 'touchend', 'touchmove',
'pointerdown', 'pointerup', 'pointermove',
'pointerenter', 'pointerleave', 'pointerover', 'pointerout']
```
#### `initPosition(): void`
初始化弹窗位置
**功能**:
- 根据 `position` 选项计算弹窗位置
- 支持预设位置(如 'center', 'top-left' 等)或坐标对象 `{ x, y }`
- 确保弹窗不超出容器边界
**位置计算逻辑**:
- `center`: `left = (containerWidth - dialogWidth) / 2`, `top = (containerHeight - dialogHeight) / 2`
- `top-left`: `left = 0`, `top = 0`
- `top-center`: `left = (containerWidth - dialogWidth) / 2`, `top = 0`
- `top-right`: `left = containerWidth - dialogWidth`, `top = 0`
- `left-center`: `left = 0`, `top = (containerHeight - dialogHeight) / 2`
- `right-center`: `left = containerWidth - dialogWidth`, `top = (containerHeight - dialogHeight) / 2`
- `bottom-left`: `left = 0`, `top = containerHeight - dialogHeight`
- `bottom-center`: `left = (containerWidth - dialogWidth) / 2`, `top = containerHeight - dialogHeight`
- `bottom-right`: `left = containerWidth - dialogWidth`, `top = containerHeight - dialogHeight`
- `{ x, y }`: 直接使用坐标值
**边界限制**:
```typescript
left = Math.max(0, Math.min(left, containerWidth - dialogWidth));
top = Math.max(0, Math.min(top, containerHeight - dialogHeight));
```
#### `initDrag(): void`
初始化拖拽功能
**功能**:
- 在标题栏上绑定 `mousedown` 事件
- 实现拖拽移动功能
- 使用 `requestAnimationFrame` 优化性能
- 使用 `capture: true` 确保事件在捕获阶段处理
**拖拽流程**:
1. `mousedown`: 记录起始位置和弹窗当前位置,缓存容器和弹窗尺寸
2. `mousemove`: 计算偏移量,更新弹窗位置,限制在容器边界内
3. `mouseup`: 清理事件监听和动画帧
**性能优化**:
- 使用 `requestAnimationFrame` 节流更新
- 缓存容器和弹窗尺寸,减少 reflow
- 使用 `capture: true` 确保事件处理
#### `initResize(): void`
初始化缩放功能
**功能**:
- 在缩放手柄上绑定 `mousedown` 事件
- 实现右下角拖拽缩放功能
- 使用 `requestAnimationFrame` 优化性能
- 限制最小尺寸
**缩放流程**:
1. `mousedown`: 记录起始位置和弹窗当前尺寸
2. `mousemove`: 计算偏移量,更新弹窗尺寸,限制最小尺寸
3. `mouseup`: 清理事件监听和动画帧
**尺寸限制**:
```typescript
newWidth = Math.max(minWidth || 100, startWidth + deltaX);
newHeight = Math.max(minHeight || 50, startHeight + deltaY);
```
---
## 3. 分化组件说明
### 3.1 BimInfoDialog
**文件路径**: `src/components/dialog/bimInfoDialog/index.ts`
**继承关系**: `BimInfoDialog extends BimDialog`
**特殊功能**:
- 预定义了信息展示的内容结构
- 包含标题、信息列表和操作按钮
- 使用固定的配置(宽度 320px可缩放可拖拽
**实现方式**:
- 在构造函数中创建内容 DOM
- 调用父类构造函数,传入预定义的配置
- 不需要重写其他方法,直接继承父类功能
**内容结构**:
```html
<div class="bim-info-dialog-content">
<h3>Model Information</h3>
<ul>
<li><strong>Name:</strong> Sample Project</li>
<li><strong>Version:</strong> 1.0.0</li>
<li><strong>Date:</strong> 当前日期</li>
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
</ul>
<button>Update Status</button>
</div>
```
**与父类的差异**:
- 固定了内容结构
- 固定了部分配置选项
- 其他功能完全继承自 `BimDialog`
---
## 4. Manager API 文档
### 4.1 DialogManager 类
**文件路径**: `src/managers/dialog-manager.ts`
**继承关系**: `DialogManager extends BimComponent`
### 4.2 构造函数
```typescript
constructor(engine: BimEngine, container: HTMLElement)
```
**参数**:
- `engine`: `BimEngine` - 引擎实例
- `container`: `HTMLElement` - 弹窗挂载的目标容器
**行为**:
- 保存容器引用
- 监听 `ui:open-dialog` 事件
- 如果事件 payload.id 是 'info',自动打开信息弹窗
### 4.3 公共方法
#### `create(options: Omit<DialogOptions, 'container'>): BimDialog`
创建一个通用弹窗
**参数**:
- `options`: `Omit<DialogOptions, 'container'>` - 弹窗配置(不需要传 container
**返回**: `BimDialog` 实例
**功能**:
- 自动使用 Manager 绑定的容器
- 创建 `BimDialog` 实例
- 应用当前主题
- 将弹窗添加到活跃列表
-`onClose` 回调中自动从列表移除
**使用示例**:
```typescript
const dialog = engine.dialog.create({
title: '测试弹窗',
content: '这是内容',
width: 400,
height: 300
});
```
#### `showInfoDialog(): void`
显示二次封装的模型信息弹窗
**功能**:
- 创建 `BimInfoDialog` 实例
- 直接显示信息弹窗
**注意**: 此方法创建的弹窗不会自动加入管理列表(遗留逻辑)
#### `updateTheme(theme: ThemeConfig): void`
响应全局主题变更
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 遍历所有活跃弹窗
- 调用每个弹窗的 `setTheme()` 方法
#### `destroy(): void`
销毁管理器
**功能**:
- 销毁所有活跃弹窗
- 清空活跃列表
---
## 5. UI 详细描述
### 5.1 DOM 结构
**完整 HTML 结构**:
```html
<div class="bim-dialog" id="[可选ID]" style="[CSS变量和尺寸]">
<!-- 标题栏 -->
<div class="bim-dialog-header [draggable]">
<span class="bim-dialog-title">标题文本</span>
<span class="bim-dialog-close">&times;</span>
</div>
<!-- 内容区域 -->
<div class="bim-dialog-content">
<!-- 用户内容HTML字符串或DOM元素 -->
</div>
<!-- 缩放手柄(如果 resizable -->
<div class="bim-dialog-resize-handle"></div>
</div>
```
### 5.2 CSS 类名和样式
#### `.bim-dialog` (根元素)
- `position: absolute` - 绝对定位
- `background-color: var(--bim-dialog-bg)` - 背景色CSS 变量)
- `border: 1px solid var(--bim-dialog-border-color)` - 边框
- `border-radius: 6px` - 圆角
- `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3)` - 阴影
- `display: flex` + `flex-direction: column` - 垂直布局
- `z-index: 10001` - 层级(确保在 3D 引擎之上)
- `min-width: 200px` - 最小宽度
- `min-height: 100px` - 最小高度
- `pointer-events: auto` - 确保可接收事件
#### `.bim-dialog-header` (标题栏)
- `height: 32px` - 固定高度
- `background-color: var(--bim-dialog-header-bg)` - 背景色
- `display: flex` + `align-items: center` + `justify-content: space-between` - 水平布局
- `padding: 0 10px` - 内边距
- `cursor: default` - 默认光标
- `user-select: none` - 禁止选中
- `border-bottom: 1px solid var(--bim-dialog-border-color)` - 底边框
#### `.bim-dialog-header.draggable` (可拖拽标题栏)
- `cursor: move` - 移动光标
#### `.bim-dialog-title` (标题文本)
- `font-size: 14px` - 字体大小
- `font-weight: 500` - 字体粗细
- `white-space: nowrap` - 不换行
- `overflow: hidden` - 隐藏溢出
- `text-overflow: ellipsis` - 文本省略
- `color: var(--bim-dialog-title-color)` - 文字颜色
#### `.bim-dialog-close` (关闭按钮)
- `cursor: pointer` - 指针光标
- `font-size: 18px` - 字体大小
- `color: #999` - 默认颜色
- `line-height: 1` - 行高
- `margin-left: 8px` - 左边距
#### `.bim-dialog-close:hover` (关闭按钮悬停)
- `color: #fff` - 悬停颜色
#### `.bim-dialog-content` (内容区域)
- `flex: 1` - 占据剩余空间
- `padding: 10px` - 内边距
- `overflow: auto` - 内容溢出时滚动
- `font-size: 14px` - 字体大小
- `color: var(--bim-dialog-text-color)` - 文字颜色
#### `.bim-dialog-resize-handle` (缩放手柄)
- `position: absolute` - 绝对定位
- `width: 10px` + `height: 10px` - 尺寸
- `bottom: 0` + `right: 0` - 右下角位置
- `cursor: se-resize` - 缩放光标
- `z-index: 10` - 层级
#### `.bim-dialog-resize-handle::after` (缩放装饰)
- 使用伪元素创建右下角斜线装饰
- `border-right` + `border-bottom` - 创建斜线效果
### 5.3 CSS 变量
**定义位置**: `:root` 或元素内联样式
**变量列表**:
- `--bim-dialog-bg`: 窗体背景颜色(默认 `rgba(17, 17, 17, 0.95)`
- `--bim-dialog-header-bg`: 标题栏背景颜色(默认 `#2a2a2a`
- `--bim-dialog-title-color`: 标题文字颜色(默认 `#fff`
- `--bim-dialog-text-color`: 内容文字颜色(默认 `#ccc`
- `--bim-dialog-border-color`: 边框颜色(默认 `#444`
### 5.4 交互行为
#### 拖拽
- **触发区域**: 标题栏(`.bim-dialog-header`
- **触发条件**: `draggable: true`
- **行为**:
- 鼠标按下标题栏时开始拖拽
- 鼠标移动时弹窗跟随移动
- 鼠标释放时结束拖拽
- 弹窗位置限制在容器边界内
#### 缩放
- **触发区域**: 缩放手柄(`.bim-dialog-resize-handle`
- **触发条件**: `resizable: true`
- **行为**:
- 鼠标按下缩放手柄时开始缩放
- 鼠标移动时弹窗尺寸跟随变化
- 鼠标释放时结束缩放
- 尺寸限制在最小尺寸以上
#### 关闭
- **触发方式**:
- 点击关闭按钮(`.bim-dialog-close`
- 调用 `close()` 方法
- **行为**: 弹窗从 DOM 移除,调用 `onClose` 回调
### 5.5 响应式行为
- 弹窗位置会根据容器尺寸自动调整,确保不超出边界
- 内容区域支持滚动(`overflow: auto`
- 标题文本过长时显示省略号(`text-overflow: ellipsis`
---
## 6. 逻辑流程详细描述
### 6.1 初始化流程
1. **构造函数调用**:
- 合并配置选项(用户配置 + 默认配置)
- 调用 `createDom()` 创建 DOM 结构
- 保存 header 和 contentArea 引用
- 自动调用 `init()`
2. **init() 方法执行**:
- 检查是否已初始化,如果是则返回
- 将弹窗元素添加到容器
- 调用 `initPosition()` 计算并设置位置
- 如果 `draggable` 为 true调用 `initDrag()`
- 如果 `resizable` 为 true调用 `initResize()`
- 标记为已初始化
- 调用 `onOpen` 回调(如果存在)
- 订阅主题变更:`themeManager.subscribe()`
- 订阅语言变更:`localeManager.subscribe()`
### 6.2 生命周期
#### 创建阶段
- 构造函数 → `createDom()``init()`
#### 运行阶段
- 响应主题变更:`setTheme()` 被调用
- 响应语言变更:`setLocales()` 被调用
- 用户交互:拖拽、缩放、关闭
#### 销毁阶段
- `close()``destroy()` 被调用
- 清理 `requestAnimationFrame`
- 取消主题和语言订阅
- 从 DOM 移除元素
- 调用 `onClose` 回调
### 6.3 事件处理流程
#### 拖拽事件流程
1. 用户在标题栏按下鼠标 → `mousedown` 事件
2. 记录起始位置和弹窗当前位置
3. 缓存容器和弹窗尺寸
4. 在 document 上绑定 `mousemove``mouseup`(捕获阶段)
5. 鼠标移动时 → `mousemove` 事件
- 计算偏移量
- 使用 `requestAnimationFrame` 更新位置
- 限制在容器边界内
6. 鼠标释放时 → `mouseup` 事件
- 清理事件监听
- 清理动画帧
#### 缩放事件流程
1. 用户在缩放手柄按下鼠标 → `mousedown` 事件
2. 记录起始位置和弹窗当前尺寸
3. 在 document 上绑定 `mousemove``mouseup`(捕获阶段)
4. 鼠标移动时 → `mousemove` 事件
- 计算偏移量
- 使用 `requestAnimationFrame` 更新尺寸
- 限制在最小尺寸以上
5. 鼠标释放时 → `mouseup` 事件
- 清理事件监听
- 清理动画帧
#### 事件拦截流程
- 弹窗根元素上绑定所有鼠标/触摸事件
- 使用 `stopPropagation()` 阻止事件冒泡
- 防止事件传递给 3D 引擎
- 使用 `passive: false` 允许阻止默认行为
### 6.4 状态管理
#### 内部状态
- `_isInitialized`: 是否已初始化
- `_isDestroyed`: 是否已销毁
- `rafId`: `requestAnimationFrame` 的 ID用于节流
#### 订阅管理
- `unsubscribeTheme`: 主题订阅取消函数
- `unsubscribeLocale`: 语言订阅取消函数
### 6.5 与其他组件的交互
- **与 DialogManager**: 通过 Manager 创建和管理
- **与 ThemeManager**: 订阅主题变更
- **与 LocaleManager**: 订阅语言变更
- **与 3D 引擎**: 通过事件拦截防止交互冲突
---
## 7. 国际化支持
### 7.1 使用的翻译键
- `options.title`: 弹窗标题(如果提供,会通过 `t()` 函数翻译)
### 7.2 语言变更处理
- 组件订阅 `localeManager.subscribe()`
- 语言变更时,`setLocales()` 方法被调用
- 更新标题文本:`titleEl.textContent = t(this.options.title)`
### 7.3 实现细节
```typescript
public setLocales(): void {
if (this.options.title) {
const titleEl = this.header.querySelector('.bim-dialog-title');
if (titleEl) {
titleEl.textContent = t(this.options.title);
}
}
}
```
---
## 8. 主题支持
### 8.1 使用的主题变量
- `theme.panelBackground``--bim-dialog-bg`
- `theme.componentHover``--bim-dialog-header-bg`
- `theme.textPrimary``--bim-dialog-title-color``--bim-dialog-text-color`
- `theme.border``--bim-dialog-border-color`
### 8.2 主题变更处理
- 组件订阅 `themeManager.subscribe()`
- 主题变更时,`setTheme()` 方法被调用
- 如果用户未自定义颜色,使用主题颜色
- 如果用户已自定义颜色,保持用户自定义
### 8.3 实现细节
```typescript
public setTheme(theme: ThemeConfig) {
const style = this.element.style;
if (!this.options.backgroundColor)
style.setProperty('--bim-dialog-bg', theme.panelBackground);
if (!this.options.headerBackgroundColor)
style.setProperty('--bim-dialog-header-bg', theme.componentHover);
// ... 其他颜色映射
}
```
---
## 9. 使用示例
### 9.1 基本使用(通过 Manager
```typescript
import { BimEngine } from 'bim-engine-sdk';
const engine = new BimEngine('container');
// 创建简单弹窗
const dialog = engine.dialog.create({
title: 'dialog.testTitle', // 使用翻译键
content: '这是内容',
width: 400,
height: 300
});
// 创建可缩放弹窗
const resizableDialog = engine.dialog.create({
title: '可缩放弹窗',
content: '<div>内容</div>',
width: 500,
height: 400,
resizable: true,
draggable: true
});
// 创建自定义位置弹窗
const positionedDialog = engine.dialog.create({
title: '自定义位置',
content: '内容',
position: { x: 100, y: 100 },
width: 300,
height: 200
});
```
### 9.2 高级使用
```typescript
// 创建带自定义样式的弹窗
const styledDialog = engine.dialog.create({
title: '自定义样式',
content: '<div>内容</div>',
backgroundColor: 'rgba(100, 0, 0, 0.95)',
headerBackgroundColor: '#cc0000',
titleColor: '#ffffff',
textColor: '#ffcccc',
borderColor: '#ff6666',
width: 300,
height: 200
});
// 创建带回调的弹窗
const callbackDialog = engine.dialog.create({
title: '带回调',
content: '内容',
onOpen: () => {
console.log('弹窗已打开');
},
onClose: () => {
console.log('弹窗已关闭');
}
});
// 动态更新内容
const dynamicDialog = engine.dialog.create({
title: '动态内容',
content: '初始内容'
});
// 稍后更新内容
dynamicDialog.setContent('<div>新内容</div>');
```
### 9.3 使用信息弹窗
```typescript
// 通过 Manager 显示信息弹窗
engine.dialog.showInfoDialog();
// 通过事件总线打开(如果配置了监听)
engine.emit('ui:open-dialog', { id: 'info' });
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 关键算法
#### 位置计算算法
```typescript
// 计算居中位置
function calculateCenterPosition(containerWidth, containerHeight, dialogWidth, dialogHeight) {
return {
left: (containerWidth - dialogWidth) / 2,
top: (containerHeight - dialogHeight) / 2
};
}
// 边界限制算法
function clampPosition(left, top, containerWidth, containerHeight, dialogWidth, dialogHeight) {
return {
left: Math.max(0, Math.min(left, containerWidth - dialogWidth)),
top: Math.max(0, Math.min(top, containerHeight - dialogHeight))
};
}
```
#### 拖拽算法
```typescript
// 拖拽位置更新
function updateDragPosition(startX, startY, currentX, currentY, startLeft, startTop) {
const deltaX = currentX - startX;
const deltaY = currentY - startY;
return {
left: startLeft + deltaX,
top: startTop + deltaY
};
}
```
#### 缩放算法
```typescript
// 缩放尺寸更新
function updateResizeSize(startX, startY, currentX, currentY, startWidth, startHeight, minWidth, minHeight) {
const deltaX = currentX - startX;
const deltaY = currentY - startY;
return {
width: Math.max(minWidth, startWidth + deltaX),
height: Math.max(minHeight, startHeight + deltaY)
};
}
```
### 10.2 性能优化点
1. **requestAnimationFrame 节流**:
- 拖拽和缩放使用 `requestAnimationFrame` 节流更新
- 避免频繁的 DOM 操作
2. **尺寸缓存**:
- 拖拽开始时缓存容器和弹窗尺寸
- 减少 `getBoundingClientRect()` 调用
3. **事件捕获**:
- 使用 `capture: true` 确保事件在捕获阶段处理
- 即使内部元素阻止冒泡,也能捕获事件
4. **CSS 变量**:
- 使用 CSS 变量应用主题
- 主题变更时无需重新渲染 DOM
### 10.3 注意事项和边界情况
1. **容器尺寸为 0**:
- 位置计算时可能出现除零或负数
- 使用 `Math.max()` 确保位置不为负
2. **弹窗尺寸大于容器**:
- 位置计算时确保弹窗不超出容器
- 使用边界限制算法
3. **快速连续操作**:
- 使用 `rafId` 防止多个动画帧同时执行
- 确保动画帧正确清理
4. **事件清理**:
- 组件销毁时必须清理所有事件监听
- 必须取消主题和语言订阅
5. **事件拦截**:
- 必须拦截所有鼠标/触摸事件
- 防止事件传递给 3D 引擎
6. **翻译键处理**:
- 标题可能是翻译键或普通文本
- 只有翻译键才需要通过 `t()` 函数处理
---
## 11. 类型定义
### 11.1 DialogOptions
```typescript
interface DialogOptions extends DialogColors {
container: HTMLElement; // 弹窗挂载的父容器(必需)
title?: string; // 弹窗标题(可选,支持翻译键)
content?: HTMLElement | string; // 弹窗内容(可选)
width?: number | string; // 宽度(可选,默认 300
height?: number | string; // 高度(可选,默认 'auto'
position?: DialogPosition; // 位置(可选,默认 'center'
draggable?: boolean; // 是否可拖拽(可选,默认 true
resizable?: boolean; // 是否可缩放(可选,默认 false
minWidth?: number; // 最小宽度(可选,默认 200
minHeight?: number; // 最小高度(可选,默认 100
onClose?: () => void; // 关闭回调(可选)
onOpen?: () => void; // 打开回调(可选)
id?: string; // 弹窗 ID可选
}
```
### 11.2 DialogPosition
```typescript
type DialogPosition =
| 'center'
| 'top-left' | 'top-center' | 'top-right'
| 'left-center' | 'right-center'
| 'bottom-left' | 'bottom-center' | 'bottom-right'
| { x: number; y: number };
```
### 11.3 DialogColors
```typescript
interface DialogColors {
backgroundColor?: string; // 窗体背景颜色
headerBackgroundColor?: string; // 标题栏背景颜色
titleColor?: string; // 标题文字颜色
textColor?: string; // 内容文字颜色
borderColor?: string; // 边框颜色
}
```
---
## 12. 文件清单
### 12.1 相关文件
- `src/components/dialog/index.ts` - 主组件类
- `src/components/dialog/index.type.ts` - 类型定义
- `src/components/dialog/index.css` - 样式文件
- `src/components/dialog/bimInfoDialog/index.ts` - 信息弹窗(分化组件)
- `src/components/dialog/bimInfoDialog/index.css` - 信息弹窗样式
- `src/managers/dialog-manager.ts` - 管理器类
### 12.2 依赖文件
- `src/types/component.ts` - IBimComponent 接口
- `src/themes/types.ts` - ThemeConfig 类型
- `src/services/theme.ts` - ThemeManager
- `src/services/locale.ts` - LocaleManager
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 2024-XX-XX | 初始创建 | AI Assistant |
---
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

601
docs/components/engine.md Normal file
View File

@@ -0,0 +1,601 @@
# Engine 组件详细文档
> 本文档详细描述 Engine 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `Engine`
- **文件路径**: `src/components/engine/index.ts`
- **类型定义**: `src/components/engine/types.ts`
- **实现接口**: `IBimComponent`
- **用途**: 封装第三方 3D 引擎 SDK提供统一的 3D 引擎管理接口
### 1.2 在 SDK 中的位置
- Engine 组件是独立的 3D 引擎组件
- 必须通过 `EngineManager` 使用,不允许直接使用
- `EngineManager` 位于 `src/managers/engine-manager.ts`
- 采用延迟初始化模式,需要用户主动调用 `init()` 方法
### 1.3 第三方 SDK 依赖
- 依赖 `src/bim-engine-sdk.es.js` 中的 `createEngine` 函数
- 通过依赖注入方式使用,不直接导入
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: EngineOptions)
```
**参数**:
- `options`: `EngineOptions` - 3D 引擎配置选项
**默认配置**:
```typescript
{
backgroundColor: 0x1a1a1a, // 默认深色背景
version: 'v1', // 默认使用 v1 版本
showStats: false, // 默认不显示统计
showViewCube: true // 默认显示视图立方体
}
```
**行为**:
- 解析容器元素
- 如果容器没有 id生成一个唯一的 id
- 保存配置选项(设置默认值)
### 2.2 公共方法
#### `init(): void`
初始化组件(实现 `IBimComponent` 接口)
**功能**:
- 检查是否已初始化或已销毁
- 创建引擎配置对象
- 调用 `createEngineSDK()` 创建引擎实例
- 标记为已初始化
- 订阅主题变化
- 应用当前主题
**错误处理**:
- 如果创建失败,抛出错误并标记为未初始化
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 根据主题名称选择背景色:
- `dark`: 使用深色背景 `0x1a1a1a`
- `light`: 使用浅色背景 `0xf5f5f5`
- `custom`: 使用配置中的 `backgroundColor` 或默认值
- 尝试更新引擎背景色:
- 如果引擎有 `setBackgroundColor()` 方法,调用它
- 否则,如果引擎有 `scene.background`,设置其颜色
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 3D 引擎组件暂时不需要本地化
- 方法为空实现
#### `isInitialized(): boolean`
检查是否已初始化
**返回**: `boolean` - 是否已初始化
#### `loadModel(url: string, options?: ModelLoadOptions): void`
加载 3D 模型
**参数**:
- `url`: `string` - 模型文件 URL必需
- `options`: `ModelLoadOptions` (可选) - 加载选项
**功能**:
- 检查引擎是否已初始化
- 检查 URL 是否提供
- 调用引擎的 `loader.loadModel()` 方法加载模型
**加载选项**:
- `position`: `[number, number, number]` - 模型位置 [x, y, z]
- `rotation`: `[number, number, number]` - 模型旋转(弧度)[x, y, z]
- `scale`: `[number, number, number]` - 模型缩放 [x, y, z]
- `id`: `string` - 模型 ID可选
#### `getEngine(): any`
获取原始 3D 引擎实例
**返回**: 第三方 3D 引擎实例或 `null`
**功能**:
- 返回底层 3D 引擎实例
- 用于直接调用第三方引擎的其他 API
#### `destroy(): void`
销毁组件(实现 `IBimComponent` 接口)
**功能**:
- 取消主题订阅
- 清空容器内容
- 标记为已销毁和未初始化
**注意**: 不销毁引擎实例本身(由第三方 SDK 管理)
---
## 3. 分化组件说明
**Engine 组件没有分化组件**
---
## 4. Manager API 文档
### 4.1 EngineManager 类
**文件路径**: `src/managers/engine-manager.ts`
**继承关系**: `EngineManager extends BimComponent`
### 4.2 构造函数
```typescript
constructor(engine: BimEngine, container: HTMLElement)
```
**参数**:
- `engine`: `BimEngine` - 引擎实例
- `container`: `HTMLElement` - 3D 引擎挂载的目标容器
**行为**:
- 保存容器引用
- **不自动初始化引擎**(延迟初始化模式)
### 4.3 公共方法
#### `initialize(options?: Omit<EngineOptions, 'container'>): boolean`
初始化 3D 引擎
**参数**:
- `options`: `Omit<EngineOptions, 'container'>` (可选) - 引擎配置选项
**返回**: `boolean` - 是否初始化成功
**功能**:
- 如果已经初始化,先销毁旧的实例
- 创建 `Engine` 组件实例
- 调用组件的 `init()` 方法初始化引擎
- 返回初始化结果
**错误处理**:
- 如果初始化失败,返回 `false` 并输出错误信息
#### `isInitialized(): boolean`
检查 3D 引擎是否已初始化
**返回**: `boolean` - 是否已初始化
#### `loadModel(url: string, options?: ModelLoadOptions): void`
加载 3D 模型
**参数**:
- `url`: `string` - 模型文件 URL
- `options`: `ModelLoadOptions` (可选) - 加载选项
**功能**:
- 检查引擎是否已初始化
- 如果未初始化,输出错误信息
- 调用引擎组件的 `loadModel()` 方法
#### `getEngine(): any`
获取原始 3D 引擎实例
**返回**: 第三方 3D 引擎实例或 `null`
**功能**:
- 用于直接调用第三方引擎的其他 API
- 如果引擎未初始化,返回 `null` 并输出警告
#### `destroy(): void`
销毁 3D 引擎实例
**功能**:
- 调用引擎组件的 `destroy()` 方法
- 清空引擎实例引用
---
## 5. UI 详细描述
### 5.1 DOM 结构
**Engine 组件不直接创建 UI 元素**,而是:
1. **使用容器元素**:
- 容器元素由外部提供(通过 `EngineOptions.container`
- 如果容器没有 id组件会生成一个唯一的 id
2. **第三方 SDK 创建 UI**:
- 第三方 SDK 的 `createEngine()` 函数会在容器内创建 3D 场景
- 可能包括:
- Canvas 元素WebGL 渲染)
- 统计面板(如果 `showStats: true`
- 视图立方体(如果 `showViewCube: true`
### 5.2 容器 ID 生成
**算法**:
```typescript
containerId = `engine-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
```
**格式**: `engine-container-[时间戳]-[随机字符串]`
**示例**: `engine-container-1704067200000-k3j9x2m1p`
### 5.3 第三方 SDK 配置
**传递给 `createEngine()` 的配置对象**:
```typescript
{
containerId: string, // 容器 ID
backgroundColor: number, // 背景色(十六进制数字)
version: 'v1' | 'v2', // WebGL 版本
showStats: boolean, // 是否显示统计
showViewCube: boolean // 是否显示视图立方体
}
```
---
## 6. 逻辑流程详细描述
### 6.1 初始化流程
1. **构造函数调用**:
- 解析容器元素
- 生成或使用容器 ID
- 保存配置选项(设置默认值)
2. **init() 方法执行**(用户主动调用):
- 检查是否已初始化,如果是则返回
- 检查是否已销毁,如果是则报错
- 创建引擎配置对象
- 调用 `createEngineSDK()` 创建引擎实例
- 检查引擎实例是否创建成功
- 标记为已初始化
- 订阅主题变化:`themeManager.subscribe()`
- 应用当前主题
### 6.2 生命周期
#### 创建阶段
- 构造函数 → 保存配置
#### 初始化阶段(延迟)
- 用户调用 `init()` → 创建引擎实例 → 订阅主题
#### 运行阶段
- 响应主题变更:`setTheme()` 被调用
- 加载模型:`loadModel()` 被调用
- 用户交互3D 场景交互(由第三方 SDK 处理)
#### 销毁阶段
- `destroy()` 被调用
- 取消主题订阅
- 清空容器
- 更新状态
### 6.3 主题变更流程
1. 主题变更 → `themeManager` 通知订阅者
2. `setTheme()` 方法被调用
3. 根据主题名称选择背景色:
- `dark``0x1a1a1a`
- `light``0xf5f5f5`
- `custom` → 使用配置值或默认值
4. 尝试更新引擎背景色:
- 方法 1: 调用 `engine.setBackgroundColor(backgroundColor)`
- 方法 2: 设置 `engine.scene.background.setHex(backgroundColor)`
### 6.4 模型加载流程
1. 用户调用 `loadModel(url, options)`
2. 检查引擎是否已初始化
3. 检查 URL 是否提供
4. 调用 `engine.loader.loadModel(url, options)`
5. 第三方 SDK 处理模型加载
### 6.5 状态管理
#### 内部状态
- `_isInitialized`: 是否已初始化
- `_isDestroyed`: 是否已销毁
- `engine`: 第三方 3D 引擎实例
- `containerId`: 容器 ID
- `options`: 引擎配置选项
#### 订阅管理
- `unsubscribeTheme`: 主题订阅取消函数
### 6.6 与其他组件的交互
- **与 EngineManager**: 通过 Manager 创建和管理
- **与 ThemeManager**: 订阅主题变更
- **与第三方 SDK**: 通过 `createEngine()` 函数创建引擎实例
---
## 7. 国际化支持
### 7.1 使用的翻译键
**Engine 组件不使用翻译键**3D 引擎不需要国际化)。
### 7.2 语言变更处理
- `setLocales()` 方法为空实现
- 不订阅语言变更
---
## 8. 主题支持
### 8.1 使用的主题变量
- 根据主题名称选择背景色:
- `dark` 主题 → `0x1a1a1a`(深色背景)
- `light` 主题 → `0xf5f5f5`(浅色背景)
- `custom` 主题 → 使用配置值或默认值
### 8.2 主题变更处理
- 组件订阅 `themeManager.subscribe()`
- 主题变更时,`setTheme()` 方法被调用
- 根据主题名称选择背景色
- 尝试更新引擎背景色(通过引擎 API
### 8.3 实现细节
```typescript
public setTheme(theme: ThemeConfig): void {
let backgroundColor: number;
if (theme.name === 'dark') {
backgroundColor = 0x1a1a1a;
} else if (theme.name === 'light') {
backgroundColor = 0xf5f5f5;
} else {
backgroundColor = this.options.backgroundColor ?? 0x1a1a1a;
}
// 尝试更新引擎背景色
if (this.engine && typeof this.engine.setBackgroundColor === 'function') {
this.engine.setBackgroundColor(backgroundColor);
} else if (this.engine && this.engine.scene) {
if (this.engine.scene.background) {
this.engine.scene.background.setHex(backgroundColor);
}
}
}
```
---
## 9. 使用示例
### 9.1 基本使用(通过 EngineManager
```typescript
import { BimEngine } from 'bim-engine-sdk';
const engine = new BimEngine('container');
// 初始化 3D 引擎(延迟初始化)
const success = engine.initEngine({
backgroundColor: 0x333333,
version: 'v1',
showStats: true,
showViewCube: true
});
if (success) {
console.log('3D 引擎初始化成功');
}
// 加载模型
engine.engine.loadModel('/model/building.glb', {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
```
### 9.2 高级使用
```typescript
// 检查引擎是否已初始化
if (engine.engine.isInitialized()) {
// 加载模型
engine.engine.loadModel('/model/model.glb');
}
// 获取原始引擎实例,调用第三方 API
const rawEngine = engine.engine.getEngine();
if (rawEngine) {
// 调用第三方 SDK 的其他方法
rawEngine.someOtherMethod();
}
```
### 9.3 主题切换
```typescript
// 切换主题会自动更新 3D 场景背景色
engine.setTheme('dark'); // 深色背景
engine.setTheme('light'); // 浅色背景
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 关键算法
#### 容器 ID 生成算法
```typescript
function generateContainerId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `engine-container-${timestamp}-${random}`;
}
```
#### 主题背景色选择算法
```typescript
function selectBackgroundColor(theme: ThemeConfig, defaultColor: number): number {
if (theme.name === 'dark') {
return 0x1a1a1a; // 深色背景
} else if (theme.name === 'light') {
return 0xf5f5f5; // 浅色背景
} else {
return defaultColor; // 自定义主题,使用配置值
}
}
```
#### 引擎背景色更新算法
```typescript
function updateEngineBackground(engine: any, color: number): void {
// 方法 1: 如果引擎有 setBackgroundColor 方法
if (engine && typeof engine.setBackgroundColor === 'function') {
engine.setBackgroundColor(color);
return;
}
// 方法 2: 如果引擎有 scene.background
if (engine && engine.scene && engine.scene.background) {
engine.scene.background.setHex(color);
return;
}
// 方法 3: 无法更新(引擎可能不支持)
}
```
### 10.2 性能优化点
1. **延迟初始化**:
- 引擎不自动初始化,由用户主动调用
- 减少不必要的资源消耗
2. **状态检查**:
- 初始化前检查状态,避免重复初始化
- 销毁后禁止初始化,避免错误
3. **错误处理**:
- 所有操作都有错误检查
- 提供有意义的错误信息
### 10.3 注意事项和边界情况
1. **容器 ID 唯一性**:
- 容器 ID 必须唯一
- 如果容器已有 id使用现有 id
- 如果容器没有 id生成唯一 id
2. **延迟初始化**:
- 引擎不会自动初始化
- 用户必须主动调用 `init()``initialize()`
- 初始化前调用其他方法会报错
3. **引擎实例管理**:
- 引擎实例由第三方 SDK 创建
- 组件不负责销毁引擎实例本身
- 只负责清理订阅和容器
4. **主题更新兼容性**:
- 不同第三方 SDK 的 API 可能不同
- 需要尝试多种方式更新背景色
- 如果都不支持,静默失败
5. **模型加载**:
- 模型 URL 必须是可访问的
- 模型格式由第三方 SDK 支持
- 加载选项由第三方 SDK 处理
6. **错误处理**:
- 初始化失败时,标记为未初始化
- 提供有意义的错误信息
- 不抛出未捕获的异常
7. **第三方 SDK 依赖**:
- 依赖 `bim-engine-sdk.es.js` 文件
- 通过动态导入或全局变量访问
- 需要确保 SDK 文件已加载
---
## 11. 类型定义
### 11.1 EngineOptions
```typescript
interface EngineOptions {
container: HTMLElement; // 容器元素(必需)
backgroundColor?: number; // 背景颜色(可选,默认 0x1a1a1a
version?: 'v1' | 'v2'; // WebGL 版本(可选,默认 'v1'
showStats?: boolean; // 是否显示性能统计(可选,默认 false
showViewCube?: boolean; // 是否显示视图立方体(可选,默认 true
}
```
### 11.2 ModelLoadOptions
```typescript
interface ModelLoadOptions {
position?: [number, number, number]; // 模型位置 [x, y, z](可选)
rotation?: [number, number, number]; // 模型旋转(弧度)[x, y, z](可选)
scale?: [number, number, number]; // 模型缩放 [x, y, z](可选)
id?: string; // 模型 ID可选
}
```
---
## 12. 文件清单
### 12.1 相关文件
- `src/components/engine/index.ts` - 主组件类
- `src/components/engine/types.ts` - 类型定义
- `src/managers/engine-manager.ts` - 管理器类
- `src/bim-engine-sdk.es.js` - 第三方 SDK依赖
### 12.2 依赖文件
- `src/types/component.ts` - IBimComponent 接口
- `src/themes/types.ts` - ThemeConfig 类型
- `src/services/theme.ts` - ThemeManager
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 2024-XX-XX | 初始创建 | AI Assistant |
---
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

66
docs/components/menu.md Normal file
View File

@@ -0,0 +1,66 @@
# Menu 组件文档
## 1. 组件概述
`BimMenu` 是一个通用的菜单列表组件。
- **位置**: `src/components/menu/index.ts`
- **功能**: 渲染一组 `BimMenuItem`,支持分组、排序、图标、快捷键提示和多级子菜单。
- **特点**: 数据驱动(通过 Item 类实例),支持国际化。
## 2. 组件类 API
### `BimMenu`
#### 构造函数
`new BimMenu(options: MenuOptions)`
#### 方法
| 方法名 | 参数 | 返回值 | 描述 |
|:----|:---|:---|:---|
| `init` | `()` | `void` | 初始化,渲染 DOM |
| `destroy` | `()` | `void` | 销毁,清理事件和子菜单 |
| `getElement` | `()` | `HTMLElement` | 获取根 DOM 元素 (实现 IRightKeyContent) |
### `BimMenuItem` (基类)
所有菜单项必须继承此抽象类。
| 方法/属性 | 类型 | 描述 |
|:----|:---|:---|
| `id` | `string` | 唯一标识 |
| `group` | `string` | 分组 ID (默认 'default') |
| `order` | `number` | 排序权重 |
| `getLabel()` | `string` | 获取显示文本 |
| `onClick()` | `void` | 点击回调 |
| `getIcon()` | `string?` | 获取图标 HTML |
| `getChildren()` | `BimMenuItem[]?` | 获取子菜单项 |
## 3. UI 详细描述
- **根元素**: `<ul class="bim-menu">`
- **分组分割线**: `<li class="bim-menu-divider">`
- **菜单项**: `<li class="bim-menu-item">`
- 图标槽: `.bim-menu-item-icon`
- 文本槽: `.bim-menu-item-label`
- 箭头槽: `.bim-menu-item-arrow` (仅当有子菜单时)
## 4. 逻辑流程
1. **分组与排序**:
- 根据 `item.group` 将项归类。
- 根据 `options.groupOrder` 对组进行排序。
- 组内根据 `item.order` 排序。
2. **多级菜单**:
- 鼠标进入 (`mouseenter`) 带子菜单的项时,创建新的 `BimMenu` 实例。
- 将新实例挂载到临时的 fixed 容器中。
- 自动计算位置(通常在父项右侧,空间不足时翻转)。
## 5. 类型定义
```typescript
export interface MenuOptions {
items: BimMenuItem[];
groupOrder?: string[];
}
```

View File

@@ -0,0 +1,58 @@
# RightKey 组件文档
## 1. 组件概述
`BimRightKey` 是一个通用的右键浮层容器组件。
- **位置**: `src/components/right-key/index.ts`
- **功能**: 负责在指定坐标 (x, y) 显示内容,处理边界检测防止溢出屏幕,以及点击外部自动关闭。
- **特点**: 它不关心内容是什么,通过 `mount()` 方法挂载任意实现 `IRightKeyContent` 接口的内容组件。
## 2. 组件类 API
### `BimRightKey`
#### 方法
| 方法名 | 参数 | 返回值 | 描述 |
|:----|:---|:---|:---|
| `init` | `()` | `void` | 初始化,绑定全局点击事件 |
| `mount` | `(content: IRightKeyContent)` | `void` | 挂载内容组件 |
| `show` | `(x: number, y: number)` | `void` | <20><><EFBFBD>指定位置显示会自动调整位置防止溢出 |
| `hide` | `()` | `void` | 隐藏容器 |
| `destroy` | `()` | `void` | 销毁容器,解绑事件 |
| `setOnClose` | `(callback: () => void)` | `void` | 设置关闭时的回调 |
## 3. Manager API
### `RightKeyManager`
- **位置**: `src/managers/right-key-manager.ts`
- **功能**: 监听容器的 `contextmenu` 事件,决定显示什么内容。
#### 方法
| 方法名 | 参数 | 返回值 | 描述 |
|:----|:---|:---|:---|
| `registerHandler` | `(handler: (e) => BimMenuItem[])` | `void` | 注册上下文处理器 |
| `showMenu` | `(x, y, items, groupOrder?)` | `void` | 手动显示菜单 |
| `hide` | `()` | `void` | 隐藏 |
## 4. UI 详细描述
- **容器**: `<div class="bim-right-key"></div>`
- **定位**: `position: fixed`
- **层级**: `z-index: 10000` (默认)
## 5. 实现细节
- **边界检测**: 在 `show(x, y)` 时,会计算容器宽高和视口宽高。如果 `x + width > viewportWidth`,则 `x = x - width`(向左展开)。同理处理垂直方向。
- **点击外部关闭**: 监听 `document``mousedown` 事件,如果点击目标不在容器内,则调用 `hide()`
## 6. 类型定义
```typescript
export interface IRightKeyContent {
getElement(): HTMLElement;
destroy(): void;
}
```

View File

@@ -18,8 +18,10 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"dev:demo": "cd demo && npm run dev", "copy:demo": "mkdir -p demo/lib && cp dist/bim-engine-sdk.es.js dist/bim-engine-sdk.umd.js dist/bim-engine-sdk.umd.js.map demo/lib/",
"dev:demo-vue": "cd demo-vue && npm run dev", "copy:demo-vue": "mkdir -p demo-vue/public/lib && cp dist/bim-engine-sdk.es.js dist/bim-engine-sdk.umd.js dist/bim-engine-sdk.umd.js.map demo-vue/public/lib/",
"dev:demo": "npm run build && npm run copy:demo && cd demo && npm run dev",
"dev:demo-vue": "npm run build && npm run copy:demo-vue && cd demo-vue && npm run dev",
"dev:all": "npm run dev:demo & npm run dev:demo-vue" "dev:all": "npm run dev:demo & npm run dev:demo-vue"
}, },
"keywords": [ "keywords": [

View File

@@ -3,6 +3,7 @@ import { ToolbarManager } from './managers/toolbar-manager';
import { ButtonGroupManager } from './managers/button-group-manager'; import { ButtonGroupManager } from './managers/button-group-manager';
import { DialogManager } from './managers/dialog-manager'; import { DialogManager } from './managers/dialog-manager';
import { EngineManager } from './managers/engine-manager'; import { EngineManager } from './managers/engine-manager';
import { RightKeyManager } from './managers/right-key-manager';
import type { EngineOptions, ModelLoadOptions } from './components/engine'; import type { EngineOptions, ModelLoadOptions } from './components/engine';
import { localeManager } from './services/locale'; import { localeManager } from './services/locale';
import { themeManager } from './services/theme'; import { themeManager } from './services/theme';
@@ -22,6 +23,7 @@ export class BimEngine extends EventEmitter {
public buttonGroup: ButtonGroupManager | null = null; // 通用 public buttonGroup: ButtonGroupManager | null = null; // 通用
public dialog: DialogManager | null = null; public dialog: DialogManager | null = null;
public engine: EngineManager | null = null; // 3D 引擎管理器 public engine: EngineManager | null = null; // 3D 引擎管理器
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
public get localeManager() { return localeManager; } public get localeManager() { return localeManager; }
public get themeManager() { return themeManager; } public get themeManager() { return themeManager; }
@@ -77,6 +79,7 @@ export class BimEngine extends EventEmitter {
this.dialog = new DialogManager(this, this.wrapper); this.dialog = new DialogManager(this, this.wrapper);
this.toolbar = new ToolbarManager(this, this.wrapper); this.toolbar = new ToolbarManager(this, this.wrapper);
this.buttonGroup = new ButtonGroupManager(this, this.wrapper); this.buttonGroup = new ButtonGroupManager(this, this.wrapper);
this.rightKey = new RightKeyManager(this, this.wrapper);
// 初始主题 // 初始主题
@@ -120,6 +123,7 @@ export class BimEngine extends EventEmitter {
this.toolbar?.destroy(); this.toolbar?.destroy();
this.buttonGroup?.destroy(); this.buttonGroup?.destroy();
this.engine?.destroy(); this.engine?.destroy();
this.rightKey?.destroy();
this.dialog = null; this.dialog = null;
this.container.innerHTML = ''; this.container.innerHTML = '';
this.clear(); // Clear all events this.clear(); // Clear all events

View File

@@ -0,0 +1,15 @@
import { BimEngine } from "../../../bim-engine";
import { MenuItemConfig } from "../item";
export const fourMenuButton = (engine: BimEngine): MenuItemConfig => {
return {
id: "fourMenu",
label: "menu.info",
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile')
engine.dialog?.showInfoDialog()
engine.rightKey?.hide()
}
}
}

View File

@@ -0,0 +1,18 @@
import { BimEngine } from "../../../bim-engine";
import { MenuItemConfig } from "../item";
import { fourMenuButton } from "./four";
import { secondMenuButton } from "./second";
export const homeMenuButton = (engine: BimEngine): MenuItemConfig => {
return {
id: "homeMenu",
label: "menu.home",
group: 'home',
children: [secondMenuButton(engine), fourMenuButton(engine)],
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
engine.dialog?.showInfoDialog()
engine.rightKey?.hide()
}
}
}

View File

@@ -0,0 +1,16 @@
import { BimEngine } from "../../../bim-engine";
import { MenuItemConfig } from "../item";
export const infoMenuButton = (engine: BimEngine): MenuItemConfig => {
return {
id: "infoMenu",
label: "menu.info",
group: 'info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile')
engine.dialog?.showInfoDialog()
engine.rightKey?.hide()
}
}
}

View File

@@ -0,0 +1,15 @@
import { BimEngine } from "../../../bim-engine";
import { MenuItemConfig } from "../item";
export const secondMenuButton = (engine: BimEngine): MenuItemConfig => {
return {
id: "infoMenu",
label: "menu.info",
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => {
console.log('dianjile')
engine.dialog?.showInfoDialog()
engine.rightKey?.hide()
}
}
}

View File

@@ -0,0 +1,83 @@
.bim-menu {
display: flex;
flex-direction: column;
background: var(--bim-ui_bg_color, #2b2d30);
border-radius: 4px;
padding: 4px 0;
min-width: 160px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
user-select: none;
color: var(--bim-ui_text_primary, #ffffff);
}
.bim-menu-group {
display: flex;
flex-direction: column;
}
.bim-menu-divider {
height: 1px;
background-color: var(--bim-ui_border_color, #3e4145);
margin: 4px 0;
}
.bim-menu-item {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 13px;
position: relative;
color: var(--bim-ui_text_primary, #ffffff);
}
.bim-menu-item:hover {
background-color: var(--bim-ui_bg_hover, #3e4145);
}
.bim-menu-item.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.bim-menu-item-icon {
width: 16px;
height: 16px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.bim-menu-item-icon svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.bim-menu-item-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bim-menu-item-arrow {
width: 12px;
height: 12px;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
}
.bim-menu-item-arrow svg {
width: 100%;
height: 100%;
fill: currentColor;
}

View File

@@ -0,0 +1,274 @@
import { IBimComponent } from '../../types/component';
import { ThemeConfig } from '../../themes/types';
import { localeManager, t } from '../../services/locale';
import { MenuItemConfig } from './item';
import { MenuOptions } from './types';
import './index.css';
import { themeManager } from '../../services/theme';
/**
* 通用菜单列表组件
* 负责渲染一组菜单项,支持分组、排序、图标、快捷键提示和递归多级子菜单。
* 它不包含定位逻辑,仅负责内容渲染。
*/
export class BimMenu implements IBimComponent {
public element: HTMLElement;
private options: MenuOptions;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
// 当前激活的子菜单引用,用于自动关闭
private activeSubMenu: { menu: BimMenu; container: HTMLElement } | null = null;
constructor(options: MenuOptions) {
this.options = options;
this.element = document.createElement('ul');
this.element.className = 'bim-menu';
}
/**
* 初始化组件
* 渲染 DOM 结构并订阅语言变更
*/
public init(): void {
this.render();
// 订阅语言变更事件,实现国际化自动更新
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
});
// 自动订阅主题变更
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.setTheme(theme);
});
}
/**
* 设置主题
* @param theme 全局主题配置
*/
public setTheme(theme: ThemeConfig) {
const style = this.element.style;
style.setProperty('--bim-ui_bg_color', theme.panelBackground);
style.setProperty('--bim-ui_text_primary', theme.textPrimary);
style.setProperty('--bim-ui_border_color', theme.border);
style.setProperty('--bim-ui_bg_hover', theme.componentHover);
}
/**
* 响应语言变更
* 重新渲染整个菜单以更新文本
*/
public setLocales(): void {
this.element.innerHTML = '';
this.render();
}
/**
* 销毁组件
* 清理事件监听、子菜单和 DOM 元素
*/
public destroy(): void {
// 取消语言订阅
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
// 关闭并销毁所有打开的子菜单
this.closeSubMenu();
// 移除自身 DOM
this.element.remove();
}
/**
* 获取组件根元素
* 实现 IRightKeyContent 接口,允许被 RightKey 容器挂载
*/
public getElement(): HTMLElement {
return this.element;
}
/**
* 核心渲染逻辑
* 处理分组、排序和 DOM 生成
*/
private render(): void {
const { items, groupOrder } = this.options;
// 1. 数据分桶:按 group 字段将菜单项分组
const groups = new Map<string, MenuItemConfig[]>();
const defaultGroup = 'default';
items.forEach(item => {
const groupName = item.group || defaultGroup;
if (!groups.has(groupName)) {
groups.set(groupName, []);
}
groups.get(groupName)!.push(item);
});
// 2. 确定分组顺序
let sortedGroupKeys: string[] = [];
if (groupOrder) {
// 优先按照 groupOrder 指定的顺序排序
sortedGroupKeys = groupOrder.filter(g => groups.has(g));
// 将未在 groupOrder 中定义的组追加到最后
for (const key of groups.keys()) {
if (!sortedGroupKeys.includes(key)) {
sortedGroupKeys.push(key);
}
}
} else {
// 如果未指定顺序,则按默认遍历顺序
sortedGroupKeys = Array.from(groups.keys());
}
// 3. 渲染分组和组内项
sortedGroupKeys.forEach((groupName, index) => {
// 除了第一组外,每组之前插入分割线
if (index > 0) {
const divider = document.createElement('li');
divider.className = 'bim-menu-divider';
this.element.appendChild(divider);
}
const groupItems = groups.get(groupName)!;
// 组内排序:根据 item.order 升序排列
groupItems.sort((a, b) => (a.order || 0) - (b.order || 0));
groupItems.forEach(item => {
// 仅渲染可见的项
if (item.visible !== false) {
this.element.appendChild(this.createItemElement(item));
}
});
});
}
/**
* 创建单个菜单项的 DOM 元素
*/
private createItemElement(item: MenuItemConfig): HTMLElement {
const li = document.createElement('li');
// 根据状态设置样式类
const isEnabled = !item.disabled;
li.className = `bim-menu-item ${isEnabled ? '' : 'disabled'}`;
// 1. 图标区域 (Icon Slot)
const iconDiv = document.createElement('div');
iconDiv.className = 'bim-menu-item-icon';
if (item.icon) {
iconDiv.innerHTML = item.icon;
}
li.appendChild(iconDiv);
// 2. 文本区域 (Label Slot)
const labelDiv = document.createElement('div');
labelDiv.className = 'bim-menu-item-label';
// 获取翻译后的文本
labelDiv.textContent = t(item.label);
li.appendChild(labelDiv);
// 3. 子菜单指示器 (Arrow Slot)
const children = item.children;
const hasChildren = children && children.length > 0;
if (hasChildren) {
const arrowDiv = document.createElement('div');
arrowDiv.className = 'bim-menu-item-arrow';
// 简单的右箭头 SVG
arrowDiv.innerHTML = '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
li.appendChild(arrowDiv);
// 绑定子菜单交互事件
// 鼠标移入:打开子菜单
li.addEventListener('mouseenter', () => this.openSubMenu(item, li));
} else {
// 鼠标移入普通项:关闭当前已打开的子菜单
li.addEventListener('mouseenter', () => this.closeSubMenu());
}
// 4. 绑定点击事件
if (isEnabled) {
// Debug Log: 检查是否绑定了事件
// console.log(`[BimMenu] Binding click for ${item.id}, hasChildren: ${hasChildren}, hasOnClick: ${!!item.onClick}`);
li.addEventListener('click', (e) => {
e.stopPropagation(); // 防止冒泡
console.log(`[BimMenu] Clicked item: ${item.id}`);
// 如果是叶子节点(没有子菜单),则触发点击动作
if (!hasChildren) {
if (item.onClick) {
console.log(`[BimMenu] Executing onClick for ${item.id}`);
item.onClick();
} else {
console.warn(`[BimMenu] No onClick handler for ${item.id}`);
}
}
});
}
return li;
}
/**
* 打开子菜单
* @param item 当前菜单项
* @param parentLi 触发的 DOM 元素(用于定位)
*/
private openSubMenu(item: MenuItemConfig, parentLi: HTMLElement): void {
const children = item.children;
if (!children || children.length === 0) return;
// 如果当前已经打开了子菜单,先关闭它
this.closeSubMenu();
// 创建子菜单容器 (模拟一个临时的悬浮层)
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.zIndex = '10001'; // 确保比父菜单层级高
// 初步计算位置:位于父项右侧
const rect = parentLi.getBoundingClientRect();
container.style.top = `${rect.top}px`;
container.style.left = `${rect.right}px`;
// 关键修复:阻止 mousedown 冒泡
// 防止点击子菜单时触发 BimRightKey 的全局关闭逻辑(因为它认为点击发生在主菜单外部)
container.addEventListener('mousedown', (e) => e.stopPropagation());
// 递归创建新的 BimMenu 实例
const subMenu = new BimMenu({ items: children });
subMenu.init();
container.appendChild(subMenu.element);
document.body.appendChild(container);
// 保存引用以便后续清理
this.activeSubMenu = { menu: subMenu, container };
// 边界检测:如果超出屏幕右侧,则向左展开
const subRect = container.getBoundingClientRect();
if (subRect.right > window.innerWidth) {
container.style.left = `${rect.left - subRect.width}px`;
}
// TODO: 垂直方向边界检测
}
/**
* 关闭当前激活的子菜单
*/
private closeSubMenu(): void {
if (this.activeSubMenu) {
this.activeSubMenu.menu.destroy();
this.activeSubMenu.container.remove();
this.activeSubMenu = null;
}
}
}

View File

@@ -0,0 +1,14 @@
/**
* 菜单项配置接口 (用于简化的对象配置)
*/
export interface MenuItemConfig {
id: string;
label: string;
onClick?: () => void;
icon?: string;
group?: string;
order?: number;
children?: MenuItemConfig[];
disabled?: boolean;
visible?: boolean;
}

View File

@@ -0,0 +1,38 @@
import { MenuItemConfig } from './item';
/**
* <20><><EFBFBD>单组件配置选项
*/
export interface MenuOptions {
/**
* 菜单项列表
* 可以是扁平数组,组件会根据 group 字段自动分组
*/
items: MenuItemConfig[];
/**
* 分组显示顺序
* 包含组 ID 的字符串数组。
* 例如: ['view', 'edit', 'tools']
* 未在此数组中定义的组将按默认顺序排在最后。
*/
groupOrder?: string[];
}
/**
* 右键容器内容接口
* 任何想要挂载到 RightKey 容器中的组件都必须实现此接口
*/
export interface IRightKeyContent {
/**
* 获取组件的根 DOM 元素
* 容器将把此元素 append 到自身内部
*/
getElement(): HTMLElement;
/**
* 销毁组件
* 容器在关闭或切换内容时会调用此方法,组件应在此清理资源
*/
destroy(): void;
}

View File

@@ -0,0 +1,11 @@
.bim-right-key {
position: fixed;
z-index: 10000;
display: none;
background: transparent;
/* Container styles if needed, but usually the content provides the visual box */
}
.bim-right-key.visible {
display: block;
}

View File

@@ -0,0 +1,156 @@
import { IBimComponent } from '../../types/component';
import { ThemeConfig } from '../../themes/types';
import { IRightKeyContent, RightKeyOptions } from './types';
import './index.css';
/**
* 右键浮层容器组件 (RightKey)
* 这是一个纯粹的定位容器,负责在屏幕指定位置显示内容。
* 它不关心具体内容是什么,只处理定位、边界检测和关闭逻辑。
*/
export class BimRightKey implements IBimComponent {
private element: HTMLElement;
private content: IRightKeyContent | null = null;
private isVisible: boolean = false;
private onCloseCallback?: () => void;
constructor(options?: RightKeyOptions) {
this.element = document.createElement('div');
this.element.className = `bim-right-key ${options?.className || ''}`;
// 设置层级,默认很高以覆盖其他 UI
if (options?.zIndex) {
this.element.style.zIndex = options.zIndex.toString();
}
// 挂载到 body 以便进行固定定位
document.body.appendChild(this.element);
}
public init(): void {
// 绑定全局点击事件,用于实现"点击外部关闭"
document.addEventListener('mousedown', this.handleGlobalClick);
// 阻止在容器自身上触发系统默认右键菜单
this.element.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
});
}
public setTheme(_theme: ThemeConfig): void {
// 容器本身通常是透明的,主题样式主要由内容组件处理
// 如果容器需要背景色,可以在这里设置
// 使用 _theme 前缀避免 TS 未使用变量报错
}
public setLocales(): void {
// 容器不包含文本,无需处理国际化
// 内容组件的国际化由内容组件自身处理
}
public destroy(): void {
document.removeEventListener('mousedown', this.handleGlobalClick);
this.unmountContent();
this.element.remove();
}
/**
* 设置关闭时的回调函数
* 通常用于通知 Manager 状态变更
*/
public setOnClose(callback: () => void): void {
this.onCloseCallback = callback;
}
/**
* 挂载内容组件
* @param content 实现了 IRightKeyContent 接口的组件实例
*/
public mount(content: IRightKeyContent): void {
// 先卸载旧内容,防止内存泄漏
this.unmountContent();
this.content = content;
this.element.appendChild(content.getElement());
}
/**
* 卸载当前内容
*/
public unmountContent(): void {
if (this.content) {
this.content.destroy(); // 重要:调用组件销毁方法清理资源
this.element.innerHTML = '';
this.content = null;
}
}
/**
* 在指定位置显示容器
* 包含智能边界检测逻辑,防止溢出屏幕
* @param x 目标 X 坐标 (通常是鼠标点击位置)
* @param y 目标 Y 坐标
*/
public show(x: number, y: number): void {
this.element.classList.add('visible');
this.isVisible = true;
// 1. 先定位到目标位置,以便测量尺寸
this.element.style.left = `${x}px`;
this.element.style.top = `${y}px`;
// 2. 获取容器<E5AEB9><E599A8><EFBFBD>寸和视口尺寸
const rect = this.element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let newX = x;
let newY = y;
// 3. 水平方向边界检测:如果溢出右边界,则向左对齐
if (x + rect.width > viewportWidth) {
newX = x - rect.width;
}
// 4. 垂直方向边界检测:如果溢出下边界,则向上对齐
if (y + rect.height > viewportHeight) {
newY = y - rect.height;
}
// 5. 应用修正后的坐标
this.element.style.left = `${newX}px`;
this.element.style.top = `${newY}px`;
}
/**
* 隐藏容器
*/
public hide(): void {
this.element.classList.remove('visible');
this.isVisible = false;
// 为了状态重置,通常隐藏时也卸载内容
this.unmountContent();
if (this.onCloseCallback) {
this.onCloseCallback();
}
}
/**
* 处理全局点击事件
* 用于检测是否点击了容器外部
*/
private handleGlobalClick = (e: MouseEvent): void => {
if (!this.isVisible) return;
// 如果点击的是容器内部,不做处理
if (this.element.contains(e.target as Node)) {
return;
}
// 点击外部,关闭容器
this.hide();
};
}

View File

@@ -0,0 +1,18 @@
export interface IRightKeyContent {
/**
* 获取组件的根 DOM 元素
*/
getElement(): HTMLElement;
/**
* 销毁组件
*/
destroy(): void;
}
export interface RightKeyOptions {
/** 自定义 CSS 类名 */
className?: string;
/** 层级 (z-index) */
zIndex?: number;
}

View File

@@ -7,39 +7,15 @@ export { Toolbar } from './components/button-group/toolbar';
// 导出相关类型定义 // 导出相关类型定义
export type { OptButton, ButtonGroup, ButtonGroupOptions, ClickPayload } from './components/button-group/index.type'; export type { OptButton, ButtonGroup, ButtonGroupOptions, ClickPayload } from './components/button-group/index.type';
// 导出 RightKey/Menu 组件
export type { MenuItemConfig } from './components/menu/item';
export { BimMenu } from './components/menu';
export { BimRightKey } from './components/right-key';
// 导出主引擎类 // 导出主引擎类
export { BimEngine }; export { BimEngine };
// 导出 3D 引擎相关类型 // 导出 3D 引擎相关类型
export type { EngineOptions, ModelLoadOptions } from './components/engine'; export type { EngineOptions, ModelLoadOptions } from './components/engine';
// 导出 createEngine 函数(从第三方 SDK 重新导出) export { createEngine } from './bim-engine-sdk.es.js';
// 注意createEngine 的实际实现来自 bim-engine-sdk.es.js
//
// 使用方式:
// 1. 直接从 SDK 文件导入(推荐,如 Vue 示例):
// import { createEngine } from '/engine/bim-engine-sdk.es.js';
//
// 2. 从主入口导入(如果构建配置支持):
// import { createEngine } from 'bim-engine-sdk';
//
// 示例:
// ```javascript
// const engine = createEngine({
// containerId: 'vue2-container',
// backgroundColor: 0x333333,
// version: 'v1',
// showStats: true,
// showViewCube: true
// });
//
// engine.loader.loadModel(url, {
// position: [10, -5, 0],
// rotation: [0, 0, 0],
// scale: [1, 1, 1]
// });
// ```
// 重新导出 createEngine从 SDK 文件)
// 注意:如果直接导入失败,用户应该直接从 bim-engine-sdk.es.js 文件导入
export { createEngine } from './bim-engine-sdk.es.js';

View File

@@ -21,4 +21,8 @@ export const enUS: TranslationDictionary = {
testTitle: 'Test Dialog', testTitle: 'Test Dialog',
testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>', testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>',
}, },
menu: {
info: "info",
home: "home",
}
}; };

View File

@@ -23,6 +23,10 @@ export interface TranslationDictionary {
testTitle: string; testTitle: string;
testContent: string; testContent: string;
}; };
menu: {
info: string;
home: string;
}
} }
/** /**

View File

@@ -21,4 +21,8 @@ export const zhCN: TranslationDictionary = {
testTitle: '测试弹窗', testTitle: '测试弹窗',
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>', testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
}, },
menu: {
info: "信息",
home: "首页"
}
}; };

View File

@@ -0,0 +1,99 @@
import { BimComponent } from '../core/component';
import { BimEngine } from '../bim-engine';
import { BimRightKey } from '../components/right-key';
import { BimMenu } from '../components/menu';
import { MenuItemConfig } from '../components/menu/item';
import { infoMenuButton } from '../components/menu/buttons/info';
import { homeMenuButton } from '../components/menu/buttons/home';
/**
* 右键菜单管理器 (RightKeyManager)
* 负责协调右键交互流程:
* 1. 监听 Canvas/容器的 contextmenu 事件
* 2. 通过注册的处理器 (Handler) 获取需要显示的菜单项
* 3. 实例化 Menu 组件并装载到 RightKey 容器中显示
*/
export class RightKeyManager extends BimComponent {
private container: HTMLElement;
private rightKeyPanel: BimRightKey;
// 存储注册的上下文处理器
// 每个处理<E5A484><E79086><EFBFBD>接收鼠标事件返回一组菜单项如果没有对应菜单则返回 null
private contextHandlers: Array<(e: MouseEvent) => MenuItemConfig[] | null> = [];
constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container;
// 初始化右键容器,设置极高的层级以覆盖所有 UI
this.rightKeyPanel = new BimRightKey({ zIndex: 9000 });
this.rightKeyPanel.init();
this.initEventListeners();
}
private initEventListeners(): void {
this.container.addEventListener('contextmenu', this.handleContextMenu);
}
public destroy(): void {
this.container.removeEventListener('contextmenu', this.handleContextMenu);
this.rightKeyPanel.destroy();
}
/**
* 注册上下文菜单处理器
* @param handler 处理函数,接收鼠标事件,返回菜单项数组
*/
public registerHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void {
this.contextHandlers.push(handler);
}
/**
* 手动显示菜单
* 允许外部直接调用以显示特定的菜单,不一定依赖右键事件
* @param x 屏幕 X 坐标
* @param y 屏幕 Y 坐标
* @param items 菜单项列表
* @param groupOrder 可<><E58FAF>的分组顺序
*/
public showMenu(x: number, y: number, items: MenuItemConfig[], groupOrder?: string[]): void {
if (!items || items.length === 0) return;
// 1. 创建菜单内容组件
const menu = new BimMenu({ items, groupOrder });
menu.init(); // 必须初始化以生成 DOM
// 2. 将菜单挂载到右键容器
this.rightKeyPanel.mount(menu);
// 3. 显示容器
this.rightKeyPanel.show(x, y);
}
/**
* 隐藏右键菜单
*/
public hide(): void {
this.rightKeyPanel.hide();
}
/**
* 处理右键点击事件
*/
private handleContextMenu = (e: MouseEvent): void => {
// 阻止浏览器默认的右键菜单
e.preventDefault();
let items: MenuItemConfig[] = [];
items.push(infoMenuButton(this.engine))
items.push(infoMenuButton(this.engine))
items.push(infoMenuButton(this.engine))
items.push(homeMenuButton(this.engine))
if (items && items.length > 0) {
this.showMenu(e.clientX, e.clientY, items);
} else {
// 如果没有任何内容,则关闭可能存在的菜单
this.hide();
}
};
}

View File

@@ -7,11 +7,11 @@ export const darkTheme: ThemeConfig = {
name: 'dark', name: 'dark',
primary: '#0078d4', primary: '#0078d4',
primaryHover: '#0063b1', primaryHover: '#0063b1',
// 修改:背景色统一为浅灰,不再跟随深色模式变黑 // 修改:背景色统一为浅灰,不再跟随深色模式变黑
background: '#f5f5f5', background: '#f5f5f5',
panelBackground: 'rgba(30, 30, 30, 0.9)', panelBackground: 'rgba(30, 30, 30, 0.9)',
// 注意:如果背景是浅色,主文字颜色通常需要是深色才能看清 // 注意:如果背景是浅色,主文字颜色通常需要是深色才能看清
// 但这里的 textPrimary 主要是用于 UI 组件内部的。 // 但这里的 textPrimary 主要是用于 UI 组件内部的。
// 如果 BimEngine wrapper 上的文字直接显示在 background 上, // 如果 BimEngine wrapper 上的文字直接显示在 background 上,
@@ -19,53 +19,53 @@ export const darkTheme: ThemeConfig = {
// 目前架构中: // 目前架构中:
// theme.textPrimary 会应用到 wrapper.style.color (BimEngine.ts) // theme.textPrimary 会应用到 wrapper.style.color (BimEngine.ts)
// 以及 Toolbar/Dialog 的文字颜色。 // 以及 Toolbar/Dialog 的文字颜色。
// 如果背景是浅灰,而 wrapper 文字设置为白色 (#ffffff),那就看不清了。 // 如果背景是浅灰,而 wrapper 文字设置为白色 (#ffffff),那就看不清了。
// 这是一个语义冲突: // 这是一个语义冲突:
// 1. Panel (Toolbar/Dialog) 是黑底,需要白字。 // 1. Panel (Toolbar/Dialog) 是黑底,需要白字。
// 2. Background (Wrapper) 是白底,需要黑字。 // 2. Background (Wrapper) 是白底,需要黑字。
// 既然您要求背景统一浅灰,那么 Wrapper 上的“直接子文本”应该是深色。 // 既然您要求背景统一浅灰,那么 Wrapper 上的“直接子文本”应该是深色。
// 但 Toolbar/Dialog 仍然是深色模式(黑底),它们需要白字。 // 但 Toolbar/Dialog 仍然是深色模式(黑底),它们需要白字。
// 妥协方案: // 妥协方案:
// 保持 textPrimary 为白色为了适配黑<E9858D><E9BB91><EFBFBD>的 Toolbar/Dialog // 保持 textPrimary 为白色为了适配黑<E9858D><E9BB91><EFBFBD>的 Toolbar/Dialog
// 但是在 BimEngine 中如果背景强制改为浅色Wrapper 的默认文字颜色可能需要单独处理, // 但是在 BimEngine 中如果背景强制改为浅色Wrapper 的默认文字颜色可能需要单独处理,
// 或者我们可以认为 "Wrapper" 主要是承载 UI 组件的,直接写在 Wrapper 上的文字(标题/描述) // 或者我们可以认为 "Wrapper" 主要是承载 UI 组件的,直接写在 Wrapper 上的文字(标题/描述)
// 应该有自己的样式,而不是直接继承 theme.textPrimary。 // 应该有自己的样式,而不是直接继承 theme.textPrimary。
// 在之前的 BimEngine.ts 中: // 在之前的 BimEngine.ts 中:
// this.wrapper.style.color = theme.textPrimary; // this.wrapper.style.color = theme.textPrimary;
// 如果背景变浅灰,这里 textPrimary 还是白色的话,标题就看不见了。 // 如果背景变浅灰,这里 textPrimary 还是白色的话,标题就看不见了。
// 所以,深色模式下: // 所以,深色模式下:
// 背景:浅灰 // 背景:浅灰
// 组件:深黑 // 组件:深黑
// 组件文字:白 // 组件文字:白
// 页面文字:黑 (问题点) // 页面文字:黑 (问题点)
// 让我们先按您的要求改背景。通常这种情况下ThemeConfig 可能需要区分 // 让我们先按您的要求改背景。通常这种情况下ThemeConfig 可能需要区分
// contentText (页面内容文字) 和 uiText (组件文字)。 // contentText (页面内容文字) 和 uiText (组件文字)。
// 但为了不破坏现有结构,我将假定 textPrimary 主要服务于 UI 组件。 // 但为了不破坏现有结构,我将假定 textPrimary 主要服务于 UI 组件。
// 为了让 Wrapper 上的标题可见,我们可能需要在 BimEngine 中移除对 wrapper.style.color 的强制设置, // 为了让 Wrapper 上的标题可见,我们可能需要在 BimEngine 中移除对 wrapper.style.color 的强制设置,
// 或者在 presets 里把 textPrimary 改回来?不对,改回来 Toolbar 就看不清了。 // 或者在 presets 里把 textPrimary 改回来?不对,改回来 Toolbar 就看不清了。
// 方案:我将仅修改 background。 // 方案:我将仅修改 background。
// 至于 Wrapper 上的标题BimEngine 标题),由于在最新的 BimEngine.ts 中 // 至于 Wrapper 上的标题BimEngine 标题),由于在最新的 BimEngine.ts 中
// <20><><EFBFBD>们已经移除了 titleEl 和 descEl在之前的重构中 // <20><><EFBFBD>们已经移除了 titleEl 和 descEl在之前的重构中
// 所以现在 Wrapper 里主要是 Toolbar 和 Dialog它们有自己的 panelBackground。 // 所以现在 Wrapper 里主要是 Toolbar 和 Dialog它们有自己的 panelBackground。
// 只要 Toolbar/Dialog 内部正常即可。 // 只要 Toolbar/Dialog 内部正常即可。
textPrimary: '#ffffff', textPrimary: '#ffffff',
textSecondary: '#cccccc', textSecondary: '#cccccc',
border: '#444444', border: '#444444',
icon: '#cccccc', icon: '#cccccc',
iconActive: '#ffffff', iconActive: '#ffffff',
componentBackground: 'transparent', componentBackground: 'transparent',
componentHover: '#333333', componentHover: '#4e4d4dff',
componentActive: 'rgba(255, 255, 255, 0.1)' componentActive: 'rgba(255, 255, 255, 0.1)'
}; };
@@ -76,19 +76,19 @@ export const lightTheme: ThemeConfig = {
name: 'light', name: 'light',
primary: '#0078d4', primary: '#0078d4',
primaryHover: '#106ebe', primaryHover: '#106ebe',
// 统一为浅灰 // 统一为浅灰
background: '#f5f5f5', background: '#f5f5f5',
panelBackground: '#ffffff', panelBackground: '#ffffff',
textPrimary: '#333333', textPrimary: '#333333',
textSecondary: '#666666', textSecondary: '#666666',
border: '#e0e0e0', border: '#e0e0e0',
icon: '#555555', icon: '#555555',
iconActive: '#0078d4', iconActive: '#0078d4',
componentBackground: 'transparent', componentBackground: 'transparent',
componentHover: '#f0f0f0', componentHover: '#f0f0f0',
componentActive: '#e0e0e0' componentActive: '#e0e0e0'