use std::collections::{BTreeMap, BTreeSet};
use crate::ast::node::Node;
use crate::diagnostics::Diagnostic;
use crate::tokens::ResolvedToken;
use node::shared::{node_rotate_deg, pv_to_dim, resolve_axis};
mod node;
pub(super) use node::shared::{
AnchorParentCtx, check_sibling_anchors, node_bbox, node_id_and_span, node_role,
};
#[derive(Clone, Copy)]
pub(super) struct WalkCtx<'a> {
pub(super) resolved_tokens: &'a BTreeMap<String, ResolvedToken>,
pub(super) declared_asset_ids: &'a BTreeSet<String>,
pub(super) declared_style_ids: &'a BTreeSet<String>,
pub(super) declared_component_ids: &'a BTreeSet<String>,
pub(super) component_local_ids: &'a BTreeMap<String, BTreeSet<String>>,
pub(super) all_node_ids: &'a BTreeSet<String>,
pub(super) zone_ids: &'a BTreeSet<&'a str>,
}
#[derive(Clone, Copy)]
pub(super) struct WalkPos {
pub(super) page_px_bounds: Option<(f64, f64)>,
pub(super) in_flow_parent: bool,
pub(super) enclosing_frame: Option<(f64, f64, f64, f64)>,
pub(super) in_container: bool,
pub(super) parent_box_known: bool,
}
pub(super) fn walk_node(
node: &Node,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
pos: WalkPos,
diagnostics: &mut Vec<Diagnostic>,
) {
if let Some((fx, fy, fw, fh)) = pos.enclosing_frame
&& let Some((page_w, page_h)) = pos.page_px_bounds
&& let Some((nx, ny, nw, nh)) = node_bbox(node, page_w, page_h)
{
const EPSILON: f64 = 0.5;
let over_left = nx < fx - EPSILON;
let over_top = ny < fy - EPSILON;
let over_right = nx + nw > fx + fw + EPSILON;
let over_bottom = ny + nh > fy + fh + EPSILON;
if over_left || over_top || over_right || over_bottom {
let (node_id, node_span) = node_id_and_span(node);
diagnostics.push(Diagnostic::advisory(
"frame.child_overflow",
format!(
"node '{}' (bbox {nx}, {ny}, {nw}, {nh}) protrudes beyond its \
enclosing frame (bbox {fx}, {fy}, {fw}, {fh})",
node_id
),
node_span,
Some(node_id.to_owned()),
));
}
}
let geom_required = !pos.in_flow_parent;
let parent_ctx = AnchorParentCtx {
in_container: pos.in_container,
parent_box_known: pos.parent_box_known,
};
if let Some((page_w, page_h)) = pos.page_px_bounds
&& let Some((nx, ny, nw, nh)) = node_bbox(node, page_w, page_h)
{
let (ax, ay, aw, ah) = match node_rotate_deg(node) {
Some(deg) if deg != 0.0 => {
let rad = deg.to_radians();
let cos = rad.cos();
let sin = rad.sin();
let cx = nx + nw / 2.0;
let cy = ny + nh / 2.0;
let hw = nw / 2.0;
let hh = nh / 2.0;
let locals: [(f64, f64); 4] = [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)];
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for (lx, ly) in locals {
let rx = cx + lx * cos - ly * sin;
let ry = cy + lx * sin + ly * cos;
min_x = min_x.min(rx);
min_y = min_y.min(ry);
max_x = max_x.max(rx);
max_y = max_y.max(ry);
}
(min_x, min_y, max_x - min_x, max_y - min_y)
}
_ => (nx, ny, nw, nh),
};
if ax < 0.0 || ay < 0.0 || ax + aw > page_w || ay + ah > page_h {
let (node_id, node_span) = node_id_and_span(node);
diagnostics.push(Diagnostic::advisory(
"layout.off_canvas",
format!(
"node '{}' extends outside the page bounds (0, 0, {page_w}, {page_h})",
node_id
),
node_span,
Some(node_id.to_owned()),
));
}
}
match node {
Node::Rect(r) => {
node::check_rect(
r,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Ellipse(e) => {
node::check_ellipse(
e,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Line(l) => {
node::check_line(l, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Text(t) => {
node::check_text(
t,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Code(c) => {
node::check_code(
c,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Image(img) => {
node::check_image(
img,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Shape(s) => {
node::check_shape(
s,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Pattern(p) => {
node::check_pattern(
p,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
let mut motif_seen: BTreeSet<String> = BTreeSet::new();
let mut motif_diags: Vec<Diagnostic> = Vec::new();
walk_node(
&p.motif,
ctx,
&mut motif_seen,
referenced_token_ids,
pos,
&mut motif_diags,
);
}
Node::Chart(c) => {
node::check_chart(
c,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
}
Node::Light(l) => {
node::check_light(
l,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
diagnostics,
);
}
Node::Mesh(m) => {
node::check_mesh(
m,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
diagnostics,
);
}
Node::Polygon(poly) => {
node::check_polygon(poly, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Polyline(poly) => {
node::check_polyline(poly, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Instance(inst) => {
node::check_instance(inst, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Field(field) => {
node::check_field(
field,
ctx,
seen_ids,
referenced_token_ids,
parent_ctx,
diagnostics,
);
}
Node::Toc(toc) => {
node::check_toc(
toc,
ctx,
seen_ids,
referenced_token_ids,
parent_ctx,
diagnostics,
);
}
Node::Footnote(footnote) => {
node::check_footnote(footnote, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Connector(c) => {
node::check_connector(c, ctx, seen_ids, referenced_token_ids, diagnostics);
}
Node::Frame(f) => {
node::check_frame(
f,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
let children_in_flow = matches!(f.layout.as_deref(), Some("flow") | Some("grid"));
let frame_box = match pos.page_px_bounds {
Some((page_w, page_h)) => pv_to_dim(f.x.as_ref())
.and_then(|d| resolve_axis(d, page_w))
.zip(pv_to_dim(f.y.as_ref()).and_then(|d| resolve_axis(d, page_h)))
.zip(pv_to_dim(f.w.as_ref()).and_then(|d| resolve_axis(d, page_w)))
.zip(pv_to_dim(f.h.as_ref()).and_then(|d| resolve_axis(d, page_h)))
.map(|(((x, y), w), h)| (x, y, w, h)),
None => None,
};
check_sibling_anchors(&f.children, diagnostics);
for child in &f.children {
walk_node(
child,
ctx,
seen_ids,
referenced_token_ids,
WalkPos {
page_px_bounds: pos.page_px_bounds,
in_flow_parent: children_in_flow,
enclosing_frame: frame_box,
in_container: true,
parent_box_known: true,
},
diagnostics,
);
}
}
Node::Group(g) => {
node::check_group(
g,
ctx,
seen_ids,
referenced_token_ids,
parent_ctx,
diagnostics,
);
let group_box_known = g.w.is_some() && g.h.is_some();
check_sibling_anchors(&g.children, diagnostics);
for child in &g.children {
walk_node(
child,
ctx,
seen_ids,
referenced_token_ids,
WalkPos {
page_px_bounds: pos.page_px_bounds,
in_flow_parent: false,
enclosing_frame: pos.enclosing_frame,
in_container: true,
parent_box_known: group_box_known,
},
diagnostics,
);
}
}
Node::Table(t) => {
node::check_table(
t,
ctx,
seen_ids,
referenced_token_ids,
geom_required,
parent_ctx,
diagnostics,
);
for row in &t.rows {
for cell in &row.cells {
for child in &cell.children {
walk_node(
child,
ctx,
seen_ids,
referenced_token_ids,
WalkPos {
page_px_bounds: pos.page_px_bounds,
in_flow_parent: true,
enclosing_frame: pos.enclosing_frame,
in_container: false,
parent_box_known: false,
},
diagnostics,
);
}
}
}
}
Node::Unknown(u) => {
node::check_unknown(u, seen_ids, diagnostics);
for child in &u.children {
walk_node(
child,
ctx,
seen_ids,
referenced_token_ids,
WalkPos {
page_px_bounds: pos.page_px_bounds,
in_flow_parent: true,
enclosing_frame: pos.enclosing_frame,
in_container: false,
parent_box_known: false,
},
diagnostics,
);
}
}
}
}