- 开发工具
- 数据可视化
【免费下载链接】penrose
Create beautiful diagrams just by typing notation in plain text.
导读
本文以 Penrose 仓库中 docs/graphical-api.md 这份设计笔记为核心,完整展开其中记录的contains图形约束算法设计——从“BBox 粗判 + Level Set(水平集)精判”的两阶段伪代码,到坐标系统与变换、运行时优化构想,以及intersects、overlap等关联目标函数。文章同时结合仓库源码(Constraints.ts、BBox.ts、Minkowski.ts)印证这些设计在当前实现中的落点,帮助读者理解 Penrose 如何在可微优化框架内表达“A 包含 B”这类空间关系。
一、设计笔记的背景:为什么需要专门的contains约束
Penrose 的核心工作方式是把“画图”编码为一个数值优化问题:所有约束(constraint)和优化目标(objective)都被实现为可微的能量函数,优化器不断降低总能量,直到得到一个满足语义的布局。这一点在 docs-site/docs/ref/constraints.md 中有系统说明:约束必须写成零基不等式p(x) > 0的形式,Penrose 会惩罚违反程度,且所有数值运算都要通过自动微分(autodiff)算子完成。
contains正是这样一类约束:它要求形状s1包含形状s2,可选一个padding作为两者尺寸之间的安全边距。这个约束看似简单,但对任意形状对(圆、多边形、矩形、Group 等)都要给出一个可微的、且不引入过多性能开销的能量值,因此需要认真设计算法——这正是 docs/graphical-api.md 记录这份设计笔记的动机。
从当前源码看,contains已在 Constraints.ts 中落地为按形状类型分派(dispatch)的通用函数:Circle–Circle 走containsCircles,Polygon–Polygon 走containsPolys,矩形类走containsRects,Group 走containsGroupShape,无法精确处理时退回 BBox 近似并给出BBoxApproximationWarning。设计笔记中“先粗判、后精判”的思路正是为了减少这些分派在通用情形下的计算量。
二、两阶段判定算法:BBox 粗筛 + Level Set 精判
设计笔记的核心是一段针对通用对象contains A B的伪代码,其总体策略是:
- 第一阶段(BBox 粗判):先看
A与B的包围盒(bounding box,简称 BBox)是否满足包含关系; - 短路返回:如果 BBox 层面就能判定结果(
d != 0),直接返回该距离值; - 第二阶段(Level Set 精判):仅当 BBox 判定不充分(结果为 0,即边界上无法确定)时,才进入 Level Set 的逐像素精确比较。
笔记给出的原始伪代码如下:
-- Top-level function on two generic objects contains A B = let d = contains (bbox A) (bbox B) if d != 0 then d else contains (levelSet A) (levelSet B) contains (BBox a) (BBox b) = if ! ( a.L > b. L .... ) return dist( a.center, b.center) - Epsilon else 0 contains (LevelSet a) (LevelSet b) = -- Assuming we have globally uniform grid resolution for x in width that they overlap for y in height that they overlap if ( b.grid[ x,y ] <= 0 ) if ( a.grid[ x,y ] > 0 ) -- return farthest “worst” pixel distance function -- average or handle points return 0逐段解读其中的关键设计决策:
- BBox 阶段返回距离值:当
A的包围盒在某个维度上不能覆盖B的包围盒时,直接用dist(a.center, b.center) - Epsilon作为能量值。这保证了约束值在“明显不满足”时是一个正数(即被惩罚),并且是连续可微的——距离中心差是位置坐标的平滑函数。Epsilon是一个容差项,避免把“刚好相切”误判为满足或违反。 - BBox 阶段返回 0 表示“无法确定”:当两个包围盒在所有维度上都满足包含关系时,BBox 判定只能说明“
B在盒层面位于A内”,但A的真实形状可能是凹陷的、非凸的,边界附近的点是否真的在A内无法从盒得出。此时返回 0 作为哨兵值,触发第二阶段。 - Level Set 阶段逐像素比较:在全局统一网格分辨率(globally uniform grid resolution)的假设下,遍历两个网格重叠区域内的每个像素
(x, y):如果b.grid[x, y] <= 0(B的网格值非正,说明该像素在B内或边界上),而a.grid[x, y] > 0(A的网格值为正,说明该像素在A外),则说明存在B的点落到了A之外,即包含关系被违反。返回值应体现“最坏”像素的距离函数值,或者做平均处理。
与当前源码实现的关系
设计笔记中的两阶段思想,在当前仓库中体现为两个层面:
- BBox 是实际的降级路径:
contains的通用分支(两个形状都非 Circle/Polygon/Rect 等已知组合)会退回containsRects(bboxFromShape(s1), bboxFromShape(s2), padding),同时返回一个BBoxApproximationWarning,提示“当前结果只是包围盒近似”(见 Constraints.ts)。BBox 的数据结构在 BBox.ts 中定义,由width、height、center组成,并提供角点(corners)、区间(intervals)、边(edges)等辅助接口。 - 精确判定按形状类型特化:仓库并没有用统一的 Level Set 网格,而是为每种形状组合给出精确的解析能量函数,例如:
containsCircles:d - (r1 - r2 - padding),即“圆心距减去半径差”,完全对应 docs-site/docs/ref/constraints.md 中推导的圆包含能量表达式(Constraints.ts);containsPolys/containsPolyCircle/containsCirclePoly:把“多边形包含”转化为“每个关键点都被包含”的能量(maxN取最坏点,对应笔记中“return farthest worst pixel distance”的取最大惩罚思想,Constraints.ts);containsGroupShape:对 Group 先判断成员形状是否包含目标,再结合裁剪形状(clip path)共同判定,采用minN(成员满足其一即可)与andConstraint(裁剪必须同时满足)的组合(Constraints.ts)。
换句话说,笔记中“BBox 粗判短路、Level Set 精判兜底”的分层策略,在实现上被等价地落实为“已知形状组合走精确解析式、未知组合退回 BBox 并告警”的分派策略。
三、坐标系统与变换:grid / math / screen 三套坐标
设计笔记明确指出,系统内同时存在三套坐标系统,任何涉及 Level Set 网格的运算都必须清楚自己在哪套坐标系下:
- 网格坐标(grid coordinates):Level Set 的离散像素坐标,即
grid[x, y]中的x, y; - 数学坐标(math coordinates):系统默认的连续坐标,所有形状属性(圆心、半径、顶点等)都以它为准;
- 屏幕坐标(screen coordinates):前端渲染使用的坐标,与 Canvas 画布相关。
笔记给出了两组变换关系:
| 变换 | 涉及参数 |
|---|---|
| math ↔ screen | 平移(由 CANVAS 尺寸决定) |
| grid ↔ math | 平移 + 缩放(由 Level Set 分辨率、网格左上角在数学坐标中的位置决定) |
这两条变换关系在实践中意味着:
- 网格与数学坐标之间不是简单平移:网格分辨率(每单位距离多少个像素)决定了缩放因子,而网格左上角(top-left corner)的数学坐标决定了平移偏移。任何“把形状的连续坐标换算成网格下标”的操作,都需要
offset = (mathCoord - gridTopLeft) * resolution这类换算。 - 屏幕坐标与数学坐标之间,在 Penrose 的渲染链路中由画布尺寸决定平移量。当前渲染器实现在 packages/core/src/renderer 目录下,各形状的 SVG 属性(如
cx、cy、r)均从数学坐标经画布变换映射而来。
笔记还提到一个与此相关的开放问题:对齐(Alignment)。当两个 Level Set 网格分辨率或原点不一致时,“用像素分辨率重算重叠区域”是最直接的思路(Idea 1),但这会带来性能开销,还可能要求用户一开始就按像素分辨率输入 SDF,并可能在上/下采样与插值中引入误差。该问题在笔记中列为待决项,说明它属于设计考量而非最终实现。
四、性能优化构想:按需细化 Level Set
由于 Level Set 的逐像素比较是 O(重叠像素数) 的操作,笔记专门记录了解决慢运行时的构想:
Idea 1:compute finer levelset on demand—— 如果只需 BBox 即可完成空间查询,就先在较粗的分辨率上计算 Level Set;只有当 BBox 测试结果不确定(not deterministic)时,才在局部细化到更精细的 Level Set。
这是一种典型的**渐进式精度(progressive refinement)**策略:先以粗网格快速排除大量无需精确判定的情况,再对少数边界情况投入精细计算。它与第二节伪代码中的短路逻辑一脉相承——两阶段设计的目的正是让“绝大多数情况”停留在廉价的第一阶段。
从当前实现看,仓库选择了另一种等价工程路径来规避这一性能问题:contains为每种已知形状组合提供解析的、O(1) 或 O(顶点数) 的精确能量(如containsCircles只有一次ops.vdist),从而完全避免了网格化的逐像素开销;只有未知组合才退回 BBox。这与笔记“用 Level Set 兜底”的初衷一致,但用解析式替代了网格扫描,从源码结构看,这是设计笔记落地时做出的关键简化。
五、关联目标:intersects与overlap
笔记末尾列出了其他相关目标函数:intersects与overlap。这两者与contains共同构成 Penrose 中形状间空间关系的基础集合:
overlapping(s1, s2, overlap):要求两个形状以一定的重叠量相交,overlap参数控制最小重叠量(默认 0),其能量由形状距离shapeDistance与重叠量的组合构成(Constraints.ts);在形状距离计算触发 BBox 近似时,会改写告警信息为overlapping(s1, s2)签名,便于用户定位问题(Constraints.ts)。disjoint(s1, s2, padding):要求两形状不相交,语义上等价于“overlapping取负”,即把overlapping的能量取反(Constraints.ts)。touching则取其绝对值,表示相切状态。- 圆与椭圆的解析实现:
overlappingEllipses、overlappingCircleEllipse通过隐式椭圆函数(ImplicitShapes.ts)实现,在 Constraints.ts 中注册。 intersects:在设计笔记中作为待实现目标列出;当前源码中intersects更多以函数查询形式出现(如 Functions.ts 中“射线与形状求交”的rayIntersect系列,见 Functions.ts),用于几何查询而非约束能量。
这些约束都在 constrDict 中统一注册,随后作为 Style 语言的内建约束(builtin constraints)暴露给用户,可在.style文件中直接书写,例如:
contains s1 s2 padding: 10.0 overlapping s1 s2 overlap: 5.0 disjoint s1 s2 padding: 3.0其中padding/overlap参数均有默认值 0(见 constrDictGeneral 中各个条目的params声明),表示“恰好包含 / 恰好重叠”。
六、设计笔记中的开放问题与后续演进
从笔记行文可以推断,这份文档是contains图形约束开发早期的设计备忘,其中明确标记的开放问题包括:
- Level Set 对齐问题:重叠区域的重算、初始 SDF 的分辨率输入要求、上下采样与插值误差——这些决定了“网格化方案”能否落地;
- 性能路径:是否按需细化 Level Set,取决于 BBox 测试的判定确定性(determinism)程度;
- 目标函数集合的补全:
intersects、overlap与contains的最终统一语义。
对照当前仓库,上述问题的大部分已在源码层面得到工程化解法:contains的精确能量按形状组合特化(Constraints.ts),Minkowski 和(Minkowski.ts)为凸多边形提供解析的有符号距离函数 SDF,rectangleDifference为包围盒差提供解析解——这些都让“逐像素 Level Set”不再是唯一选择。因此,设计笔记与当前实现构成了一个完整的演进脉络:从“两阶段网格算法”的构想,到“BBox 兜底 + 解析特化”的落地,读者可以通过对照这两份材料,深入理解 Penrose 图形约束系统的设计取舍。
延伸阅读
- 约束与目标函数的系统讲解:Writing Constraints & Objectives
contains/overlapping/disjoint的实现:Constraints.ts- BBox 数据结构与辅助函数:BBox.ts
- Minkowski 和与 SDF 实现:Minkowski.ts
- 函数库与射线求交等几何查询:Functions.ts
- 隐式形状(椭圆、半平面)定义:ImplicitShapes.ts
- 开发工具
- 数据可视化
【免费下载链接】penrose
Create beautiful diagrams just by typing notation in plain text.
相关推荐
Penrose 实战教程:用 `predicate` 声明关系、用 `ensure` 约束绘制子集包含图
Penrose 实战教程:用 predicate 声明关系、用 ensure 约束绘制子集包含图 本文是 Penrose 系列教程的第二篇,围绕仓库文档 pre
开发工具数据可视化cytoscape.js 集合包含关系判定:eles.contains() / eles.has() 的用法与源码原理
cytoscape.js 集合包含关系判定:eles.contains / eles.has 的用法与源码原理 eles.contains eles 是 cyt
数据可视化Penrose约束系统:从几何关系到优化目标的转换
Penrose约束系统:从几何关系到优化目标的转换 Penrose作为一个通过文本符号生成精美 diagrams 的开源项目,其核心在于将用户定义的几何关系转化
开发工具数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考