# flight-ux-pixi-editor 使用说明

## 项目概述

**flight-ux-pixi-editor** 是一个基于 **PixiJS v8** 的 2D 图形编辑器内核库，属于 DACS（Digital Aviation Cockpit System）Flight UX 项目的一部分。

它提供了完整的画布编辑能力：绘制图形、选中变换、缩放平移、标尺、撤销重做、事件系统、数据流引擎等，专为**航空座舱 UI 设计工具**打造。

---

## 架构总览

```
Editor (主控制器)
├── Viewport          — 视口：缩放、平移、坐标转换
├── Gizmos            — 工具图层容器
│   ├── SelectTools   — 选中交互管理器
│   ├── DrawTools     — 绘制工具管理器
│   └── Ruler         — 标尺系统
├── History           — 撤销/重做（快照式，最多 100 条）
└── WidgetNode        — 根节点（渲染树）
    ├── NodeData      — 图形节点（矩形、圆形、文字…）
    └── WidgetData    — 组件节点（可嵌套子节点）
```

---

## 快速开始

### 1. 安装与引入

```typescript
import { Editor } from 'flight-ux-pixi-editor';
```

### 2. 初始化编辑器

```typescript
const editor = new Editor();

// 传入一个 DOM 容器元素
await editor.init(document.getElementById('canvas-container')!);
```

初始化后，PixiJS 的 canvas 会自动添加到容器中，背景色为 `#1e1e1e`（深色主题）。

### 3. 加载数据

```typescript
import type { WidgetData } from 'flight-ux-pixi-editor';

const data: WidgetData = {
  id: 'widget-1',
  name: '仪表盘',
  props: [
    { id: 'p1', name: 'speed', type: 'number', defaultValue: 0 },
  ],
  dataFlows: [],
  children: [
    {
      id: 'node-1',
      name: '背景',
      type: 'rect',
      transform: {
        x: 100, y: 100,
        scaleX: 1, scaleY: 1,
        originX: 0, originY: 0,
        pivotX: 0, pivotY: 0,
        rotation: 0, zIndex: 0,
        alpha: 1, visible: true,
      },
      props: {
        width: 400, height: 300, radius: 8,
        fill: '#2a2a2a', stroke: '#444444', strokeWidth: 1,
      },
    },
  ],
};

editor.renderData(data);
```

### 4. 导出数据

```typescript
const exported: WidgetData = editor.exportData();
console.log(JSON.stringify(exported, null, 2));
```

---

## 核心 API

### Editor 类

| 方法 | 说明 |
|------|------|
| `init(container)` | 初始化 PixiJS 应用，将 canvas 添加到容器 |
| `renderData(data)` | 渲染 WidgetData 到画布 |
| `exportData()` | 导出当前画布的 WidgetData |
| `undo()` / `redo()` | 撤销 / 重做 |
| `getScreenSize()` | 获取画布屏幕尺寸 `{ width, height }` |
| `generateId(length?)` | 生成随机 ID（默认 8 位） |
| `on(event, handler)` | 监听事件（代理自 stage） |
| `destroy()` | 销毁编辑器，释放资源 |

### 属性访问

```typescript
editor.app          // PixiJS Application 实例
editor.stage        // PixiJS Stage（根容器）
editor.viewport     // Viewport 实例
editor.gizmos       // Gizmos 容器
editor.history      // History 实例
editor.drawTools    // DrawTools 工具管理器
editor.ruler        // Ruler 标尺系统
editor.cursor       // 鼠标光标样式（可读写）
```

---

## 绘制工具 (DrawTools)

### 内置工具列表

