☰
CAD Sketcher 集成机制解析:从 Sketch 到 Body,把草图变成可控的 Blender 常规几何
2026/10/14 16:05:04 网站建设 项目流程
  • 桌面应用
  • 3D建模

【免费下载链接】CAD_Sketcher

Constraint-based geometry sketcher for blender

项目地址:https://gitcode.com/gh_mirrors/ca/CAD_Sketcher
点击查看免费下载

CAD Sketcher(Blender 约束式几何草图扩展)的核心集成设计是:草图(sketch)只作为纯曲线源,实体结果落在独立的 body(网格对象)上,body 的修改器堆栈负责读取草图、网格化并执行拉伸、旋转、布尔等操作。本文基于文档 integration.md,结合 utilities/convert_nodes.py、utilities/body.py 等源码,完整讲解 body 修改器堆栈的每个参数、转换管线在 Geometry Nodes 中的实际实现、三条已知限制(精度、元素索引、路径拼接)的底层成因,以及保证实体结果稳定的建模最佳实践。

1. 核心概念:Sketch 是源,Body 是结果

integration.md 开篇给出的定义是整篇文档的骨架:

一个 sketch 是一个 Curves 对象,承载你绘制的所有几何,仅此而已。你所看到的实体是它的body:一个网格对象,其修改器堆栈读取 sketch、将其转换为网格,然后执行拉伸、旋转和布尔运算。

这一定位带来的直接收益是:转换结果是普通 Blender 几何。你可以对整个堆栈执行 Apply(应用修改器)、导出、转换,或把它喂给任何网格工具——而在你这样做之前,它始终跟随 sketch 变化。

