讲讲 Suika 图形编辑器项目的目录结构

443 阅读17分钟

大家好,我是前端西瓜哥。

最近有人想学习我开源的 suika 图形编辑器,问我下面的项目结构哪些文件对应什么功能,文件太多看不太懂。

那我就在这里简单展开讲讲,内容很多,建议跳着看,或者不看直接收藏了,收藏就是会了。

suika 是我维护的一个开源图形编辑器项目,目前 star 数 700+,欢迎 star。

github 地址:

github.com/F-star/suik…

线上体验:

blog.fstars.wang/app/suika/

总览

suika 项目使用了 monorepo,基于 pnpm 实现,将代码拆分到不同包下。

注意目录结构为文章发布时的状态,之后可能会变更。

monorepo 的包在 packages 和 apps 目录下。

图片

首先看 packages 目录。

packages
├── common
├── components
├── core
├── geo
└── icons

  • common:@suika/common 包放一些公共方法,通常都是些工具方法,其他所有包都可以引用它。

  • geo:@suika/geo 包都是一些几何算法。

  • core:@suika/core 包为编辑器内核,编辑器的业务逻辑都在这里。

  • icon:@suika/icon 包为一些图标 React 组件,定位类似 antd 的 @ant-design/icon

  • component:@suika/component 包为一些公共组件。

然后是 apps 目录。

apps
├── docs
├── suika
└── suika-multiplayer

apps 目录放的是业务的包,就是最终的构建产物,一个 application,通常不会被其他包引用。

  • suika:@suika/suika 包,将编辑器内核和 UI 组合一起,得到一个单机图形编辑器。

  • suika-multiplayer:@suika/multiplayer 包,其实就是 @suika/suika 拷贝过来,然后加了一点多人协同功能,后端代码在另一个项目。

  • docs:@suika/docs 包,suika 的文档,使用了 vitepress,目前还没有内容。

common

@suika/common 放一些公共的方法,可以被其他包引用的。

基本都是些工具方法。

packages/common/src
├── array_util.ts     # 一些数组方法
├── color.ts
├── common.ts
├── event_emitter.ts  # 发布订阅类
├── index.ts
├── lodash.ts
└── vite-env.d.ts

没有什么特别的,简单说一些。

其中 event_emitter.ts 文件下的 EventEmitter 类用来做发布订阅模式,很多地方都会用到。另外,它是类型安全,这样不用害怕写错事件名了。

lodash.ts 就是把少量用到的 lodash 方法导入然后重新导出。

geo

@suika/geo 下是一些平面几何算法的方法,根据不同的图形,划分到不同的文件下。

packages/geo/src/geo
├── constant.ts            # 一些常量
├── geo_angle.ts           # 角
├── geo_bezier.ts          # 三阶贝塞尔
├── geo_bezier_class.ts    # 三阶贝塞尔(类形式)
├── geo_box.ts             # 盒(通常用于包围盒计算)
├── geo_circle.ts          # 圆
├── geo_ellipse.ts         # 椭圆
├── geo_line.ts            # 线
├── geo_matrix.ts          # 矩阵
├── geo_matrix_class.ts    # 矩阵(类形式)
├── geo_path.ts            # 路径
├── geo_path_class.ts      # 路径(类形式)
├── geo_point.ts           # 点
├── geo_polygon.ts         # 多边形
├── geo_rect.ts            # 矩形
├── geo_resize_line.ts     # 缩放线
├── geo_resize_rect
│   ├── geo_resize_rect.ts # 缩放矩形(用于缩放图形)
│   └── index.ts
├── geo_star.ts            # 星形
├── geo_text.ts            # 文本
└── index.ts

举个例子,geo_rect 是矩形相关的几何方法,像是两个矩形是否碰撞,是否为包含关系。

geo_beziser 是和贝赛尔曲线相关的几何算法,如点到曲线的最近点(投影)、求贝赛尔的包围盒等等。

大部分都是纯函数,即没有使用类似 GeoRect 这种的类的形式。

这样是为了更好的原子化,其他模块通常只是简单地调用一个方法,没有必要专门实例化,同时我们可以随意组合这些方法。

但还是有些是需要用类的形式的

比如矩阵类 Matrix,这样可以进行链式操作组合,参与各种计算,可读性更好。当然我也提供纯函数的形式。顺带一提这个 Matrix 是从 pixi.js 里面拷贝过来,进行了一些修改。

