buildPaintOpsFromScene function
Implementation
List<PaintOp> buildPaintOpsFromScene(
CanvasSceneDocument doc,
ComputedScene computed,
) {
final ops = <PaintOp>[];
// Background
final backgroundRect = Rect2D.fromLTWH(
0,
0,
doc.artboardSize.w,
doc.artboardSize.h,
);
if (doc.backgroundOpacity > 0) {
switch (doc.backgroundFill) {
case CanvasFillNone():
break;
case CanvasFillSolid(:final color):
ops.add(
FillRectOp(
backgroundRect,
_applyOpacityToArgb(color, doc.backgroundOpacity),
),
);
break;
case CanvasFillGradient(:final grad):
ops.add(
FillRectGradientOp(
backgroundRect,
resolveLinearGradient(
grad,
doc.artboardSize,
opacity: doc.backgroundOpacity,
),
),
);
break;
}
}
for (final item in computed.drawList) {
final leafId = item.leafId;
final leaf = computed.nodeById[leafId];
if (leaf == null) continue;
final world = computed.worldById[leafId];
if (world == null) continue;
final (six: s6) = toABCDExy(world);
ops.add(SaveOp());
final set = SetTransformOp(s6[0], s6[1], s6[2], s6[3], s6[4], s6[5]);
if (!set.isIdentity) ops.add(set);
switch (leaf) {
case TextNode(data: final d):
final txt = spacedText(d.text, d.letterSpacing);
switch (d.fill) {
case CanvasFillSolid(:final color):
ops.add(
DrawTextOp(
text: txt,
family: d.fontFamily,
weight: d.fontWeight,
size: d.fontSize,
originBaselineCenter: const Vec2(0, 0),
gradient: null,
solid: color,
shadowOffset: d.shadowOffset,
),
);
break;
case CanvasFillGradient(:final grad):
final resolved = resolveLinearGradient(grad, doc.artboardSize);
ops.add(
DrawTextOp(
text: txt,
family: d.fontFamily,
weight: d.fontWeight,
size: d.fontSize,
originBaselineCenter: const Vec2(0, 0),
gradient: resolved,
// Provide a deterministic solid fallback for shadow rendering.
solid: _shadowColorFromFill(d.fill),
shadowOffset: d.shadowOffset,
),
);
break;
case CanvasFillNone():
// Should be impossible for TextData; skip defensively.
break;
}
break;
case IconNode(id: final id, data: final d):
final iconText = computed.iconTextById[id];
final iconPath = computed.iconPathIRById[id];
if (iconText != null) {
switch (d.fill) {
case CanvasFillSolid(:final color):
ops.add(
DrawTextOp(
text: iconText.glyph,
family: iconText.family,
weight: iconText.weight,
size: d.sizePx,
originBaselineCenter: const Vec2(0, 0),
gradient: null,
solid: color,
shadowOffset: d.shadowOffset,
),
);
break;
case CanvasFillGradient(:final grad):
final resolved = resolveLinearGradient(grad, doc.artboardSize);
ops.add(
DrawTextOp(
text: iconText.glyph,
family: iconText.family,
weight: iconText.weight,
size: d.sizePx,
originBaselineCenter: const Vec2(0, 0),
gradient: resolved,
// Deterministic shadow color derived from fill.
solid: _shadowColorFromFill(d.fill),
shadowOffset: d.shadowOffset,
),
);
break;
case CanvasFillNone():
// Should be impossible for icons; skip defensively.
break;
}
} else if (iconPath != null) {
// Unified mental model: apply CanvasIconData.fill to *path-based* icons too.
switch (d.fill) {
case CanvasFillNone():
// Should be impossible; safe no-op.
break;
case CanvasFillSolid():
// Ensure IR has fill enabled; some renderers treat fill=null as no-op.
final c = _shadowColorFromFill(d.fill);
final ir = (iconPath.style.fill == null)
? PathIR(iconPath.cmds, iconPath.style.copyWith(fill: c))
: iconPath;
ops.add(FillPathOp(ir));
break;
case CanvasFillGradient(:final grad):
final resolved = resolveLinearGradient(grad, doc.artboardSize);
// Ensure IR has fill enabled.
final seed = grad.color1;
final ir = (iconPath.style.fill == null)
? PathIR(iconPath.cmds, iconPath.style.copyWith(fill: seed))
: iconPath;
ops.add(FillPathGradientOp(ir, resolved));
break;
}
if (iconPath.style.stroke != null && iconPath.style.strokeWidth > 0) {
ops.add(StrokePathOp(iconPath));
}
}
break;
case ImageNode(id: final id):
final placement = computed.imagePlacementById[id];
if (placement != null) {
ops.add(DrawImageOp(id, placement.src, placement.dst));
}
break;
case PathNode(id: final id, data: final d):
final ir = computed.pathIRById[id];
if (ir == null) break;
switch (d.fill) {
case CanvasFillNone():
break;
case CanvasFillSolid():
if (ir.style.fill != null) ops.add(FillPathOp(ir));
break;
case CanvasFillGradient(:final grad):
final resolved = resolveLinearGradient(grad, doc.artboardSize);
ops.add(FillPathGradientOp(ir, resolved));
break;
}
if (ir.style.stroke != null && ir.style.strokeWidth > 0) {
ops.add(StrokePathOp(ir));
}
break;
default:
break;
}
ops.add(RestoreOp());
}
return ops;
}