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) {
806 return Some(item.transform);
807 }
808 let nodes = self.nodes.borrow();
809 let mut tf = Affine::IDENTITY;
810 let mut cur = Some(id);
811 while let Some(c) = cur {
812 let n = nodes.get(c.0 as usize)?;
813 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
814 cur = n.parent;
815 }
816 Some(tf)
817 }
818
819 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
823 let node_tf = self.world_transform(id)?;
824 let frame_tf = self.world_transform(frame)?;
825 let (w, h) = measure(&self.nodes.borrow(), id);
826 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
827 Some(kurbo::Rect::new(x0, y0, x1, y1))
828 }
829
830 pub fn frame_size(&self) -> (f64, f64) {
832 (f64::from(self.size.0), f64::from(self.size.1))
833 }
834
835 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
837 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
838 let n = nodes.get(id.0 as usize)?;
839 if n.name.as_deref() == Some(name) {
840 return Some(id);
841 }
842 for &c in &n.children {
843 if let Some(hit) = walk(nodes, c, name) {
844 return Some(hit);
845 }
846 }
847 None
848 }
849 walk(&self.nodes.borrow(), root, name)
850 }
851
852 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
854 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
855 let Some(n) = nodes.get(id.0 as usize) else {
856 return;
857 };
858 if matches!(n.kind, NodeKind::Path { .. }) {
859 out.push(id);
860 }
861 for &c in &n.children {
862 walk(nodes, c, out);
863 }
864 }
865 let mut out = Vec::new();
866 walk(&self.nodes.borrow(), root, &mut out);
867 out
868 }
869
870 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
872 let nodes = self.nodes.borrow();
873 let mut out = Vec::new();
874 let mut stack = vec![Self::ROOT];
875 while let Some(id) = stack.pop() {
876 let n = &nodes[id.0 as usize];
877 out.push((id, n.parent, n.kind.name(), n.name.clone()));
878 stack.extend(n.children.iter().rev());
879 }
880 out
881 }
882
883 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
885 let nodes = self.nodes.borrow();
886 let Some(n) = nodes.get(id.0 as usize) else {
887 return Vec::new();
888 };
889 let f = |v: f64| format!("{v:.3}");
890 let s = n.scale.get();
891 let mut out = vec![
892 ("x".into(), f(n.x.get())),
893 ("y".into(), f(n.y.get())),
894 ("rotation".into(), f(n.rotation.get())),
895 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
896 ("opacity".into(), f(n.opacity.get())),
897 ("fill".into(), n.fill.get().to_string()),
898 ("stroke".into(), n.stroke.get().to_string()),
899 ("line_width".into(), f(n.line_width.get())),
900 ("start".into(), f(n.start.get())),
901 ("end".into(), f(n.end.get())),
902 ];
903 let (w, h) = measure(&nodes, id);
904 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
905 match &n.kind {
906 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
907 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
908 NodeKind::Txt {
909 text,
910 font_size,
911 weight,
912 ..
913 } => {
914 out.push(("text".into(), text.get()));
915 out.push(("font_size".into(), f(font_size.get())));
916 out.push(("weight".into(), f(weight.get())));
917 }
918 NodeKind::Line { points, .. } => {
919 out.push(("points".into(), points.get().len().to_string()))
920 }
921 NodeKind::Layout { gap, padding, .. } => {
922 out.push(("gap".into(), f(gap.get())));
923 out.push(("padding".into(), f(padding.get())));
924 }
925 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
926 _ => {}
927 }
928 out
929 }
930
931 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
936 let list = self.draw_list();
937 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
938 let mut order = 0u32;
939 for item in &list {
940 let slot = &mut drawn[item.node.0 as usize];
941 if slot.is_none() {
942 *slot = Some((item.transform, order));
943 order += 1;
944 }
945 }
946 let nodes = self.nodes.borrow();
947 let mut out = Vec::new();
948 let mut stack = vec![Self::ROOT];
949 while let Some(id) = stack.pop() {
950 let n = &nodes[id.0 as usize];
951 stack.extend(n.children.iter().rev());
952 let (w, h) = measure(&nodes, id);
953 let (tf, order) = drawn[id.0 as usize]
954 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
955 let (text, text_overflow) = text_info(n);
956 out.push(InspectNode {
957 id: id.0,
958 parent: n.parent.map(|p| p.0),
959 kind: n.kind.name(),
960 name: n.name.clone().unwrap_or_else(|| {
961 n.source
962 .clone()
963 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
964 }),
965 source: n.source.clone(),
966 bounds: aabb(tf, w, h),
967 opacity: n.opacity.get().clamp(0.0, 1.0),
968 order,
969 text,
970 fill: n.fill.get(),
971 text_overflow,
972 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
973 in_camera: {
974 let mut up = n.parent;
975 let mut found = false;
976 while let Some(p) = up {
977 let pn = &nodes[p.0 as usize];
978 if matches!(pn.kind, NodeKind::Camera { .. }) {
979 found = true;
980 break;
981 }
982 up = pn.parent;
983 }
984 found
985 },
986 shader: match &n.kind {
987 NodeKind::Shader { preset, .. } => Some(preset.name()),
988 _ => None,
989 },
990 params: params_of(n),
991 });
992 }
993 out
994 }
995}
996
997#[derive(Debug, Clone, PartialEq)]
999pub struct InspectNode {
1000 pub id: u32,
1002 pub parent: Option<u32>,
1004 pub kind: &'static str,
1006 pub name: String,
1008 pub source: Option<String>,
1010 pub bounds: [f64; 4],
1013 pub opacity: f64,
1015 pub order: u32,
1017 pub text: Option<String>,
1019 pub fill: Color,
1021 pub text_overflow: f64,
1024 pub parent_kind: Option<&'static str>,
1026 pub in_camera: bool,
1029 pub shader: Option<&'static str>,
1031 pub params: Vec<(String, f64)>,
1034}
1035
1036fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1038 let mut out = Vec::new();
1039 if let NodeKind::Shader { uniforms, .. } = &n.kind {
1040 for u in uniforms {
1041 match &u.value {
1042 crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1043 out.push((u.name.clone(), v.get()));
1044 }
1045 _ => {}
1046 }
1047 }
1048 }
1049 if let Some(e) = &n.effect {
1050 out.push(("effect.strength".into(), e.strength.get()));
1051 out.push(("effect.speed".into(), e.speed.get()));
1052 }
1053 out
1054}
1055
1056thread_local! {
1057 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1058}
1059
1060pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1064 ADD_AT.with(|c| {
1065 let prev = c.get();
1066 if prev.is_none() {
1067 c.set(Some((file, line)));
1068 }
1069 prev
1070 })
1071}
1072
1073#[track_caller]
1079pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1080 let loc = std::panic::Location::caller();
1081 let prev = push_add_at(loc.file(), loc.line());
1082 let out = f();
1083 pop_add_at(prev);
1084 out
1085}
1086
1087pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1088 ADD_AT.with(|c| c.set(prev));
1089}
1090
1091fn current_add_at() -> Option<(&'static str, u32)> {
1092 ADD_AT.with(|c| c.get())
1093}
1094
1095fn source_label(file: &str, line: u32) -> String {
1097 let file = file.replace('\\', "/");
1098 let rel = file
1099 .rfind("/src/")
1100 .map_or(file.as_str(), |i| &file[i + 1..]);
1101 format!("{rel}:{line}")
1102}
1103
1104fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1105 let pts = [
1106 (-w / 2.0, -h / 2.0),
1107 (w / 2.0, -h / 2.0),
1108 (w / 2.0, h / 2.0),
1109 (-w / 2.0, h / 2.0),
1110 ];
1111 let mut b = [
1112 f64::INFINITY,
1113 f64::INFINITY,
1114 f64::NEG_INFINITY,
1115 f64::NEG_INFINITY,
1116 ];
1117 for (x, y) in pts {
1118 let p = tf * Point::new(x, y);
1119 b[0] = b[0].min(p.x);
1120 b[1] = b[1].min(p.y);
1121 b[2] = b[2].max(p.x);
1122 b[3] = b[3].max(p.y);
1123 }
1124 b
1125}
1126
1127fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1128 let mut chain = Vec::new();
1129 let mut cur = Some(id);
1130 while let Some(c) = cur {
1131 chain.push(c);
1132 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1133 }
1134 let mut tf = Affine::IDENTITY;
1135 for c in chain.into_iter().rev() {
1136 let Some(n) = nodes.get(c.0 as usize) else {
1137 break;
1138 };
1139 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1140 }
1141 tf
1142}
1143
1144fn text_info(n: &NodeData) -> (Option<String>, f64) {
1145 let (body, max_width) = match &n.kind {
1146 NodeKind::Txt {
1147 text, spans, style, ..
1148 } => {
1149 let spans = spans.get();
1150 let body = if spans.is_empty() {
1151 text.get()
1152 } else {
1153 spans.into_iter().map(|s| s.text).collect()
1154 };
1155 (body, style.max_width)
1156 }
1157 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1158 _ => return (None, 0.0),
1159 };
1160 let overflow = match (max_width, n.text_draw()) {
1161 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1162 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1163 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1164 _ => 0.0,
1165 };
1166 (Some(body), overflow)
1167}
1168
1169pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1170 let n = &nodes[id.0 as usize];
1171 match &n.kind {
1172 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1173 NodeKind::Circle { size, .. }
1174 | NodeKind::Polygon { size, .. }
1175 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1176 NodeKind::Img { width, height, .. }
1177 | NodeKind::Grid { width, height, .. }
1178 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1179 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1180 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1181 Some(TextDraw::Morph { size, .. }) => size,
1182 Some(TextDraw::Diff(frame)) => frame.size,
1183 None => (0.0, 0.0),
1184 },
1185 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1186 let bb = n
1187 .outline((0.0, 0.0))
1188 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1189 (bb.width(), bb.height())
1190 }
1191 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1192 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1194 NodeKind::Custom {
1195 measure: Some(m), ..
1196 } => m(),
1197 NodeKind::Custom { width, height, .. } => (*width, *height),
1198 NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1199 NodeKind::Video { width, height, .. } => (width.get(), height.get()),
1200 NodeKind::Group => {
1201 let mut bb: Option<kurbo::Rect> = None;
1202 for &c in &n.children {
1203 let child = &nodes[c.0 as usize];
1204 let (w, h) = measure(nodes, c);
1205 let r = child
1206 .local_transform(Vec2::ZERO, (w, h))
1207 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1208 bb = Some(bb.map_or(r, |b| b.union(r)));
1209 }
1210 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1211 }
1212 }
1213}
1214
1215fn resizable(kind: &NodeKind) -> bool {
1217 matches!(
1218 kind,
1219 NodeKind::Rect { .. }
1220 | NodeKind::Layout { .. }
1221 | NodeKind::Img { .. }
1222 | NodeKind::Grid { .. }
1223 | NodeKind::Svg { .. }
1224 | NodeKind::Shader { .. }
1225 | NodeKind::Video { .. }
1226 )
1227}
1228
1229fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1230 let n = a.len().max(b.len());
1231 for i in 0..n {
1232 match (a.get(i), b.get(i)) {
1233 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1234 (Some(x), _) => return Some(x.node),
1235 (_, Some(y)) => return Some(y.node),
1236 (None, None) => {}
1237 }
1238 }
1239 None
1240}
1241
1242fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1243 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1244 match (a, b) {
1245 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1246 (
1247 Stroke {
1248 path: p,
1249 paint: c,
1250 style: s,
1251 },
1252 Stroke {
1253 path: q,
1254 paint: d,
1255 style: t,
1256 },
1257 ) => p == q && c == d && s == t,
1258 (
1259 Image {
1260 rect: r,
1261 opacity: o,
1262 ..
1263 },
1264 Image {
1265 rect: s,
1266 opacity: p,
1267 ..
1268 },
1269 ) => r == s && o == p,
1270 (PushLayer(x), PushLayer(y)) => x == y,
1271 (PopLayer, PopLayer) => true,
1272 (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1273 | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1274 (Primitive::EndEffect, Primitive::EndEffect) => true,
1275 _ => false,
1276 }
1277}
1278
1279#[allow(clippy::too_many_arguments)]
1282fn visit(
1283 nodes: &[NodeData],
1284 id: NodeId,
1285 parent_tf: Affine,
1286 placement: Option<FlexBox>,
1287 flex: Option<&FlexMap>,
1288 time: f64,
1289 inside: bool,
1290 out: &mut Vec<DrawItem>,
1291) {
1292 let n = &nodes[id.0 as usize];
1293 let opacity = n.opacity.get().clamp(0.0, 1.0);
1294 if opacity <= 0.0 {
1295 return;
1296 }
1297 let size = match placement {
1298 Some(b) if resizable(&n.kind) => {
1299 let s = n.scale.get();
1300 (
1301 b.size.0 / s.x.abs().max(1e-9),
1302 b.size.1 / s.y.abs().max(1e-9),
1303 )
1304 }
1305 _ => measure(nodes, id),
1306 };
1307 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1308 let tf = parent_tf * n.local_transform(offset, size);
1309
1310 let blend = n.blend.get();
1313 let blur = n.blur.get().max(0.0);
1314 let shadow_color = n.shadow_color.get();
1315 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1316 color: shadow_color,
1317 blur: n.shadow_blur.get().max(0.0),
1318 offset: n.shadow_offset.get(),
1319 });
1320 let layered = blend != BlendMode::Normal
1321 || blur > 0.0
1322 || shadow.is_some()
1323 || (opacity < 1.0 && !n.children.is_empty());
1324 let effect = n.effect.as_ref().and_then(|e| {
1325 if inside {
1326 cycle(n, "This effect is inside a layer or another effect");
1327 return None;
1328 }
1329 Some(e.shade(tf * Point::ZERO, time))
1330 });
1331 if let Some(shade) = &effect {
1332 out.push(DrawItem {
1333 node: id,
1334 transform: tf,
1335 prim: Primitive::BeginEffect(shade.clone()),
1336 });
1337 }
1338 if let Some(shade) = crate::shader::read_frame(n, size, tf, time, opacity) {
1339 if inside || effect.is_some() {
1340 cycle(
1341 n,
1342 "lens_drops reads the frame, but it is inside a layer or an effect",
1343 );
1344 } else {
1345 out.push(DrawItem {
1346 node: id,
1347 transform: tf,
1348 prim: Primitive::ReadFrame(shade),
1349 });
1350 }
1351 }
1352 if layered {
1353 out.push(DrawItem {
1354 node: id,
1355 transform: tf,
1356 prim: Primitive::PushLayer(Layer {
1357 opacity,
1358 blend,
1359 clip: None,
1360 blur,
1361 shadow,
1362 }),
1363 });
1364 }
1365 let paint_opacity = if layered { 1.0 } else { opacity };
1366 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1367 out.push(DrawItem {
1368 node: id,
1369 transform: tf,
1370 prim,
1371 });
1372 }
1373 if let NodeKind::Shader { .. } = &n.kind
1374 && crate::shader::problems(n).is_empty()
1375 && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1376 {
1377 out.push(DrawItem {
1378 node: id,
1379 transform: tf,
1380 prim,
1381 });
1382 }
1383
1384 if !n.children.is_empty() {
1385 let clip = n.clip.get().then(|| {
1387 n.outline(size).unwrap_or_else(|| {
1388 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1389 .to_path(0.1)
1390 })
1391 });
1392 let clipped = clip.is_some();
1393 if let Some(path) = clip {
1394 out.push(DrawItem {
1395 node: id,
1396 transform: tf,
1397 prim: Primitive::PushLayer(Layer::clip(path)),
1398 });
1399 }
1400 let own;
1403 let map: Option<&FlexMap> = match (&n.kind, flex) {
1404 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1405 (NodeKind::Layout { .. }, None) => {
1406 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1407 Some(&own)
1408 }
1409 _ => None,
1410 };
1411 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1412 order.sort_by(|a, b| {
1414 let za = nodes[a.1.0 as usize].z_index.get();
1415 let zb = nodes[b.1.0 as usize].z_index.get();
1416 za.total_cmp(&zb)
1417 });
1418 for (_, c) in order {
1419 let placement = map.and_then(|m| m.get(&c).copied());
1420 let inside = inside || layered || clipped || effect.is_some();
1421 visit(nodes, c, tf, placement, map, time, inside, out);
1422 }
1423 if clipped {
1424 out.push(DrawItem {
1425 node: id,
1426 transform: tf,
1427 prim: Primitive::PopLayer,
1428 });
1429 }
1430 }
1431 if layered {
1432 out.push(DrawItem {
1433 node: id,
1434 transform: tf,
1435 prim: Primitive::PopLayer,
1436 });
1437 }
1438 if effect.is_some() {
1439 out.push(DrawItem {
1440 node: id,
1441 transform: tf,
1442 prim: Primitive::EndEffect,
1443 });
1444 }
1445}
1446
1447fn cycle(n: &NodeData, what: &str) {
1449 crate::shader::push_diag(crate::shader::ShaderDiag {
1450 node: n
1451 .name
1452 .clone()
1453 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0)),
1454 source: n.source.clone(),
1455 rule: "input_cycle",
1456 hint: format!(
1457 "{what} (a group with opacity below 1, a blur, a clip, a blend or a shadow). \
1458 Its pixels are not finished there, so the shader is skipped. \
1459 Put the effect on that group, or move the node out of it."
1460 ),
1461 });
1462}
1463
1464impl DrawItem {
1465 fn hit(&self, p: Point) -> bool {
1466 let local = self.transform.inverse() * p;
1467 match &self.prim {
1468 Primitive::Fill { path, .. } => {
1469 (is_text_like(path) && path.bounding_box().contains(local))
1470 || closed(path).contains(local)
1471 }
1472 Primitive::Stroke { path, style, .. } => {
1473 let tol = (style.width / 2.0)
1474 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1475 path.segments().any(|s| {
1476 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1477 })
1478 }
1479 Primitive::Image { rect, .. } => rect.contains(local),
1480 Primitive::PushLayer(_)
1481 | Primitive::PopLayer
1482 | Primitive::ReadFrame(_)
1483 | Primitive::BeginEffect(_)
1484 | Primitive::EndEffect => false,
1485 }
1486 }
1487}
1488
1489fn closed(path: &BezPath) -> BezPath {
1491 let mut out = BezPath::new();
1492 let mut open = false;
1493 for el in path.elements() {
1494 match el {
1495 kurbo::PathEl::MoveTo(_) => {
1496 if open {
1497 out.close_path();
1498 }
1499 open = true;
1500 }
1501 kurbo::PathEl::ClosePath => open = false,
1502 _ => {}
1503 }
1504 out.push(*el);
1505 }
1506 if open {
1507 out.close_path();
1508 }
1509 out
1510}
1511
1512fn is_text_like(path: &BezPath) -> bool {
1515 path.elements()
1516 .iter()
1517 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1518 .count()
1519 > 3
1520}
1521
1522thread_local! {
1523 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1524}
1525
1526impl SceneGraph {
1527 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1529 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1530 struct Restore(Option<Rc<SceneGraph>>);
1531 impl Drop for Restore {
1532 fn drop(&mut self) {
1533 let prev = self.0.take();
1534 CURRENT.with(|c| *c.borrow_mut() = prev);
1535 }
1536 }
1537 let _restore = Restore(prev);
1538 f()
1539 }
1540}
1541
1542pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1543 CURRENT.with(|c| {
1544 let c = c.borrow();
1545 f(c.as_ref()
1546 .expect("oxframe: node handles can only be used while a scene is running"))
1547 })
1548}