还有一个是贝赛尔类 GeoBezier,因为需要用到缓存,比如图形拾取时可能需要频繁求最近点时,就需要一个查找表 lut 提前算好曲线的一些点的位置。

当做新需求用到一些新的几何方法,如果是 比较底层的通用算法,建议放到 geo 包下。如果觉得通用性不强,可不放到 geo 下,即放到用到这个方法的文件附近。后来如果有更多地方用到这个局部几何方法,可以再放到 geo包下。

core

@suika/core 是图形编辑器内核,是整个图形编辑器项目的核心,管理着编辑器的所有业务逻辑,其下有大量的模块。这些模块经过合理的设计,能良好地组织工作在一起。

@suika/suika 不允许有任何 UI 相关的逻辑。该包会提供必要的事件给 UI 层,UI 层逻辑只能在 @suika/suika 里实现。

packages/core/src/
├── Img_manager.ts
├── canvas_dragger.ts
├── clipboard.ts
├── commands                  # 历史记录命令相关
│   ├── add_graphs.ts
│   ├── command_manager.ts
│   ├── index.ts
│   ├── macro.ts
│   ├── reparent.ts
│   ├── set_elements_attrs.ts
│   ├── type.ts
│   └── update_graphics_attrs_cmd.ts
├── constant.ts
├── control_handle_manager    # 控制点管理
│   ├── control_handle.ts
│   ├── control_handle_manager.ts
│   ├── index.ts
│   ├── type.ts
│   └── util.ts
├── cursor_manager            # 光标管理
│   ├── cursor-icons
│   │   ├── suika-cursor-default.png
│   │   ├── ...
│   ├── cursor.css
│   ├── cursor_manager.ts
│   ├── index.ts
│   └── util.ts
├── editor.ts                 # 编辑器类(根类)
├── export_manager.ts
├── graphics                  # 图形类
│   ├── rect.ts
│   ├── ...
│   └── utils.ts
├── grid.ts                   # 网格
├── host_event_manager        # 原生事件的封装和注册
│   ├── command_key_binding.ts
│   ├── host_event_manager.ts
│   ├── index.ts
│   ├── mouse_event_manager.ts
│   └── move_graphs_key_binding.ts
├── index.ts
├── key_binding_manager.ts   # 键盘事件封装
├── paint.ts                
├── path_editor              # 路径编辑器(切换为钢笔工具时触发)
│   ├── index.ts
│   ├── path_editor.ts
│   ├── selected_control.ts
│   └── type.ts
├── perf_monitor.ts
├── ref_line.ts              # 图形参考线吸附和绘制
├── ruler.ts                 # 标尺
├── scene
│   └── scene_graph.ts       # 场景树渲染
├── selected_box.ts          # 选中框
├── selected_elements.ts     # 选中图形管理
├── service                  # 操作图形并保存到历史记录的服务
│   ├── align_and_record.ts
│   ├── ...
├── setting.ts               # 全局设置(类似环境变量)
├── snap.ts                  # 快捷吸附方法(目前没有内容)
├── text                     # 文本编辑器
│   ├── range_manager.ts
│   └── text_editor.ts
├── to_svg.ts
├── tools                    # 工具和工具管理类
│   ├── index.ts
│   ├── tool_drag_canvas.ts
│   ├── tool_draw_ellipse.ts
│   ├── ...
├── transaction.ts           # “事务”:历史记录操作的高级封装(常用)
├── type.ts
├── utils                    # 杂七杂八的工具方法
│   ├── canvas.ts
│   ├── common.ts
│   ├── frame.ts
│   ├── geo.ts
│   ├── group.ts
│   ├── index.ts
│   └── raf_throttle.ts
├── viewport_manager.ts      # 画布视口
├── vite-env.d.ts
└── zoom_manager.ts          # 画布缩放

可以看到,模块很多,这里有 122 个文件。随着功能越来越多,文件只会又来越多。

下面简单介绍一下这些模块。

基础模块

  • editor.ts:SuikaEditor 类,基本所有模块都会放在 SuikaEditor 的下,同时也会注入到其他模块下,模块可以通过 editor 这一桥梁,访问另一个模块。虽然不符合最小知识原则,但胜在灵活。一些 常用的方法可以放到或代理到 editor 下,比如场景坐标系到视口坐标系的坐标转换。

  • setting.tsSetting 类存放编辑器的所有的设置,类似环境变量。设置项是拍平的,这样方便读写。设置项需要通过 setting.get('snapToObject') 的方式来获取,这样你才知道你在使用设置,写也同样道理。使用了 TypeScript 确保类型安全。

