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 pub offscreen: bool,
304}
305
306impl NodeData {
307 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
310 if let NodeKind::Camera { zoom } = &self.kind {
311 let (x, y) = self.placed.get();
315 return Affine::translate(offset)
316 * Affine::rotate(-self.rotation.get().to_radians())
317 * Affine::scale(zoom.get().max(1e-6))
318 * Affine::translate((-x, -y));
319 }
320 let s = self.scale.get();
321 let a = self.anchor.get();
322 let (x, y) = self.placed.get();
323 Affine::translate(offset + Vec2::new(x, y))
324 * Affine::rotate(self.rotation.get().to_radians())
325 * Affine::scale_non_uniform(s.x, s.y)
326 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
327 }
328
329 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
331 match &self.kind {
332 NodeKind::Txt {
333 text,
334 spans,
335 font_size,
336 weight,
337 style,
338 morph,
339 morph_t,
340 } => {
341 let style = TextStyle {
342 size: font_size.get().max(0.1),
343 weight: weight.get().clamp(1.0, 1000.0),
344 ..style.clone()
345 };
346 if let Some((from, to)) = morph.get() {
347 let a = oxframe_text::layout(&from, &style);
348 let b = oxframe_text::layout(&to, &style);
349 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
350 return Some(TextDraw::Morph { glyphs, size });
351 }
352 let spans = spans.get();
353 Some(TextDraw::Styled(if spans.is_empty() {
354 crate::text::rich(&[RichSpan::new(text.get())], &style)
355 } else {
356 crate::text::rich(&spans, &style)
357 }))
358 }
359 NodeKind::Code {
360 code,
361 edit_from,
362 edit_to,
363 editing,
364 edit_t,
365 edit_flash,
366 lang,
367 theme,
368 font_size,
369 style,
370 focus,
371 dim,
372 } => {
373 let style = TextStyle {
374 size: font_size.get().max(0.1),
375 ..style.clone()
376 };
377 if editing.get() {
378 let (frame, _) = crate::text::layout_diff(
379 &edit_from.get(),
380 &edit_to.get(),
381 *lang,
382 crate::text::DiffView {
383 theme,
384 style: &style,
385 focus: focus.get(),
386 dim: dim.get(),
387 t: edit_t.get(),
388 flash: edit_flash.get(),
389 },
390 );
391 return Some(TextDraw::Diff(frame));
392 }
393 Some(TextDraw::Styled(crate::text::code(
394 &code.get(),
395 *lang,
396 theme,
397 &style,
398 focus.get(),
399 dim.get(),
400 )))
401 }
402 _ => None,
403 }
404 }
405
406 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
408 const TOLERANCE: f64 = 0.1;
409 match &self.kind {
410 NodeKind::Video { radius, .. } => {
411 let (w, h) = size;
412 Some(
413 kurbo::RoundedRect::new(
414 -w / 2.0,
415 -h / 2.0,
416 w / 2.0,
417 h / 2.0,
418 radius.get().max(0.0),
419 )
420 .to_path(TOLERANCE),
421 )
422 }
423 NodeKind::Group
424 | NodeKind::Svg { .. }
425 | NodeKind::Camera { .. }
426 | NodeKind::Img { .. }
427 | NodeKind::Txt { .. }
428 | NodeKind::Code { .. }
429 | NodeKind::Grid { .. }
430 | NodeKind::Custom { .. }
431 | NodeKind::Particles { .. }
432 | NodeKind::Shader { .. } => None,
433 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
434 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
435 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
436 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
437 Some(if r > 0.0 {
438 rect.to_rounded_rect(r).to_path(TOLERANCE)
439 } else {
440 rect.to_path(TOLERANCE)
441 })
442 }
443 NodeKind::Circle {
444 start_angle,
445 end_angle,
446 pie,
447 ..
448 } => {
449 let r = size.0.max(0.0) / 2.0;
452 let (a0, a1) = (start_angle.get(), end_angle.get());
453 let sweep = (a1 - a0).clamp(-360.0, 360.0);
454 if sweep.abs() >= 360.0 - 1e-9 {
455 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
456 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
457 }
458 let arc = kurbo::Arc::new(
459 (0.0, 0.0),
460 (r, r),
461 (a0 - 90.0).to_radians(),
462 sweep.to_radians(),
463 0.0,
464 );
465 let mut p = BezPath::new();
466 if *pie {
467 p.move_to((0.0, 0.0));
468 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
469 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
470 p.close_path();
471 } else {
472 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
473 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
474 }
475 Some(p)
476 }
477 NodeKind::Polygon { sides, .. } => {
478 let n = sides.get().round().max(3.0) as usize;
479 Some(regular(n, size.0.max(0.0) / 2.0, None))
480 }
481 NodeKind::Star { points, inner, .. } => {
482 let n = points.get().round().max(2.0) as usize;
483 let r = size.0.max(0.0) / 2.0;
484 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
485 }
486 NodeKind::Path { data } => Some(data.get()),
487 NodeKind::Line {
488 points,
489 closed,
490 smooth,
491 ..
492 } => {
493 let pts = points.get();
494 if *smooth && pts.len() > 2 {
495 return Some(catmull_rom(&pts, *closed));
496 }
497 let mut p = BezPath::new();
498 for (i, v) in pts.iter().enumerate() {
499 if i == 0 {
500 p.move_to(v.to_point());
501 } else {
502 p.line_to(v.to_point());
503 }
504 }
505 if *closed && pts.len() > 2 {
506 p.close_path();
507 }
508 Some(p)
509 }
510 }
511 }
512}
513
514fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
516 let count = if inner.is_some() { 2 * n } else { n };
517 let mut p = BezPath::new();
518 for i in 0..count {
519 let radius = match inner {
520 Some(ri) if i % 2 == 1 => ri,
521 _ => r,
522 };
523 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
524 let pt = Point::new(a.cos() * radius, a.sin() * radius);
525 if i == 0 {
526 p.move_to(pt);
527 } else {
528 p.line_to(pt);
529 }
530 }
531 p.close_path();
532 p
533}
534
535fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
537 let n = pts.len();
538 let at = |i: isize| -> Vec2 {
539 if closed {
540 pts[i.rem_euclid(n as isize) as usize]
541 } else {
542 pts[i.clamp(0, n as isize - 1) as usize]
543 }
544 };
545 let mut p = BezPath::new();
546 p.move_to(pts[0].to_point());
547 let segs = if closed { n } else { n - 1 };
548 for i in 0..segs as isize {
549 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
550 let c1 = p1 + (p2 - p0) / 6.0;
551 let c2 = p2 - (p3 - p1) / 6.0;
552 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
553 }
554 if closed {
555 p.close_path();
556 }
557 p
558}
559
560pub(crate) enum TextDraw {
561 Styled(Styled),
562 Morph {
564 glyphs: Vec<(BezPath, f64)>,
565 size: (f64, f64),
566 },
567 Diff(DiffFrame),
569}
570
571pub struct SceneGraph {
573 nodes: RefCell<Vec<NodeData>>,
574 size: (u32, u32),
575 time: Cell<f64>,
577 shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
579}
580
581impl SceneGraph {
582 pub const ROOT: NodeId = NodeId(0);
584
585 pub fn new(width: u32, height: u32) -> Rc<Self> {
587 let graph = Rc::new(Self {
588 nodes: RefCell::new(Vec::new()),
589 size: (width, height),
590 time: Cell::new(0.0),
591 shader_diags: RefCell::new(Vec::new()),
592 });
593 graph.insert(
594 None,
595 NodeKind::Group,
596 crate::BaseProps {
597 name: Some("view".into()),
598 ..Default::default()
599 },
600 );
601 graph
602 }
603
604 pub fn size(&self) -> (u32, u32) {
606 self.size
607 }
608
609 pub fn set_time(&self, seconds: f64) {
612 self.time.set(seconds.max(0.0));
613 }
614
615 pub fn time(&self) -> f64 {
617 self.time.get()
618 }
619
620 pub fn len(&self) -> usize {
622 self.nodes.borrow().len()
623 }
624
625 pub fn is_empty(&self) -> bool {
627 self.len() == 0
628 }
629
630 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
636 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
637 }
638
639 pub(crate) fn next_id(&self) -> u32 {
640 self.len() as u32
641 }
642
643 pub(crate) fn insert(
644 &self,
645 parent: Option<NodeId>,
646 kind: NodeKind,
647 base: crate::BaseProps,
648 ) -> NodeId {
649 let mut nodes = self.nodes.borrow_mut();
650 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
651 let label = format!("{}#{}", kind.name(), id.0);
652 let sig = |field: &str| format!("{label}.{field}");
653 nodes.push(NodeData {
654 id,
655 name: base.name,
656 source: current_add_at().map(|(file, line)| source_label(file, line)),
657 parent,
658 children: Vec::new(),
659 x: Signal::new(base.x).with_name(sig("x")),
660 y: Signal::new(base.y).with_name(sig("y")),
661 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
662 scale: Signal::new(base.scale).with_name(sig("scale")),
663 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
664 fill: Signal::new(base.fill).with_name(sig("fill")),
665 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
666 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
667 start: Signal::new(base.start).with_name(sig("start")),
668 end: Signal::new(base.end).with_name(sig("end")),
669 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
670 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
671 dash: Signal::new(base.dash).with_name(sig("dash")),
672 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
673 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
674 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
675 blur: Signal::new(base.blur).with_name(sig("blur")),
676 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
677 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
678 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
679 clip: Signal::new(base.clip).with_name(sig("clip")),
680 blend: Signal::new(base.blend).with_name(sig("blend")),
681 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
682 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
683 grow: Signal::new(base.grow).with_name(sig("grow")),
684 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
685 basis: Signal::new(base.basis).with_name(sig("basis")),
686 margin: Signal::new(base.margin).with_name(sig("margin")),
687 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
688 place: base.place,
689 placed: Cell::new((base.x, base.y)),
690 effect: None,
691 offscreen: base.offscreen,
692 kind,
693 });
694 if let Some(p) = parent {
695 nodes[p.0 as usize].children.push(id);
696 }
697 id
698 }
699
700 pub(crate) fn detach(&self, id: NodeId) {
701 let mut nodes = self.nodes.borrow_mut();
702 if let Some(p) = nodes[id.0 as usize].parent.take() {
703 nodes[p.0 as usize].children.retain(|c| *c != id);
704 }
705 }
706
707 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
708 self.detach(id);
709 let mut nodes = self.nodes.borrow_mut();
710 nodes[id.0 as usize].parent = Some(parent);
711 nodes[parent.0 as usize].children.push(id);
712 }
713
714 pub fn measure(&self, id: NodeId) -> (f64, f64) {
716 self.resolve_places();
717 let nodes = self.nodes.borrow();
718 measure(&nodes, id)
719 }
720
721 pub(crate) fn set_effect(&self, id: NodeId, effect: crate::post::EffectData) {
722 self.nodes.borrow_mut()[id.0 as usize].effect = Some(effect);
723 }
724
725 pub fn has_post(&self) -> bool {
729 self.nodes.borrow().iter().any(|n| {
730 n.effect.is_some()
731 || matches!(
732 n.kind,
733 NodeKind::Shader {
734 preset: crate::shader::Preset::LensDrops,
735 ..
736 }
737 )
738 || crate::shader::reads_frame_input(n)
739 })
740 }
741
742 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
743 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
744 }
745
746 fn resolve_places(&self) {
747 let nodes = self.nodes.borrow();
748 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
749 }
750
751 pub fn draw_list(&self) -> Vec<DrawItem> {
753 self.resolve_places();
754 let nodes = self.nodes.borrow();
755 let mut out = Vec::new();
756 crate::shader::set_scene_size((f64::from(self.size.0), f64::from(self.size.1)));
757 crate::shader::clear_diags();
758 for n in nodes.iter() {
759 for diag in crate::shader::problems(n) {
760 crate::shader::push_diag(diag);
761 }
762 }
763 visit(
764 &nodes,
765 Self::ROOT,
766 Affine::IDENTITY,
767 None,
768 None,
769 self.time.get(),
770 &mut out,
771 );
772 *self.shader_diags.borrow_mut() = crate::shader::take_diags();
773 out
774 }
775
776 pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
778 self.shader_diags.borrow().clone()
779 }
780
781 pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
784 let a = self.draw_list();
785 let b = self.draw_list();
786 let id = first_drift(&a, &b)?;
787 let n = self.node(id);
788 let label = n
789 .name
790 .clone()
791 .or_else(|| n.source.clone())
792 .unwrap_or_else(|| n.kind.name().to_owned());
793 Some((label, n.source.clone()))
794 }
795
796 pub fn pick(&self, p: Point) -> Option<NodeId> {
798 self.draw_list()
799 .iter()
800 .rev()
801 .find(|item| item.node != Self::ROOT && item.hit(p))
802 .map(|item| item.node)
803 }
804
805 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
807 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
812 return Some(item.transform);
813 }
814 self.resolve_places();
815 let nodes = self.nodes.borrow();
816 if let Some(Some((tf, _))) = placements(&nodes).get(id.0 as usize) {
817 return Some(*tf);
818 }
819 let mut tf = Affine::IDENTITY;
820 let mut cur = Some(id);
821 while let Some(c) = cur {
822 let n = nodes.get(c.0 as usize)?;
823 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
824 cur = n.parent;
825 }
826 Some(tf)
827 }
828
829 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
833 let node_tf = self.world_transform(id)?;
834 let frame_tf = self.world_transform(frame)?;
835 let (w, h) = measure(&self.nodes.borrow(), id);
836 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
837 Some(kurbo::Rect::new(x0, y0, x1, y1))
838 }
839
840 pub fn frame_size(&self) -> (f64, f64) {
842 (f64::from(self.size.0), f64::from(self.size.1))
843 }
844
845 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
847 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
848 let n = nodes.get(id.0 as usize)?;
849 if n.name.as_deref() == Some(name) {
850 return Some(id);
851 }
852 for &c in &n.children {
853 if let Some(hit) = walk(nodes, c, name) {
854 return Some(hit);
855 }
856 }
857 None
858 }
859 walk(&self.nodes.borrow(), root, name)
860 }
861
862 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
864 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
865 let Some(n) = nodes.get(id.0 as usize) else {
866 return;
867 };
868 if matches!(n.kind, NodeKind::Path { .. }) {
869 out.push(id);
870 }
871 for &c in &n.children {
872 walk(nodes, c, out);
873 }
874 }
875 let mut out = Vec::new();
876 walk(&self.nodes.borrow(), root, &mut out);
877 out
878 }
879
880 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
882 let nodes = self.nodes.borrow();
883 let mut out = Vec::new();
884 let mut stack = vec![Self::ROOT];
885 while let Some(id) = stack.pop() {
886 let n = &nodes[id.0 as usize];
887 out.push((id, n.parent, n.kind.name(), n.name.clone()));
888 stack.extend(n.children.iter().rev());
889 }
890 out
891 }
892
893 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
895 let nodes = self.nodes.borrow();
896 let Some(n) = nodes.get(id.0 as usize) else {
897 return Vec::new();
898 };
899 let f = |v: f64| format!("{v:.3}");
900 let s = n.scale.get();
901 let mut out = vec![
902 ("x".into(), f(n.x.get())),
903 ("y".into(), f(n.y.get())),
904 ("rotation".into(), f(n.rotation.get())),
905 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
906 ("opacity".into(), f(n.opacity.get())),
907 ("fill".into(), n.fill.get().to_string()),
908 ("stroke".into(), n.stroke.get().to_string()),
909 ("line_width".into(), f(n.line_width.get())),
910 ("start".into(), f(n.start.get())),
911 ("end".into(), f(n.end.get())),
912 ];
913 let (w, h) = measure(&nodes, id);
914 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
915 match &n.kind {
916 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
917 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
918 NodeKind::Txt {
919 text,
920 font_size,
921 weight,
922 ..
923 } => {
924 out.push(("text".into(), text.get()));
925 out.push(("font_size".into(), f(font_size.get())));
926 out.push(("weight".into(), f(weight.get())));
927 }
928 NodeKind::Line { points, .. } => {
929 out.push(("points".into(), points.get().len().to_string()))
930 }
931 NodeKind::Layout { gap, padding, .. } => {
932 out.push(("gap".into(), f(gap.get())));
933 out.push(("padding".into(), f(padding.get())));
934 }
935 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
936 _ => {}
937 }
938 out
939 }
940
941 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
946 let list = self.draw_list();
947 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
948 let mut order = 0u32;
949 for item in &list {
950 let slot = &mut drawn[item.node.0 as usize];
951 if slot.is_none() {
952 *slot = Some((item.transform, order));
953 order += 1;
954 }
955 }
956 let nodes = self.nodes.borrow();
957 let placed = placements(&nodes);
959 let mut out = Vec::new();
960 let mut stack = vec![Self::ROOT];
961 while let Some(id) = stack.pop() {
962 let n = &nodes[id.0 as usize];
963 stack.extend(n.children.iter().rev());
964 let order = drawn[id.0 as usize].map_or(u32::MAX, |(_, o)| o);
965 let (tf, (w, h)) = placed[id.0 as usize]
966 .unwrap_or_else(|| (transform_without_layout(&nodes, id), measure(&nodes, id)));
967 let (text, text_overflow) = text_info(n);
968 out.push(InspectNode {
969 id: id.0,
970 parent: n.parent.map(|p| p.0),
971 kind: n.kind.name(),
972 name: n.name.clone().unwrap_or_else(|| {
973 n.source
974 .clone()
975 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
976 }),
977 source: n.source.clone(),
978 bounds: aabb(tf, w, h),
979 opacity: if offscreen(&nodes, id) {
981 0.0
982 } else {
983 n.opacity.get().clamp(0.0, 1.0)
984 },
985 order,
986 text,
987 fill: n.fill.get(),
988 text_overflow,
989 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
990 in_camera: {
991 let mut up = n.parent;
992 let mut found = false;
993 while let Some(p) = up {
994 let pn = &nodes[p.0 as usize];
995 if matches!(pn.kind, NodeKind::Camera { .. }) {
996 found = true;
997 break;
998 }
999 up = pn.parent;
1000 }
1001 found
1002 },
1003 shader: match &n.kind {
1004 NodeKind::Shader { preset, .. } => Some(preset.name()),
1005 _ => None,
1006 },
1007 reads_frame: matches!(
1008 n.kind,
1009 NodeKind::Shader {
1010 preset: crate::shader::Preset::LensDrops,
1011 ..
1012 }
1013 ) || crate::shader::reads_frame_input(n),
1014 params: params_of(n),
1015 });
1016 }
1017 out
1018 }
1019}
1020
1021#[derive(Debug, Clone, PartialEq)]
1023pub struct InspectNode {
1024 pub id: u32,
1026 pub parent: Option<u32>,
1028 pub kind: &'static str,
1030 pub name: String,
1032 pub source: Option<String>,
1034 pub bounds: [f64; 4],
1037 pub opacity: f64,
1039 pub order: u32,
1041 pub text: Option<String>,
1043 pub fill: Color,
1045 pub text_overflow: f64,
1048 pub parent_kind: Option<&'static str>,
1050 pub in_camera: bool,
1053 pub shader: Option<&'static str>,
1055 pub reads_frame: bool,
1058 pub params: Vec<(String, f64)>,
1064}
1065
1066fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1068 let mut out = Vec::new();
1069 if let NodeKind::Shader { uniforms, .. } = &n.kind {
1070 for u in uniforms {
1071 match &u.value {
1072 crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1073 out.push((u.name.clone(), v.get()));
1074 }
1075 _ => {}
1076 }
1077 }
1078 }
1079 if let NodeKind::Video { data, time, .. } = &n.kind {
1082 out.push(("frame".into(), data.index_at(time.get()) as f64));
1083 }
1084 if let Some(e) = &n.effect {
1085 out.push(("effect.strength".into(), e.strength.get()));
1086 out.push(("effect.speed".into(), e.speed.get()));
1087 }
1088 let (start, end) = (n.start.get(), n.end.get());
1091 if start > 0.0 {
1092 out.push(("start".into(), start));
1093 }
1094 if end < 1.0 {
1095 out.push(("end".into(), end));
1096 }
1097 let shadow = n.shadow_color.get();
1098 if shadow.a > 0.0 {
1099 out.push(("shadow".into(), n.shadow_blur.get() * f64::from(shadow.a)));
1100 }
1101 out
1102}
1103
1104thread_local! {
1105 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1106}
1107
1108pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1112 ADD_AT.with(|c| {
1113 let prev = c.get();
1114 if prev.is_none() {
1115 c.set(Some((file, line)));
1116 }
1117 prev
1118 })
1119}
1120
1121#[track_caller]
1127pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1128 let loc = std::panic::Location::caller();
1129 let prev = push_add_at(loc.file(), loc.line());
1130 let out = f();
1131 pop_add_at(prev);
1132 out
1133}
1134
1135pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1136 ADD_AT.with(|c| c.set(prev));
1137}
1138
1139fn current_add_at() -> Option<(&'static str, u32)> {
1140 ADD_AT.with(|c| c.get())
1141}
1142
1143fn source_label(file: &str, line: u32) -> String {
1145 let file = file.replace('\\', "/");
1146 let rel = file
1147 .rfind("/src/")
1148 .map_or(file.as_str(), |i| &file[i + 1..]);
1149 format!("{rel}:{line}")
1150}
1151
1152fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1153 let pts = [
1154 (-w / 2.0, -h / 2.0),
1155 (w / 2.0, -h / 2.0),
1156 (w / 2.0, h / 2.0),
1157 (-w / 2.0, h / 2.0),
1158 ];
1159 let mut b = [
1160 f64::INFINITY,
1161 f64::INFINITY,
1162 f64::NEG_INFINITY,
1163 f64::NEG_INFINITY,
1164 ];
1165 for (x, y) in pts {
1166 let p = tf * Point::new(x, y);
1167 b[0] = b[0].min(p.x);
1168 b[1] = b[1].min(p.y);
1169 b[2] = b[2].max(p.x);
1170 b[3] = b[3].max(p.y);
1171 }
1172 b
1173}
1174
1175fn placements(nodes: &[NodeData]) -> Vec<Option<(Affine, (f64, f64))>> {
1179 fn walk(
1180 nodes: &[NodeData],
1181 id: NodeId,
1182 parent_tf: Affine,
1183 placement: Option<FlexBox>,
1184 flex: Option<&FlexMap>,
1185 out: &mut [Option<(Affine, (f64, f64))>],
1186 ) {
1187 let n = &nodes[id.0 as usize];
1188 let size = match placement {
1190 Some(b) if resizable(&n.kind) => {
1191 let s = n.scale.get();
1192 (
1193 b.size.0 / s.x.abs().max(1e-9),
1194 b.size.1 / s.y.abs().max(1e-9),
1195 )
1196 }
1197 _ => measure(nodes, id),
1198 };
1199 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1200 let tf = parent_tf * n.local_transform(offset, size);
1201 out[id.0 as usize] = Some((tf, size));
1202 let own;
1203 let map: Option<&FlexMap> = match (&n.kind, flex) {
1204 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1205 (NodeKind::Layout { .. }, None) => {
1206 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1207 Some(&own)
1208 }
1209 _ => None,
1210 };
1211 for &c in &n.children {
1212 let placement = map.and_then(|m| m.get(&c).copied());
1213 walk(nodes, c, tf, placement, map, out);
1214 }
1215 }
1216 let mut out = vec![None; nodes.len()];
1217 walk(
1218 nodes,
1219 SceneGraph::ROOT,
1220 Affine::IDENTITY,
1221 None,
1222 None,
1223 &mut out,
1224 );
1225 out
1226}
1227
1228fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1229 let mut chain = Vec::new();
1230 let mut cur = Some(id);
1231 while let Some(c) = cur {
1232 chain.push(c);
1233 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1234 }
1235 let mut tf = Affine::IDENTITY;
1236 for c in chain.into_iter().rev() {
1237 let Some(n) = nodes.get(c.0 as usize) else {
1238 break;
1239 };
1240 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1241 }
1242 tf
1243}
1244
1245fn text_info(n: &NodeData) -> (Option<String>, f64) {
1246 let (body, max_width) = match &n.kind {
1247 NodeKind::Txt {
1248 text, spans, style, ..
1249 } => {
1250 let spans = spans.get();
1251 let body = if spans.is_empty() {
1252 text.get()
1253 } else {
1254 spans.into_iter().map(|s| s.text).collect()
1255 };
1256 (body, style.max_width)
1257 }
1258 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1259 _ => return (None, 0.0),
1260 };
1261 let overflow = match (max_width, n.text_draw()) {
1262 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1263 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1264 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1265 _ => 0.0,
1266 };
1267 (Some(body), overflow)
1268}
1269
1270pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1271 let n = &nodes[id.0 as usize];
1272 match &n.kind {
1273 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1274 NodeKind::Circle { size, .. }
1275 | NodeKind::Polygon { size, .. }
1276 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1277 NodeKind::Img { width, height, .. }
1278 | NodeKind::Grid { width, height, .. }
1279 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1280 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1281 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1282 Some(TextDraw::Morph { size, .. }) => size,
1283 Some(TextDraw::Diff(frame)) => frame.size,
1284 None => (0.0, 0.0),
1285 },
1286 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1287 let bb = n
1288 .outline((0.0, 0.0))
1289 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1290 (bb.width(), bb.height())
1291 }
1292 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1293 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1295 NodeKind::Custom {
1296 measure: Some(m), ..
1297 } => m(),
1298 NodeKind::Custom { width, height, .. } => (*width, *height),
1299 NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1300 NodeKind::Video { width, height, .. } => (width.get(), height.get()),
1301 NodeKind::Group => {
1302 let mut bb: Option<kurbo::Rect> = None;
1303 for &c in &n.children {
1304 let child = &nodes[c.0 as usize];
1305 let (w, h) = measure(nodes, c);
1306 let r = child
1307 .local_transform(Vec2::ZERO, (w, h))
1308 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1309 bb = Some(bb.map_or(r, |b| b.union(r)));
1310 }
1311 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1312 }
1313 }
1314}
1315
1316fn resizable(kind: &NodeKind) -> bool {
1318 matches!(
1319 kind,
1320 NodeKind::Rect { .. }
1321 | NodeKind::Layout { .. }
1322 | NodeKind::Img { .. }
1323 | NodeKind::Grid { .. }
1324 | NodeKind::Svg { .. }
1325 | NodeKind::Shader { .. }
1326 | NodeKind::Video { .. }
1327 )
1328}
1329
1330fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1331 let n = a.len().max(b.len());
1332 for i in 0..n {
1333 match (a.get(i), b.get(i)) {
1334 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1335 (Some(x), _) => return Some(x.node),
1336 (_, Some(y)) => return Some(y.node),
1337 (None, None) => {}
1338 }
1339 }
1340 None
1341}
1342
1343pub(crate) fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1344 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1345 match (a, b) {
1346 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1347 (
1348 Stroke {
1349 path: p,
1350 paint: c,
1351 style: s,
1352 },
1353 Stroke {
1354 path: q,
1355 paint: d,
1356 style: t,
1357 },
1358 ) => p == q && c == d && s == t,
1359 (
1360 Image {
1361 rect: r,
1362 opacity: o,
1363 ..
1364 },
1365 Image {
1366 rect: s,
1367 opacity: p,
1368 ..
1369 },
1370 ) => r == s && o == p,
1371 (PushLayer(x), PushLayer(y)) => x == y,
1372 (PopLayer, PopLayer) => true,
1373 (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1374 | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1375 (Primitive::EndEffect, Primitive::EndEffect) => true,
1376 _ => false,
1377 }
1378}
1379
1380fn visit(
1383 nodes: &[NodeData],
1384 id: NodeId,
1385 parent_tf: Affine,
1386 placement: Option<FlexBox>,
1387 flex: Option<&FlexMap>,
1388 time: f64,
1389 out: &mut Vec<DrawItem>,
1390) {
1391 let n = &nodes[id.0 as usize];
1392 let opacity = n.opacity.get().clamp(0.0, 1.0);
1393 if opacity <= 0.0 {
1394 return;
1395 }
1396 let size = match placement {
1397 Some(b) if resizable(&n.kind) => {
1398 let s = n.scale.get();
1399 (
1400 b.size.0 / s.x.abs().max(1e-9),
1401 b.size.1 / s.y.abs().max(1e-9),
1402 )
1403 }
1404 _ => measure(nodes, id),
1405 };
1406 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1407 let tf = parent_tf * n.local_transform(offset, size);
1408
1409 let blend = n.blend.get();
1412 let blur = n.blur.get().max(0.0);
1413 let shadow_color = n.shadow_color.get();
1414 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1415 color: shadow_color,
1416 blur: n.shadow_blur.get().max(0.0),
1417 offset: n.shadow_offset.get(),
1418 });
1419 let layered = blend != BlendMode::Normal
1420 || blur > 0.0
1421 || shadow.is_some()
1422 || (opacity < 1.0 && !n.children.is_empty());
1423 let effect = n.effect.as_ref().map(|e| e.shade(tf * Point::ZERO, time));
1424 if let Some(shade) = &effect {
1425 out.push(DrawItem {
1426 node: id,
1427 transform: tf,
1428 prim: Primitive::BeginEffect(shade.clone()),
1429 });
1430 }
1431 if let Some(shade) = crate::shader::read_frame(nodes, n, size, tf, time, opacity) {
1432 out.push(DrawItem {
1433 node: id,
1434 transform: tf,
1435 prim: Primitive::ReadFrame(shade),
1436 });
1437 }
1438 if layered {
1439 out.push(DrawItem {
1440 node: id,
1441 transform: tf,
1442 prim: Primitive::PushLayer(Layer {
1443 opacity,
1444 blend,
1445 clip: None,
1446 blur,
1447 shadow,
1448 }),
1449 });
1450 }
1451 let paint_opacity = if layered { 1.0 } else { opacity };
1452 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1453 out.push(DrawItem {
1454 node: id,
1455 transform: tf,
1456 prim,
1457 });
1458 }
1459 if let NodeKind::Shader { .. } = &n.kind
1460 && crate::shader::problems(n).is_empty()
1461 && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1462 {
1463 out.push(DrawItem {
1464 node: id,
1465 transform: tf,
1466 prim,
1467 });
1468 }
1469
1470 if !n.children.is_empty() {
1471 let clip = n.clip.get().then(|| {
1473 n.outline(size).unwrap_or_else(|| {
1474 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1475 .to_path(0.1)
1476 })
1477 });
1478 let clipped = clip.is_some();
1479 if let Some(path) = clip {
1480 out.push(DrawItem {
1481 node: id,
1482 transform: tf,
1483 prim: Primitive::PushLayer(Layer::clip(path)),
1484 });
1485 }
1486 let own;
1489 let map: Option<&FlexMap> = match (&n.kind, flex) {
1490 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1491 (NodeKind::Layout { .. }, None) => {
1492 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1493 Some(&own)
1494 }
1495 _ => None,
1496 };
1497 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1498 order.sort_by(|a, b| {
1500 let za = nodes[a.1.0 as usize].z_index.get();
1501 let zb = nodes[b.1.0 as usize].z_index.get();
1502 za.total_cmp(&zb)
1503 });
1504 for (_, c) in order {
1505 if nodes[c.0 as usize].offscreen {
1506 continue;
1507 }
1508 let placement = map.and_then(|m| m.get(&c).copied());
1509 visit(nodes, c, tf, placement, map, time, out);
1510 }
1511 if clipped {
1512 out.push(DrawItem {
1513 node: id,
1514 transform: tf,
1515 prim: Primitive::PopLayer,
1516 });
1517 }
1518 }
1519 if layered {
1520 out.push(DrawItem {
1521 node: id,
1522 transform: tf,
1523 prim: Primitive::PopLayer,
1524 });
1525 }
1526 if effect.is_some() {
1527 out.push(DrawItem {
1528 node: id,
1529 transform: tf,
1530 prim: Primitive::EndEffect,
1531 });
1532 }
1533}
1534
1535fn offscreen(nodes: &[NodeData], id: NodeId) -> bool {
1537 let mut cur = Some(id);
1538 while let Some(c) = cur {
1539 let n = &nodes[c.0 as usize];
1540 if n.offscreen {
1541 return true;
1542 }
1543 cur = n.parent;
1544 }
1545 false
1546}
1547
1548pub(crate) fn offscreen_items(
1551 nodes: &[NodeData],
1552 id: NodeId,
1553 time: f64,
1554) -> (Vec<DrawItem>, kurbo::Rect) {
1555 let n = &nodes[id.0 as usize];
1556 let parent = n
1557 .parent
1558 .map_or(Affine::IDENTITY, |p| transform_without_layout(nodes, p));
1559 let mut out = Vec::new();
1560 visit(nodes, id, parent, None, None, time, &mut out);
1561 let (w, h) = measure(nodes, id);
1562 let [x0, y0, x1, y1] = aabb(transform_without_layout(nodes, id), w, h);
1563 (out, kurbo::Rect::new(x0, y0, x1, y1))
1564}
1565
1566impl DrawItem {
1567 fn hit(&self, p: Point) -> bool {
1568 let local = self.transform.inverse() * p;
1569 match &self.prim {
1570 Primitive::Fill { path, .. } => {
1571 (is_text_like(path) && path.bounding_box().contains(local))
1572 || closed(path).contains(local)
1573 }
1574 Primitive::Stroke { path, style, .. } => {
1575 let tol = (style.width / 2.0)
1576 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1577 path.segments().any(|s| {
1578 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1579 })
1580 }
1581 Primitive::Image { rect, .. } => rect.contains(local),
1582 Primitive::PushLayer(_)
1583 | Primitive::PopLayer
1584 | Primitive::ReadFrame(_)
1585 | Primitive::BeginEffect(_)
1586 | Primitive::EndEffect => false,
1587 }
1588 }
1589}
1590
1591fn closed(path: &BezPath) -> BezPath {
1593 let mut out = BezPath::new();
1594 let mut open = false;
1595 for el in path.elements() {
1596 match el {
1597 kurbo::PathEl::MoveTo(_) => {
1598 if open {
1599 out.close_path();
1600 }
1601 open = true;
1602 }
1603 kurbo::PathEl::ClosePath => open = false,
1604 _ => {}
1605 }
1606 out.push(*el);
1607 }
1608 if open {
1609 out.close_path();
1610 }
1611 out
1612}
1613
1614fn is_text_like(path: &BezPath) -> bool {
1617 path.elements()
1618 .iter()
1619 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1620 .count()
1621 > 3
1622}
1623
1624thread_local! {
1625 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1626}
1627
1628impl SceneGraph {
1629 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1631 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1632 struct Restore(Option<Rc<SceneGraph>>);
1633 impl Drop for Restore {
1634 fn drop(&mut self) {
1635 let prev = self.0.take();
1636 CURRENT.with(|c| *c.borrow_mut() = prev);
1637 }
1638 }
1639 let _restore = Restore(prev);
1640 f()
1641 }
1642}
1643
1644pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1645 CURRENT.with(|c| {
1646 let c = c.borrow();
1647 f(c.as_ref()
1648 .expect("oxframe: node handles can only be used while a scene is running"))
1649 })
1650}