跳转至

无法回抽材料的连续路径生成

结论先行:当材料不能回抽时,「连续」不是风格选择,而是可打印性的前提。 一条线到底可以用等值线串链或空间填充曲线做出来,但路线选错会让空移占比从 1% 跳到 97%; 而路径连续之后,真正的第二个瓶颈是段数——46000 条 0.1 mm 级小线段足以压垮运动队列。

一、约束从哪里来

回抽(retract)在常规 FDM 里承担三件事:断料、泄压、防止空移拉丝。这三件事在膏体、颗粒、高填充材料上全部失效,原因不同但结果一致:

常规 FDM 混凝土 / 膏体 / 高填充
回抽防流涎 回抽会把料柱拉断、把颗粒桥接在喉部 → 堵管、再启动时断流
G0 空移到任意位置 空移期间仍在出料或渗料 → 拉丝、拖尾、塌边
逐层独立打印,层间靠热熔粘接 层间必须机械连续过渡,冷缝(cold joint)是结构弱点
路径断裂只影响外观 路径断裂直接产生一次起停 → 起停点是强度最差的位置

因此对这类材料,路径规划的目标函数要改写成:

minimize  空移总长度(理想为 0)
subject to 全程挤出不中断、Z 单调不降、无回抽指令

注意最后一条「Z 单调不降」—— 它排除了所有需要下探的路径,也排除了普通切片器逐层回到同一 Z 的做法。

二、先定义「真连续」

这一条踩过坑:肉眼看图和判定路径断裂都是不可靠的。曾经有一份 918 万行的 G-code 被判定为「路径断断续续」,复查发现 G0 只有 353 条,且全部集中在开头 354 行(预热与就位),其余全是连续 G1;图上看到的虚线是绘图软件的降采样伪影。

所以判定必须走统计,不走视觉:

# 全文件扫空移指令,按行号分布判断是否只在开头
grep -n -E "^(G0|G1) .*[XY]" file.gcode | awk '{print $1}' | head
grep -c "^G0" file.gcode
指标 口径 合格线
空移占比 空移总长 / 路径总长 连续填充 < 2%;> 20% 说明串链没做
空移条数 每层 G0 计数 等值线族 0;空间填充曲线 ≤ 15
单条空移长度 最大值 不得超过特征尺寸的 20%
段长中位数 相邻点距离 与线宽同量级(0.5–1.0 mm 常见)
段长 p99 / max 分布尾部 不应出现 > 25 mm 的单段(除非是真实直边)

段长分布是区分「路径真碎了」和「预览器显示的假象」的唯一可靠手段:中位数正常、只有极少数长段 → 是抽稀伪影;中位数本身跳到几十毫米 → 路径真的被并线了。

口径本身也要固定,否则指标会互相打架。同一个「空移占比」,分母取「挤出总长」还是取「包围盒对角线 × 层数」,结果可以差 3 倍以上;工程上统一取挤出总长(G1 的 XY 位移累加),并且在报告里写明。同理,「每层 10 条空移」对 89 层的小件是 890 次起停,对大件可能只是噪声 —— 必须同时给绝对条数和占比。

三、四条路线的实测对比

同一件几何、同一套工艺参数下的横向对比(枕形件 420×340×88 mm,线宽 1 mm、间距 10 mm、层高 1 mm、89 层):

方案 实现思路 空移 文件规模 形态 结论
Cura GRID 连续化 扫描线 + 奇偶配对(0°/90° 两次扫描)→ 最近端点贪心串链 96.8%(6900+ 条/层,每条跨约 350 mm) 479 KB 横向平行网格、各向同性好 ❌ 平行线两端无天然连接点,几何上不可能真连续
阿基米德弦 + 朴素裁剪 r = a + bθ 螺旋族直接与轮廓求交 29 段 / 6682.8 mm(47%) — 平滑螺旋外观 ⚠️ 外观好,但不串联就废
费马螺旋(等值线同心) 等值线环层 + 环点串接 0/层 17.5 MB / 54 万行 / 449.5 m 同心螺旋、单向强度偏弱 ✅ 真零回抽
Hilbert 空间填充网格 网格单元按 Hilbert 序连续访问 约 10/层(89 层共 961 次 > 15 mm) 3.3 MB / 10.3 万行 / 436 m 正交网格蜿蜒、各向同性较好 ✅ 网格结构 + 连续的平衡点

这张表最重要的结论是几何互斥性:Cura 系网格填充(GRID / LINES / ZIGZAG)在源码层就是 generateLinearBasedInfill —— 扫描线加奇偶规则成对生成,GRID 只是做 0° 和 90° 两次扫描。平行线段的两端天生没有可连接的邻段,connectLines() 沿边界串线能救回一部分,但救不回「真连续」。

