1use std::collections::{HashMap, HashSet, VecDeque};
2
3use crate::engine::box_tree::{BoxKind, BoxNode};
4use crate::engine::layout_pass::{BoxLayout, LayoutResult};
5use crate::engine::paint_pass::animated_transform;
6use crate::engine::renderer::GlyphMetric;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct NodeRef {
10 pub id: String,
11 pub prop: String,
12}
13
14pub fn scan_node_refs(src: &str) -> Vec<NodeRef> {
15 let bytes = src.as_bytes();
16 let mut out = Vec::new();
17 let mut i = 0usize;
18 while i + 4 <= bytes.len() {
19 let is_word_start = i == 0 || !is_ident_byte(bytes[i - 1]);
20 if is_word_start && &bytes[i..i + 4] == b"node" {
21 let after_ident = i + 4;
22 let is_word_end = bytes.get(after_ident).is_none_or(|&b| !is_ident_byte(b));
23 if is_word_end {
24 if let Some((node_ref, next)) = try_parse_node_call(src, after_ident) {
25 out.push(node_ref);
26 i = next;
27 continue;
28 }
29 }
30 }
31 i += 1;
32 }
33 out
34}
35
36fn is_ident_byte(b: u8) -> bool {
37 b.is_ascii_alphanumeric() || b == b'_'
38}
39
40fn skip_ws(bytes: &[u8], mut i: usize) -> usize {
41 while i < bytes.len() && (bytes[i] as char).is_whitespace() {
42 i += 1;
43 }
44 i
45}
46
47fn try_parse_node_call(src: &str, after_ident: usize) -> Option<(NodeRef, usize)> {
48 let bytes = src.as_bytes();
49 let mut i = skip_ws(bytes, after_ident);
50 if bytes.get(i) != Some(&b'(') {
51 return None;
52 }
53 i = skip_ws(bytes, i + 1);
54 let (id, next) = scan_string_literal(src, i)?;
55 i = skip_ws(bytes, next);
56 if bytes.get(i) != Some(&b',') {
57 return None;
58 }
59 i = skip_ws(bytes, i + 1);
60 let (prop, next) = scan_string_literal(src, i)?;
61 i = skip_ws(bytes, next);
62 if bytes.get(i) != Some(&b')') {
63 return None;
64 }
65 Some((NodeRef { id, prop }, i + 1))
66}
67
68fn scan_string_literal(src: &str, start: usize) -> Option<(String, usize)> {
69 let bytes = src.as_bytes();
70 if bytes.get(start) != Some(&b'"') {
71 return None;
72 }
73 let mut j = start + 1;
74 let mut s = String::new();
75 loop {
76 match bytes.get(j) {
77 None => return None,
78 Some(b'"') => return Some((s, j + 1)),
79 Some(b'\\') if bytes.get(j + 1) == Some(&b'"') => {
80 s.push('"');
81 j += 2;
82 }
83 Some(b'\\') if bytes.get(j + 1) == Some(&b'\\') => {
84 s.push('\\');
85 j += 2;
86 }
87 Some(_) => {
88 let ch = src[j..].chars().next()?;
89 s.push(ch);
90 j += ch.len_utf8();
91 }
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, thiserror::Error)]
97pub enum DepsError {
98 #[error("node(\"...\") dependency cycle: {}", .chain.join(" -> "))]
99 Cycle { chain: Vec<String> },
100 #[error(
101 "node \"{referencing}\" references unknown id \"{target}\" via node(\"{target}\", ...)"
102 )]
103 UnknownId { referencing: String, target: String },
104 #[error(
105 "node \"{referencing}\" references \"{target}\" from another scene \
106 (reference_cross_scene: scenes are independent render units)"
107 )]
108 CrossScene { referencing: String, target: String },
109 #[error("duplicate node id \"{0}\" declared more than once in the same scene")]
110 DuplicateId(String),
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Eq)]
114pub struct DepGraph {
115 order: Vec<String>,
116}
117
118impl DepGraph {
119 pub fn order(&self) -> &[String] {
120 &self.order
121 }
122
123 pub fn build(
124 nodes: &[(String, Vec<NodeRef>)],
125 other_scene_ids: &HashSet<String>,
126 ) -> Result<DepGraph, DepsError> {
127 let mut known: HashSet<&str> = HashSet::with_capacity(nodes.len());
128 for (id, _) in nodes {
129 if !known.insert(id.as_str()) {
130 return Err(DepsError::DuplicateId(id.clone()));
131 }
132 }
133
134 let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
135 let mut indegree: HashMap<&str, usize> =
136 nodes.iter().map(|(id, _)| (id.as_str(), 0)).collect();
137
138 for (id, refs) in nodes {
139 for r in refs {
140 if !known.contains(r.id.as_str()) {
141 if other_scene_ids.contains(&r.id) {
142 return Err(DepsError::CrossScene {
143 referencing: id.clone(),
144 target: r.id.clone(),
145 });
146 }
147 return Err(DepsError::UnknownId {
148 referencing: id.clone(),
149 target: r.id.clone(),
150 });
151 }
152 dependents
153 .entry(r.id.as_str())
154 .or_default()
155 .push(id.as_str());
156 *indegree.get_mut(id.as_str()).expect("id is known") += 1;
157 }
158 }
159
160 let mut queue: VecDeque<&str> = nodes
161 .iter()
162 .map(|(id, _)| id.as_str())
163 .filter(|id| indegree[id] == 0)
164 .collect();
165 let mut order: Vec<String> = Vec::with_capacity(nodes.len());
166 while let Some(id) = queue.pop_front() {
167 order.push(id.to_string());
168 if let Some(deps) = dependents.get(id) {
169 for &dep in deps {
170 let e = indegree.get_mut(dep).expect("dependent is known");
171 *e -= 1;
172 if *e == 0 {
173 queue.push_back(dep);
174 }
175 }
176 }
177 }
178
179 if order.len() != nodes.len() {
180 let resolved: HashSet<&str> = order.iter().map(|s| s.as_str()).collect();
181 let residual: Vec<&str> = nodes
182 .iter()
183 .map(|(id, _)| id.as_str())
184 .filter(|id| !resolved.contains(id))
185 .collect();
186 return Err(DepsError::Cycle {
187 chain: find_cycle_chain(&residual, nodes),
188 });
189 }
190
191 Ok(DepGraph { order })
192 }
193}
194
195fn find_cycle_chain(residual: &[&str], nodes: &[(String, Vec<NodeRef>)]) -> Vec<String> {
196 let deps: HashMap<&str, Vec<&str>> = nodes
197 .iter()
198 .map(|(id, refs)| (id.as_str(), refs.iter().map(|r| r.id.as_str()).collect()))
199 .collect();
200 let residual_set: HashSet<&str> = residual.iter().copied().collect();
201
202 let start = residual[0];
203 let mut path: Vec<&str> = vec![start];
204 let mut current = start;
205 loop {
206 let next = deps
207 .get(current)
208 .into_iter()
209 .flatten()
210 .find(|n| residual_set.contains(*n))
211 .copied();
212 match next {
213 Some(n) => {
214 if let Some(start_idx) = path.iter().position(|&x| x == n) {
215 let mut chain: Vec<String> =
216 path[start_idx..].iter().map(|s| s.to_string()).collect();
217 chain.push(n.to_string());
218 return chain;
219 }
220 path.push(n);
221 current = n;
222 }
223 None => {
224 return path.iter().map(|s| s.to_string()).collect();
225 }
226 }
227 }
228}
229
230#[derive(Debug, Clone, Default, PartialEq)]
231pub struct TextMetrics {
232 pub text_width: f32,
233 pub cap_height: f32,
234 pub ascender: f32,
235 pub baseline: f32,
236 pub glyphs: Vec<GlyphMetric>,
237}
238
239pub trait TextMetricsProvider {
240 fn text_metrics(
241 &self,
242 payload: &(dyn std::any::Any + Send + Sync),
243 content_box_width: f32,
244 ) -> Option<TextMetrics>;
245}
246
247pub struct NoTextMetrics;
248
249impl TextMetricsProvider for NoTextMetrics {
250 fn text_metrics(
251 &self,
252 _payload: &(dyn std::any::Any + Send + Sync),
253 _content_box_width: f32,
254 ) -> Option<TextMetrics> {
255 None
256 }
257}
258
259#[derive(Debug, Clone, Copy, Default)]
260struct Transform {
261 tx: f32,
262 ty: f32,
263 scale: f32,
264 rotation: f32,
265 opacity: f32,
266}
267
268#[derive(Debug, Clone)]
269pub struct ResolvedNode {
270 layout: BoxLayout,
271 transform: Transform,
272 text: Option<TextMetrics>,
273}
274
275impl ResolvedNode {
276 pub fn new(
277 layout: BoxLayout,
278 transform: (f32, f32, f32, f32, f32),
279 text: Option<TextMetrics>,
280 ) -> Self {
281 let (tx, ty, scale, rotation, opacity) = transform;
282 Self {
283 layout,
284 transform: Transform {
285 tx,
286 ty,
287 scale,
288 rotation,
289 opacity,
290 },
291 text,
292 }
293 }
294
295 pub fn prop(&self, prop: &str) -> Option<f64> {
296 match prop {
297 "x" => Some(self.layout.x as f64),
298 "y" => Some(self.layout.y as f64),
299 "width" => Some(self.layout.width as f64),
300 "height" => Some(self.layout.height as f64),
301 "cx" => Some(self.layout.cx() as f64),
302 "cy" => Some(self.layout.cy() as f64),
303 "right" => Some(self.layout.right() as f64),
304 "bottom" => Some(self.layout.bottom() as f64),
305 "tx" => Some(self.transform.tx as f64),
306 "ty" => Some(self.transform.ty as f64),
307 "scale" => Some(self.transform.scale as f64),
308 "rotation" => Some(self.transform.rotation as f64),
309 "opacity" => Some(self.transform.opacity as f64),
310 "textWidth" => self.text.as_ref().map(|t| t.text_width as f64),
311 "capHeight" => self.text.as_ref().map(|t| t.cap_height as f64),
312 "ascender" => self.text.as_ref().map(|t| t.ascender as f64),
313 "baseline" => self.text.as_ref().map(|t| t.baseline as f64),
314 "glyph_count" => self.text.as_ref().map(|t| t.glyphs.len() as f64),
315 _ => glyph_prop(self.text.as_ref(), prop),
316 }
317 }
318}
319
320fn glyph_prop(text: Option<&TextMetrics>, prop: &str) -> Option<f64> {
321 let text = text?;
322 if let Some(n) = prop.strip_prefix("glyph_x:") {
323 let idx: usize = n.parse().ok()?;
324 return text.glyphs.get(idx).map(|g| g.x as f64);
325 }
326 if let Some(n) = prop.strip_prefix("glyph_cx:") {
327 let idx: usize = n.parse().ok()?;
328 return text.glyphs.get(idx).map(|g| (g.x + g.width / 2.0) as f64);
329 }
330 None
331}
332
333pub fn snapshot_node(
334 node: &BoxNode,
335 layout: &LayoutResult,
336 viewport: (f32, f32),
337 text_provider: &dyn TextMetricsProvider,
338) -> Option<ResolvedNode> {
339 let box_layout = *layout.get(node.id)?;
340 let transform = animated_transform(&node.css, &box_layout, viewport);
341 let text = match &node.kind {
342 BoxKind::Component(payload) | BoxKind::Ghost(payload) => {
343 let (_, _, content_w, _) = box_layout.content_box();
344 text_provider.text_metrics(payload.as_ref(), content_w)
345 }
346 BoxKind::Container => None,
347 };
348 Some(ResolvedNode::new(box_layout, transform, text))
349}
350
351#[derive(Debug, Clone, Default)]
352pub struct ResolvedFrame {
353 nodes: HashMap<String, ResolvedNode>,
354}
355
356impl ResolvedFrame {
357 pub fn new() -> Self {
358 Self::default()
359 }
360
361 pub fn insert(&mut self, id: impl Into<String>, node: ResolvedNode) {
362 self.nodes.insert(id.into(), node);
363 }
364
365 pub fn node_prop(&self, id: &str, prop: &str) -> Option<f64> {
366 self.nodes.get(id)?.prop(prop)
367 }
368}
369
370pub struct FrameScope<'a>(pub &'a ResolvedFrame);
371
372impl crate::expr::Scope for FrameScope<'_> {
373 fn var(&self, _name: &str) -> Option<f64> {
374 None
375 }
376
377 fn node_prop(&self, id: &str, prop: &str) -> Option<f64> {
378 self.0.node_prop(id, prop)
379 }
380}
381
382#[cfg(test)]
383mod scan_tests {
384 use super::*;
385
386 #[test]
387 fn finds_a_single_reference() {
388 let refs = scan_node_refs(r#"= node("badge_3", "tx") + 1"#);
389 assert_eq!(
390 refs,
391 vec![NodeRef {
392 id: "badge_3".into(),
393 prop: "tx".into()
394 }]
395 );
396 }
397
398 #[test]
399 fn finds_multiple_references() {
400 let refs = scan_node_refs(r#"= node("a", "cx") - node("b", "cy")"#);
401 assert_eq!(
402 refs,
403 vec![
404 NodeRef {
405 id: "a".into(),
406 prop: "cx".into()
407 },
408 NodeRef {
409 id: "b".into(),
410 prop: "cy".into()
411 },
412 ]
413 );
414 }
415
416 #[test]
417 fn tolerates_whitespace_variations() {
418 let refs = scan_node_refs(r#"=node( "a" ,"tx" )"#);
419 assert_eq!(
420 refs,
421 vec![NodeRef {
422 id: "a".into(),
423 prop: "tx".into()
424 }]
425 );
426 }
427
428 #[test]
429 fn handles_escaped_quotes_in_the_id() {
430 let refs = scan_node_refs(r#"= node("a\"b", "x")"#);
431 assert_eq!(
432 refs,
433 vec![NodeRef {
434 id: "a\"b".into(),
435 prop: "x".into()
436 }]
437 );
438 }
439
440 #[test]
441 fn does_not_match_node_as_a_substring_of_a_longer_identifier() {
442 assert!(scan_node_refs(r#"= anode("a", "x") + nodeFoo("b", "y")"#).is_empty());
443 }
444
445 #[test]
446 fn no_reference_in_plain_arithmetic() {
447 assert!(scan_node_refs("= $W / 2 + cos($i)").is_empty());
448 }
449}
450
451#[cfg(test)]
452mod graph_tests {
453 use super::*;
454
455 fn nodes(pairs: &[(&str, &[(&str, &str)])]) -> Vec<(String, Vec<NodeRef>)> {
456 pairs
457 .iter()
458 .map(|(id, refs)| {
459 (
460 id.to_string(),
461 refs.iter()
462 .map(|(rid, prop)| NodeRef {
463 id: rid.to_string(),
464 prop: prop.to_string(),
465 })
466 .collect(),
467 )
468 })
469 .collect()
470 }
471
472 #[test]
473 fn independent_nodes_keep_declaration_order() {
474 let n = nodes(&[("a", &[]), ("b", &[]), ("c", &[])]);
475 let g = DepGraph::build(&n, &HashSet::new()).unwrap();
476 assert_eq!(g.order(), &["a", "b", "c"]);
477 }
478
479 #[test]
480 fn a_dependent_resolves_after_its_dependency_even_when_declared_first() {
481 let n = nodes(&[("line", &[("badge", "tx")]), ("badge", &[])]);
482 let g = DepGraph::build(&n, &HashSet::new()).unwrap();
483 let pos = |id: &str| g.order().iter().position(|x| x == id).unwrap();
484 assert!(pos("badge") < pos("line"));
485 }
486
487 #[test]
488 fn chain_of_three_resolves_in_dependency_order() {
489 let n = nodes(&[("c", &[("b", "x")]), ("b", &[("a", "x")]), ("a", &[])]);
490 let g = DepGraph::build(&n, &HashSet::new()).unwrap();
491 assert_eq!(g.order(), &["a", "b", "c"]);
492 }
493
494 #[test]
495 fn direct_two_node_cycle_is_reported_with_both_names() {
496 let n = nodes(&[("a", &[("b", "x")]), ("b", &[("a", "x")])]);
497 let err = DepGraph::build(&n, &HashSet::new()).unwrap_err();
498 match err {
499 DepsError::Cycle { chain } => {
500 assert!(chain.contains(&"a".to_string()));
501 assert!(chain.contains(&"b".to_string()));
502 assert_eq!(chain.first(), chain.last());
503 }
504 other => panic!("expected Cycle, got {other:?}"),
505 }
506 }
507
508 #[test]
509 fn three_node_cycle_is_reported() {
510 let n = nodes(&[
511 ("a", &[("b", "x")]),
512 ("b", &[("c", "x")]),
513 ("c", &[("a", "x")]),
514 ]);
515 let err = DepGraph::build(&n, &HashSet::new()).unwrap_err();
516 match err {
517 DepsError::Cycle { chain } => {
518 for id in ["a", "b", "c"] {
519 assert!(
520 chain.contains(&id.to_string()),
521 "chain missing {id}: {chain:?}"
522 );
523 }
524 }
525 other => panic!("expected Cycle, got {other:?}"),
526 }
527 }
528
529 #[test]
530 fn self_reference_is_a_one_node_cycle() {
531 let n = nodes(&[("a", &[("a", "x")])]);
532 let err = DepGraph::build(&n, &HashSet::new()).unwrap_err();
533 assert!(matches!(err, DepsError::Cycle { .. }));
534 }
535
536 #[test]
537 fn duplicate_id_in_the_same_scene_is_rejected() {
538 let n = nodes(&[("dup", &[]), ("other", &[]), ("dup", &[])]);
539 let err = DepGraph::build(&n, &HashSet::new()).unwrap_err();
540 match err {
541 DepsError::DuplicateId(id) => assert_eq!(id, "dup"),
542 other => panic!("expected DuplicateId, got {other:?}"),
543 }
544 }
545
546 #[test]
547 fn reference_to_an_id_declared_nowhere_is_unknown() {
548 let n = nodes(&[("a", &[("ghost", "x")])]);
549 let err = DepGraph::build(&n, &HashSet::new()).unwrap_err();
550 match err {
551 DepsError::UnknownId {
552 referencing,
553 target,
554 } => {
555 assert_eq!(referencing, "a");
556 assert_eq!(target, "ghost");
557 }
558 other => panic!("expected UnknownId, got {other:?}"),
559 }
560 }
561
562 #[test]
563 fn reference_to_an_id_from_another_scene_is_cross_scene() {
564 let n = nodes(&[("a", &[("other_scene_node", "x")])]);
565 let mut other = HashSet::new();
566 other.insert("other_scene_node".to_string());
567 let err = DepGraph::build(&n, &other).unwrap_err();
568 match err {
569 DepsError::CrossScene {
570 referencing,
571 target,
572 } => {
573 assert_eq!(referencing, "a");
574 assert_eq!(target, "other_scene_node");
575 }
576 other => panic!("expected CrossScene, got {other:?}"),
577 }
578 }
579}
580
581#[cfg(test)]
582mod resolution_tests {
583 use super::*;
584 use crate::expr::Expr;
585
586 fn layout(x: f32, y: f32, w: f32, h: f32) -> BoxLayout {
587 BoxLayout {
588 x,
589 y,
590 width: w,
591 height: h,
592 ..Default::default()
593 }
594 }
595
596 #[test]
597 fn geometry_family_reads_from_layout() {
598 let mut frame = ResolvedFrame::new();
599 frame.insert(
600 "box",
601 ResolvedNode::new(
602 layout(10.0, 20.0, 100.0, 50.0),
603 (0.0, 0.0, 1.0, 0.0, 1.0),
604 None,
605 ),
606 );
607 assert_eq!(frame.node_prop("box", "x"), Some(10.0));
608 assert_eq!(frame.node_prop("box", "y"), Some(20.0));
609 assert_eq!(frame.node_prop("box", "width"), Some(100.0));
610 assert_eq!(frame.node_prop("box", "height"), Some(50.0));
611 assert_eq!(frame.node_prop("box", "cx"), Some(60.0));
612 assert_eq!(frame.node_prop("box", "cy"), Some(45.0));
613 assert_eq!(frame.node_prop("box", "right"), Some(110.0));
614 assert_eq!(frame.node_prop("box", "bottom"), Some(70.0));
615 }
616
617 #[test]
618 fn animated_transform_family_reads_through() {
619 let mut frame = ResolvedFrame::new();
620 frame.insert(
621 "chip",
622 ResolvedNode::new(
623 layout(0.0, 0.0, 10.0, 10.0),
624 (12.5, -3.0, 1.5, 45.0, 0.8),
625 None,
626 ),
627 );
628 assert_eq!(frame.node_prop("chip", "tx"), Some(12.5));
629 assert_eq!(frame.node_prop("chip", "ty"), Some(-3.0));
630 assert_eq!(frame.node_prop("chip", "scale"), Some(1.5));
631 assert_eq!(frame.node_prop("chip", "rotation"), Some(45.0));
632 assert_eq!(frame.node_prop("chip", "opacity"), Some(0.8_f32 as f64));
633 }
634
635 #[test]
636 fn text_and_glyph_families_are_none_without_text_metrics() {
637 let mut frame = ResolvedFrame::new();
638 frame.insert(
639 "box",
640 ResolvedNode::new(
641 layout(0.0, 0.0, 10.0, 10.0),
642 (0.0, 0.0, 1.0, 0.0, 1.0),
643 None,
644 ),
645 );
646 assert_eq!(frame.node_prop("box", "textWidth"), None);
647 assert_eq!(frame.node_prop("box", "glyph_count"), None);
648 assert_eq!(frame.node_prop("box", "glyph_x:0"), None);
649 }
650
651 #[test]
652 fn text_and_glyph_families_read_through() {
653 let text = TextMetrics {
654 text_width: 88.0,
655 cap_height: 14.0,
656 ascender: 18.0,
657 baseline: 22.0,
658 glyphs: vec![
659 GlyphMetric {
660 x: 0.0,
661 width: 10.0,
662 },
663 GlyphMetric {
664 x: 10.0,
665 width: 12.0,
666 },
667 ],
668 };
669 let mut frame = ResolvedFrame::new();
670 frame.insert(
671 "sentence",
672 ResolvedNode::new(
673 layout(0.0, 0.0, 100.0, 30.0),
674 (0.0, 0.0, 1.0, 0.0, 1.0),
675 Some(text),
676 ),
677 );
678 assert_eq!(frame.node_prop("sentence", "textWidth"), Some(88.0));
679 assert_eq!(frame.node_prop("sentence", "capHeight"), Some(14.0));
680 assert_eq!(frame.node_prop("sentence", "ascender"), Some(18.0));
681 assert_eq!(frame.node_prop("sentence", "baseline"), Some(22.0));
682 assert_eq!(frame.node_prop("sentence", "glyph_count"), Some(2.0));
683 assert_eq!(frame.node_prop("sentence", "glyph_x:1"), Some(10.0));
684 assert_eq!(frame.node_prop("sentence", "glyph_cx:1"), Some(16.0));
685 assert_eq!(frame.node_prop("sentence", "glyph_x:5"), None);
686 }
687
688 #[test]
689 fn unresolved_reference_surfaces_as_unknown_ident_through_expr() {
690 let frame = ResolvedFrame::new();
691 let scope = FrameScope(&frame);
692 let expr = Expr::parse(r#"= node("missing", "x")"#).unwrap();
693 let err = expr.eval(&scope).unwrap_err();
694 assert!(matches!(err, crate::expr::ExprError::UnknownIdent(_)));
695 }
696
697 #[test]
698 fn expression_over_a_resolved_node_matches_hand_computed_value() {
699 let mut frame = ResolvedFrame::new();
700 frame.insert(
701 "chip3",
702 ResolvedNode::new(
703 layout(500.0, 300.0, 40.0, 40.0),
704 (77.0, -12.0, 1.0, 0.0, 1.0),
705 None,
706 ),
707 );
708 let scope = FrameScope(&frame);
709 let expr = Expr::parse(r#"= node("chip3", "cx") + node("chip3", "tx")"#).unwrap();
710 let got = expr.eval(&scope).unwrap();
711 assert_eq!(got, 520.0 + 77.0);
712 }
713
714 #[test]
715 fn orbiting_node_reference_tracks_every_frame_with_no_one_frame_lag() {
716 let n = vec![
717 (
718 "line".to_string(),
719 vec![
720 NodeRef {
721 id: "badge".to_string(),
722 prop: "tx".to_string(),
723 },
724 NodeRef {
725 id: "badge".to_string(),
726 prop: "ty".to_string(),
727 },
728 ],
729 ),
730 ("badge".to_string(), vec![]),
731 ];
732 let graph = DepGraph::build(&n, &HashSet::new()).unwrap();
733 assert_eq!(graph.order(), &["badge", "line"]);
734
735 let orbit_transform = |angle_deg: f32| -> (f32, f32, f32, f32, f32) {
736 let (s, c) = angle_deg.to_radians().sin_cos();
737 (c * 200.0, s * 200.0, 1.0, 0.0, 1.0)
738 };
739
740 for angle in [0.0f32, 37.0, 90.0, 181.0, 269.5, 350.0] {
741 let mut frame = ResolvedFrame::new();
742 for id in graph.order() {
743 match id.as_str() {
744 "badge" => {
745 frame.insert(
746 "badge",
747 ResolvedNode::new(
748 layout(0.0, 0.0, 20.0, 20.0),
749 orbit_transform(angle),
750 None,
751 ),
752 );
753 }
754 "line" => {
755 let scope = FrameScope(&frame);
756 let x2 = Expr::parse(r#"= node("badge", "tx")"#)
757 .unwrap()
758 .eval(&scope)
759 .unwrap();
760 let y2 = Expr::parse(r#"= node("badge", "ty")"#)
761 .unwrap()
762 .eval(&scope)
763 .unwrap();
764 frame.insert(
765 "line",
766 ResolvedNode::new(
767 layout(x2 as f32, y2 as f32, 0.0, 0.0),
768 (0.0, 0.0, 1.0, 0.0, 1.0),
769 None,
770 ),
771 );
772 }
773 other => panic!("unexpected id {other}"),
774 }
775 }
776
777 let (expected_tx, expected_ty, ..) = orbit_transform(angle);
778 let line = &frame.nodes["line"];
779 assert_eq!(
780 line.layout.x, expected_tx,
781 "angle {angle}: line.x2 lagged behind badge.tx"
782 );
783 assert_eq!(
784 line.layout.y, expected_ty,
785 "angle {angle}: line.y2 lagged behind badge.ty"
786 );
787 }
788 }
789
790 #[test]
791 fn resolving_in_declaration_order_instead_of_topological_order_fails_loudly() {
792 let mut frame = ResolvedFrame::new();
793 let scope = FrameScope(&frame);
794 let err = Expr::parse(r#"= node("badge", "tx")"#)
795 .unwrap()
796 .eval(&scope)
797 .unwrap_err();
798 assert!(matches!(err, crate::expr::ExprError::UnknownIdent(_)));
799
800 frame.insert(
801 "badge",
802 ResolvedNode::new(
803 layout(0.0, 0.0, 20.0, 20.0),
804 (99.0, 0.0, 1.0, 0.0, 1.0),
805 None,
806 ),
807 );
808 let scope = FrameScope(&frame);
809 let got = Expr::parse(r#"= node("badge", "tx")"#)
810 .unwrap()
811 .eval(&scope)
812 .unwrap();
813 assert_eq!(got, 99.0);
814 }
815}