use std::collections::BTreeMap;
use zenith_core::{
Diagnostic, LineNode, Point, PolygonNode, PolylineNode, ResolvedToken, Span, Style, dim_to_px,
};
use crate::ir::{Paint, SceneCommand, StrokeAlign};
use super::super::RenderCtx;
use super::super::paint::{
apply_gradient_opacity, resolve_property_color, resolve_property_gradient,
};
use super::super::style_prop;
use super::super::util::{resolve_property_dimension_px, rotation_degrees, unsupported_unit_diag};
use super::common::resolve_dash_params;
pub(in crate::compile) fn compile_line(
line: &LineNode,
resolved: &BTreeMap<String, ResolvedToken>,
style_map: &BTreeMap<&str, &Style>,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
if line.visible == Some(false) {
return;
}
let (Some(x1d), Some(y1d), Some(x2d), Some(y2d)) = (&line.x1, &line.y1, &line.x2, &line.y2)
else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"line '{}' is missing one or more endpoint properties (x1, y1, x2, y2); \
skipped",
line.id
),
line.source_span,
Some(line.id.clone()),
));
return;
};
let Some(x1_raw) = dim_to_px(x1d.value, &x1d.unit) else {
diagnostics.push(unsupported_unit_diag(
"line",
&line.id,
"x1",
line.source_span,
));
return;
};
let Some(y1_raw) = dim_to_px(y1d.value, &y1d.unit) else {
diagnostics.push(unsupported_unit_diag(
"line",
&line.id,
"y1",
line.source_span,
));
return;
};
let Some(x2_raw) = dim_to_px(x2d.value, &x2d.unit) else {
diagnostics.push(unsupported_unit_diag(
"line",
&line.id,
"x2",
line.source_span,
));
return;
};
let Some(y2_raw) = dim_to_px(y2d.value, &y2d.unit) else {
diagnostics.push(unsupported_unit_diag(
"line",
&line.id,
"y2",
line.source_span,
));
return;
};
let x1 = x1_raw + ctx.dx;
let y1 = y1_raw + ctx.dy;
let x2 = x2_raw + ctx.dx;
let y2 = y2_raw + ctx.dy;
let stroke_prop = line
.stroke
.as_ref()
.or_else(|| style_prop(&line.style, style_map, "stroke"));
let Some(stroke_prop) = stroke_prop else {
return;
};
let Some(mut color) = resolve_property_color(stroke_prop, resolved, diagnostics, &line.id)
else {
return;
};
let node_opacity = line.opacity.unwrap_or(1.0).clamp(0.0, 1.0);
color.a = (color.a as f64 * node_opacity * ctx.opacity).round() as u8;
let sw = line
.stroke_width
.clone()
.or_else(|| style_prop(&line.style, style_map, "stroke-width").cloned());
let stroke_width: f64 = resolve_property_dimension_px(sw.as_ref(), resolved, 1.0);
let (stroke_dash, stroke_gap, stroke_linecap) = resolve_dash_params(
line.stroke_dash.as_ref(),
line.stroke_gap.as_ref(),
line.stroke_linecap.as_deref(),
resolved,
);
commands.push(SceneCommand::StrokeLine {
x1,
y1,
x2,
y2,
color,
stroke_width,
stroke_dash,
stroke_gap,
stroke_linecap,
});
}
fn resolve_flat_points(
points: &[Point],
node_kind: &str,
node_id: &str,
source_span: Option<Span>,
ctx: RenderCtx,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<Vec<f64>> {
let mut flat: Vec<f64> = Vec::with_capacity(points.len() * 2);
for (idx, pt) in points.iter().enumerate() {
let (Some(xd), Some(yd)) = (&pt.x, &pt.y) else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"{} '{}' point[{}] is missing x or y coordinate; skipped",
node_kind, node_id, idx
),
source_span,
Some(node_id.to_owned()),
));
return None;
};
let Some(px) = dim_to_px(xd.value, &xd.unit) else {
diagnostics.push(unsupported_unit_diag(
node_kind,
node_id,
"point x",
source_span,
));
return None;
};
let Some(py) = dim_to_px(yd.value, &yd.unit) else {
diagnostics.push(unsupported_unit_diag(
node_kind,
node_id,
"point y",
source_span,
));
return None;
};
flat.push(px + ctx.dx);
flat.push(py + ctx.dy);
}
Some(flat)
}
pub(in crate::compile) fn compile_polygon(
poly: &PolygonNode,
resolved: &BTreeMap<String, ResolvedToken>,
style_map: &BTreeMap<&str, &Style>,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
if poly.visible == Some(false) {
return;
}
let Some(flat_points) = resolve_flat_points(
&poly.points,
"polygon",
&poly.id,
poly.source_span,
ctx,
diagnostics,
) else {
return;
};
if flat_points.len() < 6 {
return;
}
let node_opacity = poly.opacity.unwrap_or(1.0).clamp(0.0, 1.0);
let even_odd = poly.fill_rule.as_deref() == Some("evenodd");
let rot = rotation_degrees(poly.rotate.as_ref());
if let Some(angle) = rot {
let (cx, cy) = flat_points_centroid_center(&flat_points);
commands.push(SceneCommand::PushTransform {
angle_deg: angle,
cx,
cy,
});
}
let fill_prop = poly
.fill
.as_ref()
.or_else(|| style_prop(&poly.style, style_map, "fill"));
if let Some(fill_prop) = fill_prop {
let fill_op = node_opacity * ctx.opacity;
if let Some(mut gradient) = resolve_property_gradient(fill_prop, resolved, &poly.id) {
apply_gradient_opacity(&mut gradient, fill_op, 1.0);
commands.push(SceneCommand::FillPolygon {
points: flat_points.clone(),
paint: Paint::Gradient(gradient),
even_odd,
});
} else if let Some(mut color) =
resolve_property_color(fill_prop, resolved, diagnostics, &poly.id)
{
color.a = (color.a as f64 * fill_op).round() as u8;
commands.push(SceneCommand::FillPolygon {
points: flat_points.clone(),
paint: Paint::solid(color),
even_odd,
});
}
}
let stroke_prop = poly
.stroke
.as_ref()
.or_else(|| style_prop(&poly.style, style_map, "stroke"));
if let Some(stroke_prop) = stroke_prop
&& let Some(mut color) =
resolve_property_color(stroke_prop, resolved, diagnostics, &poly.id)
{
color.a = (color.a as f64 * node_opacity * ctx.opacity).round() as u8;
let sw = poly
.stroke_width
.clone()
.or_else(|| style_prop(&poly.style, style_map, "stroke-width").cloned());
let stroke_width = resolve_property_dimension_px(sw.as_ref(), resolved, 1.0);
let align = match poly.stroke_alignment.as_deref() {
Some("inside") => StrokeAlign::Inside,
Some("outside") => StrokeAlign::Outside,
_ => StrokeAlign::Center,
};
commands.push(SceneCommand::StrokePolyline {
points: flat_points,
color,
stroke_width,
closed: true,
align,
fill_even_odd: even_odd,
});
}
if rot.is_some() {
commands.push(SceneCommand::PopTransform);
}
}
pub(in crate::compile) fn compile_polyline(
poly: &PolylineNode,
resolved: &BTreeMap<String, ResolvedToken>,
style_map: &BTreeMap<&str, &Style>,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
if poly.visible == Some(false) {
return;
}
let Some(flat_points) = resolve_flat_points(
&poly.points,
"polyline",
&poly.id,
poly.source_span,
ctx,
diagnostics,
) else {
return;
};
if flat_points.len() < 4 {
return;
}
let node_opacity = poly.opacity.unwrap_or(1.0).clamp(0.0, 1.0);
let even_odd = poly.fill_rule.as_deref() == Some("evenodd");
let rot = rotation_degrees(poly.rotate.as_ref());
if let Some(angle) = rot {
let (cx, cy) = flat_points_centroid_center(&flat_points);
commands.push(SceneCommand::PushTransform {
angle_deg: angle,
cx,
cy,
});
}
let fill_prop = poly
.fill
.as_ref()
.or_else(|| style_prop(&poly.style, style_map, "fill"));
if let Some(fill_prop) = fill_prop {
let fill_op = node_opacity * ctx.opacity;
if let Some(mut gradient) = resolve_property_gradient(fill_prop, resolved, &poly.id) {
apply_gradient_opacity(&mut gradient, fill_op, 1.0);
commands.push(SceneCommand::FillPolygon {
points: flat_points.clone(),
paint: Paint::Gradient(gradient),
even_odd,
});
} else if let Some(mut color) =
resolve_property_color(fill_prop, resolved, diagnostics, &poly.id)
{
color.a = (color.a as f64 * fill_op).round() as u8;
commands.push(SceneCommand::FillPolygon {
points: flat_points.clone(),
paint: Paint::solid(color),
even_odd,
});
}
}
let stroke_prop = poly
.stroke
.as_ref()
.or_else(|| style_prop(&poly.style, style_map, "stroke"));
if let Some(stroke_prop) = stroke_prop
&& let Some(mut color) =
resolve_property_color(stroke_prop, resolved, diagnostics, &poly.id)
{
color.a = (color.a as f64 * node_opacity * ctx.opacity).round() as u8;
let sw = poly
.stroke_width
.clone()
.or_else(|| style_prop(&poly.style, style_map, "stroke-width").cloned());
let stroke_width = resolve_property_dimension_px(sw.as_ref(), resolved, 1.0);
commands.push(SceneCommand::StrokePolyline {
points: flat_points,
color,
stroke_width,
closed: false,
align: StrokeAlign::Center,
fill_even_odd: false,
});
}
if rot.is_some() {
commands.push(SceneCommand::PopTransform);
}
}
pub(super) fn flat_points_centroid_center(flat: &[f64]) -> (f64, f64) {
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for pair in flat.chunks_exact(2) {
let &[px, py] = pair else { continue };
if px < min_x {
min_x = px;
}
if px > max_x {
max_x = px;
}
if py < min_y {
min_y = py;
}
if py > max_y {
max_y = py;
}
}
if min_x.is_infinite() {
return (0.0, 0.0);
}
((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
}