所以:要网格结构又要零空移,只能走空间填充曲线(Hilbert / Moore)或同心族(等值线 / 费马螺旋)。 前者强度各向同性更好,后者路径更短、外观更顺。

四、等值线路线的四个步骤

Step 1:隐式函数 → 等值线

把填充图案写成隐式曲面 f(x,y,z) = 0,在每层截面上提取等值线。以 Gyroid 为例:

k = 2 * math.pi / pitch
F = (np.sin(k*X)*np.cos(k*Y)
   + np.sin(k*Y)*np.cos(k*Z)
   + np.sin(k*Z)*np.cos(k*X))

from skimage import measure
contours = measure.find_contours(F, level=0)   # 每层一组不连续线段

注意 find_contours 对大网格(> 500×500)内存敏感,工程上要按层分块算,不要一次吃整个体素场。

Step 2:裁剪到截面轮廓

from shapely.geometry import LineString
clipped = LineString(pts).intersection(section_polygon)   # 只留轮廓内的段

裁剪后每层会变成 N 段互不相连的线段 —— 这就是「断」的源头,也是下一步要解决的问题。

Step 3:贪心串链(关键)

最近邻贪心把 N 段串成 1 条:每次从剩余段里挑端点离当前点最近的一段,必要时反转方向,并在跳接处插值补点。

def chain_segments(segments):
    remaining = segments.copy()
    path = list(remaining.pop(0).coords)

    while remaining:
        last = Point(path[-1])
        best = min(remaining, key=lambda s: min(
            last.distance(Point(s.coords[0])),
            last.distance(Point(s.coords[-1]))))

        # 端点更近的一侧作为入口 → 必要时反转
        pts = best.coords[::-1] if (last.distance(Point(best.coords[-1]))
              < last.distance(Point(best.coords[0]))) else best.coords

        # 跳接处按固定步长插值,保证挤出不断流
        d = hypot(last.x - pts[0][0], last.y - pts[0][1])
        for t in np.linspace(0, 1, max(int(d / 1.5), 1)):
            path.append(tuple(np.array(last) + t * (np.array(pts[0]) - np.array(last))))

        path.extend(pts)
        remaining.remove(best)
    return path

贪心不是最优解(本质是开放 TSP),但实测够用:等值线族本身局部连续,贪心的错误累积没有想象中大。

插值步长(示例里 1.5 mm)是唯一需要调的参数:太小 → 点密度暴增、段数翻倍;太大 → 过渡段变成一段长直线,材料在跳接处堆积成瘤。经验值是取线宽的 1–2 倍。

Step 4:螺旋上升,Z 连续

不分层,Z 在整层上线性插值,用斜坡代替台阶:

for li in range(layers):
    z0 = 2.0 + li * layer_h
    for i, (x, y) in enumerate(chained):
        progress = i / len(chained)
        z = z0 + layer_h * progress      # 无台阶,层间是连续斜坡
        steps.append(Point(x, y, z))

这样整件是一个螺旋,没有一次 Z 抬升,也没有一次回抽。

Step 5:出码

import fullcontrol as fc
controls = fc.GcodeControls(printer_name='generic')
gcode = fc.transform(steps, 'gcode', controls)

工具链分工:

工具 用途
trimesh 读 STL、截截面轮廓
shapely 几何布尔、intersection 裁剪
scikit-image find_contours 等值线提取
numpy 网格与插值计算
fullcontrol 点序列 → G-code
matplotlib 路径可视化(注意抽稀伪影)

五、拐角与接缝怎么处理

连续路径没有「回抽保护」,所以每一次转向都是工艺事件。三类接缝各有各的处理法:

接缝类型 失效表现 处理法 实测效果
同层线段跳接 过渡段材料堆积 按线宽 1–2 倍步长插值补点 可接受;距离大时仍会堆
环 → 环换环 从当前点直连下一环任意起点 → 横穿零件 环加密到 1 mm 采样 + 把下一环旋转到距当前点最近的顶点起步 换环段从最长 370 mm 降到 10.00–14.14 mm,无 > 30 mm 段
螺纹起点(径向接缝) 所有换环点落在同一角度 → 叠成一条径向伤疤 需环间切向过渡的真螺旋(FillSpiralInset 那一类做法) 未解决,是同心底噪的固有缺陷

第三条是目前的天花板:同心族能做到零空移,但做不到零接缝。要彻底消掉径向接缝,路径本身的拓扑必须是螺旋而不是同心环 —— 这也是费马螺旋在结构件上比同心等值线更受欢迎的原因。

另一个几何上的取舍:等值线族做轮廓裁剪时,按点采样裁 vs 按网格单元裁结果差一个数量级。同一件几何按点裁剪得到 556 段 + 544 条跳线(失败);改成「以格心是否在轮廓内判定保留整格,再按 Hilbert 序号排序」后,相邻格天然在路径上相邻,只剩块缝约 12 条跳线/层。

