Skip to main content

oxframe_2d/
graph.rs

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/// Index of a node in its [`SceneGraph`]. Assigned in creation order, so it
15/// is deterministic too.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct NodeId(
18    /// Position in creation order, starting at 0 for the root view.
19    pub u32,
20);
21
22/// Main axis of a [`Layout`](crate::Layout).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Direction {
25    /// Left to right.
26    #[default]
27    Row,
28    /// Top to bottom.
29    Column,
30}
31
32/// Cross-axis alignment of children in a [`Layout`](crate::Layout).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum LayoutAlign {
35    /// Top (in a row) or left (in a column).
36    Start,
37    /// Centered.
38    #[default]
39    Center,
40    /// Bottom (in a row) or right (in a column).
41    End,
42    /// Fill the cross axis (resizes rects, images and nested layouts).
43    Stretch,
44}
45
46/// Main-axis distribution of children; only matters with a fixed size.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Justify {
49    /// Packed at the start.
50    Start,
51    /// Packed in the middle.
52    #[default]
53    Center,
54    /// Packed at the end.
55    End,
56    /// First and last at the edges, equal space between.
57    SpaceBetween,
58    /// Equal space around each child (half-size at the edges).
59    SpaceAround,
60    /// Equal space between children and at the edges.
61    SpaceEvenly,
62}
63
64/// Node type and its type-specific properties (internal representation).
65#[doc(hidden)]
66pub enum NodeKind {
67    Group,
68    /// Looks at `x`/`y` with `zoom` and `rotation`; its children are drawn
69    /// through it (see [`NodeData::local_transform`]).
70    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        /// Degrees clockwise from 12 o'clock; a full turn draws a circle.
81        start_angle: Signal<f64>,
82        end_angle: Signal<f64>,
83        /// Close an arc through the center (pie slice).
84        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 radius as a fraction of the outer one.
94        inner: Signal<f64>,
95    },
96    Path {
97        data: Signal<BezPath>,
98    },
99    Line {
100        points: Signal<Vec<Vec2>>,
101        closed: bool,
102        /// Smooth curve through the points (Catmull-Rom).
103        smooth: bool,
104        start_arrow: bool,
105        end_arrow: bool,
106        arrow_size: Signal<f64>,
107    },
108    Txt {
109        text: Signal<String>,
110        /// When non-empty, replaces `text` with styled runs.
111        spans: Signal<Vec<RichSpan>>,
112        font_size: Signal<f64>,
113        weight: Signal<f64>,
114        style: TextStyle,
115        /// Active glyph morph: (from, to) and progress 0..1.
116        morph: Signal<Option<(String, String)>>,
117        morph_t: Signal<f64>,
118    },
119    Code {
120        code: Signal<String>,
121        /// Source a running diff started from. Ignored unless `editing` is set.
122        edit_from: Signal<String>,
123        /// Source a running diff is going to.
124        edit_to: Signal<String>,
125        /// A diff tween is in progress.
126        editing: Signal<bool>,
127        /// Progress of the diff, 0 = `edit_from`, 1 = `edit_to`.
128        edit_t: Signal<f64>,
129        /// Flash a background behind text the diff inserts.
130        edit_flash: Signal<bool>,
131        lang: Lang,
132        theme: CodeTheme,
133        font_size: Signal<f64>,
134        style: TextStyle,
135        /// Focused lines (1-based, inclusive); (0, 0) focuses all.
136        focus: Signal<Vec2>,
137        /// Focused characters of the source, `x..y` (0-based); (0, 0)
138        /// focuses all.
139        focus_span: Signal<Vec2>,
140        /// Alpha of unfocused lines.
141        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 children onto several lines.
153        wrap: bool,
154        /// CSS grid with this many equal columns instead of flexbox.
155        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    /// An imported SVG. Draws nothing itself; its children are the file's
168    /// groups, paths and images. `width` and `height` are the fitted size.
169    Svg {
170        width: Signal<f64>,
171        height: Signal<f64>,
172    },
173    /// Geometry drawn by a closure on every paint. `width` and `height` are
174    /// the layout box; the closure's shapes are not measured.
175    Custom {
176        draw: crate::draw::DrawFn,
177        width: f64,
178        height: f64,
179        /// The box from the node's current values ([`Draw::size`](crate::Draw::size)).
180        /// `None` keeps `width` and `height`.
181        measure: Option<crate::node_api::SizeFn>,
182        /// The signals of a `#[derive(Node)]` node, for its handle.
183        state: Option<Rc<dyn std::any::Any>>,
184        /// `"Custom"`, or the type name of a derived node, for `check`.
185        type_name: &'static str,
186    },
187    /// Particles integrated from bursts. The box is a point; the dots are
188    /// drawn in the node's coordinates.
189    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    /// A video clip: the frame at `time`, in a box like an image.
199    Video {
200        data: Rc<crate::video::VideoData>,
201        width: Signal<f64>,
202        height: Signal<f64>,
203        /// Seconds into the clip.
204        time: Signal<f64>,
205        /// Clip seconds per scene second while playing.
206        rate: Signal<f64>,
207        /// Corner radius of the box.
208        radius: Signal<f64>,
209    },
210    /// Pixels from a WGSL `shade` function. No children in this version.
211    /// Presets and a custom `shade` are both run on the CPU.
212    Shader {
213        code: crate::shader::ShaderCode,
214        /// Built-in this node is, or `Custom` for WGSL of your own.
215        preset: crate::shader::Preset,
216        width: Signal<f64>,
217        height: Signal<f64>,
218        uniforms: Vec<crate::shader::Uniform>,
219        /// Named inputs from `.input(..)`.
220        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/// Stored state of one node (internal representation).
250#[doc(hidden)]
251pub struct NodeData {
252    pub id: NodeId,
253    pub name: Option<String>,
254    /// `file:line` of the `add` call that created the node, when known.
255    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    /// Degrees, clockwise (y points down).
262    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    /// Drawn part of the outline, as fractions of its length.
269    pub start: Signal<f64>,
270    pub end: Signal<f64>,
271    /// Overrides `fill` when set (local coordinates).
272    pub fill_gradient: Signal<Option<Gradient>>,
273    pub stroke_gradient: Signal<Option<Gradient>>,
274    /// Dash pattern (on, off, on, …) in local units; empty = solid.
275    pub dash: Signal<Vec<f64>>,
276    pub dash_offset: Signal<f64>,
277    pub line_cap: Signal<LineCap>,
278    pub line_join: Signal<LineJoin>,
279    /// Gaussian blur of the node and its children (local units).
280    pub blur: Signal<f64>,
281    pub shadow_color: Signal<Color>,
282    pub shadow_blur: Signal<f64>,
283    pub shadow_offset: Signal<Vec2>,
284    /// Children are clipped to this node's outline.
285    pub clip: Signal<bool>,
286    pub blend: Signal<BlendMode>,
287    /// Paint order among siblings (higher on top; ties keep insertion order).
288    pub z_index: Signal<f64>,
289    /// Which point of the node's box sits at (x, y) and is the pivot of
290    /// rotation and scale: (-1, -1) top-left, (0, 0) center, (1, 1)
291    /// bottom-right.
292    pub anchor: Signal<Vec2>,
293    // Flex item properties (used when the parent is a `Layout`).
294    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    /// Recomputed every frame. `None` means `x` and `y` are used as they are.
300    pub place: Option<crate::place::Place>,
301    /// Position after [`place`](Self::place) is resolved. Draw uses this.
302    pub placed: Cell<(f64, f64)>,
303    /// Shader effect on the node's own pixels, from [`Node::effect`](crate::Node::effect).
304    pub effect: Option<crate::post::EffectData>,
305    /// Drawn only as a shader input, never on screen.
306    pub offscreen: bool,
307}
308
309impl NodeData {
310    /// Transform relative to the parent, plus a layout-assigned offset.
311    /// `size` is the node's box, needed for the anchor.
312    pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
313        if let NodeKind::Camera { zoom } = &self.kind {
314            // The view through the camera: the point it looks at comes to
315            // the camera's origin, scaled by zoom and turned against its
316            // rotation.
317            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    /// Text to draw for `Txt` and `Code` nodes.
333    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    /// Untrimmed outline in local coordinates, centered on the origin.
414    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                // Start at the top, clockwise — like a clock hand, which is
457                // what `end` animations expect.
458                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
521/// Regular polygon (or star with an inner radius), first vertex at the top.
522fn 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
542/// Smooth curve through the points (uniform Catmull-Rom as cubic Béziers).
543fn 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    /// Glyphs with their alpha, already centered.
570    Morph {
571        glyphs: Vec<(BezPath, f64)>,
572        size: (f64, f64),
573    },
574    /// A code diff, glyphs in a top-left layout box.
575    Diff(DiffFrame),
576}
577
578/// Arena of nodes for one scene run. Node 0 is the view (root).
579pub struct SceneGraph {
580    nodes: RefCell<Vec<NodeData>>,
581    size: (u32, u32),
582    /// Seconds of the frame last stepped. [`draw_list`](Self::draw_list) paints this time.
583    time: Cell<f64>,
584    /// WGSL problems from the last [`draw_list`](Self::draw_list).
585    shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
586}
587
588impl SceneGraph {
589    /// The root node (the scene's view).
590    pub const ROOT: NodeId = NodeId(0);
591
592    /// An empty scene of the given size in pixels, with only the root.
593    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    /// Scene size in pixels.
612    pub fn size(&self) -> (u32, u32) {
613        self.size
614    }
615
616    /// Time, in seconds, that the next [`draw_list`](Self::draw_list) paints.
617    /// The engine sets this after each frame.
618    pub fn set_time(&self, seconds: f64) {
619        self.time.set(seconds.max(0.0));
620    }
621
622    /// Time the last [`set_time`](Self::set_time) stored.
623    pub fn time(&self) -> f64 {
624        self.time.get()
625    }
626
627    /// Number of nodes, including the root.
628    pub fn len(&self) -> usize {
629        self.nodes.borrow().len()
630    }
631
632    /// Never true: the root always exists.
633    pub fn is_empty(&self) -> bool {
634        self.len() == 0
635    }
636
637    /// Borrows a node's stored state.
638    ///
639    /// # Panics
640    ///
641    /// If `id` is not a node of this graph.
642    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    /// Size of a node's box in its own (unscaled) coordinates.
722    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    /// Some node reads or changes drawn pixels (an effect, or a shader
733    /// that reads the frame). When false, the draw list has no markers and
734    /// a renderer can draw it in one go.
735    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    /// Flattened, paint-ordered list of everything to draw.
759    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    /// WGSL problems from the last [`draw_list`](Self::draw_list).
784    pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
785        self.shader_diags.borrow().clone()
786    }
787
788    /// `Some` when painting this frame twice does not produce the same
789    /// primitives. The string is the node's name, or its `add` call site.
790    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    /// Topmost node under `p` (scene coordinates), excluding the view.
804    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    /// World transform of a node (including layout offsets).
813    pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
814        // The draw list already composes transforms; reuse it for nodes
815        // that draw. Those that don't (a Layout row, a transparent node) are
816        // placed by the same walk, layout included; a node outside the tree
817        // by its parents alone.
818        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    /// The box of `id` (its size around its origin) in the coordinate frame
837    /// of `frame`'s children: for a camera, where the node sits in the
838    /// world the camera looks at. Axis-aligned.
839    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    /// Frame size in pixels.
848    pub fn frame_size(&self) -> (f64, f64) {
849        (f64::from(self.size.0), f64::from(self.size.1))
850    }
851
852    /// First descendant of `root` (including `root`) whose name is `name`.
853    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    /// Path nodes under `root`, in paint order.
870    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    /// `(id, parent, kind, name)` of every attached node, depth-first.
888    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    /// Current signal values of a node, for the inspector.
901    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    /// One record per attached node, in tree order, with scene-space bounds.
949    ///
950    /// `order` is paint order (0 is the back). Nodes that are not drawn
951    /// (fully transparent) get `u32::MAX`.
952    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        // Where every node is, drawn or not (a Layout row draws nothing).
965        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                // An offscreen node is never seen, so `check` skips it.
987                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/// A node as `oxframe inspect` and the visual linter see it.
1029#[derive(Debug, Clone, PartialEq)]
1030pub struct InspectNode {
1031    /// Node index.
1032    pub id: u32,
1033    /// Parent index; `None` for the root.
1034    pub parent: Option<u32>,
1035    /// Node type, e.g. `"Rect"`.
1036    pub kind: &'static str,
1037    /// `.name(..)`, or the `add` call site, or `Kind#id`.
1038    pub name: String,
1039    /// `file:line` of the `add` call, when it was recorded.
1040    pub source: Option<String>,
1041    /// Axis-aligned bounds in scene coordinates: min x, min y, max x, max y.
1042    /// The origin is the center of the frame, y points down.
1043    pub bounds: [f64; 4],
1044    /// 0 (invisible) to 1.
1045    pub opacity: f64,
1046    /// Paint order among drawn nodes; `u32::MAX` if the node is not drawn.
1047    pub order: u32,
1048    /// Text of a `Txt` or `Code` node.
1049    pub text: Option<String>,
1050    /// Fill color.
1051    pub fill: Color,
1052    /// How many pixels the text is wider than its `max_width` (0 if it fits
1053    /// or `max_width` is unset).
1054    pub text_overflow: f64,
1055    /// Type of the parent, if any.
1056    pub parent_kind: Option<&'static str>,
1057    /// Seen through a [`Camera`](crate::Camera): leaving the frame is then
1058    /// usually the camera's framing, not a layout mistake.
1059    pub in_camera: bool,
1060    /// Preset of a `Shader` node: `"waves"`, `"lens_drops"` or `"custom"`.
1061    pub shader: Option<&'static str>,
1062    /// A shader that reads the frame drawn before it (`lens_drops`, or an
1063    /// `Input::frame()` input): what is under it shows through.
1064    pub reads_frame: bool,
1065    /// Numeric shader parameters (`amp`, `wetness`, …), the strength and
1066    /// speed of an effect on the node (`effect.strength`), how much of it is
1067    /// drawn (`start` above 0, `end` below 1) and its glow or shadow
1068    /// (`shadow`: blur times opacity), and the clip frame a `Video` shows
1069    /// (`frame`). Empty otherwise.
1070    pub params: Vec<(String, f64)>,
1071}
1072
1073/// Values that change a shader's picture without moving its box.
1074fn 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    // A playing clip changes the picture every frame; a paused or finished
1087    // one does not.
1088    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    // What changes the picture without moving the box: the drawn part of an
1096    // outline or a text (`start`/`end`), and a glow or shadow.
1097    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
1115/// Records where an `add` happens. An add inside another one (a component
1116/// building its parts) keeps the outer site: `check` points at the user's
1117/// line, not at the component's code.
1118pub(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/// Runs `f` so that the nodes it adds report the caller's file and line in
1129/// `check` and `inspect`.
1130///
1131/// For methods that add nodes after the node itself was added, such as a
1132/// plot on a set of axes. Mark the method `#[track_caller]`.
1133#[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
1150/// `src/main.rs:42` rather than an absolute build path.
1151fn 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
1182/// World transform and size of every node as [`visit`] places it, layout
1183/// boxes included, whether or not the node draws anything: an empty row of
1184/// a `Layout` sits where its children are drawn. `None` for detached nodes.
1185fn 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        // The same size and offset as `visit`.
1196        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        // A window onto its children, not a shape: no box of its own.
1301        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
1323/// Kinds whose drawn size follows the box a layout gives them.
1324fn 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
1387/// Markers may sit inside layers and effects: the engine draws such a
1388/// layer in parts on its own (see `oxframe::parts`).
1389fn 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    // A layer is needed when the node's effects apply to it and its children
1417    // as a whole; plain leaves just fold opacity into their paints.
1418    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        // Nodes without an outline (groups, text, images) clip to their box.
1479        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        // Boxes of the children: from the enclosing layout tree, or a new
1494        // tree when this is a top-level layout.
1495        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        // Stable: equal z keeps insertion order.
1506        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
1542/// True when `id` or one of its ancestors is offscreen.
1543fn 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
1555/// The draw list of an offscreen node and its children alone, and its box
1556/// in scene coordinates.
1557pub(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
1598/// Fills implicitly close open subpaths; kurbo's winding does not.
1599fn 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
1621/// Glyph runs are hard to hit exactly; treat many-subpath fills (text) by
1622/// their bounding box.
1623fn 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    /// Makes this graph the target of node handles for the duration of `f`.
1637    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}