likec4 布局引擎 @likec4/layouts 深度解析:Graphviz 编排、动态视图流程控制与 AI 语义布局
【免费下载链接】likec4Visualize, collaborate, and evolve the software architecture with always actual and live diagrams from your code项目地址: https://gitcode.com/GitHub_Trending/li/likec4
@likec4/layouts是 LikeC4 项目负责"把计算好的视图变成最终坐标"的布局引擎包,它以 Graphviz(WASM)为核心后端,把各类视图翻译为 DOT 再解析回带坐标的图数据。本文基于 packages/layouts/CHANGELOG.md 的版本演进脉络,结合仓库源码,系统讲解布局引擎的架构管线、并发队列、v1.59.0 引入的动态视图流程控制块(alt/opt/loop/try等)、v1.57.0 的 AI 辅助语义布局,以及 v1.48.0 的图标定制能力,帮助你理解并正确使用这套布局能力。
模块定位:@likec4/layouts 在 LikeC4 中负责什么
包自身的 README 只有一句话,但信息量很足:
Layout algorithms for views. At the moment, delegates to Graphviz WASM (hpcc-js-wasm).
也就是说,该包不重新发明布局算法,而是把 LikeC4 的各类视图(元素视图、部署视图、动态视图、项目总览视图)翻译成 Graphviz 的 DOT 图描述,交给 Graphviz 求解,再把求解结果解析回 LikeC4 自己的DiagramView数据结构。从 package.json 可以看到它的对外导出面:
| 导出子路径 | 用途 |
|---|---|
. | 主入口:GraphvizLayouter、QueueGraphvizLayoter、GraphvizWasmAdapter及类型 |
./sequence | 序列图(动态视图)独立布局算法 |
./graphviz/binary | Graphviz 二进制适配器(GraphvizBinaryAdapter) |
./ai | AI 布局提示的 LLM 输入输出与增强管道 |
依赖上,除了@hpcc-js/wasm-graphviz(WASM 版 Graphviz)、ts-graphviz(DOT 建模)之外,还引入了@lume/kiwi(Cassowary 约束求解器,用于序列图布局)、p-queue/p-limit(并发队列)、zod(AI 输出解析校验)等。
核心架构:视图 → DOT → Graphviz → 布局结果
GraphvizPort:与后端解耦的端口抽象
GraphvizLayoter.ts 定义了GraphvizPort接口,它是布局引擎与具体 Graphviz 后端之间的抽象层:
| 方法 | 说明 |
|---|---|
unflatten(dot) | 对 DOT 做"解平铺"后处理,改善纵横比与边排布(对应 graphviz 的unflatten工具) |
acyclic(dot) | 移除图中的环,返回无环 DOT |
layoutJson(dot) | 执行布局,返回 Graphviz 的 JSON 形式结果 |
svg(dot) | 直接产出 SVG 渲染结果 |
dispose() | 释放后端资源 |
GraphvizLayouter构造函数默认使用new GraphvizWasmAdapter()作为端口,也可以通过changePort()在运行时切换后端,例如替换为二进制 Graphviz 适配器(见 binary/GraphvizBinaryAdapter.ts)。
GraphvizWasmAdapter:默认 WASM 后端的两处工程细节
GraphvizWasmAdapter.ts 中两处实现细节值得注意:
- 并发限制为 1:
concurrency恒为1,且用p-limit(1)串行化所有调用,避免 WASM 实例并发加载问题(源码注释原文:"limit to 1 concurrency to avoid wasm loading issues")。 - 内存保护:每执行 20 次操作后主动
Graphviz.unload()并重新加载,规避 WASM 长驻内存问题;失败时(非语法错误)会卸载实例并随机延迟 30–300ms 后自动重试一次,语法错误则直接抛出。
按视图类型选择 Printer
getPrinter(GraphvizLayoter.ts)根据视图类型分派不同的 DOT 打印机:
- 动态视图 →
DynamicViewPrinter - 部署视图 →
DeploymentViewPrinter - 元素视图 →
ElementViewPrinter - 项目总览 →
ProjectsViewPrinter(独立方法layoutProjectsView)
每个 Printer 都继承自DotPrinter,负责把 LikeC4 的节点、容器(compound)、关系(edge)及主题样式翻译成 DOT 属性。快照目录snapshots中存放了ElementViewPrinter-*.dot、DeploymentViewPrinter-index.dot、ProjectsViewPrinter-*.dot等真实输出,是理解各 Printer 行为的直接证据。
完整布局管线
GraphvizLayouter.layout()(GraphvizLayoter.ts)的执行链路为:
- 由 Printer 生成 DOT 源;
- 对元素视图额外执行
unflatten(失败仅告警,不中断); - 通过
dotToJson()调用graphviz.layoutJson(dot)得到 JSON; parseGraphvizJson(json, view)把 JSON 解析回DiagramView;- 若结果是动态视图,额外执行
calcSequenceLayout(diagram)计算序列图布局,合并进sequenceLayout字段(对应LayoutedDynamicView)。
printToDot()则提供"只出 DOT 不求解"的调试能力,且接受可选的AILayoutHints——一旦传入 hints,就会改用AiLayoutViewPrinter输出受提示影响的 DOT。
并发控制:QueueGraphvizLayoter 与批处理
QueueGraphvizLayoter.ts 在GraphvizLayouter之上叠加了p-queue队列,构造参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
concurrency | 跟随graphvizPort.concurrency(WASM 为 1) | 并发上限,最小 1 |
timeout | 20_000(毫秒) | 单个操作超时 |
throwOnTimeout | true | 超时是否视为异常 |
它重写了layout/layoutProjectsView,所有任务都进入队列串行执行;切换端口时还会同步队列并发数。batchLayout()面向"一次性布局整组视图"的场景,支持:
cancelToken:取消令牌,批次等待或逐任务调度期间可随时中止;onSuccess/onError:逐任务回调;- 批处理互斥:若已有批次在处理,后续批次会等待其
onIdle后再递归重试,防止批次并发; - 背压机制
waitForQueueToShrink:队列积压超过concurrency + 2时等待收缩再继续提交,避免 WASM 端一次性堆积过多任务。
动态视图流程控制块(v1.59.0 核心新增)
v1.59.0(PR #3084)是 CHANGELOG 中内容最重的一次变更:动态视图的流程控制。此前动态视图的步骤只能平行展开(parallel),现在可以把步骤分组为带可选标题的流程块:
opt、loop、break块alt+when/else分支try/catch/finally块
CHANGELOG 给出的完整 DSL 示例:
dynamic view example { customer -> app 'opens app' alt { when 'authorized' { app -> api 'requests data' } else 'not authorized' { app -> customer 'shows login' } } }渲染与交互行为
- 序列图渲染:这些块在序列图中渲染为嵌套的 frame(嵌套框),并且放大(zoom)时参与方(actor)保持可见,修复了 issue #3074;
- Sequence Outline 面板:走查(walkthrough)过程中,停靠的 Sequence Outline 面板把流程展示为一棵与块嵌套结构一致的可折叠树——每个步骤带编号,每个操作符带彩色类型标签与步骤计数,可点击跳转到任意步骤。
底层实现
- 在 DOT 侧,DynamicViewPrinter.ts 的
postBuild()会删除TBbalance并设置ordering='in',保证步骤按输入顺序从上到下排布;边标签带步骤号(stepEdgeLabel),回退方向(edge.dir === 'back')或重复出现的目标节点会设置minlen=0削弱 rank 约束。 - 在序列图侧,sequence/layouter.ts 的
SequenceViewLayouter使用@lume/kiwi约束求解器(Cassowary 算法)建模:actor 框、行(inner/outer 双层边界)、compound 区域与 subflow 区域全部编译为线性约束,并通过Strength(required/strong/medium/soft/weak)分级求解;求解器maxIterations提高到 2000 以容纳复杂流程的约束规模。折叠的 subflow 会把其覆盖的行高度置 0 并标记collapsed,供渲染侧隐藏细节。
实验性声明与已解决问题
CHANGELOG 明确标注:"Flow control blocks are experimental — syntax and rendering may change",并邀请用户在 discussions 中反馈。本次变更同时解决 #2745、#2993、#3074 三个 issue。
AI 辅助语义布局(v1.57.0 实验性)
v1.57.0 引入实验性的AI 布局顾问:让 LLM 分析图的语义,给出 graphviz 布局提示(rank 约束、边权重、不可见边),从而让布局更可读、视觉更均衡。
工作管道
enhanceLayoutWithAI.ts 完整实现了四段式管道:
prepareLLMInput(view):把视图序列化为 JSON,并建立 NodeId/EdgeId 到完整节点/边数据的映射;- 组装 system prompt(
prompt-system.md生成)与 user prompt; provider.sendRequest(...):通过AILayoutProvider接口调用 LLM(该接口由 VSCode 扩展基于vscode.lmAPI 实现,或由直接 API 提供商实现,包内刻意不依赖 VSCode);parseOutput(...):解析 LLM 返回的 JSON 为AILayoutHints。
关键设计是失败即降级:任何异常都只记录warn并返回undefined,布局回落到普通 Graphviz——AI 增强永远不会阻断出图。布局侧则由GraphvizLayouter.aiLayout()配合AiLayoutViewPrinter(AiLayoutPrinter.ts)把 hints 写入 DOT。
AILayoutHints 结构
ai/types.ts 定义了 LLM 必须产出的 JSON 结构:
| 字段 | 取值/含义 |
|---|---|
direction | 整体方向:TB/BT/LR/RL |
ranks | 批量指定节点组处于same/source/sink/min/max层 |
edgeWeight | 按 EdgeId 覆盖边权重(牵引力) |
edgeMinlen | 按 EdgeId 覆盖最小层距 |
reverseRank | 反转这些边在 rank 中的方向(打破环、交换源/目标层序) |
excludeFromRanking | 这些边不参与排名(等价于 Graphvizconstraint=false),边仍可见 |
edgeOrder | 建议的边输出顺序,影响边路由 |
nodeOrder | 建议的节点输出顺序,影响节点排布 |
invisibleEdges | 由 AI 添加的不可见边(source/target,可选weight/minlen),用于微调层序 |
reasoning | LLM 的推理说明,供调试与展示 |
提示词规则要点
ai/prompt-system.md 是注入 LLM 的约束规范,核心规则包括:minlen控制在 0–4(0 不增层只定层内顺序);weight控制在 1–10(强调的边用 6–10);reverseRank不改语义只改层序;excludeFromRanking对应constraint=false;不可见边与普通边同等影响 rank。VSCode 侧通过 chat participant 与命令触发 AI 布局增强,能力入口见 packages/vscode 的 chat 集成。
元素图标定制(v1.48.0)
v1.48.0 为元素样式增加了图标定制项:
iconColor:图标颜色;iconSize:图标尺寸;iconPosition:图标位置,支持left/right/top/bottom。
底层上,packages/core/src/styles/types.ts 定义了iconPosition?: IconPosition与iconSizes映射表,packages/core/src/styles/LikeC4Styles.ts 的iconSize()方法会把枚举尺寸解析为主题中的具体像素值(缺省时回落到defaults.size)。这些样式属性最终由各 Printer 写入 DOT 节点属性,从而同时作用于布局与渲染。
演进与清理:ManualLayoutV1 移除(v1.52.0)
v1.52.0(PR #2713)移除了已废弃的 ManualLayoutV1 及配套迁移命令。这意味着手动画布布局的旧一代格式不再被支持,新项目应使用当前的 manual-layout 机制(核心实现在 packages/core/src/manual-layout),避免依赖 V1 格式的迁移路径。
版本节奏与依赖同步
CHANGELOG 覆盖 1.46.2 → 1.59.3 的版本历史,其中绝大多数为 Patch,且每次都同步跟随@likec4/core与@likec4/log升版(当前 1.59.3 依赖@likec4/core@1.59.3、@likec4/log@1.59.3)。布局引擎与核心数据模型、日志模块保持严格同版发布,升级时建议三个包一起升。除依赖同步外的实质变更仅有:1.59.0(流程控制)、1.57.0(AI 布局)、1.52.0(移除 ManualLayoutV1)、1.48.0(图标定制)。
测试与验证
包内测试覆盖了各 Printer 与 WASM 适配器,可作为深入源码的入口:
- wasm/GraphvizWasmAdapter.spec.ts 及对应 快照;
- ElementViewPrinter.spec.ts、DeploymentViewPrinter.spec.ts、ProjectsViewPrinter.spec.ts 分别校验三类视图的 DOT 输出;
- sequence/utils.spec.ts 覆盖序列图工具函数;
- graphviz/fixtures提供模型与视图快照样例。
仓库内还留有真实用法示例,例如 examples/rank-for-better-layout/demo-rank-for-better-layout.c4 演示通过 rank 技巧改善布局的可写写法。
小结
@likec4/layouts的演进史勾勒出一条清晰的路线:先以 Graphviz WASM 为后端解决"能布局"(1.46–1.52 的依赖同步与清理),再通过流程控制块(1.59.0)与 AI 布局顾问(1.57.0)把动态视图与智能排布推向"布得更好"。理解GraphvizLayouter→QueueGraphvizLayoter→ 各Printer→GraphvizPort的层次结构,是二次开发或排查布局问题的最短路径;而AILayoutHints的 JSON 结构则是接入自定义 AI 布局服务时的契约核心。
【免费下载链接】likec4Visualize, collaborate, and evolve the software architecture with always actual and live diagrams from your code项目地址: https://gitcode.com/GitHub_Trending/li/likec4
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考