ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2D板材排版套料开源代码实战:从算法选型到利用率优化

2D板材排版套料开源代码实战:从算法选型到利用率优化

简介:这是一份面向制造业与CAD/CAM开发者的2D板材排版套料开源代码,聚焦激光切割、水切割等场景下的排样优化问题。项目以遗传算法为核心,通过选择、交叉与变异迭代求解板材利用率近似最优解,适合具备一定前端与算法基础、希望研究自动排样系统的技术人员。压缩包为rar格式,共88个文件、约2.53MB,以JavaScript源码为主体,辅以SVG矢量图形、字体文件(eot/woff/ttf/woff2)、CSS样式与HTML页面,另含少量配置与说明文档,整体结构接近一个可直接运行的Web应用。目前已有6261人学习下载。读者可从中获取完整的遗传算法排样实现、SVG图形解析与布局渲染逻辑、适应度评估与并行计算模块,以及可二次定制的工程骨架,便于理解算法落地细节并扩展切割工艺约束。

1. 2D板材排版套料开源代码:从一张钢板到一份可执行排样方案

车间里最常见的浪费不是切坏了,而是切剩了。一张 2440×1220 的板材,排得好能出 12 个件,排得差只能出 9 个,中间差的不是手艺,是算法。2D板材排版套料开源代码要解决的就是这件事:在给定板材尺寸、零件形状、数量、纹理方向和间距约束下,自动算出怎么摆最省料。它适合三类人——做激光切割/等离子下料的工艺员、想给自家 MES 或 CAM 加排样模块的开发者、以及做钣金家具或广告字牌的小厂技术负责人。你不需要从零推导几何算法,开源社区里已经有可跑的骨架,关键是知道选哪套、怎么接自己的数据、参数怎么调才不翻车。

2. 套料问题的数学骨架与开源方案选型

2.1 排样问题到底难在哪:NP-hard 与三类约束

2D 排样在学术上叫 2D Irregular Strip Packing 或 2D Bin Packing,前者允许零件旋转、板材无限长,后者板材固定、零件数量有限。钣金下料多数落在后者:板材尺寸固定,零件数量确定,目标是最小化板材张数或最大化利用率。它的难度在于组合爆炸——10 个零件有 10! 种排列,100 个零件就是天文数字,所以没有多项式时间的精确解,工程上全靠启发式加局部搜索。

约束分三类,必须先在代码里建模清楚:

  • 几何约束:零件之间不重叠、不超出板材边界、零件可旋转角度集合(常见 0°/90°,也有 0°/180° 或任意角)。
  • 工艺约束:零件间距(kerf 补偿)、边距、纹理方向锁定、共边切割是否允许。
  • 数量约束:每种零件需求数量、可混排的板材规格集合。

很多开源代码跑出来结果难看,不是算法差,是约束没建模对。比如忘了设间距,排出来零件贴在一起,实际切割直接报废。

2.2 开源方案怎么选:三类技术路线对比

目前能拿到的 2D 套料开源代码大致分三类,选型时按你的零件形状复杂度决定:

路线代表思路适合形状利用率上限上手难度
矩形装箱MaxRects / Guillotine全是矩形件85%~92%低
多边形 NestingNFP + 启发式任意多边形88%~95%中高
栅格/像素法位图布尔运算异形、含孔80%~88%中

矩形件占多数的钣金厂,直接用 MaxRects 类实现就够了,代码短、跑得快。异形件(比如法兰、支架)必须上 NFP(No-Fit Polygon,临界多边形)路线,核心是先算两两零件的不重叠可行域,再用启发式在可行域里选落点。栅格法实现简单但精度受网格分辨率限制,分辨率调高内存爆炸,调低边缘锯齿导致实际切不出来,一般只做快速预览。

我一般建议:先用矩形装箱跑通数据链路,确认 DXF 解析、数量汇总、结果导出没问题,再换 NFP 内核。这样出问题时能快速定位是数据层还是算法层。

2.3 最小可跑骨架:数据流与模块划分

一套能落地的套料代码,模块划分比算法本身更重要。常见做法是拆成四层:

