27 落笔
✅ 已验证 · 全部在 Krita 6.0.2.1 + PyQt6 + Xvfb 上实测
Node.paint*
node.paintLine(QPoint(x0, y0), QPoint(x1, y1), pressure0, pressure1)
node.paintPath(QPainterPath(...))
node.paintPolygon([QPointF(...), ...], strokeStyle, fillStyle)
node.paintRectangle(QRectF(...), strokeStyle, fillStyle)
node.paintEllipse(QRectF(...), strokeStyle, fillStyle)
node.paintAbility() # "PAINT" / "VECTOR" / "UNPAINTABLE"
用当前 brush preset + 当前前景色(view.setCurrentBrushPreset / setForeGroundColor / setBrushSize / setPaintingOpacity)。
- 画在你传的那个 node 上,与 active node 无关。
setActiveNode(L2)后n1.paintLine(...)→ n1 变 5438 px,L2 零变化。 - pressure 真的驱动 brush engine:两头收尖的 pressure 曲线,粗细剖面 14 → 29 → 29 → 25 px。段间接缝不可见,可以逐段
paintLine做 taper。 paintLine收QPoint(整数),paintPath收QPointF(浮点)。canvas.line()用分段paintPath同时拿到浮点精度和粗细变化。fillStyle默认"None",要填充传"ForegroundColor"。填充是二值的,不抗锯齿(边界带偏差 0.498)。- group 的
paintAbility()是"UNPAINTABLE"。
canvas.Session 的封装
| 方法 | 做什么 |
|---|---|
stroke(pts, pressures, node) | 逐段 paintLine,pressure 线性过渡 |
path(pts, node, smooth=True) | 一次 paintPath,quadTo 平滑 |
line(pts, width, node, segments=3) | 分 3 段 paintPath,每段换笔径。段数不是越多越好(5 段 aliased 2.6%,12 段 14.5%) |
dab(x, y, pressure, node) | 一个点 |
brush(color, size, opacity, flow, preset) | 不 pump 的快路径,相同值跳过 |
set_preset(name, size, opacity, flow) | 子串匹配,找不到打印警告;带 pump |
set_color(r, g, b) | 带 pump |
stroke() 与 path() 的选择要看几何:长平滑曲线上 path() 干净 23–47%;短线段上反过来(stroke() K90 0.032 最好,path() 0.0437 最差),点少时 quadTo 会改变形状。
吞吐
8192×12288,单层空白文档(probes/probe_stroke_budget.py):
| 操作 | 吞吐 |
|---|---|
paintLine(~300px) | 450–850 笔/秒,与 size 无关 |
paintPath(8 点) | 310–390 笔/秒 |
不 pump 换色 + paintLine | 605 笔/秒 |
带 pump 换色 + paintLine | 71 笔/秒 |
真实图层树上(307 节点):path() 297 笔/秒,stroke() 逐段 130。每次调用约 233 KB 撤销栈,清不掉——预算是内存(第 19 章)。
8K 上填一块边缘平滑的色块
四条路同题对比(probes/probe_fill.py,判据是边界带里 |覆盖率−解析真值|,完美约 0.02–0.08,二值约 0.5):
| 路子 | 边界带偏差 | 耗时 |
|---|---|---|
矢量层 addShapesFromSvg | 0.0828 | 1.5s |
select_mask(抗锯齿 mask)+ 42 笔 | 0.3305 | 6.4s |
select_mask(二值)+ 42 笔 | 0.3668 | 1.4s |
paintPolygon(fill) | 0.4981 | 0.8s |
| 不加约束大笔硬涂 | 0.4361 | — |
只有矢量层接近解析级。select_mask 留不住分数覆盖率,paintPolygon 是二值填充。
擦除
「用笔擦」做不到:view.setEraserMode(True)、setCurrentBlendingMode("erase")、node.setBlendingMode("erase") 对 paintLine 全部无效(擦掉 0 px)。唯一有效的是清 alpha(s.erase(box=, mask=))。
这条判错过一次:一次测量显示 erase 擦掉了 2198 px。那是读到过期像素的伪影——换成读到稳定再判,三种齐刷刷是 0。
异步读数
image 操作是异步的。waitForDone() + refreshProjection() + pump 不保证 pixelData() 已更新。判「有没有生效」要循环读到连续两次相同(s.settle() / s.stable())。读一次就下结论可能得到相反答案。