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