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 Shader {
198 code: crate::shader::ShaderCode,
199 preset: crate::shader::Preset,
201 width: Signal<f64>,
202 height: Signal<f64>,
203 uniforms: Vec<crate::shader::Uniform>,
204 inputs: Vec<(String, crate::post::Input)>,
206 },
207}
208
209impl NodeKind {
210 pub fn name(&self) -> &'static str {
211 match self {
212 Self::Group => "Group",
213 Self::Camera { .. } => "Camera",
214 Self::Rect { .. } => "Rect",
215 Self::Circle { .. } => "Circle",
216 Self::Polygon { .. } => "Polygon",
217 Self::Star { .. } => "Star",
218 Self::Path { .. } => "Path",
219 Self::Line { .. } => "Line",
220 Self::Txt { .. } => "Txt",
221 Self::Code { .. } => "Code",
222 Self::Img { .. } => "Img",
223 Self::Layout { .. } => "Layout",
224 Self::Grid { .. } => "Grid",
225 Self::Svg { .. } => "Svg",
226 Self::Custom { type_name, .. } => type_name,
227 Self::Particles { .. } => "Particles",
228 Self::Shader { .. } => "Shader",
229 }
230 }
231}
232
233#[doc(hidden)]
235pub struct NodeData {
236 pub id: NodeId,
237 pub name: Option<String>,
238 pub source: Option<String>,
240 pub parent: Option<NodeId>,
241 pub children: Vec<NodeId>,
242 pub kind: NodeKind,
243 pub x: Signal<f64>,
244 pub y: Signal<f64>,
245 pub rotation: Signal<f64>,
247 pub scale: Signal<Vec2>,
248 pub opacity: Signal<f64>,
249 pub fill: Signal<Color>,
250 pub stroke: Signal<Color>,
251 pub line_width: Signal<f64>,
252 pub start: Signal<f64>,
254 pub end: Signal<f64>,
255 pub fill_gradient: Signal<Option<Gradient>>,
257 pub stroke_gradient: Signal<Option<Gradient>>,
258 pub dash: Signal<Vec<f64>>,
260 pub dash_offset: Signal<f64>,
261 pub line_cap: Signal<LineCap>,
262 pub line_join: Signal<LineJoin>,
263 pub blur: Signal<f64>,
265 pub shadow_color: Signal<Color>,
266 pub shadow_blur: Signal<f64>,
267 pub shadow_offset: Signal<Vec2>,
268 pub clip: Signal<bool>,
270 pub blend: Signal<BlendMode>,
271 pub z_index: Signal<f64>,
273 pub anchor: Signal<Vec2>,
277 pub grow: Signal<f64>,
279 pub shrink: Signal<f64>,
280 pub basis: Signal<Option<f64>>,
281 pub margin: Signal<f64>,
282 pub align_self: Signal<Option<LayoutAlign>>,
283 pub place: Option<crate::place::Place>,
285 pub placed: Cell<(f64, f64)>,
287 pub effect: Option<crate::post::EffectData>,
289}
290
291impl NodeData {
292 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
295 if let NodeKind::Camera { zoom } = &self.kind {
296 let (x, y) = self.placed.get();
300 return Affine::translate(offset)
301 * Affine::rotate(-self.rotation.get().to_radians())
302 * Affine::scale(zoom.get().max(1e-6))
303 * Affine::translate((-x, -y));
304 }
305 let s = self.scale.get();
306 let a = self.anchor.get();
307 let (x, y) = self.placed.get();
308 Affine::translate(offset + Vec2::new(x, y))
309 * Affine::rotate(self.rotation.get().to_radians())
310 * Affine::scale_non_uniform(s.x, s.y)
311 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
312 }
313
314 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
316 match &self.kind {
317 NodeKind::Txt {
318 text,
319 spans,
320 font_size,
321 weight,
322 style,
323 morph,
324 morph_t,
325 } => {
326 let style = TextStyle {
327 size: font_size.get().max(0.1),
328 weight: weight.get().clamp(1.0, 1000.0),
329 ..style.clone()
330 };
331 if let Some((from, to)) = morph.get() {
332 let a = oxframe_text::layout(&from, &style);
333 let b = oxframe_text::layout(&to, &style);
334 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
335 return Some(TextDraw::Morph { glyphs, size });
336 }
337 let spans = spans.get();
338 Some(TextDraw::Styled(if spans.is_empty() {
339 crate::text::rich(&[RichSpan::new(text.get())], &style)
340 } else {
341 crate::text::rich(&spans, &style)
342 }))
343 }
344 NodeKind::Code {
345 code,
346 edit_from,
347 edit_to,
348 editing,
349 edit_t,
350 edit_flash,
351 lang,
352 theme,
353 font_size,
354 style,
355 focus,
356 dim,
357 } => {
358 let style = TextStyle {
359 size: font_size.get().max(0.1),
360 ..style.clone()
361 };
362 if editing.get() {
363 let (frame, _) = crate::text::layout_diff(
364 &edit_from.get(),
365 &edit_to.get(),
366 *lang,
367 crate::text::DiffView {
368 theme,
369 style: &style,
370 focus: focus.get(),
371 dim: dim.get(),
372 t: edit_t.get(),
373 flash: edit_flash.get(),
374 },
375 );
376 return Some(TextDraw::Diff(frame));
377 }
378 Some(TextDraw::Styled(crate::text::code(
379 &code.get(),
380 *lang,
381 theme,
382 &style,
383 focus.get(),
384 dim.get(),
385 )))
386 }
387 _ => None,
388 }
389 }
390
391 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
393 const TOLERANCE: f64 = 0.1;
394 match &self.kind {
395 NodeKind::Group
396 | NodeKind::Svg { .. }
397 | NodeKind::Camera { .. }
398 | NodeKind::Img { .. }
399 | NodeKind::Txt { .. }
400 | NodeKind::Code { .. }
401 | NodeKind::Grid { .. }
402 | NodeKind::Custom { .. }
403 | NodeKind::Particles { .. }
404 | NodeKind::Shader { .. } => None,
405 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
406 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
407 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
408 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
409 Some(if r > 0.0 {
410 rect.to_rounded_rect(r).to_path(TOLERANCE)
411 } else {
412 rect.to_path(TOLERANCE)
413 })
414 }
415 NodeKind::Circle {
416 start_angle,
417 end_angle,
418 pie,
419 ..
420 } => {
421 let r = size.0.max(0.0) / 2.0;
424 let (a0, a1) = (start_angle.get(), end_angle.get());
425 let sweep = (a1 - a0).clamp(-360.0, 360.0);
426 if sweep.abs() >= 360.0 - 1e-9 {
427 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
428 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
429 }
430 let arc = kurbo::Arc::new(
431 (0.0, 0.0),
432 (r, r),
433 (a0 - 90.0).to_radians(),
434 sweep.to_radians(),
435 0.0,
436 );
437 let mut p = BezPath::new();
438 if *pie {
439 p.move_to((0.0, 0.0));
440 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
441 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
442 p.close_path();
443 } else {
444 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
445 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
446 }
447 Some(p)
448 }
449 NodeKind::Polygon { sides, .. } => {
450 let n = sides.get().round().max(3.0) as usize;
451 Some(regular(n, size.0.max(0.0) / 2.0, None))
452 }
453 NodeKind::Star { points, inner, .. } => {
454 let n = points.get().round().max(2.0) as usize;
455 let r = size.0.max(0.0) / 2.0;
456 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
457 }
458 NodeKind::Path { data } => Some(data.get()),
459 NodeKind::Line {
460 points,
461 closed,
462 smooth,
463 ..
464 } => {
465 let pts = points.get();
466 if *smooth && pts.len() > 2 {
467 return Some(catmull_rom(&pts, *closed));
468 }
469 let mut p = BezPath::new();
470 for (i, v) in pts.iter().enumerate() {
471 if i == 0 {
472 p.move_to(v.to_point());
473 } else {
474 p.line_to(v.to_point());
475 }
476 }
477 if *closed && pts.len() > 2 {
478 p.close_path();
479 }
480 Some(p)
481 }
482 }
483 }
484}
485
486fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
488 let count = if inner.is_some() { 2 * n } else { n };
489 let mut p = BezPath::new();
490 for i in 0..count {
491 let radius = match inner {
492 Some(ri) if i % 2 == 1 => ri,
493 _ => r,
494 };
495 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
496 let pt = Point::new(a.cos() * radius, a.sin() * radius);
497 if i == 0 {
498 p.move_to(pt);
499 } else {
500 p.line_to(pt);
501 }
502 }
503 p.close_path();
504 p
505}
506
507fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
509 let n = pts.len();
510 let at = |i: isize| -> Vec2 {
511 if closed {
512 pts[i.rem_euclid(n as isize) as usize]
513 } else {
514 pts[i.clamp(0, n as isize - 1) as usize]
515 }
516 };
517 let mut p = BezPath::new();
518 p.move_to(pts[0].to_point());
519 let segs = if closed { n } else { n - 1 };
520 for i in 0..segs as isize {
521 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
522 let c1 = p1 + (p2 - p0) / 6.0;
523 let c2 = p2 - (p3 - p1) / 6.0;
524 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
525 }
526 if closed {
527 p.close_path();
528 }
529 p
530}
531
532pub(crate) enum TextDraw {
533 Styled(Styled),
534 Morph {
536 glyphs: Vec<(BezPath, f64)>,
537 size: (f64, f64),
538 },
539 Diff(DiffFrame),
541}
542
543pub struct SceneGraph {
545 nodes: RefCell<Vec<NodeData>>,
546 size: (u32, u32),
547 time: Cell<f64>,
549 shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
551}
552
553impl SceneGraph {
554 pub const ROOT: NodeId = NodeId(0);
556
557 pub fn new(width: u32, height: u32) -> Rc<Self> {
559 let graph = Rc::new(Self {
560 nodes: RefCell::new(Vec::new()),
561 size: (width, height),
562 time: Cell::new(0.0),
563 shader_diags: RefCell::new(Vec::new()),
564 });
565 graph.insert(
566 None,
567 NodeKind::Group,
568 crate::BaseProps {
569 name: Some("view".into()),
570 ..Default::default()
571 },
572 );
573 graph
574 }
575
576 pub fn size(&self) -> (u32, u32) {
578 self.size
579 }
580
581 pub fn set_time(&self, seconds: f64) {
584 self.time.set(seconds.max(0.0));
585 }
586
587 pub fn time(&self) -> f64 {
589 self.time.get()
590 }
591
592 pub fn len(&self) -> usize {
594 self.nodes.borrow().len()
595 }
596
597 pub fn is_empty(&self) -> bool {
599 self.len() == 0
600 }
601
602 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
608 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
609 }
610
611 pub(crate) fn next_id(&self) -> u32 {
612 self.len() as u32
613 }
614
615 pub(crate) fn insert(
616 &self,
617 parent: Option<NodeId>,
618 kind: NodeKind,
619 base: crate::BaseProps,
620 ) -> NodeId {
621 let mut nodes = self.nodes.borrow_mut();
622 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
623 let label = format!("{}#{}", kind.name(), id.0);
624 let sig = |field: &str| format!("{label}.{field}");
625 nodes.push(NodeData {
626 id,
627 name: base.name,
628 source: current_add_at().map(|(file, line)| source_label(file, line)),
629 parent,
630 children: Vec::new(),
631 x: Signal::new(base.x).with_name(sig("x")),
632 y: Signal::new(base.y).with_name(sig("y")),
633 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
634 scale: Signal::new(base.scale).with_name(sig("scale")),
635 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
636 fill: Signal::new(base.fill).with_name(sig("fill")),
637 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
638 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
639 start: Signal::new(base.start).with_name(sig("start")),
640 end: Signal::new(base.end).with_name(sig("end")),
641 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
642 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
643 dash: Signal::new(base.dash).with_name(sig("dash")),
644 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
645 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
646 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
647 blur: Signal::new(base.blur).with_name(sig("blur")),
648 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
649 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
650 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
651 clip: Signal::new(base.clip).with_name(sig("clip")),
652 blend: Signal::new(base.blend).with_name(sig("blend")),
653 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
654 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
655 grow: Signal::new(base.grow).with_name(sig("grow")),
656 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
657 basis: Signal::new(base.basis).with_name(sig("basis")),
658 margin: Signal::new(base.margin).with_name(sig("margin")),
659 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
660 place: base.place,
661 placed: Cell::new((base.x, base.y)),
662 effect: None,
663 kind,
664 });
665 if let Some(p) = parent {
666 nodes[p.0 as usize].children.push(id);
667 }
668 id
669 }
670
671 pub(crate) fn detach(&self, id: NodeId) {
672 let mut nodes = self.nodes.borrow_mut();
673 if let Some(p) = nodes[id.0 as usize].parent.take() {
674 nodes[p.0 as usize].children.retain(|c| *c != id);
675 }
676 }
677
678 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
679 self.detach(id);
680 let mut nodes = self.nodes.borrow_mut();
681 nodes[id.0 as usize].parent = Some(parent);
682 nodes[parent.0 as usize].children.push(id);
683 }
684
685 pub fn measure(&self, id: NodeId) -> (f64, f64) {
687 self.resolve_places();
688 let nodes = self.nodes.borrow();
689 measure(&nodes, id)
690 }
691
692 pub(crate) fn set_effect(&self, id: NodeId, effect: crate::post::EffectData) {
693 self.nodes.borrow_mut()[id.0 as usize].effect = Some(effect);
694 }
695
696 pub fn has_post(&self) -> bool {
700 self.nodes.borrow().iter().any(|n| {
701 n.effect.is_some()
702 || matches!(
703 n.kind,
704 NodeKind::Shader {
705 preset: crate::shader::Preset::LensDrops,
706 ..
707 }
708 )
709 })
710 }
711
712 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
713 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
714 }
715
716 fn resolve_places(&self) {
717 let nodes = self.nodes.borrow();
718 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
719 }
720
721 pub fn draw_list(&self) -> Vec<DrawItem> {
723 self.resolve_places();
724 let nodes = self.nodes.borrow();
725 let mut out = Vec::new();
726 crate::shader::clear_diags();
727 for n in nodes.iter() {
728 for diag in crate::shader::problems(n) {
729 crate::shader::push_diag(diag);
730 }
731 }
732 visit(
733 &nodes,
734 Self::ROOT,
735 Affine::IDENTITY,
736 None,
737 None,
738 self.time.get(),
739 false,
740 &mut out,
741 );
742 *self.shader_diags.borrow_mut() = crate::shader::take_diags();
743 out
744 }
745
746 pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
748 self.shader_diags.borrow().clone()
749 }
750
751 pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
754 let a = self.draw_list();
755 let b = self.draw_list();
756 let id = first_drift(&a, &b)?;
757 let n = self.node(id);
758 let label = n
759 .name
760 .clone()
761 .or_else(|| n.source.clone())
762 .unwrap_or_else(|| n.kind.name().to_owned());
763 Some((label, n.source.clone()))
764 }
765
766 pub fn pick(&self, p: Point) -> Option<NodeId> {
768 self.draw_list()
769 .iter()
770 .rev()
771 .find(|item| item.node != Self::ROOT && item.hit(p))
772 .map(|item| item.node)
773 }
774
775 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
777 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
780 return Some(item.transform);
781 }
782 let nodes = self.nodes.borrow();
783 let mut tf = Affine::IDENTITY;
784 let mut cur = Some(id);
785 while let Some(c) = cur {
786 let n = nodes.get(c.0 as usize)?;
787 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
788 cur = n.parent;
789 }
790 Some(tf)
791 }
792
793 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
797 let node_tf = self.world_transform(id)?;
798 let frame_tf = self.world_transform(frame)?;
799 let (w, h) = measure(&self.nodes.borrow(), id);
800 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
801 Some(kurbo::Rect::new(x0, y0, x1, y1))
802 }
803
804 pub fn frame_size(&self) -> (f64, f64) {
806 (f64::from(self.size.0), f64::from(self.size.1))
807 }
808
809 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
811 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
812 let n = nodes.get(id.0 as usize)?;
813 if n.name.as_deref() == Some(name) {
814 return Some(id);
815 }
816 for &c in &n.children {
817 if let Some(hit) = walk(nodes, c, name) {
818 return Some(hit);
819 }
820 }
821 None
822 }
823 walk(&self.nodes.borrow(), root, name)
824 }
825
826 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
828 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
829 let Some(n) = nodes.get(id.0 as usize) else {
830 return;
831 };
832 if matches!(n.kind, NodeKind::Path { .. }) {
833 out.push(id);
834 }
835 for &c in &n.children {
836 walk(nodes, c, out);
837 }
838 }
839 let mut out = Vec::new();
840 walk(&self.nodes.borrow(), root, &mut out);
841 out
842 }
843
844 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
846 let nodes = self.nodes.borrow();
847 let mut out = Vec::new();
848 let mut stack = vec![Self::ROOT];
849 while let Some(id) = stack.pop() {
850 let n = &nodes[id.0 as usize];
851 out.push((id, n.parent, n.kind.name(), n.name.clone()));
852 stack.extend(n.children.iter().rev());
853 }
854 out
855 }
856
857 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
859 let nodes = self.nodes.borrow();
860 let Some(n) = nodes.get(id.0 as usize) else {
861 return Vec::new();
862 };
863 let f = |v: f64| format!("{v:.3}");
864 let s = n.scale.get();
865 let mut out = vec![
866 ("x".into(), f(n.x.get())),
867 ("y".into(), f(n.y.get())),
868 ("rotation".into(), f(n.rotation.get())),
869 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
870 ("opacity".into(), f(n.opacity.get())),
871 ("fill".into(), n.fill.get().to_string()),
872 ("stroke".into(), n.stroke.get().to_string()),
873 ("line_width".into(), f(n.line_width.get())),
874 ("start".into(), f(n.start.get())),
875 ("end".into(), f(n.end.get())),
876 ];
877 let (w, h) = measure(&nodes, id);
878 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
879 match &n.kind {
880 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
881 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
882 NodeKind::Txt {
883 text,
884 font_size,
885 weight,
886 ..
887 } => {
888 out.push(("text".into(), text.get()));
889 out.push(("font_size".into(), f(font_size.get())));
890 out.push(("weight".into(), f(weight.get())));
891 }
892 NodeKind::Line { points, .. } => {
893 out.push(("points".into(), points.get().len().to_string()))
894 }
895 NodeKind::Layout { gap, padding, .. } => {
896 out.push(("gap".into(), f(gap.get())));
897 out.push(("padding".into(), f(padding.get())));
898 }
899 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
900 _ => {}
901 }
902 out
903 }
904
905 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
910 let list = self.draw_list();
911 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
912 let mut order = 0u32;
913 for item in &list {
914 let slot = &mut drawn[item.node.0 as usize];
915 if slot.is_none() {
916 *slot = Some((item.transform, order));
917 order += 1;
918 }
919 }
920 let nodes = self.nodes.borrow();
921 let mut out = Vec::new();
922 let mut stack = vec![Self::ROOT];
923 while let Some(id) = stack.pop() {
924 let n = &nodes[id.0 as usize];
925 stack.extend(n.children.iter().rev());
926 let (w, h) = measure(&nodes, id);
927 let (tf, order) = drawn[id.0 as usize]
928 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
929 let (text, text_overflow) = text_info(n);
930 out.push(InspectNode {
931 id: id.0,
932 parent: n.parent.map(|p| p.0),
933 kind: n.kind.name(),
934 name: n.name.clone().unwrap_or_else(|| {
935 n.source
936 .clone()
937 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
938 }),
939 source: n.source.clone(),
940 bounds: aabb(tf, w, h),
941 opacity: n.opacity.get().clamp(0.0, 1.0),
942 order,
943 text,
944 fill: n.fill.get(),
945 text_overflow,
946 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
947 in_camera: {
948 let mut up = n.parent;
949 let mut found = false;
950 while let Some(p) = up {
951 let pn = &nodes[p.0 as usize];
952 if matches!(pn.kind, NodeKind::Camera { .. }) {
953 found = true;
954 break;
955 }
956 up = pn.parent;
957 }
958 found
959 },
960 shader: match &n.kind {
961 NodeKind::Shader { preset, .. } => Some(preset.name()),
962 _ => None,
963 },
964 params: params_of(n),
965 });
966 }
967 out
968 }
969}
970
971#[derive(Debug, Clone, PartialEq)]
973pub struct InspectNode {
974 pub id: u32,
976 pub parent: Option<u32>,
978 pub kind: &'static str,
980 pub name: String,
982 pub source: Option<String>,
984 pub bounds: [f64; 4],
987 pub opacity: f64,
989 pub order: u32,
991 pub text: Option<String>,
993 pub fill: Color,
995 pub text_overflow: f64,
998 pub parent_kind: Option<&'static str>,
1000 pub in_camera: bool,
1003 pub shader: Option<&'static str>,
1005 pub params: Vec<(String, f64)>,
1008}
1009
1010fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1012 let mut out = Vec::new();
1013 if let NodeKind::Shader { uniforms, .. } = &n.kind {
1014 for u in uniforms {
1015 match &u.value {
1016 crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1017 out.push((u.name.clone(), v.get()));
1018 }
1019 _ => {}
1020 }
1021 }
1022 }
1023 if let Some(e) = &n.effect {
1024 out.push(("effect.strength".into(), e.strength.get()));
1025 out.push(("effect.speed".into(), e.speed.get()));
1026 }
1027 out
1028}
1029
1030thread_local! {
1031 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1032}
1033
1034pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1038 ADD_AT.with(|c| {
1039 let prev = c.get();
1040 if prev.is_none() {
1041 c.set(Some((file, line)));
1042 }
1043 prev
1044 })
1045}
1046
1047#[track_caller]
1053pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1054 let loc = std::panic::Location::caller();
1055 let prev = push_add_at(loc.file(), loc.line());
1056 let out = f();
1057 pop_add_at(prev);
1058 out
1059}
1060
1061pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1062 ADD_AT.with(|c| c.set(prev));
1063}
1064
1065fn current_add_at() -> Option<(&'static str, u32)> {
1066 ADD_AT.with(|c| c.get())
1067}
1068
1069fn source_label(file: &str, line: u32) -> String {
1071 let file = file.replace('\\', "/");
1072 let rel = file
1073 .rfind("/src/")
1074 .map_or(file.as_str(), |i| &file[i + 1..]);
1075 format!("{rel}:{line}")
1076}
1077
1078fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1079 let pts = [
1080 (-w / 2.0, -h / 2.0),
1081 (w / 2.0, -h / 2.0),
1082 (w / 2.0, h / 2.0),
1083 (-w / 2.0, h / 2.0),
1084 ];
1085 let mut b = [
1086 f64::INFINITY,
1087 f64::INFINITY,
1088 f64::NEG_INFINITY,
1089 f64::NEG_INFINITY,
1090 ];
1091 for (x, y) in pts {
1092 let p = tf * Point::new(x, y);
1093 b[0] = b[0].min(p.x);
1094 b[1] = b[1].min(p.y);
1095 b[2] = b[2].max(p.x);
1096 b[3] = b[3].max(p.y);
1097 }
1098 b
1099}
1100
1101fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1102 let mut chain = Vec::new();
1103 let mut cur = Some(id);
1104 while let Some(c) = cur {
1105 chain.push(c);
1106 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1107 }
1108 let mut tf = Affine::IDENTITY;
1109 for c in chain.into_iter().rev() {
1110 let Some(n) = nodes.get(c.0 as usize) else {
1111 break;
1112 };
1113 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1114 }
1115 tf
1116}
1117
1118fn text_info(n: &NodeData) -> (Option<String>, f64) {
1119 let (body, max_width) = match &n.kind {
1120 NodeKind::Txt {
1121 text, spans, style, ..
1122 } => {
1123 let spans = spans.get();
1124 let body = if spans.is_empty() {
1125 text.get()
1126 } else {
1127 spans.into_iter().map(|s| s.text).collect()
1128 };
1129 (body, style.max_width)
1130 }
1131 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1132 _ => return (None, 0.0),
1133 };
1134 let overflow = match (max_width, n.text_draw()) {
1135 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1136 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1137 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1138 _ => 0.0,
1139 };
1140 (Some(body), overflow)
1141}
1142
1143pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1144 let n = &nodes[id.0 as usize];
1145 match &n.kind {
1146 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1147 NodeKind::Circle { size, .. }
1148 | NodeKind::Polygon { size, .. }
1149 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1150 NodeKind::Img { width, height, .. }
1151 | NodeKind::Grid { width, height, .. }
1152 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1153 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1154 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1155 Some(TextDraw::Morph { size, .. }) => size,
1156 Some(TextDraw::Diff(frame)) => frame.size,
1157 None => (0.0, 0.0),
1158 },
1159 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1160 let bb = n
1161 .outline((0.0, 0.0))
1162 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1163 (bb.width(), bb.height())
1164 }
1165 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1166 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1168 NodeKind::Custom {
1169 measure: Some(m), ..
1170 } => m(),
1171 NodeKind::Custom { width, height, .. } => (*width, *height),
1172 NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1173 NodeKind::Group => {
1174 let mut bb: Option<kurbo::Rect> = None;
1175 for &c in &n.children {
1176 let child = &nodes[c.0 as usize];
1177 let (w, h) = measure(nodes, c);
1178 let r = child
1179 .local_transform(Vec2::ZERO, (w, h))
1180 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1181 bb = Some(bb.map_or(r, |b| b.union(r)));
1182 }
1183 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1184 }
1185 }
1186}
1187
1188fn resizable(kind: &NodeKind) -> bool {
1190 matches!(
1191 kind,
1192 NodeKind::Rect { .. }
1193 | NodeKind::Layout { .. }
1194 | NodeKind::Img { .. }
1195 | NodeKind::Grid { .. }
1196 | NodeKind::Svg { .. }
1197 | NodeKind::Shader { .. }
1198 )
1199}
1200
1201fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1202 let n = a.len().max(b.len());
1203 for i in 0..n {
1204 match (a.get(i), b.get(i)) {
1205 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1206 (Some(x), _) => return Some(x.node),
1207 (_, Some(y)) => return Some(y.node),
1208 (None, None) => {}
1209 }
1210 }
1211 None
1212}
1213
1214fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1215 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1216 match (a, b) {
1217 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1218 (
1219 Stroke {
1220 path: p,
1221 paint: c,
1222 style: s,
1223 },
1224 Stroke {
1225 path: q,
1226 paint: d,
1227 style: t,
1228 },
1229 ) => p == q && c == d && s == t,
1230 (
1231 Image {
1232 rect: r,
1233 opacity: o,
1234 ..
1235 },
1236 Image {
1237 rect: s,
1238 opacity: p,
1239 ..
1240 },
1241 ) => r == s && o == p,
1242 (PushLayer(x), PushLayer(y)) => x == y,
1243 (PopLayer, PopLayer) => true,
1244 (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1245 | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1246 (Primitive::EndEffect, Primitive::EndEffect) => true,
1247 _ => false,
1248 }
1249}
1250
1251#[allow(clippy::too_many_arguments)]
1254fn visit(
1255 nodes: &[NodeData],
1256 id: NodeId,
1257 parent_tf: Affine,
1258 placement: Option<FlexBox>,
1259 flex: Option<&FlexMap>,
1260 time: f64,
1261 inside: bool,
1262 out: &mut Vec<DrawItem>,
1263) {
1264 let n = &nodes[id.0 as usize];
1265 let opacity = n.opacity.get().clamp(0.0, 1.0);
1266 if opacity <= 0.0 {
1267 return;
1268 }
1269 let size = match placement {
1270 Some(b) if resizable(&n.kind) => {
1271 let s = n.scale.get();
1272 (
1273 b.size.0 / s.x.abs().max(1e-9),
1274 b.size.1 / s.y.abs().max(1e-9),
1275 )
1276 }
1277 _ => measure(nodes, id),
1278 };
1279 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1280 let tf = parent_tf * n.local_transform(offset, size);
1281
1282 let blend = n.blend.get();
1285 let blur = n.blur.get().max(0.0);
1286 let shadow_color = n.shadow_color.get();
1287 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1288 color: shadow_color,
1289 blur: n.shadow_blur.get().max(0.0),
1290 offset: n.shadow_offset.get(),
1291 });
1292 let layered = blend != BlendMode::Normal
1293 || blur > 0.0
1294 || shadow.is_some()
1295 || (opacity < 1.0 && !n.children.is_empty());
1296 let effect = n.effect.as_ref().and_then(|e| {
1297 if inside {
1298 cycle(n, "This effect is inside a layer or another effect");
1299 return None;
1300 }
1301 Some(e.shade(tf * Point::ZERO, time))
1302 });
1303 if let Some(shade) = &effect {
1304 out.push(DrawItem {
1305 node: id,
1306 transform: tf,
1307 prim: Primitive::BeginEffect(shade.clone()),
1308 });
1309 }
1310 if let Some(shade) = crate::shader::read_frame(n, size, tf, time, opacity) {
1311 if inside || effect.is_some() {
1312 cycle(
1313 n,
1314 "lens_drops reads the frame, but it is inside a layer or an effect",
1315 );
1316 } else {
1317 out.push(DrawItem {
1318 node: id,
1319 transform: tf,
1320 prim: Primitive::ReadFrame(shade),
1321 });
1322 }
1323 }
1324 if layered {
1325 out.push(DrawItem {
1326 node: id,
1327 transform: tf,
1328 prim: Primitive::PushLayer(Layer {
1329 opacity,
1330 blend,
1331 clip: None,
1332 blur,
1333 shadow,
1334 }),
1335 });
1336 }
1337 let paint_opacity = if layered { 1.0 } else { opacity };
1338 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1339 out.push(DrawItem {
1340 node: id,
1341 transform: tf,
1342 prim,
1343 });
1344 }
1345 if let NodeKind::Shader { .. } = &n.kind
1346 && crate::shader::problems(n).is_empty()
1347 && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1348 {
1349 out.push(DrawItem {
1350 node: id,
1351 transform: tf,
1352 prim,
1353 });
1354 }
1355
1356 if !n.children.is_empty() {
1357 let clip = n.clip.get().then(|| {
1359 n.outline(size).unwrap_or_else(|| {
1360 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1361 .to_path(0.1)
1362 })
1363 });
1364 let clipped = clip.is_some();
1365 if let Some(path) = clip {
1366 out.push(DrawItem {
1367 node: id,
1368 transform: tf,
1369 prim: Primitive::PushLayer(Layer::clip(path)),
1370 });
1371 }
1372 let own;
1375 let map: Option<&FlexMap> = match (&n.kind, flex) {
1376 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1377 (NodeKind::Layout { .. }, None) => {
1378 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1379 Some(&own)
1380 }
1381 _ => None,
1382 };
1383 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1384 order.sort_by(|a, b| {
1386 let za = nodes[a.1.0 as usize].z_index.get();
1387 let zb = nodes[b.1.0 as usize].z_index.get();
1388 za.total_cmp(&zb)
1389 });
1390 for (_, c) in order {
1391 let placement = map.and_then(|m| m.get(&c).copied());
1392 let inside = inside || layered || clipped || effect.is_some();
1393 visit(nodes, c, tf, placement, map, time, inside, out);
1394 }
1395 if clipped {
1396 out.push(DrawItem {
1397 node: id,
1398 transform: tf,
1399 prim: Primitive::PopLayer,
1400 });
1401 }
1402 }
1403 if layered {
1404 out.push(DrawItem {
1405 node: id,
1406 transform: tf,
1407 prim: Primitive::PopLayer,
1408 });
1409 }
1410 if effect.is_some() {
1411 out.push(DrawItem {
1412 node: id,
1413 transform: tf,
1414 prim: Primitive::EndEffect,
1415 });
1416 }
1417}
1418
1419fn cycle(n: &NodeData, what: &str) {
1421 crate::shader::push_diag(crate::shader::ShaderDiag {
1422 node: n
1423 .name
1424 .clone()
1425 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0)),
1426 source: n.source.clone(),
1427 rule: "input_cycle",
1428 hint: format!(
1429 "{what} (a group with opacity below 1, a blur, a clip, a blend or a shadow). \
1430 Its pixels are not finished there, so the shader is skipped. \
1431 Put the effect on that group, or move the node out of it."
1432 ),
1433 });
1434}
1435
1436impl DrawItem {
1437 fn hit(&self, p: Point) -> bool {
1438 let local = self.transform.inverse() * p;
1439 match &self.prim {
1440 Primitive::Fill { path, .. } => {
1441 (is_text_like(path) && path.bounding_box().contains(local))
1442 || closed(path).contains(local)
1443 }
1444 Primitive::Stroke { path, style, .. } => {
1445 let tol = (style.width / 2.0)
1446 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1447 path.segments().any(|s| {
1448 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1449 })
1450 }
1451 Primitive::Image { rect, .. } => rect.contains(local),
1452 Primitive::PushLayer(_)
1453 | Primitive::PopLayer
1454 | Primitive::ReadFrame(_)
1455 | Primitive::BeginEffect(_)
1456 | Primitive::EndEffect => false,
1457 }
1458 }
1459}
1460
1461fn closed(path: &BezPath) -> BezPath {
1463 let mut out = BezPath::new();
1464 let mut open = false;
1465 for el in path.elements() {
1466 match el {
1467 kurbo::PathEl::MoveTo(_) => {
1468 if open {
1469 out.close_path();
1470 }
1471 open = true;
1472 }
1473 kurbo::PathEl::ClosePath => open = false,
1474 _ => {}
1475 }
1476 out.push(*el);
1477 }
1478 if open {
1479 out.close_path();
1480 }
1481 out
1482}
1483
1484fn is_text_like(path: &BezPath) -> bool {
1487 path.elements()
1488 .iter()
1489 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1490 .count()
1491 > 3
1492}
1493
1494thread_local! {
1495 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1496}
1497
1498impl SceneGraph {
1499 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1501 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1502 struct Restore(Option<Rc<SceneGraph>>);
1503 impl Drop for Restore {
1504 fn drop(&mut self) {
1505 let prev = self.0.take();
1506 CURRENT.with(|c| *c.borrow_mut() = prev);
1507 }
1508 }
1509 let _restore = Restore(prev);
1510 f()
1511 }
1512}
1513
1514pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1515 CURRENT.with(|c| {
1516 let c = c.borrow();
1517 f(c.as_ref()
1518 .expect("oxframe: node handles can only be used while a scene is running"))
1519 })
1520}