| 工具 | type 值 | 说明 |
|------|---------|------|
| RectTool | `rect` | 矩形（Shift 锁定正方形） |
| CircleTool | `circle` | 圆形/椭圆 |
| LineTool | `line` | 折线 |
| PolygonTool | `polygon` | 多边形 |
| RegularPolygonTool | `regularPolygon` | 正多边形 |
| BezierTool | `bezier` | 贝塞尔曲线 |
| ArcTool | `arc` | 圆弧/扇形 |
| ArcRulerTool | `arcRuler` | 标尺弧 |
| RulerTool | `ruler` | 标尺线 |
| TextTool | `text` | 文字 |
| ImageTool | `image` | 图片 |
| LegacyTool | 自定义 | 兼容旧版 GL 组件 |

### 使用方式

```typescript
// 激活绘制工具
editor.drawTools.setActive('rect');   // 切换到矩形工具
editor.drawTools.setActive('text');   // 切换到文字工具

// 清除当前工具（回到选择模式）
editor.drawTools.clearActive();

// 查询当前激活的工具
const active = editor.drawTools.getActive();

// 获取所有已注册工具
const tools = editor.drawTools.tools;

// 注册自定义工具
import { BaseTool } from 'flight-ux-pixi-editor';

class CustomTool extends BaseTool {
  readonly type = 'custom-shape';
  readonly name = '自定义图形';
  readonly desc = '绘制自定义图形';
  // 实现 onPointerDown / onPointerMove / onPointerUp
}

editor.drawTools.register(new CustomTool(editor, editor.drawTools));
```

### 自定义工具开发

继承 `BaseTool`，实现以下接口：

```typescript
abstract class BaseTool {
  abstract readonly type: string;    // 唯一标识
  abstract readonly name: string;    // 显示名称
  abstract readonly desc: string;    // 工具描述

  // 生命周期（可选覆写）
  activate() { /* 激活时调用 */ }
  deactivate() { /* 失活时调用 */ }

  // 交互钩子（按需覆写）
  onPointerDown?(e: FederatedPointerEvent): void;
  onPointerMove?(e: FederatedPointerEvent): void;
  onPointerUp?(e: FederatedPointerEvent): void;

  // 便捷属性
  get editor()       // Editor 实例
  get rootWidget()   // 根 WidgetNode
  get previewLayer() // 预览图层（DrawTools 容器）
  generateId()       // 生成唯一 ID
}
```

---

## 选中工具 (SelectTools)

### 选中交互

- **左键点击**节点 → 选中该节点，显示变换手柄
- **左键点击**空白区域 → 取消选中
- **拖拽**节点内部 → 移动节点
- **拖拽**手柄 → 缩放节点（8 方向手柄）
- 旋转后的节点手柄会**自动跟随本地坐标轴方向**

### 编程式选中

```typescript
// 通过 ID 选中（如点击工程树时联动）
editor.gizmos.getGizmo('selectTools').select('node-1');

// 清除选中
editor.gizmos.getGizmo('selectTools').clear();

// 获取当前选中 ID 列表
const ids = editor.gizmos.getGizmo('selectTools').selectedIds;
```

### 自定义选中工具

每种节点类型可以有自己的选中工具（提供不同的手柄和交互逻辑）：

```typescript
import { BaseSelectTool } from 'flight-ux-pixi-editor';

class CustomSelectTool extends BaseSelectTool {
  readonly type = 'custom-shape';

  constructor(editor: Editor) {
    super(editor);
  }

  updateLayout() {
    // 绘制自定义手柄、边界框等
  }

  onPointerDown?(e: FederatedPointerEvent): void;
  onPointerMove?(e: FederatedPointerEvent): void;
  onPointerUp?(e: FederatedPointerEvent): void;
}

// 注册
editor.gizmos.getGizmo('selectTools').register(new CustomSelectTool(editor));
```

---

## 视口 (Viewport)

### 鼠标操作

| 操作 | 效果 |
|------|------|
| 鼠标滚轮 | 以指针为中心缩放（范围 10% ~ 2000%） |
| 右键拖拽 | 平移画布 |

### 编程式控制

