1use std::cell::{Cell, Ref, RefCell};
2use std::rc::Rc;
3
4use kurbo::{Affine, BezPath, Point, Shape, Vec2};
5use oxframe_core::{BlendMode, Color, Gradient, Signal};
6use oxframe_text::{Lang, TextStyle};
7
8use crate::text::{CodeTheme, DiffFrame, RichSpan, Styled};
9
10use crate::draw::{DrawItem, Layer, LineCap, LineJoin, Primitive, Shadow};
11use crate::flex::{FlexBox, FlexMap};
12use crate::image::ImageSource;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct NodeId(
18 pub u32,
20);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Direction {
25 #[default]
27 Row,
28 Column,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum LayoutAlign {
35 Start,
37 #[default]
39 Center,
40 End,
42 Stretch,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Justify {
49 Start,
51 #[default]
53 Center,
54 End,
56 SpaceBetween,
58 SpaceAround,
60 SpaceEvenly,
62}
63
64#[doc(hidden)]
66pub enum NodeKind {
67 Group,
68 Camera {
71 zoom: Signal<f64>,
72 },
73 Rect {
74 width: Signal<f64>,
75 height: Signal<f64>,
76 radius: Signal<f64>,
77 },
78 Circle {
79 size: Signal<f64>,
80 start_angle: Signal<f64>,
82 end_angle: Signal<f64>,
83 pie: bool,
85 },
86 Polygon {
87 sides: Signal<f64>,
88 size: Signal<f64>,
89 },
90 Star {
91 points: Signal<f64>,
92 size: Signal<f64>,
93 inner: Signal<f64>,
95 },
96 Path {
97 data: Signal<BezPath>,
98 },
99 Line {
100 points: Signal<Vec<Vec2>>,
101 closed: bool,
102 smooth: bool,
104 start_arrow: bool,
105 end_arrow: bool,
106 arrow_size: Signal<f64>,
107 },
108 Txt {
109 text: Signal<String>,
110 spans: Signal<Vec<RichSpan>>,
112 font_size: Signal<f64>,
113 weight: Signal<f64>,
114 style: TextStyle,
115 morph: Signal<Option<(String, String)>>,
117 morph_t: Signal<f64>,
118 },
119 Code {
120 code: Signal<String>,
121 edit_from: Signal<String>,
123 edit_to: Signal<String>,
125 editing: Signal<bool>,
127 edit_t: Signal<f64>,
129 edit_flash: Signal<bool>,
131 lang: Lang,
132 theme: CodeTheme,
133 font_size: Signal<f64>,
134 style: TextStyle,
135 focus: Signal<Vec2>,
137 dim: Signal<f64>,
139 },
140 Img {
141 source: ImageSource,
142 width: Signal<f64>,
143 height: Signal<f64>,
144 },
145 Layout {
146 direction: Direction,
147 align: LayoutAlign,
148 justify: Justify,
149 wrap: bool,
151 columns: Option<u16>,
153 gap: Signal<f64>,
154 padding: Signal<f64>,
155 width: Signal<Option<f64>>,
156 height: Signal<Option<f64>>,
157 radius: Signal<f64>,
158 },
159 Grid {
160 width: Signal<f64>,
161 height: Signal<f64>,
162 spacing: Signal<f64>,
163 },
164 Svg {
167 width: Signal<f64>,
168 height: Signal<f64>,
169 },
170 Custom {
173 draw: crate::draw::DrawFn,
174 width: f64,
175 height: f64,
176 measure: Option<crate::node_api::SizeFn>,
179 state: Option<Rc<dyn std::any::Any>>,
181 type_name: &'static str,
183 },
184 Particles {
187 seed: u64,
188 gravity: Signal<f64>,
189 toward_camera: bool,
190 life: f64,
191 speed: f64,
192 radius: f64,
193 state: crate::sim::ParticlesState,
194 },
195 Video {
197 data: Rc<crate::video::VideoData>,
198 width: Signal<f64>,
199 height: Signal<f64>,
200 time: Signal<f64>,
202 rate: Signal<f64>,
204 radius: Signal<f64>,
206 },
207 Shader {
210 code: crate::shader::ShaderCode,
211 preset: crate::shader::Preset,
213 width: Signal<f64>,
214 height: Signal<f64>,
215 uniforms: Vec<crate::shader::Uniform>,
216 inputs: Vec<(String, crate::post::Input)>,
218 },
219}
220
221impl NodeKind {
222 pub fn name(&self) -> &'static str {
223 match self {
224 Self::Group => "Group",
225 Self::Camera { .. } => "Camera",
226 Self::Rect { .. } => "Rect",
227 Self::Circle { .. } => "Circle",
228 Self::Polygon { .. } => "Polygon",
229 Self::Star { .. } => "Star",
230 Self::Path { .. } => "Path",
231 Self::Line { .. } => "Line",
232 Self::Txt { .. } => "Txt",
233 Self::Code { .. } => "Code",
234 Self::Img { .. } => "Img",
235 Self::Layout { .. } => "Layout",
236 Self::Grid { .. } => "Grid",
237 Self::Svg { .. } => "Svg",
238 Self::Custom { type_name, .. } => type_name,
239 Self::Particles { .. } => "Particles",
240 Self::Shader { .. } => "Shader",
241 Self::Video { .. } => "Video",
242 }
243 }
244}
245
246#[doc(hidden)]
248pub struct NodeData {
249 pub id: NodeId,
250 pub name: Option<String>,
251 pub source: Option<String>,
253 pub parent: Option<NodeId>,
254 pub children: Vec<NodeId>,
255 pub kind: NodeKind,
256 pub x: Signal<f64>,
257 pub y: Signal<f64>,
258 pub rotation: Signal<f64>,
260 pub scale: Signal<Vec2>,
261 pub opacity: Signal<f64>,
262 pub fill: Signal<Color>,
263 pub stroke: Signal<Color>,
264 pub line_width: Signal<f64>,
265 pub start: Signal<f64>,
267 pub end: Signal<f64>,
268 pub fill_gradient: Signal<Option<Gradient>>,
270 pub stroke_gradient: Signal<Option<Gradient>>,
271 pub dash: Signal<Vec<f64>>,
273 pub dash_offset: Signal<f64>,
274 pub line_cap: Signal<LineCap>,
275 pub line_join: Signal<LineJoin>,
276 pub blur: Signal<f64>,
278 pub shadow_color: Signal<Color>,
279 pub shadow_blur: Signal<f64>,
280 pub shadow_offset: Signal<Vec2>,
281 pub clip: Signal<bool>,
283 pub blend: Signal<BlendMode>,
284 pub z_index: Signal<f64>,
286 pub anchor: Signal<Vec2>,
290 pub grow: Signal<f64>,
292 pub shrink: Signal<f64>,
293 pub basis: Signal<Option<f64>>,
294 pub margin: Signal<f64>,
295 pub align_self: Signal<Option<LayoutAlign>>,
296 pub place: Option<crate::place::Place>,
298 pub placed: Cell<(f64, f64)>,
300 pub effect: Option<crate::post::EffectData>,
302}
303
304impl NodeData {
305 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
308 if let NodeKind::Camera { zoom } = &self.kind {
309 let (x, y) = self.placed.get();
313 return Affine::translate(offset)
314 * Affine::rotate(-self.rotation.get().to_radians())
315 * Affine::scale(zoom.get().max(1e-6))
316 * Affine::translate((-x, -y));
317 }
318 let s = self.scale.get();
319 let a = self.anchor.get();
320 let (x, y) = self.placed.get();
321 Affine::translate(offset + Vec2::new(x, y))
322 * Affine::rotate(self.rotation.get().to_radians())
323 * Affine::scale_non_uniform(s.x, s.y)
324 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
325 }
326
327 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
329 match &self.kind {
330 NodeKind::Txt {
331 text,
332 spans,
333 font_size,
334 weight,
335 style,
336 morph,
337 morph_t,
338 } => {
339 let style = TextStyle {
340 size: font_size.get().max(0.1),
341 weight: weight.get().clamp(1.0, 1000.0),
342 ..style.clone()
343 };
344 if let Some((from, to)) = morph.get() {
345 let a = oxframe_text::layout(&from, &style);
346 let b = oxframe_text::layout(&to, &style);
347 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
348 return Some(TextDraw::Morph { glyphs, size });
349 }
350 let spans = spans.get();
351 Some(TextDraw::Styled(if spans.is_empty() {
352 crate::text::rich(&[RichSpan::new(text.get())], &style)
353 } else {
354 crate::text::rich(&spans, &style)
355 }))
356 }
357 NodeKind::Code {
358 code,
359 edit_from,
360 edit_to,
361 editing,
362 edit_t,
363 edit_flash,
364 lang,
365 theme,
366 font_size,
367 style,
368 focus,
369 dim,
370 } => {
371 let style = TextStyle {
372 size: font_size.get().max(0.1),
373 ..style.clone()
374 };
375 if editing.get() {
376 let (frame, _) = crate::text::layout_diff(
377 &edit_from.get(),
378 &edit_to.get(),
379 *lang,
380 crate::text::DiffView {
381 theme,
382 style: &style,
383 focus: focus.get(),
384 dim: dim.get(),
385 t: edit_t.get(),
386 flash: edit_flash.get(),
387 },
388 );
389 return Some(TextDraw::Diff(frame));
390 }
391 Some(TextDraw::Styled(crate::text::code(
392 &code.get(),
393 *lang,
394 theme,
395 &style,
396 focus.get(),
397 dim.get(),
398 )))
399 }
400 _ => None,
401 }
402 }
403
404 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
406 const TOLERANCE: f64 = 0.1;
407 match &self.kind {
408 NodeKind::Video { radius, .. } => {
409 let (w, h) = size;
410 Some(
411 kurbo::RoundedRect::new(
412 -w / 2.0,
413 -h / 2.0,
414 w / 2.0,
415 h / 2.0,
416 radius.get().max(0.0),
417 )
418 .to_path(TOLERANCE),
419 )
420 }
421 NodeKind::Group
422 | NodeKind::Svg { .. }
423 | NodeKind::Camera { .. }
424 | NodeKind::Img { .. }
425 | NodeKind::Txt { .. }
426 | NodeKind::Code { .. }
427 | NodeKind::Grid { .. }
428 | NodeKind::Custom { .. }
429 | NodeKind::Particles { .. }
430 | NodeKind::Shader { .. } => None,
431 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
432 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
433 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
434 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
435 Some(if r > 0.0 {
436 rect.to_rounded_rect(r).to_path(TOLERANCE)
437 } else {
438 rect.to_path(TOLERANCE)
439 })
440 }
441 NodeKind::Circle {
442 start_angle,
443 end_angle,
444 pie,
445 ..
446 } => {
447 let r = size.0.max(0.0) / 2.0;
450 let (a0, a1) = (start_angle.get(), end_angle.get());
451 let sweep = (a1 - a0).clamp(-360.0, 360.0);
452 if sweep.abs() >= 360.0 - 1e-9 {
453 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
454 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
455 }
456 let arc = kurbo::Arc::new(
457 (0.0, 0.0),
458 (r, r),
459 (a0 - 90.0).to_radians(),
460 sweep.to_radians(),
461 0.0,
462 );
463 let mut p = BezPath::new();
464 if *pie {
465 p.move_to((0.0, 0.0));
466 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
467 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
468 p.close_path();
469 } else {
470 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
471 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
472 }
473 Some(p)
474 }
475 NodeKind::Polygon { sides, .. } => {
476 let n = sides.get().round().max(3.0) as usize;
477 Some(regular(n, size.0.max(0.0) / 2.0, None))
478 }
479 NodeKind::Star { points, inner, .. } => {
480 let n = points.get().round().max(2.0) as usize;
481 let r = size.0.max(0.0) / 2.0;
482 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
483 }
484 NodeKind::Path { data } => Some(data.get()),
485 NodeKind::Line {
486 points,
487 closed,
488 smooth,
489 ..
490 } => {
491 let pts = points.get();
492 if *smooth && pts.len() > 2 {
493 return Some(catmull_rom(&pts, *closed));
494 }
495 let mut p = BezPath::new();
496 for (i, v) in pts.iter().enumerate() {
497 if i == 0 {
498 p.move_to(v.to_point());
499 } else {
500 p.line_to(v.to_point());
501 }
502 }
503 if *closed && pts.len() > 2 {
504 p.close_path();
505 }
506 Some(p)
507 }
508 }
509 }
510}
511
512fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
514 let count = if inner.is_some() { 2 * n } else { n };
515 let mut p = BezPath::new();
516 for i in 0..count {
517 let radius = match inner {
518 Some(ri) if i % 2 == 1 => ri,
519 _ => r,
520 };
521 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
522 let pt = Point::new(a.cos() * radius, a.sin() * radius);
523 if i == 0 {
524 p.move_to(pt);
525 } else {
526 p.line_to(pt);
527 }
528 }
529 p.close_path();
530 p
531}
532
533fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
535 let n = pts.len();
536 let at = |i: isize| -> Vec2 {
537 if closed {
538 pts[i.rem_euclid(n as isize) as usize]
539 } else {
540 pts[i.clamp(0, n as isize - 1) as usize]
541 }
542 };
543 let mut p = BezPath::new();
544 p.move_to(pts[0].to_point());
545 let segs = if closed { n } else { n - 1 };
546 for i in 0..segs as isize {
547 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
548 let c1 = p1 + (p2 - p0) / 6.0;
549 let c2 = p2 - (p3 - p1) / 6.0;
550 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
551 }
552 if closed {
553 p.close_path();
554 }
555 p
556}
557
558pub(crate) enum TextDraw {
559 Styled(Styled),
560 Morph {
562 glyphs: Vec<(BezPath, f64)>,
563 size: (f64, f64),
564 },
565 Diff(DiffFrame),
567}
568
569pub struct SceneGraph {
571 nodes: RefCell<Vec<NodeData>>,
572 size: (u32, u32),
573 time: Cell<f64>,
575 shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
577}
578
579impl SceneGraph {
580 pub const ROOT: NodeId = NodeId(0);
582
583 pub fn new(width: u32, height: u32) -> Rc<Self> {
585 let graph = Rc::new(Self {
586 nodes: RefCell::new(Vec::new()),
587 size: (width, height),
588 time: Cell::new(0.0),
589 shader_diags: RefCell::new(Vec::new()),
590 });
591 graph.insert(
592 None,
593 NodeKind::Group,
594 crate::BaseProps {
595 name: Some("view".into()),
596 ..Default::default()
597 },
598 );
599 graph
600 }
601
602 pub fn size(&self) -> (u32, u32) {
604 self.size
605 }
606
607 pub fn set_time(&self, seconds: f64) {
610 self.time.set(seconds.max(0.0));
611 }
612
613 pub fn time(&self) -> f64 {
615 self.time.get()
616 }
617
618 pub fn len(&self) -> usize {
620 self.nodes.borrow().len()
621 }
622
623 pub fn is_empty(&self) -> bool {
625 self.len() == 0
626 }
627
628 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
634 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
635 }
636
637 pub(crate) fn next_id(&self) -> u32 {
638 self.len() as u32
639 }
640
641 pub(crate) fn insert(
642 &self,
643 parent: Option<NodeId>,
644 kind: NodeKind,
645 base: crate::BaseProps,
646 ) -> NodeId {
647 let mut nodes = self.nodes.borrow_mut();
648 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
649 let label = format!("{}#{}", kind.name(), id.0);
650 let sig = |field: &str| format!("{label}.{field}");
651 nodes.push(NodeData {
652 id,
653 name: base.name,
654 source: current_add_at().map(|(file, line)| source_label(file, line)),
655 parent,
656 children: Vec::new(),
657 x: Signal::new(base.x).with_name(sig("x")),
658 y: Signal::new(base.y).with_name(sig("y")),
659 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
660 scale: Signal::new(base.scale).with_name(sig("scale")),
661 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
662 fill: Signal::new(base.fill).with_name(sig("fill")),
663 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
664 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
665 start: Signal::new(base.start).with_name(sig("start")),
666 end: Signal::new(base.end).with_name(sig("end")),
667 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
668 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
669 dash: Signal::new(base.dash).with_name(sig("dash")),
670 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
671 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
672 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
673 blur: Signal::new(base.blur).with_name(sig("blur")),
674 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
675 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
676 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
677 clip: Signal::new(base.clip).with_name(sig("clip")),
678 blend: Signal::new(base.blend).with_name(sig("blend")),
679 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
680 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
681 grow: Signal::new(base.grow).with_name(sig("grow")),
682 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
683 basis: Signal::new(base.basis).with_name(sig("basis")),
684 margin: Signal::new(base.margin).with_name(sig("margin")),
685 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
686 place: base.place,
687 placed: Cell::new((base.x, base.y)),
688 effect: None,
689 kind,
690 });
691 if let Some(p) = parent {
692 nodes[p.0 as usize].children.push(id);
693 }
694 id
695 }
696
697 pub(crate) fn detach(&self, id: NodeId) {
698 let mut nodes = self.nodes.borrow_mut();
699 if let Some(p) = nodes[id.0 as usize].parent.take() {
700 nodes[p.0 as usize].children.retain(|c| *c != id);
701 }
702 }
703
704 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
705 self.detach(id);
706 let mut nodes = self.nodes.borrow_mut();
707 nodes[id.0 as usize].parent = Some(parent);
708 nodes[parent.0 as usize].children.push(id);
709 }
710
711 pub fn measure(&self, id: NodeId) -> (f64, f64) {
713 self.resolve_places();
714 let nodes = self.nodes.borrow();
715 measure(&nodes, id)
716 }
717
718 pub(crate) fn set_effect(&self, id: NodeId, effect: crate::post::EffectData) {
719 self.nodes.borrow_mut()[id.0 as usize].effect = Some(effect);
720 }
721
722 pub fn has_post(&self) -> bool {
726 self.nodes.borrow().iter().any(|n| {
727 n.effect.is_some()
728 || matches!(
729 n.kind,
730 NodeKind::Shader {
731 preset: crate::shader::Preset::LensDrops,
732 ..
733 }
734 )
735 })
736 }
737
738 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
739 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
740 }
741
742 fn resolve_places(&self) {
743 let nodes = self.nodes.borrow();
744 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
745 }
746
747 pub fn draw_list(&self) -> Vec<DrawItem> {
749 self.resolve_places();
750 let nodes = self.nodes.borrow();
751 let mut out = Vec::new();
752 crate::shader::clear_diags();
753 for n in nodes.iter() {
754 for diag in crate::shader::problems(n) {
755 crate::shader::push_diag(diag);
756 }
757 }
758 visit(
759 &nodes,
760 Self::ROOT,
761 Affine::IDENTITY,
762 None,
763 None,
764 self.time.get(),
765 false,
766 &mut out,
767 );
768 *self.shader_diags.borrow_mut() = crate::shader::take_diags();
769 out
770 }
771
772 pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
774 self.shader_diags.borrow().clone()
775 }
776
777 pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
780 let a = self.draw_list();
781 let b = self.draw_list();
782 let id = first_drift(&a, &b)?;
783 let n = self.node(id);
784 let label = n
785 .name
786 .clone()
787 .or_else(|| n.source.clone())
788 .unwrap_or_else(|| n.kind.name().to_owned());
789 Some((label, n.source.clone()))
790 }
791
792 pub fn pick(&self, p: Point) -> Option<NodeId> {
794 self.draw_list()
795 .iter()
796 .rev()
797 .find(|item| item.node != Self::ROOT && item.hit(p))
798 .map(|item| item.node)
799 }
800
801 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
803 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
808 return Some(item.transform);
809 }
810 self.resolve_places();
811 let nodes = self.nodes.borrow();
812 if let Some(Some((tf, _))) = placements(&nodes).get(id.0 as usize) {
813 return Some(*tf);
814 }
815 let mut tf = Affine::IDENTITY;
816 let mut cur = Some(id);
817 while let Some(c) = cur {
818 let n = nodes.get(c.0 as usize)?;
819 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
820 cur = n.parent;
821 }
822 Some(tf)
823 }
824
825 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
829 let node_tf = self.world_transform(id)?;
830 let frame_tf = self.world_transform(frame)?;
831 let (w, h) = measure(&self.nodes.borrow(), id);
832 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
833 Some(kurbo::Rect::new(x0, y0, x1, y1))
834 }
835
836 pub fn frame_size(&self) -> (f64, f64) {
838 (f64::from(self.size.0), f64::from(self.size.1))
839 }
840
841 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
843 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
844 let n = nodes.get(id.0 as usize)?;
845 if n.name.as_deref() == Some(name) {
846 return Some(id);
847 }
848 for &c in &n.children {
849 if let Some(hit) = walk(nodes, c, name) {
850 return Some(hit);
851 }
852 }
853 None
854 }
855 walk(&self.nodes.borrow(), root, name)
856 }
857
858 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
860 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
861 let Some(n) = nodes.get(id.0 as usize) else {
862 return;
863 };
864 if matches!(n.kind, NodeKind::Path { .. }) {
865 out.push(id);
866 }
867 for &c in &n.children {
868 walk(nodes, c, out);
869 }
870 }
871 let mut out = Vec::new();
872 walk(&self.nodes.borrow(), root, &mut out);
873 out
874 }
875
876 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
878 let nodes = self.nodes.borrow();
879 let mut out = Vec::new();
880 let mut stack = vec![Self::ROOT];
881 while let Some(id) = stack.pop() {
882 let n = &nodes[id.0 as usize];
883 out.push((id, n.parent, n.kind.name(), n.name.clone()));
884 stack.extend(n.children.iter().rev());
885 }
886 out
887 }
888
889 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
891 let nodes = self.nodes.borrow();
892 let Some(n) = nodes.get(id.0 as usize) else {
893 return Vec::new();
894 };
895 let f = |v: f64| format!("{v:.3}");
896 let s = n.scale.get();
897 let mut out = vec![
898 ("x".into(), f(n.x.get())),
899 ("y".into(), f(n.y.get())),
900 ("rotation".into(), f(n.rotation.get())),
901 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
902 ("opacity".into(), f(n.opacity.get())),
903 ("fill".into(), n.fill.get().to_string()),
904 ("stroke".into(), n.stroke.get().to_string()),
905 ("line_width".into(), f(n.line_width.get())),
906 ("start".into(), f(n.start.get())),
907 ("end".into(), f(n.end.get())),
908 ];
909 let (w, h) = measure(&nodes, id);
910 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
911 match &n.kind {
912 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
913 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
914 NodeKind::Txt {
915 text,
916 font_size,
917 weight,
918 ..
919 } => {
920 out.push(("text".into(), text.get()));
921 out.push(("font_size".into(), f(font_size.get())));
922 out.push(("weight".into(), f(weight.get())));
923 }
924 NodeKind::Line { points, .. } => {
925 out.push(("points".into(), points.get().len().to_string()))
926 }
927 NodeKind::Layout { gap, padding, .. } => {
928 out.push(("gap".into(), f(gap.get())));
929 out.push(("padding".into(), f(padding.get())));
930 }
931 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
932 _ => {}
933 }
934 out
935 }
936
937 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
942 let list = self.draw_list();
943 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
944 let mut order = 0u32;
945 for item in &list {
946 let slot = &mut drawn[item.node.0 as usize];
947 if slot.is_none() {
948 *slot = Some((item.transform, order));
949 order += 1;
950 }
951 }
952 let nodes = self.nodes.borrow();
953 let placed = placements(&nodes);
955 let mut out = Vec::new();
956 let mut stack = vec![Self::ROOT];
957 while let Some(id) = stack.pop() {
958 let n = &nodes[id.0 as usize];
959 stack.extend(n.children.iter().rev());
960 let order = drawn[id.0 as usize].map_or(u32::MAX, |(_, o)| o);
961 let (tf, (w, h)) = placed[id.0 as usize]
962 .unwrap_or_else(|| (transform_without_layout(&nodes, id), measure(&nodes, id)));
963 let (text, text_overflow) = text_info(n);
964 out.push(InspectNode {
965 id: id.0,
966 parent: n.parent.map(|p| p.0),
967 kind: n.kind.name(),
968 name: n.name.clone().unwrap_or_else(|| {
969 n.source
970 .clone()
971 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
972 }),
973 source: n.source.clone(),
974 bounds: aabb(tf, w, h),
975 opacity: n.opacity.get().clamp(0.0, 1.0),
976 order,
977 text,
978 fill: n.fill.get(),
979 text_overflow,
980 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
981 in_camera: {
982 let mut up = n.parent;
983 let mut found = false;
984 while let Some(p) = up {
985 let pn = &nodes[p.0 as usize];
986 if matches!(pn.kind, NodeKind::Camera { .. }) {
987 found = true;
988 break;
989 }
990 up = pn.parent;
991 }
992 found
993 },
994 shader: match &n.kind {
995 NodeKind::Shader { preset, .. } => Some(preset.name()),
996 _ => None,
997 },
998 params: params_of(n),
999 });
1000 }
1001 out
1002 }
1003}
1004
1005#[derive(Debug, Clone, PartialEq)]
1007pub struct InspectNode {
1008 pub id: u32,
1010 pub parent: Option<u32>,
1012 pub kind: &'static str,
1014 pub name: String,
1016 pub source: Option<String>,
1018 pub bounds: [f64; 4],
1021 pub opacity: f64,
1023 pub order: u32,
1025 pub text: Option<String>,
1027 pub fill: Color,
1029 pub text_overflow: f64,
1032 pub parent_kind: Option<&'static str>,
1034 pub in_camera: bool,
1037 pub shader: Option<&'static str>,
1039 pub params: Vec<(String, f64)>,
1044}
1045
1046fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1048 let mut out = Vec::new();
1049 if let NodeKind::Shader { uniforms, .. } = &n.kind {
1050 for u in uniforms {
1051 match &u.value {
1052 crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1053 out.push((u.name.clone(), v.get()));
1054 }
1055 _ => {}
1056 }
1057 }
1058 }
1059 if let Some(e) = &n.effect {
1060 out.push(("effect.strength".into(), e.strength.get()));
1061 out.push(("effect.speed".into(), e.speed.get()));
1062 }
1063 let (start, end) = (n.start.get(), n.end.get());
1066 if start > 0.0 {
1067 out.push(("start".into(), start));
1068 }
1069 if end < 1.0 {
1070 out.push(("end".into(), end));
1071 }
1072 let shadow = n.shadow_color.get();
1073 if shadow.a > 0.0 {
1074 out.push(("shadow".into(), n.shadow_blur.get() * f64::from(shadow.a)));
1075 }
1076 out
1077}
1078
1079thread_local! {
1080 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1081}
1082
1083pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1087 ADD_AT.with(|c| {
1088 let prev = c.get();
1089 if prev.is_none() {
1090 c.set(Some((file, line)));
1091 }
1092 prev
1093 })
1094}
1095
1096#[track_caller]
1102pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1103 let loc = std::panic::Location::caller();
1104 let prev = push_add_at(loc.file(), loc.line());
1105 let out = f();
1106 pop_add_at(prev);
1107 out
1108}
1109
1110pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1111 ADD_AT.with(|c| c.set(prev));
1112}
1113
1114fn current_add_at() -> Option<(&'static str, u32)> {
1115 ADD_AT.with(|c| c.get())
1116}
1117
1118fn source_label(file: &str, line: u32) -> String {
1120 let file = file.replace('\\', "/");
1121 let rel = file
1122 .rfind("/src/")
1123 .map_or(file.as_str(), |i| &file[i + 1..]);
1124 format!("{rel}:{line}")
1125}
1126
1127fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1128 let pts = [
1129 (-w / 2.0, -h / 2.0),
1130 (w / 2.0, -h / 2.0),
1131 (w / 2.0, h / 2.0),
1132 (-w / 2.0, h / 2.0),
1133 ];
1134 let mut b = [
1135 f64::INFINITY,
1136 f64::INFINITY,
1137 f64::NEG_INFINITY,
1138 f64::NEG_INFINITY,
1139 ];
1140 for (x, y) in pts {
1141 let p = tf * Point::new(x, y);
1142 b[0] = b[0].min(p.x);
1143 b[1] = b[1].min(p.y);
1144 b[2] = b[2].max(p.x);
1145 b[3] = b[3].max(p.y);
1146 }
1147 b
1148}
1149
1150fn placements(nodes: &[NodeData]) -> Vec<Option<(Affine, (f64, f64))>> {
1154 fn walk(
1155 nodes: &[NodeData],
1156 id: NodeId,
1157 parent_tf: Affine,
1158 placement: Option<FlexBox>,
1159 flex: Option<&FlexMap>,
1160 out: &mut [Option<(Affine, (f64, f64))>],
1161 ) {
1162 let n = &nodes[id.0 as usize];
1163 let size = match placement {
1165 Some(b) if resizable(&n.kind) => {
1166 let s = n.scale.get();
1167 (
1168 b.size.0 / s.x.abs().max(1e-9),
1169 b.size.1 / s.y.abs().max(1e-9),
1170 )
1171 }
1172 _ => measure(nodes, id),
1173 };
1174 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1175 let tf = parent_tf * n.local_transform(offset, size);
1176 out[id.0 as usize] = Some((tf, size));
1177 let own;
1178 let map: Option<&FlexMap> = match (&n.kind, flex) {
1179 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1180 (NodeKind::Layout { .. }, None) => {
1181 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1182 Some(&own)
1183 }
1184 _ => None,
1185 };
1186 for &c in &n.children {
1187 let placement = map.and_then(|m| m.get(&c).copied());
1188 walk(nodes, c, tf, placement, map, out);
1189 }
1190 }
1191 let mut out = vec![None; nodes.len()];
1192 walk(
1193 nodes,
1194 SceneGraph::ROOT,
1195 Affine::IDENTITY,
1196 None,
1197 None,
1198 &mut out,
1199 );
1200 out
1201}
1202
1203fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1204 let mut chain = Vec::new();
1205 let mut cur = Some(id);
1206 while let Some(c) = cur {
1207 chain.push(c);
1208 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1209 }
1210 let mut tf = Affine::IDENTITY;
1211 for c in chain.into_iter().rev() {
1212 let Some(n) = nodes.get(c.0 as usize) else {
1213 break;
1214 };
1215 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1216 }
1217 tf
1218}
1219
1220fn text_info(n: &NodeData) -> (Option<String>, f64) {
1221 let (body, max_width) = match &n.kind {
1222 NodeKind::Txt {
1223 text, spans, style, ..
1224 } => {
1225 let spans = spans.get();
1226 let body = if spans.is_empty() {
1227 text.get()
1228 } else {
1229 spans.into_iter().map(|s| s.text).collect()
1230 };
1231 (body, style.max_width)
1232 }
1233 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1234 _ => return (None, 0.0),
1235 };
1236 let overflow = match (max_width, n.text_draw()) {
1237 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1238 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1239 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1240 _ => 0.0,
1241 };
1242 (Some(body), overflow)
1243}
1244
1245pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1246 let n = &nodes[id.0 as usize];
1247 match &n.kind {
1248 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1249 NodeKind::Circle { size, .. }
1250 | NodeKind::Polygon { size, .. }
1251 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1252 NodeKind::Img { width, height, .. }
1253 | NodeKind::Grid { width, height, .. }
1254 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1255 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1256 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1257 Some(TextDraw::Morph { size, .. }) => size,
1258 Some(TextDraw::Diff(frame)) => frame.size,
1259 None => (0.0, 0.0),
1260 },
1261 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1262 let bb = n
1263 .outline((0.0, 0.0))
1264 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1265 (bb.width(), bb.height())
1266 }
1267 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1268 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1270 NodeKind::Custom {
1271 measure: Some(m), ..
1272 } => m(),
1273 NodeKind::Custom { width, height, .. } => (*width, *height),
1274 NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1275 NodeKind::Video { width, height, .. } => (width.get(), height.get()),
1276 NodeKind::Group => {
1277 let mut bb: Option<kurbo::Rect> = None;
1278 for &c in &n.children {
1279 let child = &nodes[c.0 as usize];
1280 let (w, h) = measure(nodes, c);
1281 let r = child
1282 .local_transform(Vec2::ZERO, (w, h))
1283 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1284 bb = Some(bb.map_or(r, |b| b.union(r)));
1285 }
1286 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1287 }
1288 }
1289}
1290
1291fn resizable(kind: &NodeKind) -> bool {
1293 matches!(
1294 kind,
1295 NodeKind::Rect { .. }
1296 | NodeKind::Layout { .. }
1297 | NodeKind::Img { .. }
1298 | NodeKind::Grid { .. }
1299 | NodeKind::Svg { .. }
1300 | NodeKind::Shader { .. }
1301 | NodeKind::Video { .. }
1302 )
1303}
1304
1305fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1306 let n = a.len().max(b.len());
1307 for i in 0..n {
1308 match (a.get(i), b.get(i)) {
1309 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1310 (Some(x), _) => return Some(x.node),
1311 (_, Some(y)) => return Some(y.node),
1312 (None, None) => {}
1313 }
1314 }
1315 None
1316}
1317
1318fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1319 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1320 match (a, b) {
1321 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1322 (
1323 Stroke {
1324 path: p,
1325 paint: c,
1326 style: s,
1327 },
1328 Stroke {
1329 path: q,
1330 paint: d,
1331 style: t,
1332 },
1333 ) => p == q && c == d && s == t,
1334 (
1335 Image {
1336 rect: r,
1337 opacity: o,
1338 ..
1339 },
1340 Image {
1341 rect: s,
1342 opacity: p,
1343 ..
1344 },
1345 ) => r == s && o == p,
1346 (PushLayer(x), PushLayer(y)) => x == y,
1347 (PopLayer, PopLayer) => true,
1348 (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1349 | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1350 (Primitive::EndEffect, Primitive::EndEffect) => true,
1351 _ => false,
1352 }
1353}
1354
1355#[allow(clippy::too_many_arguments)]
1358fn visit(
1359 nodes: &[NodeData],
1360 id: NodeId,
1361 parent_tf: Affine,
1362 placement: Option<FlexBox>,
1363 flex: Option<&FlexMap>,
1364 time: f64,
1365 inside: bool,
1366 out: &mut Vec<DrawItem>,
1367) {
1368 let n = &nodes[id.0 as usize];
1369 let opacity = n.opacity.get().clamp(0.0, 1.0);
1370 if opacity <= 0.0 {
1371 return;
1372 }
1373 let size = match placement {
1374 Some(b) if resizable(&n.kind) => {
1375 let s = n.scale.get();
1376 (
1377 b.size.0 / s.x.abs().max(1e-9),
1378 b.size.1 / s.y.abs().max(1e-9),
1379 )
1380 }
1381 _ => measure(nodes, id),
1382 };
1383 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1384 let tf = parent_tf * n.local_transform(offset, size);
1385
1386 let blend = n.blend.get();
1389 let blur = n.blur.get().max(0.0);
1390 let shadow_color = n.shadow_color.get();
1391 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1392 color: shadow_color,
1393 blur: n.shadow_blur.get().max(0.0),
1394 offset: n.shadow_offset.get(),
1395 });
1396 let layered = blend != BlendMode::Normal
1397 || blur > 0.0
1398 || shadow.is_some()
1399 || (opacity < 1.0 && !n.children.is_empty());
1400 let effect = n.effect.as_ref().and_then(|e| {
1401 if inside {
1402 cycle(n, "This effect is inside a layer or another effect");
1403 return None;
1404 }
1405 Some(e.shade(tf * Point::ZERO, time))
1406 });
1407 if let Some(shade) = &effect {
1408 out.push(DrawItem {
1409 node: id,
1410 transform: tf,
1411 prim: Primitive::BeginEffect(shade.clone()),
1412 });
1413 }
1414 if let Some(shade) = crate::shader::read_frame(n, size, tf, time, opacity) {
1415 if inside || effect.is_some() {
1416 cycle(
1417 n,
1418 "lens_drops reads the frame, but it is inside a layer or an effect",
1419 );
1420 } else {
1421 out.push(DrawItem {
1422 node: id,
1423 transform: tf,
1424 prim: Primitive::ReadFrame(shade),
1425 });
1426 }
1427 }
1428 if layered {
1429 out.push(DrawItem {
1430 node: id,
1431 transform: tf,
1432 prim: Primitive::PushLayer(Layer {
1433 opacity,
1434 blend,
1435 clip: None,
1436 blur,
1437 shadow,
1438 }),
1439 });
1440 }
1441 let paint_opacity = if layered { 1.0 } else { opacity };
1442 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1443 out.push(DrawItem {
1444 node: id,
1445 transform: tf,
1446 prim,
1447 });
1448 }
1449 if let NodeKind::Shader { .. } = &n.kind
1450 && crate::shader::problems(n).is_empty()
1451 && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1452 {
1453 out.push(DrawItem {
1454 node: id,
1455 transform: tf,
1456 prim,
1457 });
1458 }
1459
1460 if !n.children.is_empty() {
1461 let clip = n.clip.get().then(|| {
1463 n.outline(size).unwrap_or_else(|| {
1464 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1465 .to_path(0.1)
1466 })
1467 });
1468 let clipped = clip.is_some();
1469 if let Some(path) = clip {
1470 out.push(DrawItem {
1471 node: id,
1472 transform: tf,
1473 prim: Primitive::PushLayer(Layer::clip(path)),
1474 });
1475 }
1476 let own;
1479 let map: Option<&FlexMap> = match (&n.kind, flex) {
1480 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1481 (NodeKind::Layout { .. }, None) => {
1482 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1483 Some(&own)
1484 }
1485 _ => None,
1486 };
1487 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1488 order.sort_by(|a, b| {
1490 let za = nodes[a.1.0 as usize].z_index.get();
1491 let zb = nodes[b.1.0 as usize].z_index.get();
1492 za.total_cmp(&zb)
1493 });
1494 for (_, c) in order {
1495 let placement = map.and_then(|m| m.get(&c).copied());
1496 let inside = inside || layered || clipped || effect.is_some();
1497 visit(nodes, c, tf, placement, map, time, inside, out);
1498 }
1499 if clipped {
1500 out.push(DrawItem {
1501 node: id,
1502 transform: tf,
1503 prim: Primitive::PopLayer,
1504 });
1505 }
1506 }
1507 if layered {
1508 out.push(DrawItem {
1509 node: id,
1510 transform: tf,
1511 prim: Primitive::PopLayer,
1512 });
1513 }
1514 if effect.is_some() {
1515 out.push(DrawItem {
1516 node: id,
1517 transform: tf,
1518 prim: Primitive::EndEffect,
1519 });
1520 }
1521}
1522
1523fn cycle(n: &NodeData, what: &str) {
1525 crate::shader::push_diag(crate::shader::ShaderDiag {
1526 node: n
1527 .name
1528 .clone()
1529 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0)),
1530 source: n.source.clone(),
1531 rule: "input_cycle",
1532 hint: format!(
1533 "{what} (a group with opacity below 1, a blur, a clip, a blend or a shadow). \
1534 Its pixels are not finished there, so the shader is skipped. \
1535 Put the effect on that group, or move the node out of it."
1536 ),
1537 });
1538}
1539
1540impl DrawItem {
1541 fn hit(&self, p: Point) -> bool {
1542 let local = self.transform.inverse() * p;
1543 match &self.prim {
1544 Primitive::Fill { path, .. } => {
1545 (is_text_like(path) && path.bounding_box().contains(local))
1546 || closed(path).contains(local)
1547 }
1548 Primitive::Stroke { path, style, .. } => {
1549 let tol = (style.width / 2.0)
1550 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1551 path.segments().any(|s| {
1552 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1553 })
1554 }
1555 Primitive::Image { rect, .. } => rect.contains(local),
1556 Primitive::PushLayer(_)
1557 | Primitive::PopLayer
1558 | Primitive::ReadFrame(_)
1559 | Primitive::BeginEffect(_)
1560 | Primitive::EndEffect => false,
1561 }
1562 }
1563}
1564
1565fn closed(path: &BezPath) -> BezPath {
1567 let mut out = BezPath::new();
1568 let mut open = false;
1569 for el in path.elements() {
1570 match el {
1571 kurbo::PathEl::MoveTo(_) => {
1572 if open {
1573 out.close_path();
1574 }
1575 open = true;
1576 }
1577 kurbo::PathEl::ClosePath => open = false,
1578 _ => {}
1579 }
1580 out.push(*el);
1581 }
1582 if open {
1583 out.close_path();
1584 }
1585 out
1586}
1587
1588fn is_text_like(path: &BezPath) -> bool {
1591 path.elements()
1592 .iter()
1593 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1594 .count()
1595 > 3
1596}
1597
1598thread_local! {
1599 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1600}
1601
1602impl SceneGraph {
1603 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1605 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1606 struct Restore(Option<Rc<SceneGraph>>);
1607 impl Drop for Restore {
1608 fn drop(&mut self) {
1609 let prev = self.0.take();
1610 CURRENT.with(|c| *c.borrow_mut() = prev);
1611 }
1612 }
1613 let _restore = Restore(prev);
1614 f()
1615 }
1616}
1617
1618pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1619 CURRENT.with(|c| {
1620 let c = c.borrow();
1621 f(c.as_ref()
1622 .expect("oxframe: node handles can only be used while a scene is running"))
1623 })
1624}