# Remote Annotation Integration Guide

本文档用于说明“白板端”和“控制端”完全隔离时，如何通过命令实现远程注释控制。

## 目标

- 白板端只负责渲染 `Excalidraw` 并执行命令
- 控制端只负责发送工具命令（不直接操作白板实例）
- 两端通过 `socket`（或其他消息通道）通信

---

## 一、白板端如何接入

白板端核心职责：

1. 挂载 `Excalidraw`，拿到 `excalidrawAPI`
2. 监听远端命令消息
3. 收到命令后执行 `excalidrawAPI.leftToolbar.dispatch(command)`

### 示例（React + socket）

```tsx
import { useEffect, useState } from "react";
import Excalidraw from "@excalidraw/excalidraw";
import type {
  ExcalidrawImperativeAPI,
  LeftToolbarCommand,
} from "@excalidraw/excalidraw";

type SocketLike = {
  on: (event: string, cb: (data: unknown) => void) => void;
  off: (event: string, cb: (data: unknown) => void) => void;
};

const isLeftToolbarCommand = (v: unknown): v is LeftToolbarCommand => {
  if (!v || typeof v !== "object") return false;
  const type = (v as { type?: unknown }).type;
  return (
    typeof type === "string" &&
    [
      "selectSelection",
      "selectFreedraw",
      "selectLine",
      "selectShape",
      "selectText",
      "selectEraser",
      "updateStyle",
    ].includes(type)
  );
};

export default function WhiteboardPage({ socket }: { socket: SocketLike }) {
  const [api, setApi] = useState<ExcalidrawImperativeAPI | null>(null);

  useEffect(() => {
    const handler = (data: unknown) => {
      if (!isLeftToolbarCommand(data)) return;
      api?.leftToolbar.dispatch(data);
    };

    socket.on("toolbar-command", handler);
    return () => socket.off("toolbar-command", handler);
  }, [socket, api]);

  return (
    <Excalidraw
      excalidrawAPI={setApi}
      showToolBar={false}
      // 透明叠加在 PPT 场景可按需开启：
      // viewModeEnabled={false}
    />
  );
}
```

### 白板端注意事项

- 建议对命令先做类型校验再执行
- 退出注释模式时及时解绑 socket 监听
- 若需“只读观看模式”，可以结合 `viewModeEnabled`

---

## 二、控制端如何调用命令

控制端核心职责：

1. 监听本地工具条交互（按钮、颜色、线宽、形状等）
2. 将操作转换成 `LeftToolbarCommand`
3. 通过 socket 发给白板端

### 示例（发送命令）

```ts
import type { LeftToolbarCommand } from "@excalidraw/excalidraw";

type SocketLike = {
  emit: (event: string, data: unknown) => void;
};

const sendToolbarCommand = (
  socket: SocketLike,
  command: LeftToolbarCommand,
) => {
  socket.emit("toolbar-command", command);
};

// 切换到荧光笔
sendToolbarCommand(socket, {
  type: "selectFreedraw",
  payload: { variant: "highlighter", strokeWidth: 6, strokeColor: "#ffcc00" },
});

// 切换到线条：空心双箭头
sendToolbarCommand(socket, {
  type: "selectLine",
  payload: { variant: "outlineDoubleArrow", strokeWidth: 2, strokeColor: "#1677ff" },
});

// 切换到形状：椭圆
sendToolbarCommand(socket, {
  type: "selectShape",
  payload: { shape: "ellipse", strokeWidth: 3, strokeColor: "#1f1f1f" },
});

// 仅更新样式（不切工具）
sendToolbarCommand(socket, {
  type: "updateStyle",
  payload: { currentItemStrokeWidth: 4, currentItemStrokeColor: "#ff4d4f" },
});
```

---

## 三、推荐事件约定

建议统一使用一个事件名：

- `toolbar-command`

消息体直接使用 `LeftToolbarCommand` JSON。

这样控制端和白板端都容易调试，也便于后续扩展（录制/回放命令流）。

---

## 四、常见问题

### 1) 为什么控制端不能直接调用白板方法？

因为是跨端隔离部署，控制端拿不到白板实例。必须通过消息通道转发命令。

### 2) 一定要用 socket 吗？

不是。只要能传 JSON 的通道都可以：`WebSocket`、`postMessage`、Electron IPC 等。

### 3) 如何保证命令安全？

- 白板端做类型校验
- 只允许白名单命令类型
- 必要时增加鉴权和房间隔离

---

## 五、下一步建议

- 接入日志：记录每次命令时间戳，方便排查问题
- 接入回放：按命令时间线回放注释过程
- 接入权限：区分“可控白板”的角色与“只读观众”角色