拐角的速度—挤出同步

连续路径里没有回抽可以「清账」,压力只能在拐角处自己消化。所以拐角必须做速度降 + 挤出量同比降,两者是同一个动作的两面:

单位长度挤出量  E/mm = A_cross / A_filament
A_cross ≈ 线宽 × 层高(矩形近似)

线宽 1.0 mm × 层高 1.0 mm → A_cross = 1.0 mm²
2.85 mm 料线 → A_filament = 6.38 mm²
⇒ E/mm = 0.157 mm 料 / mm 路径

关键点:E 必须按弧长积分,而不是按时间给。同一段路径上速度从 100 mm/s 降到 50 mm/s,若 E 按时间恒定,则单位长度的料量翻倍 —— 拐角处立刻堆瘤。反过来,若为了美观让速度恒定而只改 E,则在降速段会欠挤出、后面直段突然过挤。两个量必须同时按同一比例缩放。

实践口径:拐角内外侧弧长差越大,允许的速度上限越低。常用起点是拐角处速度 ≤ 直段的 50%,且 E 与速度同比缩放;这个比例在小线宽下通常看不出堆积,线宽越大(> 2 mm)越要保守,建议按实际料条截面回算一次 E/mm。

六、参数化生成 vs 自动生成

想把「波数、振幅、上下半径、中部鼓形」这类形体做成路径,有两条完全不同的路:

维度 函数直接生成(Grasshopper / Python) 网格 + 切片
形体来源 r = f(θ, z),参数一改路径立刻重生 必须先重建 STL,波纹只能靠几何硬表达
工艺约束 可直接写进函数(最大斜率、丝宽、层高限幅) 约束只能事后在 G-code 上补
连续性 天然可以做成单一连续螺旋 靠串链算法事后缝合,成功率看图案
修改成本 改一个数字,秒级 重建网格 + 重切片,分钟级
适用 装饰件、渐变壁厚、异形壳体、大尺度建筑构件 通用零件、需要精确内外轮廓的场合

参数化那条路的典型配置是每层对 θ 采样 500 点、层高 4 mm 往上堆 —— 500 点/层已经足够平滑,再密没有收益,只会推高段数。

出码三条支线:

路线 工具 出什么 适合
机器人专线 KUKA|prc、Robot Components、Machina、Adaxis AdaOne 机器人程序(KRL / RAPID / URScript),含姿态、奇异点、外轴 机械臂整机、百公斤级构件
G-code 专线 Silkworm(Grasshopper 插件)、FullControl、自写 Python G-code 普通龙门/FDM,多数「参数化打印」走这条
工业成品软件 Ai Build AiSync、Autodesk PowerMill / Fusion 机器人路径 + 过程控制 LFAM 量产

参数化路径有三个必踩的坑:

  1. 斜率约束:振幅 30 × 波数 14 × 层高 4 的条件下,相邻层的局部水平偏移可达十几毫米,远超挤出悬垂极限。必须按「层间偏移量」做参数化限幅 + 限速,否则外圈波浪一定塌。
  2. 挤出量必须按弧长积分:层长随 θ 连续变化,E 值若按固定流量给,波纹处必然欠挤出、直段处过挤。
  3. 连续性:这类形体最适合单一连续螺旋,而不是逐层闭合 —— 逐层闭合等于每层一次起停。

取舍判据:形体能用 3–5 个参数描述、且需要工艺约束参与几何 → 走函数路线;形体是扫描/设计出来的自由曲面 → 走切片 + 串链路线。两者不冲突,两段式打印就是混用(见 非平面与真 3D 路径)。

七、段数暴增对运动队列的压力

路径连续解决了材料问题,但把另一个问题放大了:段数。

一个曲面佛像的常规切片会产生约 46000 条 0.1 mm 级 G1 短线段(切片器把圆弧离散成短直线)。这个量级下,故障不在几何,而在运动控制栈的段切换路径上:

环节 表现 根因
段切换 偶发位置跳变、伺服报位置偏差 段管理竞态下 start_pos / ratio 取错
队列 丢段、位置基准漂移 队列满时按「丢最旧」处理,可能丢掉正在执行的段
锚定 位置直接锚到未来时刻 用队列头时刻而非执行时刻做锚点
护栏 速度上限被放行 基准陈旧 → lim = vmax × dt × 1.2 算出的限值高于轴 vmax

最隐蔽的一条是基准陈旧:段清理只重置了「是否有主机目标位置」的标志,没重置上一次的目标位置,残留值可以到 2.5 亿 counts(约 376 mm);再叠加锚点取的是队列头时刻,位置就直接锚到「执行点 + 队列深度 × 速度」。此时护栏算出 363 mm/s 而轴 vmax 只有 302 mm/s —— 跳变与报警都从这里来。