```typescript
// 坐标转换
const worldPos = editor.viewport.screenToWorld(100, 200);  // 屏幕 → 世界坐标
const screenPos = editor.viewport.worldToScreen(50, 50);   // 世界 → 屏幕坐标

// 缩放控制
editor.viewport.setZoom(1.5);              // 设置 150% 缩放（以画布中心为锚点）
editor.viewport.setZoom(2, 300, 200);      // 设置 200% 缩放（以指定点为锚点）

// 重置视图
editor.viewport.resetView();

// 获取当前缩放百分比
const percent = editor.viewport.getZoomPercent();  // 如 150

// 获取根节点
const root = editor.viewport.root;
```

---

## 标尺系统 (Ruler)

标尺自动显示在画布顶部和左侧，包含：

- **刻度线**：根据缩放级别自适应间距（1/2/5/10/20/50/100/200/500/1000/2000/5000）
- **刻度数值**：使用 BitmapText 对象池，性能优化
- **选中高亮**：选中元素时，标尺对应区域显示蓝色高亮条 + 两端数值标注 + 居中差值标注
- **RAF 批处理**：全量重绘和高亮更新分离，减少不必要的重绘

```typescript
// 手动触发标尺更新
editor.ruler.update();

// 标尺宽度（px）
const size = editor.ruler.rulerSize;  // 默认 20
```

---

## 数据模型

### WidgetData（组件）

```typescript
interface WidgetData {
  id: string;              // 唯一 ID
  name: string;            // 名称
  props: WidgetProp[];     // 属性定义列表
  dataFlows: DataFlow[];   // 数据流定义列表
  transform?: NodeTransform; // 变换（可选，仅根节点需要）
  children: (NodeData | WidgetData)[]; // 子节点（支持嵌套）
}
```

### NodeData（图形节点）

```typescript
interface NodeData {
  id: string;              // 唯一 ID
  name: string;            // 名称
  type: string;            // 类型（rect/circle/text/image/...）
  transform: NodeTransform; // 变换属性
  props: Record<string, any>; // 类型特定的属性
  events?: NodeEvent[];    // 事件列表（可选）
  maskTargetId?: string;   // 遮罩目标节点 ID（可选）
  maskInverse?: boolean;   // 遮罩是否反转（可选）
}
```

### NodeTransform（变换）

```typescript
interface NodeTransform {
  x: number;       // X 位置
  y: number;       // Y 位置
  scaleX: number;  // X 缩放
  scaleY: number;  // Y 缩放
  originX: number; // X 原点偏移
  originY: number; // Y 原点偏移
  pivotX: number;  // X 锚点
  pivotY: number;  // Y 锚点
  rotation: number;// 旋转（弧度）
  zIndex: number;  // 层级
  alpha: number;   // 透明度
  visible: boolean;// 可见性
}
```

### 各类型节点的 Props

| 类型 | Props 字段 |
|------|-----------|
| `rect` | `width`, `height`, `radius`, `fill`, `stroke`, `strokeWidth` |
| `circle` | `radius`, `fill`, `stroke`, `strokeWidth` |
| `line` | `points: {x, y}[]`, `stroke`, `strokeWidth` |
| `polygon` | `points: {x, y}[]`, `fill`, `stroke`, `strokeWidth` |
| `regularPolygon` | `sides`（边数）, `radius`, `fill`, `stroke`, `strokeWidth` |
| `bezier` | `points: {x, y}[]`（控制点）, `stroke`, `strokeWidth` |
| `arc` | `startAngle`, `endAngle`, `radius`, `fill`, `stroke`, `strokeWidth` |
| `text` | `content`, `fontSize`, `fontFamily`, `fontWeight`, `fill`, `align`, `wordWrap`, `wordWrapWidth` |
| `image` | `src`, `width`, `height` |

---

## 事件系统 (EventInjector)

在非编辑模式下，节点可以绑定**运行时事件**，通过 JavaScript 代码实现交互逻辑。

### 定义事件

