use super::rect::Rect;
use crate::layout::ir::PlacedNode;
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeKind {
Body,
Label,
}
#[derive(Clone, Debug)]
pub struct SceneNode {
pub path: String,
pub rect: Rect,
pub kind: NodeKind,
children: Vec<usize>,
}
pub struct SceneIndex {
nodes: Vec<SceneNode>,
roots: Vec<usize>,
by_path: BTreeMap<String, usize>,
}
impl SceneIndex {
pub fn build(roots: &[PlacedNode]) -> SceneIndex {
let mut idx = SceneIndex {
nodes: Vec::new(),
roots: Vec::new(),
by_path: BTreeMap::new(),
};
for r in roots {
let i = idx.walk(r, "", 0.0, 0.0);
idx.roots.push(i);
}
idx
}
fn walk(&mut self, n: &PlacedNode, prefix: &str, ox: f64, oy: f64) -> usize {
let (cx, cy) = (ox + n.cx, oy + n.cy);
let rect = Rect::new(
n.bbox.min_x + cx,
n.bbox.min_y + cy,
n.bbox.max_x + cx,
n.bbox.max_y + cy,
);
let (path, kind) = match &n.id {
Some(id) if prefix.is_empty() => (id.clone(), NodeKind::Body),
Some(id) => (format!("{prefix}.{id}"), NodeKind::Body),
None => (prefix.to_owned(), NodeKind::Label),
};
let i = self.nodes.len();
self.nodes.push(SceneNode {
path: path.clone(),
rect,
kind,
children: Vec::new(),
});
if kind == NodeKind::Body {
self.by_path.insert(path.clone(), i);
}
for c in &n.children {
let ci = self.walk(c, &path, cx, cy);
self.nodes[i].children.push(ci);
}
i
}
pub fn rect(&self, path: &str) -> Option<Rect> {
self.by_path.get(path).map(|&i| self.nodes[i].rect)
}
pub fn bounds(&self) -> Rect {
let mut rects = self.nodes.iter().map(|n| n.rect);
let first = rects.next().unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
rects.fold(first, |a, r| {
Rect::new(
a.x0.min(r.x0),
a.y0.min(r.y0),
a.x1.max(r.x1),
a.y1.max(r.y1),
)
})
}
pub fn contains(outer: &str, inner: &str) -> bool {
inner.len() > outer.len()
&& inner.starts_with(outer)
&& inner.as_bytes()[outer.len()] == b'.'
}
pub fn world_of(a: &str, b: &str) -> String {
if Self::contains(a, b) {
return a.to_owned();
}
if Self::contains(b, a) {
return b.to_owned();
}
let mut world = String::new();
for (sa, sb) in a.split('.').zip(b.split('.')) {
if sa != sb {
break;
}
if !world.is_empty() {
world.push('.');
}
world.push_str(sa);
}
if world == a || world == b {
match world.rfind('.') {
Some(i) => world.truncate(i),
None => world.clear(),
}
}
world
}
pub fn obstacle_rects(&self) -> Vec<Rect> {
self.nodes
.iter()
.filter(|n| {
!n.children
.iter()
.any(|&c| self.nodes[c].kind == NodeKind::Body)
})
.map(|n| n.rect)
.collect()
}
pub fn child_rects(&self, path: &str) -> Vec<Rect> {
let ids: &[usize] = if path.is_empty() {
&self.roots
} else {
match self.by_path.get(path) {
Some(&i) => &self.nodes[i].children,
None => &[],
}
};
ids.iter().map(|&i| self.nodes[i].rect).collect()
}
pub fn solid_rects_for(&self, endpoints: [&str; 2]) -> Vec<Rect> {
let mut out = Vec::new();
for &r in &self.roots {
self.gather(r, endpoints, false, &mut out);
}
out
}
fn gather(
&self,
i: usize,
endpoints: [&str; 2],
inside_endpoint: bool,
out: &mut Vec<Rect>,
) -> bool {
let n = &self.nodes[i];
let is_endpoint =
n.kind == NodeKind::Body && (n.path == endpoints[0] || n.path == endpoints[1]);
let mut inner = Vec::new();
let mut any_passable = false;
for &c in &n.children {
any_passable |= self.gather(c, endpoints, inside_endpoint || is_endpoint, &mut inner);
}
if is_endpoint || any_passable {
out.extend(inner);
return true;
}
if !(n.kind == NodeKind::Label && inside_endpoint) {
out.push(n.rect);
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::ir::{Bbox, PlacedNode};
use crate::resolve::{AttrMap, Markers, NodeKind};
use crate::span::Span;
fn node(
id: Option<&str>,
kind: NodeKind,
cx: f64,
cy: f64,
w: f64,
h: f64,
children: Vec<PlacedNode>,
) -> PlacedNode {
PlacedNode {
id: id.map(String::from),
kind,
type_chain: Vec::new(),
applied_styles: Vec::new(),
label: None,
attrs: AttrMap::default(),
own_style: AttrMap::default(),
markers: Markers::default(),
cx,
cy,
bbox: Bbox::centered(w, h),
rotation: 0.0,
children,
dividers: Vec::new(),
span: Span::empty(),
}
}
fn rect_node(id: &str, cx: f64, cy: f64, w: f64, h: f64) -> PlacedNode {
node(Some(id), NodeKind::Block, cx, cy, w, h, Vec::new())
}
fn scene() -> Vec<PlacedNode> {
let dog = rect_node("dog", 10.0, 5.0, 30.0, 10.0);
let garden = node(
Some("garden"),
NodeKind::Block,
100.0,
50.0,
80.0,
60.0,
vec![dog],
);
vec![rect_node("cat", 0.0, 0.0, 40.0, 20.0), garden]
}
#[test]
fn absolute_rects_accumulate_nested_offsets() {
let idx = SceneIndex::build(&scene());
assert_eq!(idx.rect("cat"), Some(Rect::new(-20.0, -10.0, 20.0, 10.0)));
assert_eq!(idx.rect("garden"), Some(Rect::new(60.0, 20.0, 140.0, 80.0)));
assert_eq!(
idx.rect("garden.dog"),
Some(Rect::new(95.0, 50.0, 125.0, 60.0))
);
assert_eq!(idx.rect("dog"), None);
}
#[test]
fn containment_follows_dot_path_ancestry() {
assert!(SceneIndex::contains("garden", "garden.dog"));
assert!(SceneIndex::contains("a.b", "a.b.c.d"));
assert!(!SceneIndex::contains("garden", "garden"));
assert!(!SceneIndex::contains("garden", "gardenia.dog"));
}
#[test]
fn world_is_the_innermost_shared_container() {
assert_eq!(SceneIndex::world_of("cat", "garden.dog"), "");
assert_eq!(SceneIndex::world_of("kitchen.bowl", "garden.dog"), "");
assert_eq!(SceneIndex::world_of("garden.dog", "garden.bird"), "garden");
assert_eq!(SceneIndex::world_of("a.b.x", "a.b.y"), "a.b");
assert_eq!(SceneIndex::world_of("garden", "garden.dog"), "garden");
assert_eq!(SceneIndex::world_of("garden.dog", "garden"), "garden");
assert_eq!(SceneIndex::world_of("x.y", "x.yz"), "x");
}
#[test]
fn child_rects_lists_one_collapsed_rect_per_direct_child() {
let idx = SceneIndex::build(&scene());
assert_eq!(
idx.child_rects(""),
vec![
Rect::new(-20.0, -10.0, 20.0, 10.0),
Rect::new(60.0, 20.0, 140.0, 80.0),
]
);
assert_eq!(
idx.child_rects("garden"),
vec![Rect::new(95.0, 50.0, 125.0, 60.0)]
);
assert_eq!(idx.child_rects("garden.dog"), Vec::new());
}
#[test]
fn solid_rects_collapse_non_endpoint_subtrees() {
let idx = SceneIndex::build(&scene());
assert_eq!(idx.solid_rects_for(["cat", "garden.dog"]), Vec::new());
assert_eq!(
idx.solid_rects_for(["cat", "cat"]),
vec![Rect::new(60.0, 20.0, 140.0, 80.0)]
);
}
#[test]
fn labels_block_inside_transparent_ancestors_but_not_inside_endpoints() {
let label = node(None, NodeKind::Text, 0.0, -25.0, 40.0, 10.0, Vec::new());
let dog = rect_node("dog", -15.0, 5.0, 20.0, 10.0);
let bird = rect_node("bird", 15.0, 5.0, 20.0, 10.0);
let garden = node(
Some("garden"),
NodeKind::Block,
0.0,
0.0,
80.0,
70.0,
vec![label, dog, bird],
);
let idx = SceneIndex::build(&[garden]);
assert_eq!(
idx.solid_rects_for(["garden.dog", "garden.bird"]),
vec![Rect::new(-20.0, -30.0, 20.0, -20.0)]
);
assert_eq!(
idx.solid_rects_for(["garden", "garden"]),
vec![
Rect::new(-25.0, 0.0, -5.0, 10.0),
Rect::new(5.0, 0.0, 25.0, 10.0)
]
);
}
#[test]
fn idd_text_is_a_body_not_a_label() {
let title = node(
Some("title"),
NodeKind::Text,
0.0,
0.0,
30.0,
10.0,
Vec::new(),
);
let idx = SceneIndex::build(&[title, rect_node("cat", 50.0, 0.0, 20.0, 10.0)]);
assert_eq!(idx.rect("title"), Some(Rect::new(-15.0, -5.0, 15.0, 5.0)));
assert_eq!(
idx.solid_rects_for(["cat", "cat"]),
vec![Rect::new(-15.0, -5.0, 15.0, 5.0)]
);
}
}