两种数值范式的差别,这是问题的本质:

维度 积分式(成熟 CNC 的做法) 查表式
位置计算 pos += vel × dt,每周期积分 tp = start_pos + dist × ratio
前提 只需一次锚定 每次段切换的 start_pos 都必须正确
段数据出错时 只产生微小偏差,自然收敛 直接跳到错值,甚至反向 → 位置跳变
队列策略 满则阻塞等实时消费;空则保持最后位置并置明确状态 满则丢最旧段
// 查表式(残留风险)
tp = (int32_t)llround(e->start_pos + dist * e->ratio);

// 积分式(每周期积分,不依赖段切换的正确性)
sl->pos_mm += (e->start_v + 0.5 * e->accel * t) * dt;
tp = (int32_t)llround(sl->pos_mm * sl->scale);

段数本身也可以从源头压:

手段 做法 效果 代价
A 固件侧圆弧 启用 gcode_arcs,把 resolution 调大 46000 → 几千段 治标;仍是短线段,只是变长
B 移植圆弧插补 实时环直接按角度积分算弧上点 治本 等于重写段流送协议
C 预处理合并 Arc Welder 把短线段合成 G2/G3,再由固件转较大 G1 段数减少约 90% 最快见效,需固件支持 G2/G3

gcode_arcs 有一个容易误解的点:它并不消除小线段,源码注释写得很清楚 —— 生成的坐标仍被转成 G1;resolution: 1.0 只是把 0.1 mm 段变成 1 mm 段。而且该模块在很多配置里默认没加载,直接发 G2/G3 会报 Unknown command。

真正该算的是段切换频率,不是段数:

段切换频率 f = v / L_seg
46000 段 × 0.1 mm = 4600 mm 路径总长

v = 100 mm/s, L_seg = 0.1 mm  → f = 1000 Hz
v = 100 mm/s, L_seg = 1.0 mm  → f = 100 Hz      (固件 resolution 调大)
v = 100 mm/s, L_seg = 1.5 mm  → f ≈ 66 Hz       (Arc Welder 合并后约 3000 条)

这个数量级说明两件事:一是固件/驱动的段切换路径每秒被调用上千次,任何一次竞态、任何一次基准陈旧都会被放大成可见的位置错误;二是把 L_seg 从 0.1 mm 抬到 1 mm 就能把频率降一个数量级 —— 这也是「A 方案治标但有效」的真实原因,不必把它看成纯粹的将就。

反过来,连续路径算法倾向于增加段数(插值补点、环加密到 1 mm),因此在同一台机器上,更连续的路径对运动栈的要求更高。两个问题要一起设计:先把路径做连续,再把段长做粗。

八、可落地的判定阈值

把上面所有实测收成一张表,用来决定一件活能不能走连续路径、以及该走哪条:

判据 阈值 不满足时的动作
材料能否回抽 不能 → 必须连续 能回抽就先用普通切片,别自找麻烦
图案类型 平行线式填充 → 不可能零空移 换 Hilbert / 等值线族 / 费马螺旋
空移占比 连续填充要求 < 2% 20% 以上说明串链缺失,先补串链再谈算法
单条空移长度 < 特征尺寸 20%(实测良好值 10–15 mm) 换环点没旋转到最近顶点
最小线间距 ≥ 0.8 mm(喷嘴 0.4 mm 时) 降填充密度或调单元尺寸,否则糊成一片
段长中位数 与线宽同量级 明显偏大 → 检查是否被预览器抽稀过
总段数 超过 2 万段/件要评估运动栈 上 Arc Welder 预处理或改用积分式 TP
层间水平偏移 超过丝宽 3–5 倍即需限速 参数化路径必须做斜率限幅

九、坑清单

  • 贪心串链不是最优解,但等值线族局部连续,实测误差可接受;不要为此上完整 TSP 求解器。
  • 插值过渡在长距离时必然堆积:跳接距离超过线宽 10 倍以上,宁可拆成两条路径也不要硬连。
  • find_contours 内存敏感:大网格要分块。
  • 剥层号/继承 Z:有些切片器只在 Z 变化时输出 Z 字,解析时若不继承上一个 Z,会把平面层几乎全部丢掉(实测丢 97% 的点)。
  • 预览器抽稀会伪造故障:按距离抽稀(例如 min_len 25)会把 0.55 mm 的真实段并成 25 mm 直线,看起来像「一堆大直线」。抽稀只允许发生在层间,不允许发生在层内。
  • 曲线层不能只看图:任何「路径碎了」的判断,先出段长分布和 G0 统计。

相关阅读