事件相关

  • key_binding_manager.tsKeyBindingManager 类对浏览器的键盘事件进行了封装。之后编辑器会完全使用这个类,不再直接使用原生事件,方便统一管理,比如高优先级的快捷键注册会阻断低优先级的注册。

  • command_key_binding.tsCommandKeyBinding 类只是绑定一些快捷键对应的命令。在编辑器初始化的时候,调用前面的 KeyBindManager 类下的方法绑定一些命令快捷键,比如 delete 会注册删除图形的逻辑。

  • mouse_event_manager.tsMouseEventManager 类是对鼠标事件做了一层封装,提供了额外的高级功能,如拖拽事件(drag)、连击事件(comboClick)。对原声的事件对象也做了封装,可以直接拿到光标所在的世界坐标、是否在画布区域外等重要的信息。

  • move_graphs_key_binding.tsMoveGraphsKeyBinding 类处理通过快捷键移动图形的逻辑。按住方向键会触发多次的键盘事件,每个都生成一个历史记录是不必要的,对此我们通过防抖来处理。

  • host_event_manager.tsHostEventManager 类负责封装一些泛用的原生事件,包括监听 Shift、Alt、Space、Command 等按键的按下释放事件,滚轮事件(wheel),右键菜单事件(contextmenu,提供给 UI 层显示右键菜单)

图形相关

graphics 目录下,全都是图形类。

packages/core/src/graphics
├── canvas.ts             # 画布类(对应 figme 的 page)
├── document.ts           # 文档类,根节点
├── ellipse.ts            # 椭圆类
├── frame
│   ├── frame.ts          # 画板或组(如果 attrs.resizeToFit 是 true)
│   └── index.ts
├── graphics
│   ├── graphics.ts       # 图形基类
│   ├── graphics_attrs.ts # 图形属性 interface
│   └── index.ts
├── graphics_manger.ts    # 图形存储管理类(id 到图形的映射,方便查找)
├── index.ts
├── line.ts               # 线类
├── path
│   ├── index.ts
│   ├── path.ts           # 路径类
│   └── type.ts
├── rect.ts               # 矩形类
├── regular_polygon.ts    # 正多边形
├── star.ts               # 星形
├── text.ts               # 文本
├── type.ts
└── utils.ts

具体哪个文件保存的是什么图形类,直接看上面的注释就好了。

它们会组合成一棵图形树,然后渲染在画布上。

需要注意的是,图形类和图形库的图形类是不一样的,叫实体类(entity)会更好。它们是 业务上的图形,不完全对应渲染上的图形对象,因为一个实体可能需要多个渲染图形的组合,具体看需求。

图形类的 attrs 属性为图形要进行持久化的属性。图形类通常会有非常多的方法,随着新需求还会越来越多,比如如果加了个新需求,需要支持导出为 png,就要再补上一个 toPng 之类的方法。

这些图形类其实更好再封装成一个 momorepo 包,防止污染,比如把 editor 往图形类里面塞,这个是不对的,这样是依赖具体的类了。

工具相关

tools 目录下都是工具类。

packages/core/src/tools
├── index.ts
├── tool_drag_canvas.ts               # 拖拽画布工具
├── tool_draw_ellipse.ts              # 绘制椭圆工具
├── tool_draw_frame.ts                # 绘制画板工具
├── tool_draw_graphics.ts             # 绘制图形工具基类(抽象类)
├── tool_draw_img.ts                  # 添加图片工具
├── tool_draw_line.ts                 # 绘制线条工具
├── tool_draw_path.ts                 # 绘制路径工具(钢笔工具)
├── tool_draw_rect.ts                 # 绘制矩形工具
├── tool_draw_regular_polygon.ts      # 绘制正多边形工具
├── tool_draw_star.ts                 # 绘制星形工具
├── tool_draw_text.ts                 # 绘制文本工具
├── tool_manager.ts                   # 工具管理类
├── tool_path_select
│   ├── index.ts
│   ├── tool_path_select.ts           # 路径选中工具(选中锚点、控制点等)
│   ├── tool_path_select_move.ts      # 路径移动工具(移动锚点、控制点等)
│   └── tool_path_select_selection.ts # 路径选区工具
├── tool_pencil.ts                    # 铅笔工具
├── tool_select
│   ├── index.ts
│   ├── tool_select.ts                # 选择工具
│   ├── tool_select_move.ts           # 选择工具下的移动图形子工具 
│   ├── tool_select_resize.ts         # 缩放图形子工具
│   ├── tool_select_rotation.ts       # 旋转图形子工具
│   ├── tool_select_selection.ts      # 绘制选区子工具
│   └── utils.ts
└── type.ts

