mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 23:28:35 +08:00
225 lines
7.3 KiB
Plaintext
225 lines
7.3 KiB
Plaintext
---
|
||
title: 画布数据结构
|
||
description: 画布本地存储、节点结构、媒体文件与清理机制
|
||
---
|
||
|
||
# 画布数据结构
|
||
|
||
本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式,以及后续接入后端存储时建议保持的兼容边界。
|
||
|
||
## 当前存储位置
|
||
|
||
当前画布项目主要保存在浏览器本地:
|
||
|
||
- 画布项目 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:canvas_store`。
|
||
- 我的素材 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:asset_store`。
|
||
- 图片 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `image_files`。
|
||
- 视频等媒体 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `media_files`。
|
||
|
||
画布 JSON 不直接长期保存大体积 base64 图片或视频。图片节点、视频节点、助手图片和素材媒体只保存展示 URL、`storageKey` 和元信息,真实 Blob 通过 `storageKey` 读取。
|
||
|
||
## 画布项目结构
|
||
|
||
每个画布项目是一个 `CanvasProject`:
|
||
|
||
```ts
|
||
type CanvasProject = {
|
||
id: string;
|
||
title: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
nodes: CanvasNodeData[];
|
||
connections: CanvasConnection[];
|
||
chatSessions: CanvasAssistantSession[];
|
||
activeChatId: string | null;
|
||
backgroundMode: "lines" | "dots" | "blank";
|
||
viewport: { x: number; y: number; k: number };
|
||
};
|
||
```
|
||
|
||
字段说明:
|
||
|
||
- `id`:画布项目 ID,当前前端生成。
|
||
- `title`:画布名称。
|
||
- `createdAt` / `updatedAt`:ISO 字符串。
|
||
- `nodes`:画布节点列表。
|
||
- `connections`:节点连线列表。
|
||
- `chatSessions`:右侧画布助手会话。
|
||
- `activeChatId`:当前选中的助手会话 ID。
|
||
- `backgroundMode`:画布背景模式。
|
||
- `viewport`:视口变换,`x/y` 是屏幕平移,`k` 是缩放比例。
|
||
|
||
## 节点结构
|
||
|
||
每个节点是一个 `CanvasNodeData`:
|
||
|
||
```ts
|
||
type CanvasNodeData = {
|
||
id: string;
|
||
type: "image" | "text" | "config" | "video";
|
||
title: string;
|
||
position: { x: number; y: number };
|
||
width: number;
|
||
height: number;
|
||
metadata?: CanvasNodeMetadata;
|
||
};
|
||
```
|
||
|
||
通用字段:
|
||
|
||
- `id`:节点 ID。
|
||
- `type`:节点类型,当前有图片、文本、生成配置、视频四类。
|
||
- `title`:节点标题。
|
||
- `position`:画布世界坐标,不是屏幕坐标。
|
||
- `width` / `height`:画布世界坐标下的节点尺寸。
|
||
- `metadata`:节点内容和业务状态。
|
||
|
||
`metadata` 当前常用字段:
|
||
|
||
```ts
|
||
type CanvasNodeMetadata = {
|
||
content?: string;
|
||
prompt?: string;
|
||
status?: "idle" | "success" | "loading" | "error";
|
||
errorDetails?: string;
|
||
fontSize?: number;
|
||
generationMode?: "text" | "image" | "video";
|
||
model?: string;
|
||
size?: string;
|
||
count?: number;
|
||
naturalWidth?: number;
|
||
naturalHeight?: number;
|
||
freeResize?: boolean;
|
||
isBatchRoot?: boolean;
|
||
batchRootId?: string;
|
||
batchChildIds?: string[];
|
||
primaryImageId?: string;
|
||
imageBatchExpanded?: boolean;
|
||
inputOrder?: string[];
|
||
storageKey?: string;
|
||
mimeType?: string;
|
||
bytes?: number;
|
||
};
|
||
```
|
||
|
||
不同节点的使用方式:
|
||
|
||
- 图片节点:`content` 是当前可展示的图片 URL,通常是 `blob:` URL;`storageKey` 指向本地图片 Blob;`naturalWidth/naturalHeight/bytes/mimeType` 保存原图信息。
|
||
- 视频节点:`content` 是当前可播放的视频 URL,通常是 `blob:` URL;`storageKey` 指向本地视频 Blob;`bytes/mimeType` 保存文件信息。
|
||
- 文本节点:`content` 保存文本内容;`fontSize` 保存字体大小;`prompt/status/errorDetails` 保存生成状态。
|
||
- 生成配置节点:`generationMode/model/size/count/inputOrder` 保存生成配置;`generationMode` 可选择文本、图片或视频;上游输入通过 `connections` 计算。
|
||
- 图片组节点:根节点用 `isBatchRoot/batchChildIds/primaryImageId/imageBatchExpanded` 记录批量生成结果;子图节点用 `batchRootId` 指回根节点。
|
||
|
||
## 连线结构
|
||
|
||
每条连线是一个 `CanvasConnection`:
|
||
|
||
```ts
|
||
type CanvasConnection = {
|
||
id: string;
|
||
fromNodeId: string;
|
||
toNodeId: string;
|
||
};
|
||
```
|
||
|
||
连线只保存节点 ID,不保存端口坐标。渲染时根据节点位置和尺寸计算路径。
|
||
|
||
删除节点时会同步删除以该节点为起点或终点的连线。删除图片组根节点时,会把对应子节点一起删除。
|
||
|
||
## 助手会话结构
|
||
|
||
助手会话保存在画布项目内:
|
||
|
||
```ts
|
||
type CanvasAssistantSession = {
|
||
id: string;
|
||
title: string;
|
||
messages: CanvasAssistantMessage[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
};
|
||
```
|
||
|
||
消息结构:
|
||
|
||
```ts
|
||
type CanvasAssistantMessage = {
|
||
id: string;
|
||
role: "user" | "assistant";
|
||
mode: "ask" | "image";
|
||
text: string;
|
||
isLoading?: boolean;
|
||
references?: CanvasAssistantReference[];
|
||
images?: CanvasAssistantImage[];
|
||
};
|
||
```
|
||
|
||
图片引用和助手生成图片也遵循同一套图片存储规则:
|
||
|
||
- `dataUrl` 字段当前可能是 `blob:` URL,也可能是旧数据中的 `data:image/...`。
|
||
- `storageKey` 存在时,以 `storageKey` 为准读取图片 Blob。
|
||
- 发送到 AI 接口前,如果接口需要 base64,会通过 `imageToDataUrl` 临时把 Blob URL 转成 data URL。
|
||
|
||
## 图片写入流程
|
||
|
||
所有新增图片应通过 `uploadImage(input)` 写入:
|
||
|
||
1. 传入 `Blob` 或 data URL。
|
||
2. 内部转成 `Blob`。
|
||
3. 生成 `storageKey`,格式为 `image:<id>`。
|
||
4. 把 Blob 写入 `image_files`。
|
||
5. 创建 `blob:` URL,并缓存在内存 `objectUrls`。
|
||
6. 读取图片宽高,返回:
|
||
|
||
```ts
|
||
type UploadedImage = {
|
||
url: string;
|
||
storageKey: string;
|
||
width: number;
|
||
height: number;
|
||
bytes: number;
|
||
mimeType: string;
|
||
};
|
||
```
|
||
|
||
图片节点会通过 `imageMetadata(image)` 写入:
|
||
|
||
```ts
|
||
{
|
||
content: image.url,
|
||
storageKey: image.storageKey,
|
||
status: "success",
|
||
naturalWidth: image.width,
|
||
naturalHeight: image.height,
|
||
bytes: image.bytes,
|
||
mimeType: image.mimeType
|
||
}
|
||
```
|
||
|
||
因此,`content` 只适合当前浏览器会话展示,不能作为长期文件标识;长期标识是 `storageKey`。
|
||
|
||
## 图片读取和旧数据迁移
|
||
|
||
打开画布时会执行图片补水:
|
||
|
||
- 如果图片节点有 `storageKey`,通过 `resolveImageUrl(storageKey, fallback)` 读取 Blob 并生成新的 `blob:` URL。
|
||
- 如果图片节点没有 `storageKey`,但 `content` 是旧的 `data:image/...`,会调用 `uploadImage(content)` 迁移到 `image_files`,并补上 `storageKey`。
|
||
- 助手消息里的引用图和生成图也会执行同类逻辑。
|
||
|
||
我的素材读取时也会做迁移:
|
||
|
||
- 有 `storageKey`:恢复 `coverUrl` 和 `data.dataUrl` 的可展示 URL。
|
||
- 无 `storageKey` 且保存了 base64:写入 `image_files`,然后更新素材里的 `storageKey`。
|
||
|
||
## 图片移除和清理
|
||
|
||
图片不是在删除节点时立即按节点逐张删除,而是做引用清理:
|
||
|
||
1. 删除节点、清空画布、删除画布、删除素材、删除助手会话时,会触发 `cleanupImages`。
|
||
2. `cleanupImages` 会收集当前仍被画布项目、素材和额外传入数据引用的所有 `storageKey`。
|
||
3. `cleanupUnusedImages` 遍历 `image_files` 中的全部图片。
|
||
4. 不在引用集合里的图片会被删除。
|
||
5. 删除时会同时 `URL.revokeObjectURL`,并从内存缓存 `objectUrls` 移除。
|
||
|
||
这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。
|