源码 utilities/body.py 的模块注释印证并补充了这一点:sketch 保持为纯粹的 Curves 对象,"没有任何修改器,没有任何改变其类型的东西"。之所以要这样拆分,是因为 Blender 拒绝应用一个会改变对象类型的修改器(对应 issue #723)——旧文件中 Curves 对象直接承载修改器时无法 Apply;拆出 body 后,body 是标准 MESH 对象,堆栈可以直接应用、可以用to_mesh读取、可以用网格工具编辑。

对象之间的绑定关系由成对属性维护,见 utilities/body.py:

  • BODY_SKETCH_KEY("slvs:body_of"):body 指回它读取的 sketch;
  • sketch_obj.slvs_body:sketch 指回承载它的 body(carrier_of())。

is_body()的判定即obj.type == "MESH" and BODY_SKETCH_KEY in obj。测试 testing/test_body.py 固化了这一约定:

  • test_the_sketch_carries_no_modifiers—— sketch 对象上修改器列表为空;
  • test_one_modifier_reads_the_sketch_and_meshes_it—— body 的堆栈恰好以CAD Sketcher Convert修改器开头;
  • test_the_body_evaluates_to_the_sketch_geometry—— 评估后 mesh 的多边形总面积接近真实圆面积(因为是细分,略小于 πR²)。

此外,body 是"零件"的锚点:body 锚定 part,workplane 挂在 body 之下,sketch 落在 workplane 上(见 utilities/body.py 的_rehome_onto_body())。退出 sketch 时选中的也正是 body 而不是曲线源对象(test_leaving_a_sketch_selects_its_body)。

2. Body 的修改器堆栈:逐个参数解析

选中 body 后,其修改器与任何对象一样显示在 Properties 编辑器中。按 integration.md 的描述与源码实现,堆栈结构如下。

2.1 CAD Sketcher Convert(节点组修改器)

这是堆栈的第一个修改器,类型为NODES,绑定的节点组全场景共享同一个,名为"CAD Sketcher Convert"(常量CONVERT_NODE_GROUP,见 utilities/convert_nodes.py)。它有四个输入插座,其中三个是用户参数:

参数类型默认值 / 范围作用
SketchObject未设置时转换传入的几何指向该 body 所基于的 Curves 对象
FillBoolTrue对闭合形状做面填充,而不是只保留边
Angular ResolutionFloat(角度)math.radians(7.5)(约 7.5°)圆弧和圆被网格化时每条边的最大角度

这三个参数的定义见 utilities/convert_nodes.py 中build_convert_node_group():

sketch = iface.new_socket(SKETCH_INPUT, in_out="INPUT", socket_type="NodeSocketObject") sketch.description = "Sketch whose curves this body is built from" fill = iface.new_socket("Fill", in_out="INPUT", socket_type="NodeSocketBool") fill.default_value = True resolution = iface.new_socket(ANGULAR_RESOLUTION_INPUT, in_out="INPUT", socket_type="NodeSocketFloat") resolution.subtype = "ANGLE" resolution.default_value = DEFAULT_ANGULAR_RESOLUTION # math.radians(7.5) resolution.min_value = MIN_ANGULAR_RESOLUTION # math.radians(0.1) resolution.max_value = math.radians(90)

Sketch输入的语义。节点组内的_source_geometry()用GeometryNodeObjectInfo(transform_space = "RELATIVE")读取该 sketch 的曲线,经过RealizeInstances后与修改器自身收到的几何JoinGeometry合并。注释明确写道:body 本身没有自己的几何("a body is a mesh object with no geometry of its own"),曲线的读入就发生在这一处;未设置Sketch时,组转换传入的几何——这正是 body 概念出现之前 sketch 自带修改器时的行为。

默认值从哪里来。文档说 Angular Resolution 的默认值来自 add-on 偏好设置,源码链路是:

  • 偏好项curve_angular_resolution(界面标签 "Curve Resolution",默认math.radians(7.5),范围 0.1°–90°,subtype="ANGLE"),见 base/preferences.py 的Preferences类,其 description 明确写着 "used for new sketches. Change it per sketch on its CAD Sketcher Convert modifier"——即偏好只影响新建sketch,既有 body 在该 body 的修改器上逐一改;
  • 新建 sketch 时由 utilities/curve_data.py 的_ensure_convert_modifier()创建名为CAD Sketcher Convert的NODES修改器,并在创建时调用_apply_default_angular_resolution(),把偏好值写入该修改器的Angular Resolution输入。

测试 testing/test_curve_resolution.py 的test_new_sketch_uses_preference验证了这条链路:把偏好改为 15° 后新建 sketch,其修改器上的值就是math.radians(15)。

节点组重建不会丢参数。一个值得了解的工程细节:修改器输入在 Blender 中按 socket 的 identifier 存储,而重建节点组接口会重新铸造 identifier。build_convert_node_group()因此在每次因CONVERT_VERSION变更而重建时,先用_snapshot_modifier_inputs()按插座名快照全场景所有使用该组的修改器的输入值,重建后再用_restore_modifier_inputs()按名回填。测试test_rebuild_keeps_modifier_values确认了版本升级后各 sketch 的Fill与Angular Resolution保持不变;test_upgrade_gives_new_input_its_default则覆盖了更恶劣的历史场景——旧文件升级到新增Angular Resolution插座时,若回填为 0 会导致每个圆无限细分甚至 Blender 挂起,恢复逻辑对新插座直接赋予默认值并修复历史遗留的 0 值。

2.2 建模工具添加的修改器

Extrude(拉伸)、Revolve(旋转)、Boolean(布尔)、Array(阵列)由 建模工具 按应用顺序追加到堆栈上,且与任何普通修改器一样可以重排。这些工具的作用方式在 modeling.md 中有完整描述:它们作用于对象而非 sketch 实体,各自往 body 上加一个节点组修改器(如"CAD Sketcher Revolve",见 operators/modifiers.py 的View3D_OT_node_revolve),因此实体结果始终跟随草图、且事后仍可在修改器上继续编辑。

2.3 构造几何与退化段的剔除

文档声明"构造几何和退化段在转换期间被丢弃,因此永远不会到达网格"。这在节点组内部有对应实现(utilities/convert_nodes.py 的build_convert_node_group()):

construction = nodes.new("GeometryNodeInputNamedAttribute") construction.data_type = "BOOLEAN" construction.inputs["Name"].default_value = "construction" spline_len = nodes.new("GeometryNodeSplineLength") degenerate, degen_a = _int_compare(nodes, links, "LESS_THAN", 2) links.new(spline_len.outputs["Point Count"], degen_a) drop = nodes.new("FunctionNodeBooleanMath") drop.operation = "OR" ... delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain = "CURVE"

即:construction属性为 True 的曲线,或点数少于 2 的退化 spline,在进入 Curve to Mesh 之前被Delete Geometry按 CURVE 域整条删掉。construction属性是实体级的布尔标志,写入链路见 model/curve_ref.py(set_attribute(attrs, "construction", ...)),在实体列表与右键菜单中均可切换(ui/panels/entities_list.py、ui/selected_menu.py)。

3. 限制之一:精度——圆弧是细分出来的多边形

文档"Limitations / Precision"一节:圆弧和圆是**细分(tessellate)**的,所以网格化后的圆是多边形,边数由 Angular Resolution 决定;曲线需要更平滑时就把该值调低。

从源码看,这个"最大角度/边"的策略实现得相当讲究,见 utilities/convert_nodes.py 的_set_curve_resolution():

  1. 按sketch_type属性筛出ARC与CIRCLE两类曲线(直线保持创建时的 resolution 1 不动);
  2. 圆弧和圆在曲线上是每段最多 90° 的 Bezier 段,因此给每段ceil(90° / angle)条边,就能保证无论扫过角多大,每条边的夹角都均匀;
  3. 角度先与MIN_ANGULAR_RESOLUTION(0.1°)取MAXIMUM,作为节点侧的护栏——因为修改器数值可能绕过插座的软限制,而 0 角度会把圆细分到 Blender 挂起;
  4. 除法结果再减去 1e-3 噪声项后取CEIL,防止浮点噪声把精确比值(90°/7.5° = 12)顶到下一个整数。

默认值 7.5° 与 "Beziér 每 90° 段 12 边" 的旧行为一致(源码注释:"Matches the Bezier default of 12 edges per 90 degree segment")。testing/test_curve_resolution.py 给出了可复算的量化基准:

  • 默认 7.5° 时,半径 1 的圆评估出48 个顶点(4 个 90° 段 × 12 边);
  • 设为 30° 时 12 个顶点,45° 时 8 个顶点;
  • 直线在任何角度设置下顶点数不变。

4. 限制之二:元素索引不稳定,不要在网格上按索引引用

文档"Limitations / Element indices"一节:生成网格的顶点和面索引在编辑之间不稳定,任何按索引引用它们的东西都可能在草图变化后失效,应改为在 body 上以程序化方式工作。

"从源码结构看",这一限制有两个来源:

  1. 拓扑本身随草图变化:实体增删、Fill 开合、Angular Resolution 改变都会改变生成的顶点/面数量与排列;
  2. Fill 的路径经过 Mesh to Curve 再进 Fill Curve,而 Fill Curve 按环路重建面,顺序并不承诺与输入边序一一对应。

作为对照,节点组尾部刻意生成了稳定的、基于来源实体的 ID:add_generated_id_nodes()基于.cad_sketcher_source_curve_id/.cad_sketcher_source_endpoint_id两个来源种子属性,按父ID × 1,000,003 + 组内序号 + 角色常量的公式派生顶点属性id与面属性cad_sketcher_face_id(GENERATED_ID_VERSION = 2)。源码注释强调这些 ID 的推导不依赖拓扑全局 Index(否则顶点索引一变,ID 就会跟着变)。也就是说:按索引脆弱、按来源实体追踪稳健——需要稳定追踪时应当使用这些生成 ID 或在上层节点流程中程序化引用,而不是裸索引。

5. 限制之三:路径如何被拼接(以及为什么)

文档"Limitations / Paths"一节:转换按共享的 start/end 点拼接几何;仅靠 coincident 约束连在一起、或者相互重叠的实体,不会被读作同一条路径。

要理解这句话,需要看转换管线的"焊接"机制,这是 utilities/convert_nodes.py 中最核心的一段:

曲线数据侧:utilities/curve_data.py 的compute_merge_ids()为每个曲线点写入merge_id整数属性——段端点按其引用的点实体(start_point_id/end_point_id)获得稠密 ID,因此汇聚到同一结点的多个端点共享同一个 ID;仅重合约束连接的端点属于不同点实体,这些点 ID 会先做并查集(union-find)归并后再编号,让角落焊接、环路闭合;内部顶点与圆的顶点保持 0,永不焊接。连通性签名未变时会跳过重算(compute_merge_ids(sketch, force=True)可强制)。

节点组侧:Curve to Mesh 得到"线框网格"后,GeometryNodeInputMeshVertexNeighbors筛出真端点(valence < 2,即线段端点 valence 1 与独立点实体 valence 0),与merge_id != 0取 AND 后作为焊接选择:

  • Blender 5.2+:GeometryNodeMergePoints(Merge Points)按Merge ID 按身份(identity)焊接——无容差、与草图尺度无关;
  • Blender < 5.2:没有 identity 焊接,回退到Merge By Distance(Distance = 1e-6),只收拢本就重合的端点,实践中与 identity 焊接等价。

源码注释明确解释了 valence 门控的原因:细分产生的内部顶点 valence 为 2,被排除在外,这样插值出的 ID 无害;valence 0 的点实体则被允许坍缩到重合的线段角上。

在此机制下,文档"Paths"一节的准确读法是:真正共享同一个 start/end 点实体的段必然被读作同一路径;仅由 coincident 约束连接的端点属于不同点实体——它们能被闭合的前提是约束求解器把两端解算到同一位置且merge_id归并生效(测试 testing/test_fill_coincident.py 正是这一回归的守护:四条独立线段用 coincident 约束连成的正方形,端点位置差 < 1e-4、共享同一 weld ID、最终实测填充面积 = 16)。而相互重叠(overlap)的实体没有任何共享端点信息可依据,永远不会被读作一条路径——这正是文档警告的场景。

此外,管线在填充前还有一段_normalize_winding():Fill Curve 的 N-gons 模式按绕向(winding)判断内外环而非纯奇偶规则,反向环会丢失孔洞;而 Mesh to Curve 派生的绕向随边遍历顺序漂移(顶点索引一变就可能翻转,填充会在"环"与"实心面"之间乱跳)。该函数用鞋带公式(shoelace,sum(cross(P, P_next).z))计算每个 spline 的有向面积,为负的(顺时针)就反转,使填充确定。测试test_rectangle_with_inner_circle_fills_as_ring验证了"矩形内含圆 = 环形填充"的端到端结果。

6. 最佳实践:让实体结果稳定可预期的四条规则

文档"Best practices"一节的四条建议,结合上面的机制逐条解释:

  1. 用共享 start/end 点连接实体,而不是让两个独立点重合——凡是你打算填充或拉伸的地方。这是最符合焊接机制的连接方式:共享端点直接共享 weld ID,不依赖约束求解的数值重合,也不存在"两条路径没接上"的歧义。
  2. 把不想要出现在结果中的几何标记为 construction。转换管线会按construction属性在进网格之前整条删除它(见第 2.3 节),既不出现在结果里,也不参与填充。
  3. 构造几何很多时,放到单独的 sketch 中,并在同一 workplane 上用第二个 sketch 引用它——把"源草图"保持干净,减小compute_merge_ids()的连通域与求解负担。
  4. 开启 Fill 时画闭合路径,并避免路径相互重叠。Fill Curve 以环路为单位填充;重叠路径会因绕向归一化与内外环判定产生不可预期的实心/镂空结果(见第 5 节的_normalize_winding()背景)。

7. 小结:一套可以在 Blender 内自由取用的几何

回到文档的开篇结论:这套"sketch 纯源 + body 常规网格"的拆分,加上可应用、可重排的标准修改器堆栈,意味着 CAD Sketcher 的产物不是一座孤岛——它是任何 Blender 网格工具都能直接处理的几何,同时在你 Apply 之前始终参数化地跟随 sketch。理解Fill与Angular Resolution两个参数(及其偏好默认值链路)、掌握"共享端点连接、构造几何隔离、闭合且互不重叠的路径"这三条纪律,就能在精度、性能与可维护性之间做出有依据的取舍。

  • 桌面应用
  • 3D建模

【免费下载链接】CAD_Sketcher

Constraint-based geometry sketcher for blender

项目地址:https://gitcode.com/gh_mirrors/ca/CAD_Sketcher
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询