# nesting_pipeline.py # 套料主流程骨架:解析 -> 预处理 -> 排样 -> 导出 from dxf_parser import load_parts # DXF 解析,输出多边形顶点 from preprocess import normalize, merge_duplicates # 单位统一、重复件合并 from nester import MaxRectsNester # 排样内核,可替换为 NFP from exporter import to_dxf, to_csv # 结果导出 def run(dxf_dir, sheet_w, sheet_h, gap, allow_rotate): parts = load_parts(dxf_dir) # 每个零件: {id, poly, qty, rotatable} parts = normalize(parts, unit="mm") # 统一到毫米,避免英寸混入 parts = merge_duplicates(parts) # 相同几何合并数量,减少搜索规模 nester = MaxRectsNester( sheet=(sheet_w, sheet_h), gap=gap, # 零件间距,等于割缝+安全余量 allow_rotate=allow_rotate # 是否允许 90 度旋转 ) layout = nester.pack(parts) to_dxf(layout, "out/nest_result.dxf") to_csv(layout, "out/report.csv") # 利用率、板材张数、每张摆放清单 return layout if __name__ == "__main__": run("./parts", 2440, 1220, gap=3.0, allow_rotate=True)

逻辑说明:load_parts负责把 DXF 里的 LWPOLYLINE、CIRCLE 等实体转成闭合多边形,这一步最容易出问题,圆弧离散精度直接决定后续几何计算是否稳定。normalize统一单位,很多翻车案例是图纸里混了英寸和毫米。merge_duplicates把几何相同的零件合并数量,能把搜索规模降一个量级。MaxRectsNester是内核,接口设计成pack(parts) -> layout,方便你后面换成 NFP 实现而不动上下游。

参数说明:gap不是随便填的,它等于割缝宽度加安全余量,激光切割一般 0.2~0.5mm 割缝,但热变形和定位误差要留 2~3mm,所以实际填 3.0 是常见值。allow_rotate对纹理件必须设 False,否则排出来纹理方向乱了,外观件直接报废。

3. 用 NFP 内核把异形件排样跑起来

3.1 NFP 的计算流程与关键几何操作

NFP 的核心思想:对零件 A 和 B,把所有让 B 与 A 刚好接触的位置连起来,形成一条闭合边界,B 的参考点落在这条边界外就说明不重叠。有了 NFP,排样就变成"在可行域里找最左下角或最省空间的落点"。

计算流程分四步:

  1. 多边形预处理:去自交、统一绕向(逆时针)、凸化分解(凹多边形拆成凸块)。
  2. 两两算 NFP:常用 Minkowski 和,凸多边形用顶点归并,凹多边形先分解再合并。
  3. 求可行域:板材边界内缩 gap,减去已放置零件的 NFP 并集。
  4. 选落点:按启发式(最低重心、最左、最小包围盒增量)在可行域顶点里挑。

凹多边形分解是性能瓶颈,也是精度陷阱。分解算法选不好,NFP 会出现自交,排出来的零件看着不重叠,实际有微小穿插,切割时才发现。

3.2 落点启发式与搜索顺序:利用率差 5% 的地方

同样的 NFP,落点策略不同,利用率能差 5 个百分点。常见启发式:

  • Bottom-Left:选最低再最左的可行点,实现简单,适合矩形件。
  • Bottom-Left-Fill:在 Bottom-Left 基础上优先填洞,异形件提升明显。
  • Minimum Bounding Box:选让整体包围盒增量最小的点,适合板材张数敏感场景。

搜索顺序同样关键。零件按面积从大到小排是常规做法,但纯面积排序会让小件最后没地方塞。我一般用"面积降序 + 长宽比分组",把细长件单独一批,避免它们在大件缝隙里卡住。

# nester.py 片段:落点选择与搜索顺序 def sort_parts(parts): # 面积降序,长宽比接近的归为一组,减少碎片 return sorted(parts, key=lambda p: (-p.area, abs(p.aspect - 1.0))) def select_position(feasible_points, placed, sheet): best = None for pt in feasible_points: # 优先最低,其次最左,最后看包围盒增量 score = (pt.y, pt.x, bbox_growth(pt, placed, sheet)) if best is None or score < best[0]: best = (score, pt) return best[1]

