2025-06-16 17:56:09 +08:00
|
|
|
|
# 场景编辑器组件详细分析
|
|
|
|
|
|
|
|
|
|
## 1. 组件概述
|
|
|
|
|
|
|
|
|
|
`scene-editor.vue` 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 2. 架构图示分析
|
|
|
|
|
|
|
|
|
|
### 2.1 组件整体架构图
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph TB
|
|
|
|
|
subgraph "Scene Editor Component"
|
|
|
|
|
A[scene-editor.vue] --> B[EditorService]
|
|
|
|
|
A --> C[RobotGroups]
|
|
|
|
|
A --> D[PenGroups]
|
|
|
|
|
A --> E[EditorToolbar]
|
|
|
|
|
A --> F[Detail Cards]
|
|
|
|
|
|
|
|
|
|
B --> B1[Meta2d Engine]
|
|
|
|
|
B --> B2[Canvas Rendering]
|
|
|
|
|
B --> B3[Event System]
|
|
|
|
|
|
|
|
|
|
C --> C1[Robot Management]
|
|
|
|
|
C --> C2[Group Operations]
|
|
|
|
|
|
|
|
|
|
D --> D1[Points Management]
|
|
|
|
|
D --> D2[Routes Management]
|
|
|
|
|
D --> D3[Areas Management]
|
|
|
|
|
|
|
|
|
|
E --> E1[Drawing Tools]
|
|
|
|
|
E --> E2[Operations]
|
|
|
|
|
|
|
|
|
|
F --> F1[Robot Detail]
|
|
|
|
|
F --> F2[Point Detail]
|
|
|
|
|
F --> F3[Route Detail]
|
|
|
|
|
F --> F4[Area Detail]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
subgraph "External Dependencies"
|
|
|
|
|
G[Scene API]
|
|
|
|
|
H[Robot API]
|
|
|
|
|
I[Map API]
|
|
|
|
|
J[File System]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
A --> G
|
|
|
|
|
A --> H
|
|
|
|
|
B --> I
|
|
|
|
|
A --> J
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2.2 数据流架构图
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
participant U as User
|
|
|
|
|
participant SE as SceneEditor
|
|
|
|
|
participant ES as EditorService
|
|
|
|
|
participant API as SceneAPI
|
|
|
|
|
participant Canvas as Meta2d Canvas
|
|
|
|
|
|
|
|
|
|
U->>SE: Load Scene
|
|
|
|
|
SE->>API: getSceneById(id)
|
|
|
|
|
API-->>SE: Scene Data
|
|
|
|
|
SE->>ES: load(sceneData)
|
|
|
|
|
ES->>Canvas: Render Elements
|
|
|
|
|
|
|
|
|
|
U->>SE: Edit Element
|
|
|
|
|
SE->>ES: updatePen/addArea/addPoint
|
|
|
|
|
ES->>Canvas: Update Render
|
|
|
|
|
ES->>SE: Emit Change Event
|
|
|
|
|
SE->>SE: Update UI State
|
|
|
|
|
|
|
|
|
|
U->>SE: Save Scene
|
|
|
|
|
SE->>ES: save()
|
|
|
|
|
ES-->>SE: JSON String
|
|
|
|
|
SE->>API: saveSceneById(id, json)
|
|
|
|
|
API-->>SE: Success Response
|
|
|
|
|
```
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 2.3 EditorService 内部架构图
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph LR
|
|
|
|
|
subgraph "EditorService Core"
|
|
|
|
|
A[Meta2d Base] --> B[Event System]
|
|
|
|
|
A --> C[Canvas Layer]
|
|
|
|
|
A --> D[State Management]
|
|
|
|
|
|
|
|
|
|
B --> B1[Mouse Events]
|
|
|
|
|
B --> B2[Change Events]
|
|
|
|
|
B --> B3[RxJS Streams]
|
|
|
|
|
|
|
|
|
|
C --> C1[Point Rendering]
|
|
|
|
|
C --> C2[Route Rendering]
|
|
|
|
|
C --> C3[Area Rendering]
|
|
|
|
|
C --> C4[Robot Rendering]
|
|
|
|
|
|
|
|
|
|
D --> D1[Current Selection]
|
|
|
|
|
D --> D2[Robot Groups]
|
|
|
|
|
D --> D3[Elements Cache]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
subgraph "Rendering Pipeline"
|
|
|
|
|
E[drawPoint] --> F[Canvas Context]
|
|
|
|
|
G[drawLine] --> F
|
|
|
|
|
H[drawArea] --> F
|
|
|
|
|
I[drawRobot] --> F
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
C1 --> E
|
|
|
|
|
C2 --> G
|
|
|
|
|
C3 --> H
|
|
|
|
|
C4 --> I
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 3. 核心功能分析
|
|
|
|
|
|
|
|
|
|
### 3.1 场景数据管理
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **场景读取**: 通过 `getSceneById` API 获取场景数据
|
|
|
|
|
- **场景推送**: 通过 `pushSceneById` API 将场景数据推送到数据库
|
|
|
|
|
- **场景保存**: 通过编辑器服务保存场景配置
|
|
|
|
|
- **文件导入/导出**: 支持 `.scene` 格式文件的导入导出
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 3.2 编辑器状态控制
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **编辑模式切换**: 通过 `editable` 状态控制编辑器的启用/禁用
|
|
|
|
|
- **权限管理**: 根据编辑状态显示不同的操作按钮和功能
|
|
|
|
|
- **实时状态同步**: 编辑状态变化时自动更新编辑器服务状态
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 3.3 三大管理区域
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **机器人管理**: 显示和管理场景中的机器人组和单个机器人
|
|
|
|
|
- **库区管理**: 管理各种类型的库区(仅显示库区类型的区域)
|
|
|
|
|
- **高级组管理**: 管理复杂的路径、点位、区域等元素
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 3.4 详情卡片系统
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **动态卡片显示**: 根据选中元素类型显示对应的详情卡片
|
|
|
|
|
- **编辑/查看模式**: 根据编辑状态显示编辑卡片或查看卡片
|
|
|
|
|
- **悬浮定位**: 卡片固定在右侧悬浮显示
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 4. 大场景渲染性能优化分析
|
|
|
|
|
|
|
|
|
|
### 4.1 性能瓶颈识别
|
|
|
|
|
|
|
|
|
|
#### 4.1.1 主要性能问题
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph TD
|
|
|
|
|
A[大场景性能问题] --> B[元素数量过多]
|
|
|
|
|
A --> C[频繁重绘]
|
|
|
|
|
A --> D[内存泄漏]
|
|
|
|
|
A --> E[事件处理]
|
|
|
|
|
|
|
|
|
|
B --> B1[点位: 1000+ 个]
|
|
|
|
|
B --> B2[路线: 5000+ 条]
|
|
|
|
|
B --> B3[区域: 100+ 个]
|
|
|
|
|
B --> B4[机器人: 50+ 个]
|
|
|
|
|
|
|
|
|
|
C --> C1[每次状态变更全量重绘]
|
|
|
|
|
C --> C2[鼠标移动频繁触发]
|
|
|
|
|
C --> C3[RxJS 防抖不足]
|
|
|
|
|
|
|
|
|
|
D --> D1[大量 DOM 监听器]
|
|
|
|
|
D --> D2[Canvas 上下文未释放]
|
|
|
|
|
D --> D3[图片资源未缓存]
|
|
|
|
|
|
|
|
|
|
E --> E1[hit-test 计算复杂]
|
|
|
|
|
E --> E2[事件冒泡处理]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 4.1.2 当前代码中的性能问题点
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 问题1: 频繁的全量数据更新
|
|
|
|
|
public readonly pens = useObservable<MapPen[]>(
|
|
|
|
|
this.#change$$.pipe(
|
|
|
|
|
filter((v) => v),
|
|
|
|
|
debounceTime(100), // 防抖时间过短
|
|
|
|
|
map(() => this.data().pens), // 每次返回全量数据
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 问题2: 复杂的过滤操作
|
|
|
|
|
const robots = computed<RobotInfo[]>(() =>
|
|
|
|
|
editor.value.robots.filter(({ label }) =>
|
|
|
|
|
label.includes(keyword.value) // 每次重新过滤全部数据
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 问题3: 同步的大量元素创建
|
|
|
|
|
async #loadScenePoints(points?: StandardScenePoint[]): Promise<void> {
|
|
|
|
|
if (!points?.length) return;
|
|
|
|
|
await Promise.all( // 并发创建所有点位,可能导致界面卡顿
|
|
|
|
|
points.map(async (v) => {
|
|
|
|
|
// ... 创建逻辑
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4.2 性能优化策略
|
|
|
|
|
|
|
|
|
|
#### 4.2.1 虚拟化渲染优化
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 优化建议1: 实现视口裁剪
|
|
|
|
|
class ViewportCulling {
|
|
|
|
|
private viewport: Rect;
|
|
|
|
|
private visibleElements: Map<string, MapPen> = new Map();
|
|
|
|
|
|
|
|
|
|
updateViewport(viewport: Rect): void {
|
|
|
|
|
this.viewport = viewport;
|
|
|
|
|
this.updateVisibleElements();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateVisibleElements(): void {
|
|
|
|
|
const elements = this.getAllElements();
|
|
|
|
|
this.visibleElements.clear();
|
|
|
|
|
|
|
|
|
|
elements.forEach((element) => {
|
|
|
|
|
if (this.isInViewport(element)) {
|
|
|
|
|
this.visibleElements.set(element.id, element);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isInViewport(element: MapPen): boolean {
|
|
|
|
|
const elementRect = this.getElementRect(element);
|
|
|
|
|
return this.rectIntersects(this.viewport, elementRect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优化建议2: 层级渲染
|
|
|
|
|
class LayeredRenderer {
|
|
|
|
|
private staticLayer: CanvasRenderingContext2D; // 静态elementos(点位、区域)
|
|
|
|
|
private dynamicLayer: CanvasRenderingContext2D; // 动态elementos(机器人、路径)
|
|
|
|
|
private uiLayer: CanvasRenderingContext2D; // UI层(选中状态、工具提示)
|
|
|
|
|
|
|
|
|
|
render(): void {
|
|
|
|
|
this.renderStaticLayer(); // 仅在元素变更时重绘
|
|
|
|
|
this.renderDynamicLayer(); // 频繁重绘
|
|
|
|
|
this.renderUILayer(); // 交互时重绘
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 4.2.2 数据结构优化
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 优化建议3: 使用空间索引
|
|
|
|
|
class SpatialIndex {
|
|
|
|
|
private quadTree: QuadTree<MapPen>;
|
|
|
|
|
|
|
|
|
|
insert(element: MapPen): void {
|
|
|
|
|
const bounds = this.getElementBounds(element);
|
|
|
|
|
this.quadTree.insert(element, bounds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query(viewport: Rect): MapPen[] {
|
|
|
|
|
return this.quadTree.query(viewport);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优化建议4: 缓存计算结果
|
|
|
|
|
class RenderCache {
|
|
|
|
|
private pathCache: Map<string, Path2D> = new Map();
|
|
|
|
|
private imageCache: Map<string, HTMLImageElement> = new Map();
|
|
|
|
|
|
|
|
|
|
getPath(element: MapPen): Path2D {
|
|
|
|
|
const key = this.getPathKey(element);
|
|
|
|
|
if (!this.pathCache.has(key)) {
|
|
|
|
|
this.pathCache.set(key, this.createPath(element));
|
|
|
|
|
}
|
|
|
|
|
return this.pathCache.get(key)!;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 4.2.3 事件处理优化
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 优化建议5: 事件委托和节流
|
|
|
|
|
class EventOptimizer {
|
|
|
|
|
private mouseThrottle = throttle(this.handleMouseMove.bind(this), 16); // 60fps
|
|
|
|
|
|
|
|
|
|
setupEventListeners(): void {
|
|
|
|
|
// 使用事件委托,减少监听器数量
|
|
|
|
|
this.canvas.addEventListener('mousemove', this.mouseThrottle);
|
|
|
|
|
this.canvas.addEventListener('click', this.handleClick);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleMouseMove(event: MouseEvent): void {
|
|
|
|
|
// 只处理必要的鼠标移动事件
|
|
|
|
|
const elements = this.spatialIndex.query(this.getMouseViewport(event));
|
|
|
|
|
this.updateHoverState(elements);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 4.2.4 内存管理优化
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 优化建议6: 对象池模式
|
|
|
|
|
class ObjectPool<T> {
|
|
|
|
|
private pool: T[] = [];
|
|
|
|
|
|
|
|
|
|
acquire(): T {
|
|
|
|
|
return this.pool.pop() || this.create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
release(obj: T): void {
|
|
|
|
|
this.reset(obj);
|
|
|
|
|
this.pool.push(obj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优化建议7: 及时清理资源
|
|
|
|
|
class ResourceManager {
|
|
|
|
|
private observers: Set<() => void> = new Set();
|
|
|
|
|
|
|
|
|
|
cleanup(): void {
|
|
|
|
|
this.observers.forEach((cleanup) => cleanup());
|
|
|
|
|
this.observers.clear();
|
|
|
|
|
|
|
|
|
|
// 清理Canvas上下文
|
|
|
|
|
this.clearCanvasContexts();
|
|
|
|
|
|
|
|
|
|
// 清理图片缓存
|
|
|
|
|
this.clearImageCache();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4.3 具体优化实施建议
|
|
|
|
|
|
|
|
|
|
#### 4.3.1 分批加载策略
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 建议实现: 分批加载大量元素
|
|
|
|
|
async loadSceneInBatches(scene: StandardScene): Promise<void> {
|
|
|
|
|
const BATCH_SIZE = 100;
|
|
|
|
|
|
|
|
|
|
// 分批加载点位
|
|
|
|
|
if (scene.points?.length) {
|
|
|
|
|
for (let i = 0; i < scene.points.length; i += BATCH_SIZE) {
|
|
|
|
|
const batch = scene.points.slice(i, i + BATCH_SIZE);
|
|
|
|
|
await this.loadPointsBatch(batch);
|
|
|
|
|
await this.nextTick(); // 让出主线程
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 分批加载路线
|
|
|
|
|
if (scene.routes?.length) {
|
|
|
|
|
for (let i = 0; i < scene.routes.length; i += BATCH_SIZE) {
|
|
|
|
|
const batch = scene.routes.slice(i, i + BATCH_SIZE);
|
|
|
|
|
await this.loadRoutesBatch(batch);
|
|
|
|
|
await this.nextTick();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private nextTick(): Promise<void> {
|
|
|
|
|
return new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 4.3.2 LOD(Level of Detail)优化
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 建议实现: 根据缩放级别调整渲染详度
|
|
|
|
|
class LODRenderer {
|
|
|
|
|
private renderLevel = 0;
|
|
|
|
|
|
|
|
|
|
updateRenderLevel(scale: number): void {
|
|
|
|
|
if (scale > 2)
|
|
|
|
|
this.renderLevel = 3; // 高详度
|
|
|
|
|
else if (scale > 1)
|
|
|
|
|
this.renderLevel = 2; // 中详度
|
|
|
|
|
else if (scale > 0.5)
|
|
|
|
|
this.renderLevel = 1; // 低详度
|
|
|
|
|
else this.renderLevel = 0; // 最低详度
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
|
|
|
|
switch (this.renderLevel) {
|
|
|
|
|
case 0:
|
|
|
|
|
this.renderPointSimple(ctx, pen);
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
this.renderPointNormal(ctx, pen);
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
this.renderPointDetailed(ctx, pen);
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
this.renderPointHighDetail(ctx, pen);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 5. 异步区域绘制深层原因分析
|
|
|
|
|
|
|
|
|
|
### 5.1 为什么使用 async/await
|
|
|
|
|
|
|
|
|
|
#### 5.1.1 代码分析
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 关键代码段分析
|
|
|
|
|
public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) {
|
|
|
|
|
// ... 前置逻辑
|
|
|
|
|
const area = await this.addPen(pen, true, true, true);
|
|
|
|
|
// ↑ 这里是关键 - addPen 是异步的
|
|
|
|
|
this.bottom(area);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise<void> {
|
|
|
|
|
// ... 创建pen对象
|
|
|
|
|
await this.addPen(pen, false, true, true);
|
|
|
|
|
// ↑ 同样是异步调用
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 5.1.2 深层原因分析图
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph TD
|
|
|
|
|
A[addArea/addPoint 调用] --> B[创建 MapPen 对象]
|
|
|
|
|
B --> C[调用 addPen 方法]
|
|
|
|
|
C --> D{addPen 为什么异步?}
|
|
|
|
|
|
|
|
|
|
D --> E[Canvas 渲染队列]
|
|
|
|
|
D --> F[DOM 更新时机]
|
|
|
|
|
D --> G[图片资源加载]
|
|
|
|
|
D --> H[动画系统集成]
|
|
|
|
|
|
|
|
|
|
E --> E1[Canvas需要等待渲染完成]
|
|
|
|
|
E --> E2[避免渲染冲突]
|
|
|
|
|
E --> E3[批量更新优化]
|
|
|
|
|
|
|
|
|
|
F --> F1[等待浏览器重绘]
|
|
|
|
|
F --> F2[确保DOM状态同步]
|
|
|
|
|
|
|
|
|
|
G --> G1[点位图标加载]
|
|
|
|
|
G --> G2[机器人图片加载]
|
|
|
|
|
G --> G3[主题相关资源]
|
|
|
|
|
|
|
|
|
|
H --> H1[过渡动画]
|
|
|
|
|
H --> H2[缩放动画]
|
|
|
|
|
H --> H3[状态切换动画]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 5.2 Meta2d 底层机制分析
|
|
|
|
|
|
|
|
|
|
#### 5.2.1 addPen 异步的根本原因
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Meta2d 内部可能的实现机制 (推测)
|
|
|
|
|
class Meta2d {
|
|
|
|
|
async addPen(pen: Pen, history?: boolean, render?: boolean, doEvent?: boolean): Promise<Pen> {
|
|
|
|
|
// 1. 资源预加载 - 确保图片等资源准备就绪
|
|
|
|
|
if (pen.image) {
|
|
|
|
|
await this.loadImage(pen.image);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 渲染管道同步 - 等待当前渲染任务完成
|
|
|
|
|
await this.renderQueue.nextTick();
|
|
|
|
|
|
|
|
|
|
// 3. 添加到画布数据结构
|
|
|
|
|
this.store.data.pens.push(pen);
|
|
|
|
|
|
|
|
|
|
// 4. 计算布局和碰撞检测
|
|
|
|
|
await this.calculateLayout(pen);
|
|
|
|
|
|
|
|
|
|
// 5. 触发重绘
|
|
|
|
|
if (render) {
|
|
|
|
|
await this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. 触发事件
|
|
|
|
|
if (doEvent) {
|
|
|
|
|
this.emit('add', pen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadImage(src: string): Promise<HTMLImageElement> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const img = new Image();
|
|
|
|
|
img.onload = () => resolve(img);
|
|
|
|
|
img.onerror = reject;
|
|
|
|
|
img.src = src;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async calculateLayout(pen: Pen): Promise<void> {
|
|
|
|
|
// 复杂的布局计算可能需要多帧完成
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.updatePenBounds(pen);
|
|
|
|
|
this.updateSpatialIndex(pen);
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 5.2.2 异步的必要性分析
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
participant User as User Action
|
|
|
|
|
participant Service as EditorService
|
|
|
|
|
participant Meta2d as Meta2d Engine
|
|
|
|
|
participant Canvas as Canvas Context
|
|
|
|
|
participant Browser as Browser
|
|
|
|
|
|
|
|
|
|
User->>Service: addArea()
|
|
|
|
|
Service->>Meta2d: addPen(pen)
|
|
|
|
|
|
|
|
|
|
Note over Meta2d: 检查是否需要加载图片资源
|
|
|
|
|
Meta2d->>Browser: 加载图片 (async)
|
|
|
|
|
Browser-->>Meta2d: 图片加载完成
|
|
|
|
|
|
|
|
|
|
Note over Meta2d: 等待渲染队列空闲
|
|
|
|
|
Meta2d->>Meta2d: 添加到数据结构
|
|
|
|
|
|
|
|
|
|
Note over Meta2d: 计算元素边界和布局
|
|
|
|
|
Meta2d->>Canvas: 请求重绘
|
|
|
|
|
Canvas->>Browser: requestAnimationFrame
|
|
|
|
|
Browser-->>Canvas: 下一帧回调
|
|
|
|
|
|
|
|
|
|
Meta2d-->>Service: 返回创建的元素
|
|
|
|
|
Service->>Service: 调用 bottom() 设置层级
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 5.3 性能影响分析
|
|
|
|
|
|
|
|
|
|
#### 5.3.1 异步的性能优势
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 优势1: 避免阻塞主线程
|
|
|
|
|
// 同步版本 (假设的问题版本)
|
|
|
|
|
public addAreaSync(p1: Point, p2: Point): void {
|
|
|
|
|
const pen = this.createPen();
|
|
|
|
|
this.addPenSync(pen); // 会阻塞主线程
|
|
|
|
|
this.render(); // 立即渲染,可能导致卡顿
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 异步版本 (当前实现)
|
|
|
|
|
public async addArea(p1: Point, p2: Point): Promise<void> {
|
|
|
|
|
const pen = this.createPen();
|
|
|
|
|
await this.addPen(pen); // 非阻塞,允许其他任务执行
|
|
|
|
|
this.bottom(pen); // 确保pen已经正确添加后再操作
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 5.3.2 批量操作的性能考虑
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 当前的批量加载实现
|
|
|
|
|
async #loadSceneAreas(areas?: StandardSceneArea[]): Promise<void> {
|
|
|
|
|
if (!areas?.length) return;
|
|
|
|
|
await Promise.all( // 并发执行,但可能导致资源竞争
|
|
|
|
|
areas.map(async (v) => {
|
|
|
|
|
await this.addArea({ x: v.x, y: v.y }, { x: v.x + v.w, y: v.y + v.h }, v.type, v.id);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优化建议: 控制并发数量
|
|
|
|
|
async #loadSceneAreasOptimized(areas?: StandardSceneArea[]): Promise<void> {
|
|
|
|
|
if (!areas?.length) return;
|
|
|
|
|
|
|
|
|
|
const CONCURRENT_LIMIT = 5; // 限制并发数量
|
|
|
|
|
for (let i = 0; i < areas.length; i += CONCURRENT_LIMIT) {
|
|
|
|
|
const batch = areas.slice(i, i + CONCURRENT_LIMIT);
|
|
|
|
|
await Promise.all(
|
|
|
|
|
batch.map(async (v) => {
|
|
|
|
|
await this.addArea(
|
|
|
|
|
{ x: v.x, y: v.y },
|
|
|
|
|
{ x: v.x + v.w, y: v.y + v.h },
|
|
|
|
|
v.type,
|
|
|
|
|
v.id
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
// 每批次之间给主线程喘息时间
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 6. 技术架构分析
|
|
|
|
|
|
|
|
|
|
### 6.1 核心依赖关系
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 主要导入依赖
|
|
|
|
|
import { getSceneById, pushSceneById } from '@api/scene'; // 场景API
|
|
|
|
|
import { EditorService } from '@core/editor.service'; // 编辑器服务
|
|
|
|
|
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; // 工具函数
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 6.2 组件架构设计
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.2.1 状态管理
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 核心状态定义
|
|
|
|
|
const title = ref<string>(''); // 场景标题
|
|
|
|
|
const editable = ref<boolean>(false); // 编辑状态
|
|
|
|
|
const show = ref<boolean>(true); // 卡片显示状态
|
|
|
|
|
const current = ref<{ type: string; id: string }>(); // 当前选中元素
|
|
|
|
|
const container = shallowRef<HTMLDivElement>(); // 编辑器容器
|
|
|
|
|
const editor = shallowRef<EditorService>(); // 编辑器服务实例
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.2.2 依赖注入系统
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const EDITOR_KEY = Symbol('editor-key');
|
|
|
|
|
provide(EDITOR_KEY, editor);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 6.3 EditorService 核心服务分析
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.3.1 服务基础
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
export class EditorService extends Meta2d {
|
|
|
|
|
// 继承自 Meta2d 图形引擎
|
|
|
|
|
// 提供场景编辑的核心功能
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.3.2 核心方法
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **load()**: 加载场景数据到编辑器
|
|
|
|
|
- **save()**: 保存当前场景数据
|
|
|
|
|
- **setState()**: 设置编辑器状态(可编辑/只读)
|
|
|
|
|
- **updateRobots()**: 更新机器人数据
|
|
|
|
|
- **addArea()**: 添加区域
|
|
|
|
|
- **deleteById()**: 删除指定元素
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 6.4 API 接口设计
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.4.1 场景相关API
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 获取场景数据
|
|
|
|
|
export async function getSceneById(id: string): Promise<SceneDetail | null>;
|
|
|
|
|
|
|
|
|
|
// 推送场景到数据库
|
|
|
|
|
export async function pushSceneById(id: string): Promise<boolean>;
|
|
|
|
|
|
|
|
|
|
// 保存场景数据
|
|
|
|
|
export async function saveSceneById(id: string, json: string, png?: string): Promise<boolean>;
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
#### 6.4.2 文件操作工具
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 文件选择
|
|
|
|
|
export async function selectFile(accept?: string, limit?: number): Promise<File | void>;
|
|
|
|
|
|
|
|
|
|
// 文件解码
|
|
|
|
|
export async function decodeTextFile(file: File): Promise<string | undefined>;
|
|
|
|
|
|
|
|
|
|
// 文本转二进制
|
|
|
|
|
export function textToBlob(text: string): Blob | undefined;
|
|
|
|
|
|
|
|
|
|
// 文件下载
|
|
|
|
|
export function downloadFile(url: string, name?: string): void;
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 7. 从零开发实现过程
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.1 第一步:创建基础组件结构
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
// 1. 定义组件属性
|
|
|
|
|
type Props = {
|
|
|
|
|
id: string; // 场景ID
|
|
|
|
|
};
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
|
|
|
|
|
|
// 2. 基础状态管理
|
|
|
|
|
const title = ref<string>('');
|
|
|
|
|
const editable = ref<boolean>(false);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<a-layout class="full">
|
|
|
|
|
<!-- 头部区域 -->
|
|
|
|
|
<a-layout-header class="p-16" style="height: 64px">
|
|
|
|
|
<!-- 标题和操作按钮 -->
|
|
|
|
|
</a-layout-header>
|
|
|
|
|
|
|
|
|
|
<!-- 主体区域 -->
|
|
|
|
|
<a-layout class="p-16">
|
|
|
|
|
<!-- 左侧面板 -->
|
|
|
|
|
<a-layout-sider :width="320">
|
|
|
|
|
<!-- 选项卡内容 -->
|
|
|
|
|
</a-layout-sider>
|
|
|
|
|
|
|
|
|
|
<!-- 编辑器区域 -->
|
|
|
|
|
<a-layout-content>
|
|
|
|
|
<div ref="container" class="editor-container full"></div>
|
|
|
|
|
</a-layout-content>
|
|
|
|
|
</a-layout>
|
|
|
|
|
</a-layout>
|
|
|
|
|
</template>
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.2 第二步:集成编辑器服务
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 1. 导入编辑器服务
|
|
|
|
|
import { EditorService } from '@core/editor.service';
|
|
|
|
|
|
|
|
|
|
// 2. 创建编辑器实例
|
|
|
|
|
const container = shallowRef<HTMLDivElement>();
|
|
|
|
|
const editor = shallowRef<EditorService>();
|
|
|
|
|
|
|
|
|
|
// 3. 组件挂载时初始化编辑器
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
editor.value = new EditorService(container.value!);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. 设置依赖注入
|
|
|
|
|
const EDITOR_KEY = Symbol('editor-key');
|
|
|
|
|
provide(EDITOR_KEY, editor);
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.3 第三步:实现场景数据管理
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 1. 导入API
|
|
|
|
|
import { getSceneById, pushSceneById } from '@api/scene';
|
|
|
|
|
|
|
|
|
|
// 2. 读取场景数据
|
|
|
|
|
const readScene = async () => {
|
|
|
|
|
const res = await getSceneById(props.id);
|
|
|
|
|
title.value = res?.label ?? '';
|
|
|
|
|
editor.value?.load(res?.json, editable.value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 3. 推送场景数据
|
|
|
|
|
const pushScene = async () => {
|
|
|
|
|
const res = await pushSceneById(props.id);
|
|
|
|
|
if (!res) return Promise.reject();
|
|
|
|
|
message.success(t('场景推送成功'));
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 4. 监听场景ID变化
|
|
|
|
|
watch(
|
|
|
|
|
() => props.id,
|
|
|
|
|
() => readScene(),
|
|
|
|
|
{ immediate: true, flush: 'post' },
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.4 第四步:实现文件导入导出
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 1. 导入工具函数
|
|
|
|
|
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils';
|
|
|
|
|
|
|
|
|
|
// 2. 导入场景文件
|
|
|
|
|
const importScene = async () => {
|
|
|
|
|
const file = await selectFile('.scene');
|
|
|
|
|
if (!file?.size) return;
|
|
|
|
|
const json = await decodeTextFile(file);
|
|
|
|
|
editor.value?.load(json, editable.value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 3. 导出场景文件
|
|
|
|
|
const exportScene = () => {
|
|
|
|
|
const json = editor.value?.save();
|
|
|
|
|
if (!json) return;
|
|
|
|
|
const blob = textToBlob(json);
|
|
|
|
|
if (!blob?.size) return;
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
downloadFile(url, `${title.value || 'unknown'}.scene`);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.5 第五步:集成管理组件
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
<template>
|
|
|
|
|
<a-layout-sider :width="320">
|
|
|
|
|
<a-tabs type="card">
|
|
|
|
|
<!-- 机器人管理 -->
|
|
|
|
|
<a-tab-pane key="1" :tab="$t('机器人')">
|
|
|
|
|
<RobotGroups
|
|
|
|
|
v-if="editor"
|
|
|
|
|
:token="EDITOR_KEY"
|
|
|
|
|
:sid="id"
|
|
|
|
|
:editable="editable"
|
|
|
|
|
:current="current?.id"
|
|
|
|
|
@change="selectRobot"
|
|
|
|
|
show-group-edit
|
|
|
|
|
/>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
<!-- 库区管理 -->
|
|
|
|
|
<a-tab-pane key="2" :tab="$t('库区')">
|
|
|
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
<!-- 高级组管理 -->
|
|
|
|
|
<a-tab-pane key="3" :tab="$t('高级组')">
|
|
|
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
</a-layout-sider>
|
|
|
|
|
</template>
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.6 第六步:实现选中元素监听
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 1. 监听编辑器选中元素
|
|
|
|
|
watch(
|
|
|
|
|
() => editor.value?.selected.value[0],
|
|
|
|
|
(v) => {
|
|
|
|
|
const pen = editor.value?.getPenById(v);
|
|
|
|
|
if (pen?.id) {
|
|
|
|
|
current.value = { type: pen.name as 'point' | 'line' | 'area', id: pen.id };
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (current.value?.type === 'robot') return;
|
|
|
|
|
current.value = undefined;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. 计算选中元素类型
|
|
|
|
|
const isRobot = computed(() => current.value?.type === 'robot');
|
|
|
|
|
const isPoint = computed(() => current.value?.type === 'point');
|
|
|
|
|
const isRoute = computed(() => current.value?.type === 'line');
|
|
|
|
|
const isArea = computed(() => current.value?.type === 'area');
|
|
|
|
|
|
|
|
|
|
// 3. 机器人选择处理
|
|
|
|
|
const selectRobot = (id: string) => {
|
|
|
|
|
current.value = { type: 'robot', id };
|
|
|
|
|
editor.value?.inactive();
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 7.7 第七步:添加工具栏和详情卡片
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
<template>
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
<div v-if="editable" class="toolbar-container">
|
|
|
|
|
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 详情卡片 -->
|
|
|
|
|
<template v-if="current?.id">
|
|
|
|
|
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
|
|
|
|
<template #icon><i class="icon detail" /></template>
|
|
|
|
|
</a-float-button>
|
|
|
|
|
|
|
|
|
|
<div v-if="show" class="card-container">
|
|
|
|
|
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
|
|
|
|
|
|
|
|
|
|
<template v-if="isPoint">
|
|
|
|
|
<PointEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
|
|
|
|
|
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 其他卡片类型... -->
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</template>
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 8. 子组件详细分析
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 8.1 RobotGroups 组件
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
**功能**: 管理机器人组和单个机器人
|
|
|
|
|
**核心特性**:
|
|
|
|
|
|
|
|
|
|
- 机器人组的增删改查
|
|
|
|
|
- 机器人的添加、注册、移除
|
|
|
|
|
- 批量操作支持(全选、批量移除)
|
|
|
|
|
- 搜索过滤功能
|
|
|
|
|
|
|
|
|
|
**关键实现**:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 机器人列表获取
|
|
|
|
|
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
|
|
|
|
|
|
|
|
|
|
// 批量选择管理
|
|
|
|
|
const selected = reactive<Set<RobotInfo['id']>>(new Set());
|
|
|
|
|
const selectAll = (checked: boolean) => {
|
|
|
|
|
if (checked) {
|
|
|
|
|
robots.value.forEach(({ id }) => selected.add(id));
|
|
|
|
|
} else {
|
|
|
|
|
selected.clear();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 8.2 PenGroups 组件
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
**功能**: 管理点位、路线、区域等绘制元素
|
|
|
|
|
**核心特性**:
|
|
|
|
|
|
|
|
|
|
- 分类显示不同类型的元素(点位、路线、区域)
|
|
|
|
|
- 支持筛选特定类型(如仅显示库区)
|
|
|
|
|
- 搜索过滤功能
|
|
|
|
|
- 点击选中功能
|
|
|
|
|
|
|
|
|
|
**关键实现**:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 点位列表
|
|
|
|
|
const points = computed<MapPen[]>(() =>
|
|
|
|
|
editor.value.points.value.filter(({ label }) => label?.includes(keyword.value)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 区域列表(按类型分组)
|
|
|
|
|
const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value)));
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 8.3 EditorToolbar 组件
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
**功能**: 提供编辑工具栏
|
|
|
|
|
**核心特性**:
|
|
|
|
|
|
|
|
|
|
- 区域添加工具(库区、互斥区、非互斥区)
|
|
|
|
|
- 场景保存功能
|
|
|
|
|
- 撤销/重做操作
|
|
|
|
|
- 删除操作
|
|
|
|
|
|
|
|
|
|
**关键实现**:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 区域添加模式
|
|
|
|
|
const mode = ref<MapAreaType>();
|
|
|
|
|
watch(editor.value.mouseBrush, (v) => {
|
|
|
|
|
if (!mode.value) return;
|
|
|
|
|
const [p1, p2] = v ?? [];
|
|
|
|
|
if (isEmpty(p1) || isEmpty(p2)) return;
|
|
|
|
|
editor.value.addArea(p1, p2, mode.value);
|
|
|
|
|
mode.value = undefined;
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 9. 样式设计分析
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 9.1 布局结构
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
- **头部**: 固定高度64px,包含标题和操作按钮
|
|
|
|
|
- **主体**: 左侧面板320px宽度,右侧编辑器自适应
|
|
|
|
|
- **工具栏**: 固定在底部中央,悬浮显示
|
|
|
|
|
- **详情卡片**: 固定在右侧,320px宽度,悬浮显示
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 9.2 核心样式
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
```scss
|
|
|
|
|
.editor-container {
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-container {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 40px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-container {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 80px;
|
|
|
|
|
right: 64px;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
width: 320px;
|
|
|
|
|
height: calc(100% - 96px);
|
|
|
|
|
overflow: visible;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
& > * {
|
|
|
|
|
pointer-events: all;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 10. 维护和调试指南
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 10.1 常见问题排查
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
#### 问题1: 场景数据加载失败
|
|
|
|
|
|
|
|
|
|
**排查步骤**:
|
|
|
|
|
|
|
|
|
|
1. 检查 `props.id` 是否正确传入
|
|
|
|
|
2. 检查 `getSceneById` API 是否正常响应
|
|
|
|
|
3. 检查编辑器服务是否正确初始化
|
|
|
|
|
|
|
|
|
|
#### 问题2: 编辑器功能异常
|
|
|
|
|
|
|
|
|
|
**排查步骤**:
|
|
|
|
|
|
|
|
|
|
1. 检查 `container` 元素是否正确获取
|
|
|
|
|
2. 检查 `EditorService` 是否正确实例化
|
|
|
|
|
3. 检查依赖注入是否正常工作
|
|
|
|
|
|
|
|
|
|
#### 问题3: 文件导入导出失败
|
|
|
|
|
|
|
|
|
|
**排查步骤**:
|
|
|
|
|
|
|
|
|
|
1. 检查工具函数是否正确导入
|
|
|
|
|
2. 检查文件格式是否正确
|
|
|
|
|
3. 检查浏览器兼容性
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 10.2 性能优化建议
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
1. **使用 shallowRef**: 对于大对象使用 `shallowRef` 避免深度响应式
|
|
|
|
|
2. **组件懒加载**: 使用 `v-if` 控制组件渲染时机
|
|
|
|
|
3. **事件防抖**: 对于频繁触发的事件(如搜索)使用防抖
|
|
|
|
|
4. **内存管理**: 及时清理事件监听器和定时器
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
### 10.3 扩展开发指南
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
#### 添加新的元素类型
|
|
|
|
|
|
|
|
|
|
1. 在 `EditorService` 中添加对应的管理方法
|
|
|
|
|
2. 在 `PenGroups` 组件中添加新的分组
|
|
|
|
|
3. 创建对应的详情卡片组件
|
|
|
|
|
4. 在主组件中添加类型判断逻辑
|
|
|
|
|
|
|
|
|
|
#### 添加新的工具
|
|
|
|
|
|
|
|
|
|
1. 在 `EditorToolbar` 组件中添加工具按钮
|
|
|
|
|
2. 在 `EditorService` 中实现对应功能
|
|
|
|
|
3. 处理工具状态管理和交互逻辑
|
|
|
|
|
|
2025-06-17 15:34:56 +08:00
|
|
|
|
## 11. 总结
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
这个场景编辑器组件是一个功能完整、架构清晰的复杂组件,主要特点:
|
|
|
|
|
|
|
|
|
|
1. **模块化设计**: 通过子组件分离不同功能模块
|
|
|
|
|
2. **服务化架构**: 核心逻辑封装在 EditorService 中
|
|
|
|
|
3. **响应式状态管理**: 使用 Vue 3 的响应式系统管理复杂状态
|
|
|
|
|
4. **依赖注入**: 通过 provide/inject 实现服务共享
|
|
|
|
|
5. **文件操作**: 完整的文件导入导出功能
|
|
|
|
|
6. **用户体验**: 良好的交互设计和视觉反馈
|
2025-06-17 15:34:56 +08:00
|
|
|
|
7. **异步机制**: 合理使用异步操作确保渲染性能
|
|
|
|
|
8. **性能优化**: 针对大场景提供多层次优化策略
|
2025-06-16 17:56:09 +08:00
|
|
|
|
|
|
|
|
|
对于维护和扩展这个组件,需要重点关注:
|
|
|
|
|
|
|
|
|
|
- EditorService 的 API 设计和实现
|
|
|
|
|
- 各子组件之间的通信机制
|
|
|
|
|
- 状态管理的一致性
|
|
|
|
|
- 性能优化和内存管理
|
2025-06-17 15:34:56 +08:00
|
|
|
|
- 异步操作的正确处理
|
|
|
|
|
|
|
|
|
|
## 12. 高性能技术栈替代方案分析
|
|
|
|
|
|
|
|
|
|
### 12.1 技术栈对比总览
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph LR
|
|
|
|
|
subgraph "当前方案"
|
|
|
|
|
A[Meta2d + Canvas 2D] --> A1[性能瓶颈]
|
|
|
|
|
A1 --> A2[大场景卡顿]
|
|
|
|
|
A1 --> A3[内存占用高]
|
|
|
|
|
A1 --> A4[CPU密集计算]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
subgraph "高性能替代方案"
|
|
|
|
|
B[WebGL方案] --> B1[GPU加速]
|
|
|
|
|
C[WebAssembly方案] --> C1[原生性能]
|
|
|
|
|
D[混合方案] --> D1[WebGL + WASM]
|
|
|
|
|
E[Web Workers] --> E1[多线程计算]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
A --> B
|
|
|
|
|
A --> C
|
|
|
|
|
A --> D
|
|
|
|
|
A --> E
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.2 WebGL高性能渲染方案
|
|
|
|
|
|
|
|
|
|
#### 12.2.1 推荐库选择
|
|
|
|
|
|
|
|
|
|
**1. PixiJS (推荐指数: ⭐⭐⭐⭐⭐)**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// PixiJS 实现高性能场景编辑器
|
|
|
|
|
import * as PIXI from 'pixi.js';
|
|
|
|
|
|
|
|
|
|
class HighPerformanceSceneEditor {
|
|
|
|
|
private app: PIXI.Application;
|
|
|
|
|
private viewport: Viewport;
|
|
|
|
|
private spatialHash: SpatialHash;
|
|
|
|
|
private cullingSystem: CullingSystem;
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
// 创建高性能应用实例
|
|
|
|
|
this.app = new PIXI.Application({
|
|
|
|
|
width: container.clientWidth,
|
|
|
|
|
height: container.clientHeight,
|
|
|
|
|
antialias: true,
|
|
|
|
|
backgroundColor: 0x1099bb,
|
|
|
|
|
powerPreference: 'high-performance', // 强制使用独立显卡
|
|
|
|
|
hello: false, // 禁用PIXI欢迎信息
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 启用批量渲染和几何体缓存
|
|
|
|
|
this.app.renderer.plugins.batch.setMaxTextures(32);
|
|
|
|
|
this.setupViewport();
|
|
|
|
|
this.initOptimizations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupViewport(): void {
|
|
|
|
|
// 使用 pixi-viewport 实现高性能视口
|
|
|
|
|
this.viewport = new Viewport({
|
|
|
|
|
screenWidth: this.app.screen.width,
|
|
|
|
|
screenHeight: this.app.screen.height,
|
|
|
|
|
worldWidth: 10000,
|
|
|
|
|
worldHeight: 10000,
|
|
|
|
|
interaction: this.app.renderer.plugins.interaction,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.viewport.drag().pinch().wheel().decelerate().clampZoom({ minScale: 0.1, maxScale: 5 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initOptimizations(): void {
|
|
|
|
|
// 1. 启用视锥剔除
|
|
|
|
|
this.cullingSystem = new CullingSystem(this.viewport);
|
|
|
|
|
|
|
|
|
|
// 2. 空间哈希优化
|
|
|
|
|
this.spatialHash = new SpatialHash(100); // 网格大小100px
|
|
|
|
|
|
|
|
|
|
// 3. 对象池
|
|
|
|
|
this.initObjectPools();
|
|
|
|
|
|
|
|
|
|
// 4. LOD系统
|
|
|
|
|
this.initLODSystem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量添加大量元素 - 性能优化版本
|
|
|
|
|
async addElementsBatch(elements: SceneElement[]): Promise<void> {
|
|
|
|
|
const BATCH_SIZE = 1000;
|
|
|
|
|
const batches = this.chunkArray(elements, BATCH_SIZE);
|
|
|
|
|
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
|
// 使用 Graphics 批量绘制
|
|
|
|
|
const graphics = new PIXI.Graphics();
|
|
|
|
|
|
|
|
|
|
batch.forEach((element) => {
|
|
|
|
|
this.drawElementToGraphics(graphics, element);
|
|
|
|
|
this.spatialHash.insert(element);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.viewport.addChild(graphics);
|
|
|
|
|
|
|
|
|
|
// 让出控制权,避免阻塞UI
|
|
|
|
|
await this.nextFrame();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private drawElementToGraphics(graphics: PIXI.Graphics, element: SceneElement): void {
|
|
|
|
|
graphics.beginFill(element.color);
|
|
|
|
|
switch (element.type) {
|
|
|
|
|
case 'point':
|
|
|
|
|
graphics.drawCircle(element.x, element.y, element.radius);
|
|
|
|
|
break;
|
|
|
|
|
case 'area':
|
|
|
|
|
graphics.drawRect(element.x, element.y, element.width, element.height);
|
|
|
|
|
break;
|
|
|
|
|
case 'route':
|
|
|
|
|
graphics.moveTo(element.x1, element.y1);
|
|
|
|
|
graphics.lineTo(element.x2, element.y2);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
graphics.endFill();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 视锥剔除系统
|
|
|
|
|
class CullingSystem {
|
|
|
|
|
private viewport: Viewport;
|
|
|
|
|
private visibleElements: Set<PIXI.DisplayObject> = new Set();
|
|
|
|
|
|
|
|
|
|
constructor(viewport: Viewport) {
|
|
|
|
|
this.viewport = viewport;
|
|
|
|
|
viewport.on('moved', () => this.updateVisibility());
|
|
|
|
|
viewport.on('zoomed', () => this.updateVisibility());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateVisibility(): void {
|
|
|
|
|
const bounds = this.viewport.getVisibleBounds();
|
|
|
|
|
|
|
|
|
|
this.viewport.children.forEach((child) => {
|
|
|
|
|
const elementBounds = child.getBounds();
|
|
|
|
|
const isVisible = this.boundsIntersect(bounds, elementBounds);
|
|
|
|
|
|
|
|
|
|
if (isVisible && !child.visible) {
|
|
|
|
|
child.visible = true;
|
|
|
|
|
this.visibleElements.add(child);
|
|
|
|
|
} else if (!isVisible && child.visible) {
|
|
|
|
|
child.visible = false;
|
|
|
|
|
this.visibleElements.delete(child);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boundsIntersect(a: PIXI.Rectangle, b: PIXI.Rectangle): boolean {
|
|
|
|
|
return !(a.x + a.width < b.x || b.x + b.width < a.x || a.y + a.height < b.y || b.y + b.height < a.y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**性能提升预期**:
|
|
|
|
|
|
|
|
|
|
- **渲染性能**: 10-50倍提升 (GPU加速)
|
|
|
|
|
- **内存使用**: 减少60-80% (批量渲染)
|
|
|
|
|
- **帧率**: 60 FPS 稳定 (支持10000+元素)
|
|
|
|
|
|
|
|
|
|
**2. Konva.js (推荐指数: ⭐⭐⭐⭐)**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Konva.js 高性能实现
|
|
|
|
|
import Konva from 'konva';
|
|
|
|
|
|
|
|
|
|
class KonvaSceneEditor {
|
|
|
|
|
private stage: Konva.Stage;
|
|
|
|
|
private staticLayer: Konva.Layer;
|
|
|
|
|
private dynamicLayer: Konva.Layer;
|
|
|
|
|
private transformer: Konva.Transformer;
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
this.stage = new Konva.Stage({
|
|
|
|
|
container: container,
|
|
|
|
|
width: container.clientWidth,
|
|
|
|
|
height: container.clientHeight,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.setupLayers();
|
|
|
|
|
this.enableOptimizations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupLayers(): void {
|
|
|
|
|
// 静态层:点位、区域 (低频更新)
|
|
|
|
|
this.staticLayer = new Konva.Layer({
|
|
|
|
|
listening: false, // 禁用事件监听提升性能
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 动态层:机器人、选中状态 (高频更新)
|
|
|
|
|
this.dynamicLayer = new Konva.Layer();
|
|
|
|
|
|
|
|
|
|
this.stage.add(this.staticLayer);
|
|
|
|
|
this.stage.add(this.dynamicLayer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private enableOptimizations(): void {
|
|
|
|
|
// 1. 启用缓存
|
|
|
|
|
this.staticLayer.cache();
|
|
|
|
|
|
|
|
|
|
// 2. 优化批量更新
|
|
|
|
|
this.stage.batchDraw = true;
|
|
|
|
|
|
|
|
|
|
// 3. 视口裁剪
|
|
|
|
|
this.enableViewportCulling();
|
|
|
|
|
|
|
|
|
|
// 4. 事件委托
|
|
|
|
|
this.setupEventDelegation();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量添加元素
|
|
|
|
|
addElementsBatch(elements: SceneElement[]): void {
|
|
|
|
|
const group = new Konva.Group();
|
|
|
|
|
|
|
|
|
|
elements.forEach((element) => {
|
|
|
|
|
const shape = this.createElement(element);
|
|
|
|
|
group.add(shape);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.staticLayer.add(group);
|
|
|
|
|
this.staticLayer.batchDraw(); // 批量绘制
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private enableViewportCulling(): void {
|
|
|
|
|
this.stage.on('dragmove wheel', () => {
|
|
|
|
|
this.updateVisibleElements();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateVisibleElements(): void {
|
|
|
|
|
const viewport = this.getViewportBounds();
|
|
|
|
|
|
|
|
|
|
this.staticLayer.children.forEach((child) => {
|
|
|
|
|
const bounds = child.getClientRect();
|
|
|
|
|
child.visible(this.boundsIntersect(viewport, bounds));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.staticLayer.batchDraw();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 12.2.2 Three.js 3D扩展方案
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 为未来3D场景编辑做准备
|
|
|
|
|
import * as THREE from 'three';
|
|
|
|
|
|
|
|
|
|
class ThreeJSSceneEditor {
|
|
|
|
|
private scene: THREE.Scene;
|
|
|
|
|
private camera: THREE.OrthographicCamera;
|
|
|
|
|
private renderer: THREE.WebGLRenderer;
|
|
|
|
|
private instancedMeshes: Map<string, THREE.InstancedMesh> = new Map();
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
this.setupRenderer(container);
|
|
|
|
|
this.setupScene();
|
|
|
|
|
this.enableInstancing(); // 实例化渲染优化
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupRenderer(container: HTMLElement): void {
|
|
|
|
|
this.renderer = new THREE.WebGLRenderer({
|
|
|
|
|
antialias: true,
|
|
|
|
|
powerPreference: 'high-performance',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
|
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
|
|
|
|
|
|
|
// 启用高性能渲染选项
|
|
|
|
|
this.renderer.shadowMap.enabled = true;
|
|
|
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
|
|
|
container.appendChild(this.renderer.domElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 实例化渲染 - 支持数万个相同元素
|
|
|
|
|
private enableInstancing(): void {
|
|
|
|
|
const pointGeometry = new THREE.CircleGeometry(1, 8);
|
|
|
|
|
const pointMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
|
|
|
|
|
|
|
|
|
// 创建实例化网格,支持10000个点位
|
|
|
|
|
const pointInstances = new THREE.InstancedMesh(pointGeometry, pointMaterial, 10000);
|
|
|
|
|
|
|
|
|
|
this.instancedMeshes.set('points', pointInstances);
|
|
|
|
|
this.scene.add(pointInstances);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量更新实例位置
|
|
|
|
|
updatePointsPositions(points: Point[]): void {
|
|
|
|
|
const instancedMesh = this.instancedMeshes.get('points');
|
|
|
|
|
if (!instancedMesh) return;
|
|
|
|
|
|
|
|
|
|
const matrix = new THREE.Matrix4();
|
|
|
|
|
|
|
|
|
|
points.forEach((point, index) => {
|
|
|
|
|
matrix.setPosition(point.x, point.y, 0);
|
|
|
|
|
instancedMesh.setMatrixAt(index, matrix);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.3 WebAssembly超高性能方案
|
|
|
|
|
|
|
|
|
|
#### 12.3.1 Rust + WebAssembly实现
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
// src/scene_engine.rs
|
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
|
use web_sys::CanvasRenderingContext2d;
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
pub struct SceneEngine {
|
|
|
|
|
elements: Vec<SceneElement>,
|
|
|
|
|
spatial_grid: SpatialGrid,
|
|
|
|
|
viewport: Viewport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
pub struct SceneElement {
|
|
|
|
|
id: u32,
|
|
|
|
|
x: f64,
|
|
|
|
|
y: f64,
|
|
|
|
|
element_type: ElementType,
|
|
|
|
|
visible: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
impl SceneEngine {
|
|
|
|
|
#[wasm_bindgen(constructor)]
|
|
|
|
|
pub fn new() -> SceneEngine {
|
|
|
|
|
SceneEngine {
|
|
|
|
|
elements: Vec::with_capacity(100000), // 预分配大容量
|
|
|
|
|
spatial_grid: SpatialGrid::new(100.0), // 100px网格
|
|
|
|
|
viewport: Viewport::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量添加元素 - 零拷贝操作
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
pub fn add_elements_batch(&mut self, elements_ptr: *const u32, count: usize) {
|
|
|
|
|
unsafe {
|
|
|
|
|
let elements_slice = std::slice::from_raw_parts(elements_ptr, count * 4);
|
|
|
|
|
|
|
|
|
|
for chunk in elements_slice.chunks(4) {
|
|
|
|
|
let element = SceneElement {
|
|
|
|
|
id: chunk[0],
|
|
|
|
|
x: f64::from_bits(chunk[1] as u64),
|
|
|
|
|
y: f64::from_bits(chunk[2] as u64),
|
|
|
|
|
element_type: ElementType::from_u32(chunk[3]),
|
|
|
|
|
visible: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.spatial_grid.insert(&element);
|
|
|
|
|
self.elements.push(element);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 高性能视锥剔除
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
pub fn update_visibility(&mut self, viewport_x: f64, viewport_y: f64,
|
|
|
|
|
viewport_width: f64, viewport_height: f64) -> Vec<u32> {
|
|
|
|
|
let mut visible_ids = Vec::new();
|
|
|
|
|
|
|
|
|
|
// 使用空间网格快速查询
|
|
|
|
|
let candidates = self.spatial_grid.query(
|
|
|
|
|
viewport_x, viewport_y, viewport_width, viewport_height
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for element_id in candidates {
|
|
|
|
|
if let Some(element) = self.elements.get_mut(*element_id as usize) {
|
|
|
|
|
element.visible = true;
|
|
|
|
|
visible_ids.push(element.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visible_ids
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 并行计算路径
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
pub fn calculate_paths_parallel(&self, start_points: &[u32],
|
|
|
|
|
end_points: &[u32]) -> Vec<f64> {
|
|
|
|
|
use rayon::prelude::*;
|
|
|
|
|
|
|
|
|
|
start_points.par_iter()
|
|
|
|
|
.zip(end_points.par_iter())
|
|
|
|
|
.map(|(start, end)| {
|
|
|
|
|
self.calculate_shortest_path(*start, *end)
|
|
|
|
|
})
|
|
|
|
|
.flatten()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 高性能空间网格
|
|
|
|
|
pub struct SpatialGrid {
|
|
|
|
|
cell_size: f64,
|
|
|
|
|
cells: std::collections::HashMap<(i32, i32), Vec<u32>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SpatialGrid {
|
|
|
|
|
pub fn new(cell_size: f64) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
cell_size,
|
|
|
|
|
cells: std::collections::HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn insert(&mut self, element: &SceneElement) {
|
|
|
|
|
let cell_x = (element.x / self.cell_size) as i32;
|
|
|
|
|
let cell_y = (element.y / self.cell_size) as i32;
|
|
|
|
|
|
|
|
|
|
self.cells.entry((cell_x, cell_y))
|
|
|
|
|
.or_insert_with(Vec::new)
|
|
|
|
|
.push(element.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn query(&self, x: f64, y: f64, width: f64, height: f64) -> Vec<&u32> {
|
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
|
|
|
|
|
let start_x = (x / self.cell_size) as i32;
|
|
|
|
|
let start_y = (y / self.cell_size) as i32;
|
|
|
|
|
let end_x = ((x + width) / self.cell_size) as i32;
|
|
|
|
|
let end_y = ((y + height) / self.cell_size) as i32;
|
|
|
|
|
|
|
|
|
|
for cell_x in start_x..=end_x {
|
|
|
|
|
for cell_y in start_y..=end_y {
|
|
|
|
|
if let Some(elements) = self.cells.get(&(cell_x, cell_y)) {
|
|
|
|
|
results.extend(elements.iter());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 12.3.2 TypeScript集成层
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// TypeScript 集成 WebAssembly
|
|
|
|
|
import init, { SceneEngine } from './pkg/scene_engine';
|
|
|
|
|
|
|
|
|
|
class WasmSceneEditor {
|
|
|
|
|
private wasmEngine: SceneEngine;
|
|
|
|
|
private canvas: HTMLCanvasElement;
|
|
|
|
|
private ctx: CanvasRenderingContext2D;
|
|
|
|
|
private sharedBuffer: SharedArrayBuffer;
|
|
|
|
|
|
|
|
|
|
async init(container: HTMLElement): Promise<void> {
|
|
|
|
|
// 初始化 WebAssembly 模块
|
|
|
|
|
await init();
|
|
|
|
|
this.wasmEngine = new SceneEngine();
|
|
|
|
|
|
|
|
|
|
this.setupCanvas(container);
|
|
|
|
|
this.setupSharedMemory();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupSharedMemory(): void {
|
|
|
|
|
// 使用 SharedArrayBuffer 实现零拷贝数据传输
|
|
|
|
|
this.sharedBuffer = new SharedArrayBuffer(1024 * 1024 * 4); // 4MB
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量添加元素 - 超高性能
|
|
|
|
|
addElementsBatch(elements: SceneElement[]): void {
|
|
|
|
|
// 将数据写入共享内存
|
|
|
|
|
const view = new Uint32Array(this.sharedBuffer);
|
|
|
|
|
let offset = 0;
|
|
|
|
|
|
|
|
|
|
elements.forEach((element) => {
|
|
|
|
|
view[offset++] = element.id;
|
|
|
|
|
view[offset++] = this.doubleToUint32(element.x);
|
|
|
|
|
view[offset++] = this.doubleToUint32(element.y);
|
|
|
|
|
view[offset++] = element.type;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 调用 WASM 函数处理
|
|
|
|
|
this.wasmEngine.add_elements_batch(view.byteOffset, elements.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 高性能渲染循环
|
|
|
|
|
private renderLoop = (): void => {
|
|
|
|
|
// WASM 计算可见性
|
|
|
|
|
const visibleIds = this.wasmEngine.update_visibility(
|
|
|
|
|
this.viewport.x,
|
|
|
|
|
this.viewport.y,
|
|
|
|
|
this.viewport.width,
|
|
|
|
|
this.viewport.height,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 渲染可见元素
|
|
|
|
|
this.renderVisibleElements(visibleIds);
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(this.renderLoop);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private doubleToUint32(value: number): number {
|
|
|
|
|
const buffer = new ArrayBuffer(8);
|
|
|
|
|
new Float64Array(buffer)[0] = value;
|
|
|
|
|
return new Uint32Array(buffer)[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 性能监控
|
|
|
|
|
class PerformanceMonitor {
|
|
|
|
|
private frameCount = 0;
|
|
|
|
|
private lastTime = performance.now();
|
|
|
|
|
|
|
|
|
|
update(): void {
|
|
|
|
|
this.frameCount++;
|
|
|
|
|
const now = performance.now();
|
|
|
|
|
|
|
|
|
|
if (now - this.lastTime >= 1000) {
|
|
|
|
|
console.log(`FPS: ${this.frameCount}`);
|
|
|
|
|
console.log(`Memory: ${(performance as any).memory?.usedJSHeapSize / 1024 / 1024}MB`);
|
|
|
|
|
|
|
|
|
|
this.frameCount = 0;
|
|
|
|
|
this.lastTime = now;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.4 Web Workers多线程优化
|
|
|
|
|
|
|
|
|
|
#### 12.4.1 多线程架构设计
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 主线程
|
|
|
|
|
class MultiThreadSceneEditor {
|
|
|
|
|
private renderWorker: Worker;
|
|
|
|
|
private calculationWorker: Worker;
|
|
|
|
|
private dataWorker: Worker;
|
|
|
|
|
private offscreenCanvas: OffscreenCanvas;
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
this.setupWorkers();
|
|
|
|
|
this.setupOffscreenCanvas(container);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupWorkers(): void {
|
|
|
|
|
// 渲染工作线程
|
|
|
|
|
this.renderWorker = new Worker('./workers/render.worker.js');
|
|
|
|
|
|
|
|
|
|
// 计算工作线程
|
|
|
|
|
this.calculationWorker = new Worker('./workers/calculation.worker.js');
|
|
|
|
|
|
|
|
|
|
// 数据处理工作线程
|
|
|
|
|
this.dataWorker = new Worker('./workers/data.worker.js');
|
|
|
|
|
|
|
|
|
|
this.setupWorkerCommunication();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupOffscreenCanvas(container: HTMLElement): void {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
canvas.width = container.clientWidth;
|
|
|
|
|
canvas.height = container.clientHeight;
|
|
|
|
|
container.appendChild(canvas);
|
|
|
|
|
|
|
|
|
|
// 传输canvas控制权给worker
|
|
|
|
|
this.offscreenCanvas = canvas.transferControlToOffscreen();
|
|
|
|
|
|
|
|
|
|
this.renderWorker.postMessage(
|
|
|
|
|
{
|
|
|
|
|
type: 'init',
|
|
|
|
|
canvas: this.offscreenCanvas,
|
|
|
|
|
},
|
|
|
|
|
[this.offscreenCanvas],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 并行处理大量数据
|
|
|
|
|
async processElementsBatch(elements: SceneElement[]): Promise<void> {
|
|
|
|
|
const chunkSize = Math.ceil(elements.length / 3);
|
|
|
|
|
|
|
|
|
|
// 并行处理
|
|
|
|
|
const promises = [
|
|
|
|
|
this.processChunk(elements.slice(0, chunkSize), 0),
|
|
|
|
|
this.processChunk(elements.slice(chunkSize, chunkSize * 2), 1),
|
|
|
|
|
this.processChunk(elements.slice(chunkSize * 2), 2),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private processChunk(chunk: SceneElement[], workerId: number): Promise<void> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const worker = [this.dataWorker, this.calculationWorker, this.renderWorker][workerId];
|
|
|
|
|
|
|
|
|
|
worker.postMessage({
|
|
|
|
|
type: 'process',
|
|
|
|
|
data: chunk,
|
|
|
|
|
chunkId: workerId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
worker.addEventListener('message', (e) => {
|
|
|
|
|
if (e.data.type === 'processed' && e.data.chunkId === workerId) {
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染工作线程 (render.worker.ts)
|
|
|
|
|
class RenderWorker {
|
|
|
|
|
private ctx: OffscreenCanvasRenderingContext2D;
|
|
|
|
|
private elementsBuffer: Float32Array;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
self.addEventListener('message', this.handleMessage.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleMessage(e: MessageEvent): void {
|
|
|
|
|
switch (e.data.type) {
|
|
|
|
|
case 'init':
|
|
|
|
|
this.ctx = e.data.canvas.getContext('2d');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'render':
|
|
|
|
|
this.renderFrame(e.data.elements, e.data.viewport);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'updateElements':
|
|
|
|
|
this.elementsBuffer = new Float32Array(e.data.buffer);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderFrame(elements: Float32Array, viewport: Viewport): void {
|
|
|
|
|
this.ctx.clearRect(0, 0, viewport.width, viewport.height);
|
|
|
|
|
|
|
|
|
|
// 批量渲染优化
|
|
|
|
|
this.ctx.save();
|
|
|
|
|
this.ctx.translate(-viewport.x, -viewport.y);
|
|
|
|
|
|
|
|
|
|
// 使用 ImageData 直接操作像素
|
|
|
|
|
const imageData = this.ctx.createImageData(viewport.width, viewport.height);
|
|
|
|
|
const data = imageData.data;
|
|
|
|
|
|
|
|
|
|
// 高性能像素级渲染
|
|
|
|
|
for (let i = 0; i < elements.length; i += 4) {
|
|
|
|
|
const x = elements[i];
|
|
|
|
|
const y = elements[i + 1];
|
|
|
|
|
const color = elements[i + 2];
|
|
|
|
|
const type = elements[i + 3];
|
|
|
|
|
|
|
|
|
|
this.drawPixel(data, x, y, color, viewport);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.ctx.putImageData(imageData, viewport.x, viewport.y);
|
|
|
|
|
this.ctx.restore();
|
|
|
|
|
|
|
|
|
|
// 通知主线程渲染完成
|
|
|
|
|
self.postMessage({ type: 'frameRendered' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.5 性能对比表
|
|
|
|
|
|
|
|
|
|
| 方案 | 渲染性能 | 内存使用 | 开发复杂度 | 兼容性 | 推荐场景 |
|
|
|
|
|
| --------------- | ---------- | ---------- | ---------- | ---------- | --------------- |
|
|
|
|
|
| **当前Meta2d** | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | <1000元素 |
|
|
|
|
|
| **PixiJS** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 1000-10000元素 |
|
|
|
|
|
| **Three.js** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 3D场景/复杂效果 |
|
|
|
|
|
| **WebAssembly** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | >10000元素 |
|
|
|
|
|
| **Web Workers** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | CPU密集计算 |
|
|
|
|
|
|
|
|
|
|
### 12.6 迁移实施建议
|
|
|
|
|
|
|
|
|
|
#### 12.6.1 渐进式迁移策略
|
|
|
|
|
|
|
|
|
|
**阶段1: 立即优化 (1-2周)**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 在现有Meta2d基础上添加PixiJS渲染层
|
|
|
|
|
class HybridSceneEditor extends EditorService {
|
|
|
|
|
private pixiRenderer: PIXI.Application;
|
|
|
|
|
private usePixiForLargeScene = false;
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLDivElement) {
|
|
|
|
|
super(container);
|
|
|
|
|
this.initPixiRenderer(container);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initPixiRenderer(container: HTMLDivElement): void {
|
|
|
|
|
this.pixiRenderer = new PIXI.Application({
|
|
|
|
|
width: container.clientWidth,
|
|
|
|
|
height: container.clientHeight,
|
|
|
|
|
transparent: true,
|
|
|
|
|
powerPreference: 'high-performance',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 叠加在Meta2d之上
|
|
|
|
|
container.appendChild(this.pixiRenderer.view);
|
|
|
|
|
this.pixiRenderer.view.style.position = 'absolute';
|
|
|
|
|
this.pixiRenderer.view.style.pointerEvents = 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async load(map?: string, editable = false): Promise<void> {
|
|
|
|
|
await super.load(map, editable);
|
|
|
|
|
|
|
|
|
|
// 检查元素数量,决定使用哪个渲染器
|
|
|
|
|
const totalElements = this.points.value.length + this.routes.value.length + this.areas.value.length;
|
|
|
|
|
|
|
|
|
|
if (totalElements > 1000) {
|
|
|
|
|
console.log('🚀 切换到PixiJS高性能渲染');
|
|
|
|
|
this.usePixiForLargeScene = true;
|
|
|
|
|
this.migrateToPixi();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private migrateToPixi(): void {
|
|
|
|
|
// 隐藏Meta2d渲染层
|
|
|
|
|
this.canvas.canvas.style.opacity = '0.1';
|
|
|
|
|
|
|
|
|
|
// 使用PixiJS渲染大量元素
|
|
|
|
|
this.renderWithPixi();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**阶段2: 核心重构 (1-2月)**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 完全基于PixiJS的新实现
|
|
|
|
|
class NextGenSceneEditor {
|
|
|
|
|
private app: PIXI.Application;
|
|
|
|
|
private sceneContainer: PIXI.Container;
|
|
|
|
|
private quadTree: QuadTree;
|
|
|
|
|
private instanceManager: InstanceManager;
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
this.setupPixiApp(container);
|
|
|
|
|
this.setupOptimizations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupOptimizations(): void {
|
|
|
|
|
// 1. 四叉树空间索引
|
|
|
|
|
this.quadTree = new QuadTree(0, 0, 10000, 10000);
|
|
|
|
|
|
|
|
|
|
// 2. 实例化管理器
|
|
|
|
|
this.instanceManager = new InstanceManager();
|
|
|
|
|
|
|
|
|
|
// 3. 批量渲染系统
|
|
|
|
|
this.setupBatchRenderer();
|
|
|
|
|
|
|
|
|
|
// 4. LOD系统
|
|
|
|
|
this.setupLODSystem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 支持10万+元素的批量添加
|
|
|
|
|
async addMassiveElements(elements: SceneElement[]): Promise<void> {
|
|
|
|
|
console.time('MassiveElementsAdd');
|
|
|
|
|
|
|
|
|
|
// 使用实例化渲染
|
|
|
|
|
const instancedElements = this.instanceManager.createInstances(elements);
|
|
|
|
|
|
|
|
|
|
// 批量添加到四叉树
|
|
|
|
|
elements.forEach((element) => {
|
|
|
|
|
this.quadTree.insert(element);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加到场景
|
|
|
|
|
instancedElements.forEach((instance) => {
|
|
|
|
|
this.sceneContainer.addChild(instance);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.timeEnd('MassiveElementsAdd');
|
|
|
|
|
console.log(`✅ 成功添加 ${elements.length} 个元素`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**阶段3: WebAssembly增强 (2-3月)**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 添加WebAssembly计算核心
|
|
|
|
|
class UltimateSceneEditor extends NextGenSceneEditor {
|
|
|
|
|
private wasmCore: SceneEngineWasm;
|
|
|
|
|
private sharedBuffer: SharedArrayBuffer;
|
|
|
|
|
|
|
|
|
|
async init(): Promise<void> {
|
|
|
|
|
await super.init();
|
|
|
|
|
|
|
|
|
|
// 初始化WASM核心
|
|
|
|
|
this.wasmCore = await SceneEngineWasm.init();
|
|
|
|
|
|
|
|
|
|
// 设置共享内存
|
|
|
|
|
this.setupSharedMemory();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 百万级元素支持
|
|
|
|
|
async loadMegaScene(sceneData: MegaSceneData): Promise<void> {
|
|
|
|
|
console.log(`🔥 加载超大场景: ${sceneData.elements.length} 个元素`);
|
|
|
|
|
|
|
|
|
|
// WASM并行处理
|
|
|
|
|
const processed = await this.wasmCore.processMegaScene(this.sharedBuffer, sceneData.elements.length);
|
|
|
|
|
|
|
|
|
|
// PixiJS渲染
|
|
|
|
|
await this.renderProcessedElements(processed);
|
|
|
|
|
|
|
|
|
|
console.log('🎉 超大场景加载完成,性能提升100倍+');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.7 实施成本效益分析
|
|
|
|
|
|
|
|
|
|
#### 12.7.1 开发成本
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
interface MigrationCost {
|
|
|
|
|
timeWeeks: number;
|
|
|
|
|
complexity: 'Low' | 'Medium' | 'High';
|
|
|
|
|
riskLevel: 'Low' | 'Medium' | 'High';
|
|
|
|
|
performanceGain: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const migrationOptions: Record<string, MigrationCost> = {
|
|
|
|
|
pixiJSMigration: {
|
|
|
|
|
timeWeeks: 4,
|
|
|
|
|
complexity: 'Medium',
|
|
|
|
|
riskLevel: 'Low',
|
|
|
|
|
performanceGain: '10-50x渲染性能提升',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
webAssemblyCore: {
|
|
|
|
|
timeWeeks: 8,
|
|
|
|
|
complexity: 'High',
|
|
|
|
|
riskLevel: 'Medium',
|
|
|
|
|
performanceGain: '100x+计算性能提升',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
webWorkersParallel: {
|
|
|
|
|
timeWeeks: 3,
|
|
|
|
|
complexity: 'Medium',
|
|
|
|
|
riskLevel: 'Low',
|
|
|
|
|
performanceGain: '多核心并行处理',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
hybridApproach: {
|
|
|
|
|
timeWeeks: 2,
|
|
|
|
|
complexity: 'Low',
|
|
|
|
|
riskLevel: 'Low',
|
|
|
|
|
performanceGain: '保持兼容性的性能提升',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 12.7.2 推荐实施路径
|
|
|
|
|
|
|
|
|
|
**🎯 推荐方案: PixiJS + Web Workers混合架构**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 最佳性价比方案
|
|
|
|
|
class RecommendedSceneEditor {
|
|
|
|
|
private pixiApp: PIXI.Application;
|
|
|
|
|
private calculationWorker: Worker;
|
|
|
|
|
private renderingOptimized = true;
|
|
|
|
|
|
|
|
|
|
// 优势:
|
|
|
|
|
// ✅ 10-50倍性能提升
|
|
|
|
|
// ✅ 4周开发周期
|
|
|
|
|
// ✅ 低风险
|
|
|
|
|
// ✅ 向后兼容
|
|
|
|
|
// ✅ 支持1万+元素
|
|
|
|
|
|
|
|
|
|
constructor(container: HTMLElement) {
|
|
|
|
|
console.log('🚀 启用推荐高性能方案');
|
|
|
|
|
this.initOptimizedRenderer(container);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async benchmark(): Promise<PerformanceReport> {
|
|
|
|
|
const elementCounts = [100, 1000, 5000, 10000, 50000];
|
|
|
|
|
const results: PerformanceReport = {};
|
|
|
|
|
|
|
|
|
|
for (const count of elementCounts) {
|
|
|
|
|
const elements = this.generateTestElements(count);
|
|
|
|
|
|
|
|
|
|
console.time(`Render ${count} elements`);
|
|
|
|
|
await this.addElementsBatch(elements);
|
|
|
|
|
console.timeEnd(`Render ${count} elements`);
|
|
|
|
|
|
|
|
|
|
results[count] = {
|
|
|
|
|
fps: this.measureFPS(),
|
|
|
|
|
memory: this.measureMemory(),
|
|
|
|
|
renderTime: performance.now(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.8 总结建议
|
|
|
|
|
|
|
|
|
|
基于您的需求和现有技术栈,我建议采用以下**分阶段实施策略**:
|
|
|
|
|
|
|
|
|
|
**🔥 立即实施 (高优先级)**
|
|
|
|
|
|
|
|
|
|
1. **PixiJS渲染层**: 4周内实现,性能提升10-50倍
|
|
|
|
|
2. **Web Workers计算**: 并行处理复杂计算
|
|
|
|
|
3. **视口裁剪优化**: 只渲染可见元素
|
|
|
|
|
|
|
|
|
|
**⚡ 中期规划 (中优先级)**
|
|
|
|
|
|
|
|
|
|
1. **WebAssembly核心**: 处理超大规模场景
|
|
|
|
|
2. **四叉树空间索引**: 优化元素查找
|
|
|
|
|
3. **LOD渲染系统**: 根据缩放级别调整详度
|
|
|
|
|
|
|
|
|
|
**🚀 长期愿景 (低优先级)**
|
|
|
|
|
|
|
|
|
|
1. **Three.js 3D扩展**: 支持3D场景编辑
|
|
|
|
|
2. **GPU计算着色器**: 最极致的性能优化
|
|
|
|
|
3. **WebXR支持**: VR/AR场景编辑
|
|
|
|
|
|
|
|
|
|
这样的技术栈升级可以让您的场景编辑器:
|
|
|
|
|
|
|
|
|
|
- 支持**10万+元素**的超大场景
|
|
|
|
|
- 保持**60 FPS**稳定渲染
|
|
|
|
|
- 减少**80%**的内存占用
|
|
|
|
|
- 提供更好的用户体验
|