```typescript
const data: WidgetData = {
  id: 'widget-1',
  name: '按钮组件',
  props: [],
  dataFlows: [],
  children: [
    {
      id: 'btn-1',
      name: '启动按钮',
      type: 'rect',
      transform: { /* ... */ },
      props: { width: 100, height: 40, fill: '#007acc' },
      events: [
        {
          id: 'ev-1',
          name: '点击启动',
          type: 'pointerdown',  // PixiJS 事件类型
          code: `
            // env: WidgetNode (父容器)
            // self: NodeProxy (当前节点代理)
            // $: WidgetProxy (选择器，即 $)
            // $event: 原始 PixiJS 事件

            // 修改自身属性
            self.fill.set('#ff0000').apply();

            // 通过 $ 查找其他节点并修改
            const indicator = $.getNodeByName('指示灯');
            if (indicator) {
              indicator.props.fill.set('#00ff00').apply();
            }

            // 统一提交（推荐方式）
            // self.fill.set('#ff0000');
            // $.getNodeByName('指示灯').props.fill.set('#00ff00');
            // $.apply();
          `,
        },
      ],
    },
  ],
};
```

### NodeProxy API（事件代码中的 self）

```typescript
// 读取 transform 属性
const x = self.x;           // 返回 TransformProp
const currentValue = self.x.valueOf();  // 或 self.x + 0

// 链式修改 transform
self.x.set(100).y.set(200).rotation.set(Math.PI / 4).apply();

// 读写 props
const val = self.getProp<number>('speed');
self.setProp('speed', 800).apply();

// 批量操作
self.setProps({ speed: 800, altitude: 30000 }).apply();
```

### WidgetProxy API（事件代码中的 $）

```typescript
// 按名称查找
const node = $.getNodeByName('标题');       // 第一个匹配
const nodes = $.getNodesByName('指示灯');   // 所有同名节点
const all = $.getAllNodes();                // 所有节点

// 按 ID 查找
const node = $.getNodeById('node-1');

// 统一提交所有修改
$.apply();
```

---

## 数据流引擎 (DataFlowInjector)

数据流实现了**声明式的数据绑定**，支持 Widget 属性之间的自动联动。

### 定义数据流

```typescript
const data: WidgetData = {
  id: 'widget-1',
  name: '速度表',
  props: [
    { id: 'p1', name: 'speed', type: 'number', defaultValue: 0 },
    { id: 'p2', name: 'needleAngle', type: 'number', defaultValue: 0 },
    { id: 'p3', name: 'displayText', type: 'string', defaultValue: '0' },
  ],
  dataFlows: [
    {
      id: 'df-1',
      source: 'props.speed * 0.005',       // 表达式：速度 × 0.005
      target: 'root.props.needleAngle',     // 写入针角度
    },
    {
      id: 'df-2',
      source: 'String(Math.round(props.speed)) + " km/h"',
      target: 'root.props.displayText',     // 写入显示文本
    },
  ],
  children: [
    // ... 节点定义
  ],
};
```

### Source 表达式语法

```
props.xxx          — 读取 Widget 属性
@meta.xxx          — 读取元数据
@meta.name[#id]    — 通过节点 ID 读取元数据
```

### Target 格式

```
root.props.xxx          — 写入 Widget 属性
nodeId.props.xxx        — 写入节点属性
nodeId.transform.xxx    — 写入节点变换属性
```

### 特性

- **拓扑排序**：自动按依赖顺序执行，避免顺序问题
- **循环依赖检测**：发现环路时降级为原始顺序并输出警告
- **Prop 验证器**：可在 WidgetProp 中定义 `validator` 表达式过滤非法值
- **精准触发**：基于 BFS 队列，同一批次每条 flow 最多执行一次
- **零运行时查找**：注入时预编译 setter，运行时直接调用

---

## 渲染器系统

### 内置渲染器