逻辑说明:sort_parts先按面积降序,再按长宽比与 1 的差值升序,让接近正方形的件优先,细长件靠后,减少它们制造窄缝的概率。select_position用元组比较实现多级排序,Python 里元组逐项比较,天然支持"最低优先、再最左、再包围盒增量最小"。

参数说明:feasible_points是可行域顶点集合,数量随零件数增长,超过 200 个零件时要做剪枝,只保留 Pareto 前沿上的点,否则单次选点就卡住。bbox_growth是包围盒增量,值越小说明这个落点让整体越紧凑。

3.3 结果校验:怎么确认排样真的能切

排样结果不能只看利用率数字,必须做几何校验。常见做法是写一个独立校验脚本,把排样结果里所有零件多边形做两两布尔交集,交集面积大于阈值就报错。

# verify.py:排样结果几何校验 from shapely.geometry import Polygon from shapely.ops import unary_union def verify(layout, sheet_w, sheet_h, gap, tol=1e-6): errors = [] for i, a in enumerate(layout.parts): pa = Polygon(a.poly) # 边界检查 if not Polygon([(0,0),(sheet_w,0),(sheet_w,sheet_h),(0,sheet_h)]).contains(pa): errors.append(f"零件 {a.id} 超出板材边界") # 重叠检查:间距不足也算错 for b in layout.parts[i+1:]: pb = Polygon(b.poly) inter = pa.buffer(gap/2).intersection(pb.buffer(gap/2)) if inter.area > tol: errors.append(f"零件 {a.id} 与 {b.id} 间距不足,重叠面积 {inter.area:.3f}") return errors

逻辑说明:用buffer(gap/2)把两个零件各外扩半个间距,再求交集,这样能把"间距不足"也纳入重叠检查,比单纯判重叠更严格。contains检查边界,注意浮点误差,必要时给板材边界加tol容差。

参数说明:tol是面积容差,设 1e-6 是防止浮点误差误报。gap要和排样时一致,否则校验标准对不上。这个脚本建议每次排样后自动跑,作为出图前的最后一道关。

4. 避坑:套料代码落地最常见的 5 个翻车点

4.1 圆弧离散精度不够导致零件"缺肉"

现象:排样结果看着正常,导出 DXF 后零件边缘是折线,圆弧处明显缺一块,切割出来尺寸超差。

原因:DXF 解析时把圆弧按固定段数离散,段数太少,曲率大的地方弦高误差大。很多开源解析器默认 16 段,对 R5 的小圆弧误差能到 0.1mm 以上。

解决:按弦高误差控制离散,而不是固定段数。弦高误差取 0.01~0.02mm,公式n = ceil(pi / acos(1 - tol/r)),r 越小段数越多。解析后立刻算多边形面积,和 DXF 里圆弧理论面积对比,偏差超过 0.5% 就报警。

4.2 单位混用:英寸图纸混进毫米板材

现象:排样利用率异常低,零件小得离谱,一张板上排了几百个件。

原因:部分 DXF 用英寸,解析后没换算,零件尺寸变成实际的 1/25.4,自然排得下。

解决:解析时读 DXF 的$INSUNITS头变量,为 1 是英寸、4 是毫米,按它换算。读不到就按包围盒尺寸猜:钣金件包围盒通常 10~2000mm,如果算出来普遍小于 10,大概率是英寸。统一在normalize里处理,下游不再关心单位。

4.3 间距设成 0 或只设割缝

现象:排样利用率很高,但切割后零件边缘有粘连,需要二次打磨。

原因:gap只填了割缝宽度(0.2mm),没留热变形和定位余量。激光切割热影响区、等离子切割挂渣、板材不平整都会吃掉余量。

解决:gap = 割缝 + 工艺余量,激光取 2~3mm,等离子取 4~6mm,火焰切割取 6~8mm。这个值按你的设备和材料试切确定,不要照搬别人的参数。