编辑器永远会有一个工具会被激活,这个工具会监听各种事件,尤其是鼠标事件,执行对应的逻辑。

  • tool_manager.tsToolManager 类用于管理工具,将监听的事件传递给实现了工具接口的对象。工具类需要实现工具接口,然后注册到 ToolManager 下。

  • 其他的工具类见上面注释。对于复杂的工具类,需要用策略模式进行进一步的拆分为子工具类,以隔离复杂度,避免大量的分支语句。

历史记录相关

commands 目录都是历史记录相关的文件,使用了命令模式。

packages/core/src/commands
├── add_graphs.ts                 # 新增图形命令
├── command_manager.ts            # 命令管理类
├── index.ts
├── macro.ts                      # 宏命令
├── reparent.ts                   # 更改父节点命令
├── set_elements_attrs.ts         # 设置
├── type.ts
└── update_graphics_attrs_cmd.ts  # 更新图形命令(万能命令,支持增删改)

  • command_manager.tsCommandManager 类通过撤销栈和重做栈维护历史记录

后面组的出现,导致大量父子元素会发生联动更新,每个命令都要专门处理太麻烦,以及协同的数据同步。

所以现在基本要用更高层级的 Transaction 来替代原来的各种有具体语义的命令了。

  • transaction.ts (不在 commands 目录下):Transaction 类是在 UpdateGraphicsAttrsCmd 类做了更高级的封装,并被广泛使用。比如移动图形时,会先记录好图形修改前的属性(recordOld 方法),然后更新图形,记录更新后的属性(update 方法),处理组导致的父子联动属性更改(updateParentSize 方法),缩放完成后提交更改(commit 方法)。

路径编辑器

路径编辑器,其实就是钢笔工具,会临时接管图形编辑器的部分事件,关闭图形编辑器的一些功能,让图形编辑器变成 “路径编辑器”。

packages/core/src/path_editor
├── index.ts
├── path_editor.ts       # 路径编辑器类
├── selected_control.ts  # 控制点选中类,维护路径中被选中的锚点、控制点等
└── type.ts

路径编辑器的核心逻辑其实在 tool_draw_path.tstool_path_select 下。

service

service 目录放的是一些 "公共服务" 的逻辑,大多是更新图形属性相关的业务。

packages/core/src/service
├── align_and_record.ts          # 图形对齐,并记录(记录是指保存到历史记录)
├── arrange_and_record.ts        # 图形排列,并记录
├── export_service.ts            # 导出服务
├── flip_and_record.ts           # 翻转图形,并记录
├── group_and_record.ts          # 对图形编组,并记录
├── import_service.ts            # 导入服务
├── index.ts
├── mutate_graphs_and_record.ts  # 批量修改图形的属性(通常是属性面板会用到)
├── remove_service.ts            # 删除图形
└── ungroup_and_record.ts        # 对组进行解组,并记录

文本相关

文本编辑器,类似路径编辑器,也会临时接管图形编辑器的事件,以及关闭图形编辑器的一些功能。

packages/core/src/text
├── range_manager.ts  # 文本选区管理
└── text_editor.ts    # 文本编辑器

工具方法

utils 目录下都是些工具方法。

packages/core/src/utils
├── canvas.ts        # canvas 2d 的一些方法,比如绘制 x。
├── common.ts        # 普通工具方法
├── frame.ts         # 画板图形用到的方法
├── geo.ts           # 一些不太泛用的几何方法
├── group.ts         # 编组相关
├── index.ts
└── raf_throttle.ts  # 动画帧回调函数

其他