| 类型 | 渲染器 | 说明 |
|------|--------|------|
| `rect` | rectRenderer | Graphics 矩形/圆角矩形 |
| `circle` | circleRenderer | Graphics 圆形 |
| `line` | lineRenderer | Graphics 折线 |
| `polygon` | polygonRenderer | Graphics 多边形 |
| `regularPolygon` | regularPolygonRenderer | Graphics 正多边形 |
| `bezier` | bezierRenderer | Graphics 贝塞尔曲线 |
| `arc` | arcRenderer | Graphics 圆弧/扇形 |
| `arcRuler` | arcRulerRenderer | Graphics 标尺弧 |
| `ruler` | rulerRenderer | Graphics 标尺线 |
| `text` | textRenderer | Text 文字 |
| `image` | imageRenderer | Sprite 图片 |
| `legacy` | legacyRender | 兼容旧版 GL 组件 |

### 注册自定义渲染器

```typescript
import { registerRenderer, type NodeRenderer } from 'flight-ux-pixi-editor';

const customRenderer: NodeRenderer = {
  create(node: NodeData) {
    // 创建 PixiJS 显示对象
    const g = new Graphics();
    this.update(g, node);
    return g;
  },
  update(target: Container, node: NodeData) {
    // 更新显示对象属性
    const g = target as Graphics;
    g.clear();
    g.circle(0, 0, node.props.radius).fill(node.props.fill);
  },
};

registerRenderer('custom-shape', customRenderer);
// 或批量注册
registerRenderer({
  'custom-shape': customRenderer,
  'another-type': anotherRenderer,
});
```

---

## 事件监听

编辑器通过 PixiJS 的 EventEmitter 派发以下自定义事件：

| 事件名 | 触发时机 | 参数 |
|--------|---------|------|
| `data:change` | 数据发生变化时 | — |
| `viewport:change` | 视口缩放/平移时 | — |
| `selection:change` | 选中状态变化时 | `Array<{ id, data, node }>` |
| `drawTool:change` | 绘制工具切换时 | `type: string \| null` |
| `prop:change` | Widget 属性变化时 | `propName, value` |

```typescript
// 监听选中变化
editor.on('selection:change', (items) => {
  console.log('选中:', items.map(i => i.id));
});

// 监听数据变化
editor.on('data:change', () => {
  const data = editor.exportData();
  // 同步到 UI 或其他系统
});

// 监听视口变化
editor.on('viewport:change', () => {
  console.log('缩放:', editor.viewport.getZoomPercent() + '%');
});
```

---

## 撤销/重做 (History)

- 自动监听 `data:change` 事件，**200ms 防抖**后保存快照
- 最多保留 **100 条**历史记录
- 避免重复保存相同状态
- undo 后新操作自动截断 redo 分支

```typescript
editor.undo();
editor.redo();

// 监听状态变化（更新 UI 按钮可用状态）
editor.history.onStateChange = (canUndo, canRedo) => {
  undoButton.disabled = !canUndo;
  redoButton.disabled = !canRedo;
};
```

---

## 遮罩系统 (Mask)

节点可以通过 `maskTargetId` 属性设置遮罩关系：

```typescript
{
  id: 'mask-shape',
  name: '遮罩形状',
  type: 'circle',
  transform: { /* ... */ },
  props: { radius: 50, fill: '#000000' },
  maskTargetId: 'target-node',  // 此节点作为遮罩，作用于 target-node
  maskInverse: false,            // false=遮罩内可见，true=遮罩外可见
}
```

---

## 销毁

```typescript
// 释放所有资源
editor.destroy();
```

---

## 技术细节

- **框架**：PixiJS v8（WebGL 渲染）
- **语言**：TypeScript
- **主题**：深色（背景 `#1e1e1e`）
- **标尺**：自适应刻度 + BitmapText 对象池 + RAF 批处理
- **历史**：JSON 快照 + 防抖 + 拓扑去重
- **数据流**：预编译 + 拓扑排序 + BFS 队列 + 零运行时查找
- **事件**：Function 编译 + NodeProxy 链式 API + WidgetProxy 选择器
- **坐标**：屏幕 ↔ 世界坐标双向转换，支持旋转节点的本地轴手柄
