///|
/// Shared DrawCommand lowering (ADR 0019). Renderers used to interpret the
/// 27 `@core.DrawCommand` variants themselves, each maintaining its own
/// transform/clip/opacity state stack. This module owns that interpretation
/// once: `lower_draw_commands` matches every variant and validates scope
/// balance, producing a neutral `DrawPlan` of `PlanOp`s that renderers
/// execute natively. Adding a DrawCommand variant now touches the core
/// contract plus this single lowering instead of four parallel interpreters.
///|
/// Neutral plan operation. Mirrors `@core.DrawCommand` payloads exactly so
/// every renderer can map ops to its native backend without semantic
/// re-interpretation.
pub(all) enum PlanOp {
Clear(@core.Color)
FillRect(@core.Rect, @core.Color)
StrokeRect(@core.Rect, @core.Color, Double)
FillRoundedRect(@core.RoundedRect, @core.Color)
StrokeRoundedRect(@core.RoundedRect, @core.Color, Double)
FillRoundedRectBrush(@core.RoundedRect, @core.Brush)
StrokeRoundedRectBrush(@core.RoundedRect, @core.Brush, Double)
DrawShadow(@core.ShadowSpec)
DrawText(@core.TextRun)
DrawImage(@core.ImageRun)
PushClip(@core.Rect)
PopClip
PushRoundedClip(@core.RoundedRect)
PopRoundedClip
PushTransform(@core.Transform2D)
PopTransform
PushOpacity(Double)
PopOpacity
PushLayer(@core.LayerSpec)
PopLayer
BeginRetainedLayer(@core.RetainedLayerSpec)
EndRetainedLayer(String)
PushFilter(@core.FilterEffect)
PopFilter
DrawPath(@core.PathSpec)
DrawShaderEffect(@core.ShaderEffectSpec)
} derive(Eq, Debug, ToJson)
///|
pub extend PlanOp with Debug::{to_repr}
///|
pub extend PlanOp with Eq::{not_equal, equal}
///|
pub extend PlanOp with ToJson::{to_json}
///|
/// A lowered draw stream: neutral ops plus diagnostics for structural
/// problems found during lowering (unbalanced clip/transform/opacity/layer/
/// filter scopes). Renderers execute `ops`; `diagnostics` is an additional
/// shared signal that does not change native execution.
pub(all) struct DrawPlan {
ops : Array[PlanOp]
diagnostics : Array[DrawPlanDiagnostic]
}
///|
pub(all) struct DrawPlanDiagnostic {
command : String
reason : String
} derive(Eq, Debug)
///|
pub extend DrawPlanDiagnostic with Debug::{to_repr}
///|
pub extend DrawPlanDiagnostic with Eq::{not_equal, equal}
///|
/// The scope kinds tracked for balance validation. Mirrors the state stacks
/// every renderer used to maintain independently.
pub(all) enum PlanScopeKind {
Clip
RoundedClip
Transform
Opacity
Layer
Filter
} derive(Eq)
///|
pub extend PlanScopeKind with Eq::{not_equal, equal}
///|
pub fn plan_scope_name(kind : PlanScopeKind) -> String {
match kind {
Clip => "clip"
RoundedClip => "rounded-clip"
Transform => "transform"
Opacity => "opacity"
Layer => "layer"
Filter => "filter"
}
}
///|
pub fn plan_scope_push_command(kind : PlanScopeKind) -> String {
match kind {
Clip => "PushClip"
RoundedClip => "PushRoundedClip"
Transform => "PushTransform"
Opacity => "PushOpacity"
Layer => "PushLayer"
Filter => "PushFilter"
}
}
///|
pub fn plan_scope_pop_command(kind : PlanScopeKind) -> String {
match kind {
Clip => "PopClip"
RoundedClip => "PopRoundedClip"
Transform => "PopTransform"
Opacity => "PopOpacity"
Layer => "PopLayer"
Filter => "PopFilter"
}
}
///|
/// The single interpretation of a `DrawCommand` stream: match every variant
/// once, maintain the transform/clip/opacity/layer/filter state stack, and
/// emit neutral ops plus balance diagnostics.
pub fn lower_draw_commands(commands : Array[@core.DrawCommand]) -> DrawPlan {
let ops : Array[PlanOp] = []
let diagnostics : Array[DrawPlanDiagnostic] = []
let scopes : Array[PlanScopeKind] = []
for command in commands {
match command {
Clear(color) => ops.push(Clear(color))
FillRect(rect, color) => ops.push(FillRect(rect, color))
StrokeRect(rect, color, width) => ops.push(StrokeRect(rect, color, width))
FillRoundedRect(rounded, color) =>
ops.push(FillRoundedRect(rounded, color))
StrokeRoundedRect(rounded, color, width) =>
ops.push(StrokeRoundedRect(rounded, color, width))
FillRoundedRectBrush(rounded, brush) =>
ops.push(FillRoundedRectBrush(rounded, brush))
StrokeRoundedRectBrush(rounded, brush, width) =>
ops.push(StrokeRoundedRectBrush(rounded, brush, width))
DrawShadow(shadow) => ops.push(DrawShadow(shadow))
DrawText(run) => ops.push(DrawText(run))
DrawImage(run) => ops.push(DrawImage(run))
PushClip(rect) => {
scopes.push(Clip)
ops.push(PushClip(rect))
}
PopClip => {
plan_pop_scope(scopes, diagnostics, Clip)
ops.push(PopClip)
}
PushRoundedClip(rounded) => {
scopes.push(RoundedClip)
ops.push(PushRoundedClip(rounded))
}
PopRoundedClip => {
plan_pop_scope(scopes, diagnostics, RoundedClip)
ops.push(PopRoundedClip)
}
PushTransform(transform) => {
scopes.push(Transform)
ops.push(PushTransform(transform))
}
PopTransform => {
plan_pop_scope(scopes, diagnostics, Transform)
ops.push(PopTransform)
}
PushOpacity(opacity) => {
scopes.push(Opacity)
ops.push(PushOpacity(opacity))
}
PopOpacity => {
plan_pop_scope(scopes, diagnostics, Opacity)
ops.push(PopOpacity)
}
PushLayer(layer) => {
scopes.push(Layer)
ops.push(PushLayer(layer))
}
PopLayer => {
plan_pop_scope(scopes, diagnostics, Layer)
ops.push(PopLayer)
}
BeginRetainedLayer(spec) => {
scopes.push(Layer)
ops.push(BeginRetainedLayer(spec))
}
EndRetainedLayer(key) => {
plan_pop_scope(scopes, diagnostics, Layer)
ops.push(EndRetainedLayer(key))
}
PushFilter(filter) => {
scopes.push(Filter)
ops.push(PushFilter(filter))
}
PopFilter => {
plan_pop_scope(scopes, diagnostics, Filter)
ops.push(PopFilter)
}
DrawPath(path) => ops.push(DrawPath(path))
DrawShaderEffect(spec) => ops.push(DrawShaderEffect(spec))
}
}
while scopes.length() > 0 {
let scope = scopes.pop().unwrap()
diagnostics.push({
command: plan_scope_push_command(scope),
reason: "push scope was never popped: \{plan_scope_name(scope)}",
})
}
{ ops, diagnostics, }
}
///|
fn plan_pop_scope(
scopes : Array[PlanScopeKind],
diagnostics : Array[DrawPlanDiagnostic],
expected : PlanScopeKind,
) -> Unit {
match scopes.pop() {
Some(actual) =>
if actual != expected {
diagnostics.push({
command: plan_scope_pop_command(expected),
reason: "pop did not match active \{plan_scope_name(actual)} scope",
})
}
None =>
diagnostics.push({
command: plan_scope_pop_command(expected),
reason: "pop has no matching push scope",
})
}
}