最后是一些比较单一的模块。

  • scene_graph.tsSceneGraph 是场景树类,其中 render 方法负责触发图形场景树的渲染,渲染图形、控制点、选中框、网格等在画布上的所有物体。

  • cursor_manager.tsCursorManger 类负责维护编辑器的光标,支持设置自定义光标(比如带旋转角度的 resize 光标),设置当前光标。

  • control_handle_manager.tsControlHandleManager 控制点管理类,管理选中图形时显示的控制点。

  • control_handle.tsControlHandle 控制点类,维护控制点如何渲染、如何被 hitTest、是否可见等逻辑

  • canvas_dragger.tsCanvasDragger 画布拖拽类。因为触发画布拖拽的方式太多(抓手工具、按住空格、按住滚轮),所以专门抽一个类出来。

  • clipboard.tsClipboardManager 剪贴板管理类,用于管理图形的复制粘贴逻辑。复制图形会将数据保存在操作系统剪贴板中,粘贴的时候会读取。这样可以实现跨图纸粘贴。

  • grid.tsGrid 类负责绘制网格

  • Img_manager.tsImgManager 图片管理类

  • paint.ts:图形的填充和描边用到的 paint(纯色、图形) 的 interface 定义,和一些默认值等。

  • perf_monitor:性能监控用的,基本没开过,开始后会有一个方形的性能监控。

  • ref_line.tsRefLine 参考线类,负责计算图形吸附的偏移值,以及吸附后参考线渲染。算法和渲染感觉可以分离一下。

  • ruler.tsRuler 类负责计算和渲染标尺

  • selected_box.tsSelectedBox 类主要负责渲染选中框、宽高提示

  • selected_elements.tsSelectedElements 类维护选中的图形,以及 hover 的图形。hover 的图形感觉可以单独抽到另一个类里,比较好?

  • snap.ts:一些封装了一些业务的吸附方法,目前只有网格吸附;

  • to_svg.ts:“复制为 SVG” 功能用到的方法;

  • viewport_manager.tsViewportManager 视口管理类负责维护编辑器的视口,缩放浏览器窗口时要通知它进行更新。

  • zoom_manager.tsZoomManager 类管理画布缩放值(zoom),支持适应画布,适应选中图形等。ViewportManager 和 ZoomManager 类职责可以考虑合并为一个 Camera 类。

icons

@suika/icon包放了一堆 React 图标组件。

图标来自:www.figma.com/community/f…

packages/icons/src
├── icons
│   ├── add-outlined.tsx
│   ├── align
│   │   ├── align-bottom.tsx
│   │   ├── align-h-center.tsx
│   │   ├── align-left.tsx
│   │   ├── align-right.tsx
│   │   ├── align-top.tsx
│   │   ├── align-v-center.tsx
│   │   └── index.tsx
│   ├── arrow-down-outlined.tsx
│   ├── check-outlined.tsx
│   ├── close-outlined.tsx
│   ├── hide-outlined.tsx
│   ├── i18n-outlined.tsx
│   ├── index.ts
│   ├── line-width-outlined.tsx
│   ├── lock-filled.tsx
│   ├── pen-outlined.tsx
│   ├── pencil-outlined.tsx
│   ├── point-solid.tsx
│   ├── remove-outlined.tsx
│   ├── right-outlined.tsx
│   ├── show-outlined.tsx
│   ├── small-caret-down-solid.tsx
│   ├── social
│   │   ├── github-outlined.tsx
│   │   └── index.ts
│   ├── tool
│   │   ├── ellipse-outlined.tsx
│   │   ├── frame-outline.tsx
│   │   ├── hand-outlined.tsx
│   │   ├── image-outlined.tsx
│   │   ├── index.ts
│   │   ├── line-outlined.tsx
│   │   ├── menu-outlined.tsx
│   │   ├── polygon-outlined.tsx
│   │   ├── rect-outlined.tsx
│   │   ├── select-outlined.tsx
│   │   ├── star-outlined.tsx
│   │   └── text-filled.tsx
│   └── unlock-filled.tsx
├── index.ts
└── vite-env.d.ts

components

@suika/components 是一个 React 组件库。

目前组件非常少,组件支持的属性也很少。

组件库要做好还是很花费精力的,目前个人并没有太多精力搞这个,影响我开发进度了。

最好还是用成熟的第三方组件库,比如 AntD。