4.4 纹理方向没锁死,外观件报废

现象:不锈钢拉丝件排样后纹理方向不一致,成品外观有明显色差。

原因:allow_rotate=True让零件旋转了 90°,纹理跟着转。

解决:对纹理件单独分组,allow_rotate=False,并且限制旋转角度集合为{0}。如果必须混排,把纹理件排在同一张板上且方向一致,非纹理件排另一张。代码里给零件加grain_locked字段,排样前按它分组。

4.5 大零件优先导致小件无处可放

现象:前几张板排得很满,最后剩一堆小件,单独开一张板利用率只有 30%。

原因:纯面积降序让大件先占满,小件最后只能在剩余碎片里找位置,碎片不够就开新板。

解决:用"分层排样"——先排大件,预留一部分区域给小件;或者用"面积降序 + 小件预分配",按面积把零件分成大中小三档,每张板按比例混排。更简单的做法是最后一张板专门排小件,用 Bottom-Left-Fill 填洞,利用率能拉回 70% 以上。

5. 把利用率再压 3%:参数调优与批量验证的实操技巧

排样代码跑通只是起点,真正拉开差距的是参数调优和批量验证。我一般按这个顺序压利用率:

第一步,固定几何内核,只调搜索顺序。把sort_parts的排序键从纯面积换成"面积 + 长宽比 + 周长",跑 20 组真实订单,看平均利用率变化。周长大的件优先,能减少窄缝。这一步通常能提 1%~2%。

第二步,调落点启发式的权重。select_position里的(pt.y, pt.x, bbox_growth)元组顺序可以换,试试(bbox_growth, pt.y, pt.x),对板材张数敏感的场景更有效。不要凭感觉,写个批量脚本跑对比:

# batch_eval.py:批量对比不同参数组合的利用率 import itertools, csv from nesting_pipeline import run orders = ["order_01", "order_02", "order_03"] # 真实订单目录 strategies = [ {"sort": "area", "pos": "bottom_left"}, {"sort": "area_ar", "pos": "bottom_left"}, {"sort": "area_ar", "pos": "bbox_growth"}, ] results = [] for order, strat in itertools.product(orders, strategies): layout = run(f"./orders/{order}", 2440, 1220, gap=3.0, allow_rotate=True, strategy=strat) results.append({ "order": order, "strategy": strat, "util": layout.utilization, # 利用率 "sheets": layout.sheet_count, # 板材张数 }) with open("eval.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=["order","strategy","util","sheets"]) writer.writeheader() writer.writerows(results)

逻辑说明:用itertools.product遍历订单和策略组合,每个组合跑一次排样,记录利用率和板材张数。strategy字典传给run,由它分发给sort_parts和select_position。跑完看eval.csv,按订单分组对比,选每个订单的最优策略,或者选全局平均最优。

参数说明:gap固定 3.0 是为了公平对比,调策略时不要动工艺参数。util是面积利用率,sheets是板材张数,实际生产更关心sheets,因为每开一张板都是成本。如果两个策略利用率差 0.5% 但板材张数一样,选跑得快的那个。

第三步,做敏感性分析。把gap从 2 到 6 每隔 1mm 跑一遍,看利用率下降曲线。如果 gap 从 3 加到 4 利用率只掉 0.3%,说明你的排样对间距不敏感,可以适当加大 gap 换切割稳定性。如果掉 2% 以上,说明排样太紧凑,要考虑换内核或调搜索顺序。

第四步,建立回归测试集。挑 10~20 个有代表性的历史订单,每次改代码后跑一遍,记录利用率和板材张数。利用率掉超过 1% 或板材张数增加,就回滚。这个习惯能防止你为了优化某个订单把整体效果搞坏。

最后说个血泪经验:不要迷信利用率数字。我见过排样利用率 94% 但实际切割报废率 8% 的方案,原因是排得太紧,热变形导致相邻零件粘连。真正该盯的指标是"每张板的实际成品数",这个数要拿试切结果校准。排样代码是工具,工艺参数才是底线。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进