use std::collections::{BTreeMap, BTreeSet};
use zenith_core::{
Anchor, AnchorEdge, Dimension, Node, Page, PropertyValue, ResolvedToken, SafeZone, anchor_xy,
dim_to_px, parse_anchor, parse_anchor_edge,
};
use super::util::resolve_geometry_px;
pub(crate) type AnchorMap = BTreeMap<String, (f64, f64)>;
#[derive(Clone, Copy)]
struct PrePassEnv<'a> {
page_w: f64,
page_h: f64,
safe_zones: &'a [SafeZone],
resolved: &'a BTreeMap<String, ResolvedToken>,
}
#[derive(Clone, Copy)]
struct ParentCtx {
parent_box: Option<(f64, f64, f64, f64)>,
acc_dx: f64,
acc_dy: f64,
}
impl ParentCtx {
const ROOT: ParentCtx = ParentCtx {
parent_box: None,
acc_dx: 0.0,
acc_dy: 0.0,
};
}
pub(crate) fn build_anchor_map(
page: &Page,
page_w: f64,
page_h: f64,
resolved: &BTreeMap<String, ResolvedToken>,
) -> AnchorMap {
let env = PrePassEnv {
page_w,
page_h,
safe_zones: &page.safe_zones,
resolved,
};
let mut map = AnchorMap::new();
let scope: BTreeMap<&str, &Node> = page
.children
.iter()
.filter_map(|n| anchor_fields(n).map(|f| (f.id, n)))
.collect();
for node in sibling_topo_order(&page.children) {
collect_anchor(node, env, ParentCtx::ROOT, &scope, &mut map);
}
map
}
fn sibling_topo_order(children: &[Node]) -> Vec<&Node> {
let mut by_id: BTreeMap<&str, &Node> = BTreeMap::new();
for node in children {
if let Some(f) = anchor_fields(node) {
by_id.insert(f.id, node);
}
}
let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
let mut adjacency: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for (&id, node) in &by_id {
in_degree.entry(id).or_insert(0);
if let Some(f) = anchor_fields(node)
&& let Some(target) = f.anchor_sibling
&& target != id
&& by_id.contains_key(target)
{
adjacency.entry(target).or_default().insert(id);
*in_degree.entry(id).or_insert(0) += 1;
}
}
let mut ready: BTreeSet<&str> = in_degree
.iter()
.filter_map(|(&id, °)| (deg == 0).then_some(id))
.collect();
let mut emitted: Vec<&str> = Vec::with_capacity(by_id.len());
while let Some(&id) = ready.first() {
ready.remove(id);
emitted.push(id);
if let Some(deps) = adjacency.get(id) {
for &dep in deps {
if let Some(deg) = in_degree.get_mut(dep) {
*deg = deg.saturating_sub(1);
if *deg == 0 {
ready.insert(dep);
}
}
}
}
}
let mut order: Vec<&Node> = Vec::with_capacity(children.len());
let mut placed: BTreeSet<&str> = BTreeSet::new();
for &id in &emitted {
if let Some(&node) = by_id.get(id)
&& placed.insert(id)
{
order.push(node);
}
}
for node in children {
match anchor_fields(node) {
Some(f) if placed.contains(f.id) => {}
_ => order.push(node),
}
}
order
}
struct AnchorFields<'a> {
id: &'a str,
anchor: Option<&'a str>,
anchor_zone: Option<&'a str>,
anchor_sibling: Option<&'a str>,
anchor_parent: Option<bool>,
anchor_edge: Option<&'a str>,
anchor_gap: Option<&'a Dimension>,
x: Option<&'a PropertyValue>,
y: Option<&'a PropertyValue>,
w: Option<&'a PropertyValue>,
h: Option<&'a PropertyValue>,
}
fn anchor_fields(node: &Node) -> Option<AnchorFields<'_>> {
let f = match node {
Node::Rect(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Ellipse(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Text(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Code(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Image(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Frame(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Group(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Shape(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Table(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Field(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Toc(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Pattern(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Chart(n) => AnchorFields {
id: n.id.as_str(),
anchor: n.anchor.as_deref(),
anchor_zone: n.anchor_zone.as_deref(),
anchor_sibling: n.anchor_sibling.as_deref(),
anchor_parent: n.anchor_parent,
anchor_edge: n.anchor_edge.as_deref(),
anchor_gap: n.anchor_gap.as_ref(),
x: n.x.as_ref(),
y: n.y.as_ref(),
w: n.w.as_ref(),
h: n.h.as_ref(),
},
Node::Line(_)
| Node::Connector(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Footnote(_)
| Node::Instance(_)
| Node::Unknown(_) => return None,
};
Some(f)
}
fn px_box(
x: Option<&PropertyValue>,
y: Option<&PropertyValue>,
w: Option<&PropertyValue>,
h: Option<&PropertyValue>,
resolved: &BTreeMap<String, ResolvedToken>,
) -> Option<(f64, f64, f64, f64)> {
let x = resolve_geometry_px(x, resolved)?;
let y = resolve_geometry_px(y, resolved)?;
let w = resolve_geometry_px(w, resolved)?;
let h = resolve_geometry_px(h, resolved)?;
Some((x, y, w, h))
}
fn collect_anchor(
node: &Node,
env: PrePassEnv,
ctx: ParentCtx,
scope: &BTreeMap<&str, &Node>,
map: &mut AnchorMap,
) {
if let Some(fields) = anchor_fields(node) {
derive_entry(fields, env, ctx, scope, map);
}
match node {
Node::Frame(frame) => {
let frame_box = px_box(
frame.x.as_ref(),
frame.y.as_ref(),
frame.w.as_ref(),
frame.h.as_ref(),
env.resolved,
);
let child_ctx = ParentCtx {
parent_box: frame_box,
acc_dx: ctx.acc_dx,
acc_dy: ctx.acc_dy,
};
let child_scope: BTreeMap<&str, &Node> = frame
.children
.iter()
.filter_map(|n| anchor_fields(n).map(|f| (f.id, n)))
.collect();
for child in sibling_topo_order(&frame.children) {
collect_anchor(child, env, child_ctx, &child_scope, map);
}
}
Node::Group(group) => {
let group_x = resolve_geometry_px(group.x.as_ref(), env.resolved).unwrap_or(0.0);
let group_y = resolve_geometry_px(group.y.as_ref(), env.resolved).unwrap_or(0.0);
let child_dx = ctx.acc_dx + group_x;
let child_dy = ctx.acc_dy + group_y;
let group_box = resolve_geometry_px(group.w.as_ref(), env.resolved)
.zip(resolve_geometry_px(group.h.as_ref(), env.resolved))
.map(|(gw, gh)| (child_dx, child_dy, gw, gh));
let child_ctx = ParentCtx {
parent_box: group_box,
acc_dx: child_dx,
acc_dy: child_dy,
};
let child_scope: BTreeMap<&str, &Node> = group
.children
.iter()
.filter_map(|n| anchor_fields(n).map(|f| (f.id, n)))
.collect();
for child in sibling_topo_order(&group.children) {
collect_anchor(child, env, child_ctx, &child_scope, map);
}
}
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Shape(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Connector(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Table(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Unknown(_) => {}
}
}
fn cross_h(anchor: Option<Anchor>, sib_x: f64, sib_w: f64, node_w: f64) -> f64 {
match anchor {
None | Some(Anchor::TopLeft) | Some(Anchor::CenterLeft) | Some(Anchor::BottomLeft) => sib_x,
Some(Anchor::TopCenter) | Some(Anchor::Center) | Some(Anchor::BottomCenter) => {
sib_x + (sib_w - node_w) / 2.0
}
Some(Anchor::TopRight) | Some(Anchor::CenterRight) | Some(Anchor::BottomRight) => {
sib_x + sib_w - node_w
}
}
}
fn cross_v(anchor: Option<Anchor>, sib_y: f64, sib_h: f64, node_h: f64) -> f64 {
match anchor {
None | Some(Anchor::TopLeft) | Some(Anchor::TopCenter) | Some(Anchor::TopRight) => sib_y,
Some(Anchor::CenterLeft) | Some(Anchor::Center) | Some(Anchor::CenterRight) => {
sib_y + (sib_h - node_h) / 2.0
}
Some(Anchor::BottomLeft) | Some(Anchor::BottomCenter) | Some(Anchor::BottomRight) => {
sib_y + sib_h - node_h
}
}
}
fn derive_entry(
fields: AnchorFields<'_>,
env: PrePassEnv,
ctx: ParentCtx,
scope: &BTreeMap<&str, &Node>,
map: &mut AnchorMap,
) {
let AnchorFields {
id,
anchor: anchor_str,
anchor_zone: anchor_zone_str,
anchor_sibling,
anchor_parent,
anchor_edge: anchor_edge_str,
anchor_gap,
x: _,
y: _,
w: w_dim,
h: h_dim,
} = fields;
let edge = anchor_edge_str.and_then(parse_anchor_edge);
if anchor_str.is_none() && edge.is_none() {
return;
}
let anchor_parsed: Option<Anchor> = match anchor_str {
Some(s) => match parse_anchor(s) {
Some(a) => Some(a),
None => {
if edge.is_none() {
return;
}
None
}
},
None => None,
};
let (Some(node_w), Some(node_h)) = (
resolve_geometry_px(w_dim, env.resolved),
resolve_geometry_px(h_dim, env.resolved),
) else {
return;
};
if let Some(zone_id) = anchor_zone_str {
let anchor = match anchor_parsed {
Some(a) => a,
None => return,
};
let (ref_x, ref_y, ref_w, ref_h) = match env.safe_zones.iter().find(|z| z.id == zone_id) {
Some(zone) => match (
dim_to_px(zone.x.value, &zone.x.unit),
dim_to_px(zone.y.value, &zone.y.unit),
dim_to_px(zone.w.value, &zone.w.unit),
dim_to_px(zone.h.value, &zone.h.unit),
) {
(Some(zx), Some(zy), Some(zw), Some(zh)) => (zx, zy, zw, zh),
_ => return,
},
None => return,
};
let (ox, oy) = anchor_xy(anchor, ref_w, ref_h, node_w, node_h);
map.insert(id.to_owned(), (ref_x + ox, ref_y + oy));
return;
}
if let Some(sib_id) = anchor_sibling {
let Some(&sib_node) = scope.get(sib_id) else {
return;
};
let Some(sib) = anchor_fields(sib_node) else {
return;
};
let (Some(sib_w), Some(sib_h)) = (
resolve_geometry_px(sib.w, env.resolved),
resolve_geometry_px(sib.h, env.resolved),
) else {
return;
};
let entry = map.get(sib_id).copied();
let sib_x = resolve_geometry_px(sib.x, env.resolved).or(entry.map(|e| e.0));
let sib_y = resolve_geometry_px(sib.y, env.resolved).or(entry.map(|e| e.1));
let (Some(sib_x), Some(sib_y)) = (sib_x, sib_y) else {
return;
};
if let Some(edge) = edge {
let gap = anchor_gap
.and_then(|d| dim_to_px(d.value, &d.unit))
.unwrap_or(0.0);
let (x, y) = match edge {
AnchorEdge::Below => (
cross_h(anchor_parsed, sib_x, sib_w, node_w),
sib_y + sib_h + gap,
),
AnchorEdge::Above => (
cross_h(anchor_parsed, sib_x, sib_w, node_w),
sib_y - gap - node_h,
),
AnchorEdge::After => (
sib_x + sib_w + gap,
cross_v(anchor_parsed, sib_y, sib_h, node_h),
),
AnchorEdge::Before => (
sib_x - gap - node_w,
cross_v(anchor_parsed, sib_y, sib_h, node_h),
),
};
map.insert(id.to_owned(), (x, y));
return;
}
let anchor = match anchor_parsed {
Some(a) => a,
None => return,
};
let (ox, oy) = anchor_xy(anchor, sib_w, sib_h, node_w, node_h);
map.insert(id.to_owned(), (sib_x + ox, sib_y + oy));
return;
}
if edge.is_some() {
return;
}
let anchor = match anchor_parsed {
Some(a) => a,
None => return,
};
if anchor_parent == Some(true) {
let Some((rx, ry, rw, rh)) = ctx.parent_box else {
return;
};
let (ox, oy) = anchor_xy(anchor, rw, rh, node_w, node_h);
map.insert(id.to_owned(), (rx + ox - ctx.acc_dx, ry + oy - ctx.acc_dy));
return;
}
let (ox, oy) = anchor_xy(anchor, env.page_w, env.page_h, node_w, node_h);
map.insert(id.to_owned(), (ox, oy));
}