packages/components/src
├── components
│   ├── button         # 按钮组件
│   │   ├── button.scss
│   │   ├── button.tsx
│   │   └── index.ts
│   ├── dropdown       # 下拉选择框
│   │   ├── dropdown-item
│   │   │   ├── dropdown-item.scss
│   │   │   ├── dropdown-item.tsx
│   │   │   └── index.ts
│   │   ├── dropdown.scss
│   │   ├── dropdown.stories.tsx
│   │   ├── dropdown.tsx
│   │   ├── index.ts
│   │   └── type.ts
│   ├── icon-button    # 图标按钮
│   │   ├── icon-button.scss
│   │   ├── icon-button.tsx
│   │   └── index.ts
│   ├── popover        # popover
│   │   ├── index.ts
│   │   ├── popover.scss
│   │   ├── popover.stories.tsx
│   │   └── popover.tsx
│   └── select         # 选择器
│       ├── index.ts
│       ├── select.scss
│       ├── select.stories.tsx
│       └── select.tsx
├── index.ts
└── vite-env.d.ts

suika

@suika/suika 是图形编辑器的最终产品包,其下构建的文件可直接进行部署。

@suika/suika 包使用了 React 组件,主要都是一些 UI 层的逻辑,如属性面板、右键菜单、工具栏。

@suika/core 下的图形编辑器类 SuikaEditor 会实例化挂载到一个 Canvas DOM 元素上,然后会把 SuikeEditor 通过 context 的方式透传给所有需要的组件

apps/suika/src
├── App.css
├── App.tsx
├── components
│   ├── Cards
│   │   ├── AlignCard                 # 对齐操作面板卡片组件
│   │   │   ├── AlignCard.scss
│   │   │   ├── AlignCard.tsx
│   │   │   └── index.ts
│   │   ├── BaseCard                  # 卡片基类组件
│   │   │   ├── index.tsx
│   │   │   └── style.scss
│   │   ├── ElementsInfoCard          # 图形属性卡片组件
│   │   │   ├── ElementsInfoCard.tsx
│   │   │   ├── index.tsx
│   │   │   └── style.scss
│   │   ├── FillCard                  # 图形填充卡片组件
│   │   │   ├── FillCard.tsx
│   │   │   └── index.ts
│   │   ├── LayerInfoCard             # 图形信息卡片(透明度)
│   │   │   ├── LayerInfoCard.scss
│   │   │   ├── LayerInfoCard.tsx
│   │   │   └── index.ts
│   │   ├── PaintCard                 # 颜色卡片基类组件
│   │   │   ├── PaintCard.scss
│   │   │   ├── PaintCard.tsx
│   │   │   └── index.tsx
│   │   └── StrokeCard                # 描边卡片组件
│   │       ├── StrokeCard.tsx
│   │       └── index.ts
│   ├── ColorPicker                   # 颜色相关
│   │   ├── ImagePicker               # 图片选择器
│   │   │   ├── ImagePicker.scss
│   │   │   ├── ImagePicker.tsx
│   │   │   └── index.ts
│   │   ├── PaintPicker               # paint 选择器(图片 + 纯色)
│   │   │   ├── PaintPicker.scss
│   │   │   ├── PaintPicker.tsx
│   │   │   └── index.ts
│   │   └── SolidPicker               # 纯色选择器
│   │       ├── SolidPicker.tsx
│   │       └── index.ts
│   ├── ContextMenu                   # 右键菜单组件
│   │   ├── ContextMenu.scss
│   │   ├── ContextMenu.tsx
│   │   ├── components
│   │   │   ├── ContextMenuItem.scss
│   │   │   ├── ContextMenuItem.tsx
│   │   │   ├── ContextMenuSep.scss
│   │   │   └── ContextMenuSep.tsx
│   │   └── index.tsx
│   ├── DebugPanel                    # debug 面板
│   │   ├── DebugPanel.tsx
│   │   └── index.ts
│   ├── Editor.scss
│   ├── Editor.tsx                    # 编辑器组件,初始化SuikaEditor的地方
│   ├── Header
│   │   ├── Header.scss
│   │   ├── Header.tsx                # 顶部栏
│   │   ├── components
│   │   │   ├── Title                 # 标题
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.scss
│   │   │   └── Toolbar
│   │   │       ├── Toolbar.scss
│   │   │       ├── Toolbar.tsx       # 工具栏
│   │   │       ├── components
│   │   │       │   └── ToolBtn       # 工具栏按钮
│   │   │       │       ├── ToolBtn.scss
│   │   │       │       ├── ToolBtn.tsx
│   │   │       │       └── index.ts
│   │   │       ├── index.ts
│   │   │       └── menu              # 左上角的下拉菜单
│   │   │           ├── Menu.tsx
│   │   │           ├── index.ts
│   │   │           └── menu.scss
│   │   └── index.ts
│   ├── InfoPanel                     # 右侧整个信息面板
│   │   ├── InfoPanel.tsx
│   │   ├── index.tsx
│   │   └── style.scss
│   ├── LayerPanel                    # 左侧图层树组件
│   │   ├── LayerPanel.scss
│   │   ├── LayerPanel.tsx
│   │   ├── LayerTree
│   │   │   ├── LayerIcon.tsx
│   │   │   ├── LayerItem.scss
│   │   │   ├── LayerItem.tsx
│   │   │   ├── LayerTree.tsx
│   │   │   ├── index.ts
│   │   │   └── type.ts
│   │   └── index.ts
│   ├── LocaleSelector                # 语言选择器(国际化)
│   │   ├── LocaleSelector.scss
│   │   ├── LocaleSelector.tsx
│   │   └── index.ts
│   ├── ZoomActions                   # 右上角的 zoom 相关下拉框
│   │   ├── ZoomActions.scss
│   │   ├── ZoomActions.tsx
│   │   ├── components
│   │   │   ├── ActionItem
│   │   │   │   ├── ActionItem.scss
│   │   │   │   ├── ActionItem.tsx
│   │   │   │   └── index.ts
│   │   │   └── ZoomInput
│   │   │       ├── ZoomInput.tsx
│   │   │       ├── index.ts
│   │   │       └── style.scss
│   │   └── index.ts
│   └── input                       # 输入框相关
│       ├── ColorHexInput.tsx       # 颜色值输入框
│       ├── CustomRuleInput         # 自定义规则输入框(基类)
│       │   ├── index.tsx
│       │   └── style.scss
│       ├── NumberInput.tsx         # 数字输入框
│       └── PercentInput.tsx        # 百分比输入框 
├── constant.ts
├── context.ts
├── events.ts                       # 事件(目前只有国际化语言更改事件)
├── global.d.ts
├── index.css
├── index.tsx
├── locale                          # 国际化文案
│   ├── en.json
│   ├── index.ts
│   └── zh.json
├── store                           # 自动保存数据到本地方法
│   └── auto-save-graphs.ts
└── vite-env.d.ts

