asd-v
v1.0.0
Published
ASD-V 数据可视化画布
Downloads
136
Maintainers
Readme
allsense-canvas 重构说明(src 引擎化设计)
AllSense Canvas:在不修改 core/ 的前提下,于 src/ 目录实现引擎化重构。目标是将“数据、事件、节点、渲染、动画、请求(网络)”等能力抽象为独立引擎与清晰接口,降低耦合,提升测试与演进效率。
重构目标
- 保持对外 API 简洁稳定(
CanvasApp门面),逐步替换内部实现为引擎化结构。 - 引擎化分层:数据引擎、事件引擎、节点引擎、渲染引擎、动画引擎、请求引擎(可按需补充线条引擎)。
- 强类型与安全:限制运行时代码字符串,建议沙箱/白名单。
- 高性能:图层化、增量刷新、离屏渲染与缓存。
- 可测试:单元/集成测试覆盖核心路径。
现有 core 架构摘要(参考对象)
根路径:core/src/
canvas/:主渲染与交互编排(canvas.ts)、图片层(canvasImage.ts)、模板与网格/标尺(canvasTemplate.ts)、放大镜与离屏(magnifierCanvas.ts、offscreen.ts)。pen/:模型与绘制(model.ts、render.ts)、文本/箭头/几何工具(text.ts、arrow.ts、utils.ts)。store/:全局状态与注册(store.ts、global.ts)。event/:事件模型与动作(event.ts)。diagrams/:内置图形与路径(包含svgPath.ts、各类形状、连线)。utils/:通用工具(克隆、颜色、对象、数学、URL、JetLinks 等网络适配)。- 其他:
options.ts、主题/消息/对话框/提示等辅助模块。
核心问题(重构关注):
- 职责集中在门面/大模块,渲染/交互/网络/事件强耦合,难以单测与增量演进。
render.ts体量巨大,计算与绘制交织,影响可维护性。- 运行时代码字符串(线条动画、socket 回调等)存在安全与类型风险。
引擎映射设计(src/engine)
本仓库已在 src/engine/ 下建立基础骨架与聚合入口:
src/engine/
data/ // 数据引擎:集中管理 pens、选择集、CRUD
event/ // 事件引擎:封装 EventBus,统一 on/off/emit
render/ // 渲染引擎:主绘制流程,后续接入 Registry 的绘制
node/ // 节点引擎:节点类图形的创建、更新、旋转、缩放与命中
line/ // 线条引擎(可选):线条创建、更新与命中(路径路由预留)
index.ts // 聚合导出顶层导出:src/index.ts 已包含 export * from './engine'。
1) 数据引擎(Data Engine)
- 核心职责:
- 管理
pens、selectedIds、paths、guides等数据; - 提供 CRUD 与批量操作;
- 与历史记录协作(
pushHistory/undo/redo由 HistoryService 维护,DataEngine 只专注数据)。
- 管理
- 对应 core:
store/store.ts+data.ts的数据模型。 - 接口建议:
init(store),add/update/remove/getById/list,select/selected。
- 迁移要点:ID→Pen 索引、
calculative运行态缓存收敛、选区与区域刷新协作。
2) 事件引擎(Event Engine)
- 核心职责:
- 封装
EventBus,提供统一on/off/emit; - 承接事件动作路由(后续可将
event.ts的动作表以服务方式实现)。
- 封装
- 对应 core:
store/store.ts中emitter与event/event.ts。 - 安全建议:事件动作涉及 JS 代码时引入沙箱/白名单;校验类型与数据范围。
3) 节点引擎(Node Engine)
- 核心职责:
- 节点类图形的创建/更新、旋转/缩放、命中判定;
- 锚点计算、文本布局(可与渲染引擎协商接口以分离计算与绘制)。
- 对应 core:
pen/model.ts、pen/utils.ts、rect/*、diagrams/*部分逻辑。 - 接口建议:
create/update/rotate/resize/hitTest(后续可加anchors(pen)、layoutText(pen))。
4) 线条引擎(Line Engine,建议加入)
- 核心职责:
- 线条几何(直线/折线/曲线);
- 路由与端点吸附;
- 箭头/标注/标签与命中;
- 与动画引擎协同(线条动画)。
- 对应 core:
diagrams/line/*、pen/arrow.ts、pen/utils.ts、svgPath.ts。 - 接口建议:
create/update/hitTest(后续:route(line)、anchors(line))。
5) 渲染引擎(Render Engine)
- 核心职责:
- 图层管理:模板层/底图层/主层/顶图层/图片层;
- 计算与绘制:
worldRect、Path2D/Canvas 二路绘制; - 增量刷新与区域重绘(PatchFlags);
- 离屏与放大镜;
- 选择框、锚点与覆盖层 UI。
- 对应 core:
canvas/*、pen/render.ts。 - 集成建议:与 RegistryService 协作,从注册表查询绘制函数;与 Node/Line 引擎分离几何计算。
6) 动画引擎(Animation Engine)
- 核心职责:
- 帧驱动(队列 +
requestAnimationFrame); - 线条动画注册与执行(函数或安全代码字符串);
- 与渲染引擎低耦合协作(只触发需要重绘的区域)。
- 帧驱动(队列 +
- 对应 core:
pen/render.ts的动画段落、lineAnimateDraws相关功能。 - 安全建议:尽量函数注册,若保留代码字符串,必须沙箱化并限定上下文。
7) 请求引擎(Request/Network Engine)
- 核心职责:
- 统一封装网络协议:HTTP/WebSocket/MQTT/SSE/SQL/IoT/JetLinks;
- 连接/订阅/心跳/重连/鉴权;
- 消息归一化(数据点映射到
setDatas/setValue(s)),与 BindingService 协作。
- 对应 core:
utils/jetLinks.ts、utils/url.ts、以及core.ts中网络相关方法。 - 接口建议:
init(store),connect(opts),disconnect(),subscribe(topic, handler),send(channel, payload)。 - 安全建议:Token/鉴权隔离;错误与超时统一上报;模板变量替换必须校验。
与 Services 的协作关系
RenderService调用RenderEngine.render(),并逐步将选择层/锚点绘制迁移到引擎内部;InteractionService将几何操作委托NodeEngine/LineEngine,自身只做事件编排与状态维护;HistoryService与DataEngine协作,统一历史入口;RegistryService作为绘制/锚点/动画注册中心,渲染/动画引擎按需查询;ThemeService/AnimationService/NetworkService/BindingService通过Store与事件引擎交互,保持低耦合。
迁移路线(建议增量)
- 注入并初始化五/六大引擎(含 LineEngine 可选),门面
CanvasApp保持对外 API 不变; - 迁移
services/render/*的选择层与锚点到RenderEngine; - 拆分
services/interaction/*的命中/旋转/缩放到NodeEngine/LineEngine; - 将 CRUD 与选区操作集中到
DataEngine,历史走HistoryService; - 收敛注册点到
RegistryService,绘制由RenderEngine查询执行; - 引入
Animation Engine的统一帧驱动与线条动画注册; - 引入
Request Engine统一网络协议与消息归一化,联动 Binding; - 单测覆盖:命中与变换、历史合并、增量渲染、事件派发、网络消息映射与错误路径。
风险与测试策略
- 风险:职责迁移期间渲染/交互耦合断裂,线条路由与动画回归;
- 缓解:先搭骨架与适配层,逐模块迁移;保持旧 API 的门面代理;
- 测试:
- 单测:Data/Node/Line 命中与变换、History 合并、Animation 帧时序;
- 集成:Render 层区域刷新与覆盖层 UI;Network 消息到绑定到渲染的闭环;
- 回归:示例工程作为验收清单。
接口草案(TypeScript 简版)
export interface IDataEngine {
init(store: Store): void;
add(pen: Pen): void; update(pen: Pen): void; remove(id: string): void;
getById(id: string): Pen | undefined; list(): Pen[];
select(ids: string[]): void; selected(): string[];
}
export interface IEventEngine {
init(store: Store): void;
on(type: string, handler: (...args: any[]) => void): void;
off(type: string, handler?: (...args: any[]) => void): void;
emit(type: string, payload?: any): void;
}
export interface INodeEngine {
init(store: Store): void;
create(pen: Pen): void; update(pen: Pen): void;
rotate(id: string, rad: number): void; resize(id: string, w: number, h: number): void;
hitTest(x: number, y: number): string | undefined;
}
export interface ILineEngine {
init(store: Store): void;
create(pen: Pen): void; update(pen: Pen): void;
hitTest(x: number, y: number): string | undefined;
}
export interface IRenderEngine {
// 在指定容器中初始化并创建基础画布(不建立观察器)
init(store: Store, holder?: HTMLElement): void;
// 创建并附着 Canvas 到容器,负责首帧渲染与自适应(推荐)
attach(store: Store, holder: HTMLElement, options?: Options): void;
// 获取当前使用的 Canvas
getCanvas(): HTMLCanvasElement | undefined;
// 渲染/尺寸更新与销毁
render(): void;
resize(): void;
updateContainerSize(opts: Partial<Options>): void;
destroy(): void;
}
export interface IRequestEngine {
init(store: Store): void;
connect(opts: any): Promise<void>;
disconnect(): Promise<void>;
subscribe(topic: string, handler: (msg: any) => void): void;
send(channel: string, payload: any): Promise<void>;
}使用示例(初始化)
import { createStore } from './src/store/Store';
import { DataEngine, EventEngine, RenderEngine, NodeEngine, LineEngine } from './src/engine';
const store = createStore();
const data = new DataEngine();
const events = new EventEngine();
const render = new RenderEngine();
const nodes = new NodeEngine();
const lines = new LineEngine();
data.init(store); events.init(store); render.attach(store, holderEl);
nodes.init(store); lines.init(store);
data.add({ id: 'rect1', name: 'rect', x: 100, y: 100, width: 120, height: 80 });
render.render();注意事项
- “根据 core 的架构,重构在
src里进行”:不要改动core/源码;仅参考其实现与 API 约定。 - SVG 拖拽命中需显式
width/height(仅d路径无法命中盒模型)。 - DOM 组件交互(上层/下层)通过
pointerEvents与domLayer/domZSplit控制; - 高级能力(旋转枢轴/吸附/对齐)建议由 Node/Render 引擎协作实现(几何接口 + 可视 UI)。
最新架构要点(职责与用法)
- CanvasApp:仅组装与调度;若传入
holder,委托RenderEngine.attach()完成创建、挂载与自适应;setOptions()内部转发到render.updateContainerSize();保留mount(holder)用于在外部控制的容器中初始化但不建立观察器的场景。 - RenderEngine:负责创建/附着
canvas、监听容器尺寸变化(ResizeObserver),在变更时更新canvas.width/height并调用resize()+render();订阅store.emitter.on('render')并通过requestAnimationFrame合并高频重绘(requestRender);提供destroy()清理观察器与移除canvas。 - 渲染层级与 DOM/Canvas 叠层:详见
src/engine/render/README.md。通过LayerManager统一管理分层根(z=2)与顶层覆盖 Canvas(z=3),每个zIndex一组,组内复用 Canvas 绘制该层的 Canvas 图元,同时承载该层的 DOM 图元。 - Pass 管线:内置按序执行的渲染 Pass(背景→网格→标尺→置底线→主图元→选择→悬停→调试),支持
registerPass扩展,Pass 类型包含canvas/dom/top三类,分别对应主/分组画布、分层根(DOM)、顶层覆盖画布。 - 事件驱动:图片加载、业务动作等通过
store.emitter.emit('render')触发;AnimationEngine周期性发出tick事件(当前渲染未直接订阅,后续动画更新可通过触发render合帧)。 - 图片与 SVG:
- 图片(
name: 'image')支持地址字段src/image/url,运行时缓存到calculative.img,onload/onerror触发render;跨域导出可在数据中设置crossOrigin: 'anonymous'。 - SVG(
name: 'svg'+d)使用calculative.path2d: Path2D缓存与ensureSvgBounds计算边界;明确ctx.fill(path, 'nonzero')以避免运行时重载歧义。
- 图片(
用法示例(直接使用 RenderEngine)
const store = createStore();
const render = new RenderEngine();
const holderEl = document.getElementById('app')!;
render.attach(store, holderEl, { background: '#fff', width: 800, height: 500 });
render.render();用法示例(使用 CanvasApp 门面)
const holder = document.getElementById('app')!;
const app = new CanvasApp(holder, { background: '#f8f9fb', width: 800, height: 500 });
app.node.createNode({ id: 'n1', x: 100, y: 80, width: 120, height: 60 });
app.render.render();
// 动态更新容器尺寸(由引擎观察器驱动到画布)
app.setOptions({ width: 900, height: 560 });说明:根据 core 架构,所有改造均在 src 完成;core/ 不做改动,仅作为参考。
图形类型要点(数据结构与渲染约束)
ImagePen:- 必填:
id、name: 'image'、定位与尺寸(x/y/width/height),以及图片地址之一(src/image/url)。 - 可选:
rotate、zIndex、crossOrigin; - 渲染:加载中显示占位(虚线矩形),加载完成自动重绘(
emit('render'))。
- 必填:
SvgPen:- 必填:
id、name: 'svg'、d路径;建议显式width/height以便命中与布局; - 渲染:
Path2D缓存与边界计算,填充采用'nonzero'规则;路径缓存类型不合法时自动重建。
- 必填:
如需进一步细化“CanvasApp 注入引擎的示例实现”和“引擎/服务依赖图”,可在本 README 中继续补充。
核心 → 引擎映射详表
为贯彻“根据 core 的架构,重构项目在 src 里面”的原则,以下将 core/src 的主要模块映射到 src 下的六大引擎(Data、Event、Render、Node、Line、Animation、Request)以及基础 Store/模型。
| Core 路径 | 引擎/模块(src) | 职责对应 | 迁移建议 |
| --- | --- | --- | --- |
| core/src/store | src/store/Store.ts、src/engine/data | 数据容器、历史与选择集 | 将历史完整策略迁移到 Store,CRUD 与选择统一由 DataEngine 管理 |
| core/src/data.ts | src/engine/data、src/models/types.ts | 数据模型与操作 | 将 Pen、Options、CanvasData 类型收敛到 models/types.ts;操作迁移至 DataEngine |
| core/src/event | src/engine/event | 事件总线与订阅 | 保留 on/off/emit 接口语义,统一暴露为 EventEngine |
| core/src/canvas | src/engine/render(协作 node/line) | 画布绘制、网格背景、标尺等 | 将绘制主循环并入 RenderEngine;命中/拾取由 NodeEngine/LineEngine 提供 |
| core/src/pen | src/engine/node、src/engine/line | 图元基础、节点/连线 | 依据图元类型拆分到节点与线引擎,通用几何与样式由 RenderEngine 使用 |
| core/src/point、core/src/rect | src/models/types.ts、node/line | 基础几何类型与计算 | 点与矩形类型迁移至 models/types.ts;相关几何算法进入 node/line 实现或共享工具 |
| core/src/options.ts | src/models/types.ts、src/store/Store.ts | 画布选项与样式 | 统一选项至 Options;Store.options 作为引擎消费入口 |
| core/src/animation | src/engine/animation | 动画循环与时间线 | 将 tick、关键帧与过渡迁移至动画引擎,广播 tick 事件驱动渲染 |
| core/src/scroll | src/engine/render、src/store/Store.ts、src/engine/event | 平移缩放、滚轮拖拽 | 视图变换由 RenderEngine 与 Store.data.scale/origin 管理;交互事件通过 EventEngine |
| core/src/diagrams | src/engine/node(扩展)、src/engine/render | 特定业务图元与绘制 | 作为节点引擎的扩展模块注册;渲染细节在 RenderEngine 插件化 |
| core/src/dialog、core/src/popconfirm、core/src/tooltip、core/src/title | src/engine/render(DOM层)或独立 UI 服务 | 浮层与 UI 组件 | 走 DOM 叠层(Pen.domLayer/Options.domZSplit)或抽象为独立 UI 服务对接 |
| core/src/map | src/engine/render(扩展)+ src/engine/request | 地图渲染与数据拉取 | 将瓦片/矢量请求适配到 RequestEngine,绘制层扩展在 RenderEngine |
| core/src/message | src/engine/event | 应用级消息 | 统一并入事件总线;必要时区分频道/命名空间 |
| core/src/theme.ts | src/engine/render、src/store/Store.ts | 主题与样式变量 | 主题选项进入 Store.options;渲染消费在 RenderEngine |
| core/src/utils | 建议新增 src/shared/utils | 通用工具方法 | 按需迁移到共享工具,避免引擎间重复实现 |
| core/index.ts | src/engine/index.ts、src/index.ts | 聚合导出 | 顶层导出由 src/index.ts 提供;引擎聚合在 src/engine/index.ts |
补充说明:
- DOM 叠层策略:若图元需要在 Canvas 上方/下方渲染,使用
Pen.domLayer与Options.domZSplit协调;相关实现位于RenderEngine。 - 历史与撤销重做:完整策略(合并、分组、跨引擎操作)在迁移时应统一落入
Store与DataEngine。当前为最小实现。 - 选择与命中:选择集在
DataEngine;命中测试由NodeEngine/LineEngine提供,渲染在RenderEngine完成视觉反馈。
引擎注入与挂载示例
以下示例展示如何在应用中注入引擎并完成挂载与渲染、交互、动画联动:
import { CanvasApp } from './src';
// 1) 初始化 App 与引擎
const app = new CanvasApp();
// 可选:配置画布选项(背景、网格、对齐等)
app.store.options = {
background: '#ffffff',
grid: true,
gridSize: 20,
domZSplit: 1000,
};
// 2) 挂载到 DOM 中的 Canvas 与可选 Holder(用于 DOM 叠层)
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const holder = document.getElementById('holder') as HTMLElement | null;
app.mount(canvas, holder || undefined);
// 3) 数据与图元操作:创建节点与线
app.node.createNode({ id: 'n1', x: 100, y: 80, width: 120, height: 60, name: '矩形' });
app.line.createLine({ id: 'l1', x1: 100, y1: 80, x2: 240, y2: 140, name: '连线' });
// 4) 渲染一次
app.render.render();
// 5) 事件与动画联动:每帧重绘(可替换为脏矩形策略)
app.event.on('tick', () => {
app.render.render();
});
app.animation.start();
// 6) 命中测试(示例:鼠标点选)
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const hitNodeId = app.node.hitTestPoint(x, y);
const hitLineId = app.line.hitTest(x, y);
if (hitNodeId) app.data.select([hitNodeId]);
else if (hitLineId) app.data.select([hitLineId]);
else app.data.select([]);
});
// 7) 网络请求(示例)
async function loadPens() {
const res = await app.request.get('/api/pens');
if (res.ok) {
const pens = await res.json();
for (const p of pens) {
if (p.x1 !== undefined) app.line.createLine(p);
else app.node.createNode(p);
}
app.render.render();
}
}依赖注入扩展示例(替换默认渲染引擎)
如需替换某个引擎(例如自定义渲染),可在构造后进行注入并重新初始化:
import { CanvasApp } from './src';
import type { IRenderEngine } from './src/engine/render';
class CustomRender implements IRenderEngine {
private base!: IRenderEngine; // 可复用默认实现作为降级
attach(store, holder, options?) {
// 在指定容器中创建并附着画布,建立自适应与上下文,订阅 store/event
}
render() {
// 自定义绘制流程(网格、节点、线、DOM 叠层)
}
}
const app = new CanvasApp();
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const holder = document.getElementById('holder') as HTMLElement;
app.render = new CustomRender();
app.render.attach(app.store, holder);
app.event.on('tick', () => app.render.render());
app.animation.start();以上映射与示例为建立统一的引擎层入口,后续迁移时请按模块逐步替换 core 中对应实现,确保:
- 数据模型与历史在
Store/DataEngine统一; - 事件在
EventEngine统一; - 渲染在
RenderEngine统一,命中/几何在Node/LineEngine; - 动画与请求分别在
Animation/RequestEngine; - 所有改造均在
src目录下进行,以便与现有服务协作并减少耦合。