suika-multiplayer

@suika/suika-multiplayer 包是 @suika/suika 复制过来,加上多人协同的逻辑。

这里只介绍不一样的地方,见下方注释。

apps/suika-multiplayer/src
├── ...
├── api-service                   # 放了一些后端接口
│   ├── api-config.ts
│   ├── api-service.ts
│   └── index.ts
├── components
│   ├── MultiCursorsView          # 多光标处理
│   │   ├── MultiCursorsView.scss
│   │   ├── MultiCursorsView.tsx
│   │   ├── index.ts
│   │   └── utils.ts
│   └── ...
├── store   
│   ├── join-room.ts              # 连接文档 id 对应的房间
│   └── y-suika.ts                # 协同数据同步逻辑(使用了 y.js)
└── ...

其他

看一下项目根目录下的一些文件。

.
├── Dockerfile
├── LICENSE
├── README.md
├── README_zh.md
├── apps
├── jest.config.js
├── nginx
├── node_modules
├── package.json
├── packages
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── screenshot.png
├── scripts
├── tsconfig.json
└── tsconfig.node.json

工程化相关。

  • .eslintrc.json:ESLint 配置。

  • packages.json:除了基础的信息,还有 prettier 的配置、lint-staged 配置、commitLint 配置。

  • .husky:使用了 husky(基于 git hook) 进行 commit message 的检验、以及格式化。

  • nginx 目录和 Dockerfile 文件没啥用,请忽略,它们和另一个后端项目有关,方便 Docker 快速启动用的。

结尾

基本简单过了一遍,希望对你了解 suika 图形编辑器项目有所帮助。

我是前端西瓜哥,关注我,学习更多图形编辑器知识。


相关阅读,

图形编辑器中用到的一些设计模式

图形编辑器:历史记录设计

图形编辑器开发:钢笔工具的实现

图形编辑器:基于 canvas的所见即所得文本编辑

图形编辑器开发:快捷键的管理

图形编辑器:工具管理和切换

使用 yjs 给图形编辑器加上多人协同编辑功能

吸附设计:学会正确地贴贴

Figma 的编组功能,比你想象的要复杂得多