Skip to main content

motion_content/vector/
surface.rs

1//! Bounded triangulation of filled vector geometry into sampleable surfaces.
2
3use super::*;
4use motion_core::{Point, geometry::flatten::Edge};
5
6const MAX_EDGES: usize = 4096;
7
8const MAX_EVENTS: usize = 65536;
9
10const MAX_TRIANGLES: usize = 65536;
11
12const MAX_WORK: u64 = 32_000_000;
13
14/// A nondegenerate tree-space triangle in a filled geometric union.
15#[derive(Clone, Copy, Debug)]
16pub struct SurfaceTriangle {
17    /// Original f64 vertices, never raster coordinates.
18    pub vertices: [Point; 3],
19    /// Positive area in squared tree units.
20    pub area: f64,
21    /// Index of the ordered fill stack retained by the owning surface.
22    pub paint_index: usize,
23}
24
25/// Immutable prepared fill silhouette for deterministic area-uniform sampling.
26/// Images/strokes are not filled path regions. Paint alpha is not a density;
27/// zero-opacity groups are omitted, clips/holes and overlapping fills are honored.
28#[derive(Clone, Debug)]
29pub struct VectorSurface {
30    triangles: Vec<SurfaceTriangle>,
31    paints: Vec<SurfacePaint>,
32    paint_stacks: Vec<Arc<[usize]>>,
33    ends: Vec<f64>,
34    area: f64,
35    tolerance: f64,
36}
37
38#[derive(Clone, Debug)]
39struct SurfacePaint {
40    paint: Paint,
41    transform: Transform,
42    bounds: Rect,
43    opacity: f64,
44}
45
46struct Region {
47    rule: FillRule,
48}
49
50struct Domain {
51    regions: Vec<usize>,
52}
53
54fn work(spent: &mut u64, amount: usize) -> Result<(), ValidationError> {
55    *spent = spent
56        .checked_add(amount as u64)
57        .ok_or_else(|| limited("Surface work overflow"))?;
58    if *spent > MAX_WORK {
59        return Err(limited("Surface exceeds 32 million work units"));
60    }
61    Ok(())
62}
63
64fn add_region(
65    path: &Path2D,
66    transform: Transform,
67    rule: FillRule,
68    tolerance: f64,
69    edges: &mut Vec<(Edge, usize)>,
70    regions: &mut Vec<Region>,
71    spent: &mut u64,
72) -> Result<usize, ValidationError> {
73    if regions.len() >= 8192 {
74        return Err(limited("Surface exceeds 8192 fill/clip regions"));
75    }
76    let index = regions.len();
77    let (source, used) = motion_core::geometry::flatten::surface_edges(
78        path,
79        transform,
80        tolerance,
81        MAX_EDGES.saturating_sub(edges.len()),
82        MAX_WORK.saturating_sub(*spent),
83    )?;
84    *spent += used;
85    if source.len() > MAX_EDGES.saturating_sub(edges.len()) {
86        return Err(limited("Surface exceeds 4096 directed edges"));
87    }
88    for edge in source {
89        if !edge.top.x.is_finite()
90            || !edge.top.y.is_finite()
91            || !edge.bottom.x.is_finite()
92            || !edge.bottom.y.is_finite()
93            || [edge.top.x, edge.top.y, edge.bottom.x, edge.bottom.y]
94                .iter()
95                .any(|value| value.abs() * 128.0 * f64::EPSILON > tolerance * 0.25)
96        {
97            return Err(invalid(
98                "precision",
99                "Surface coordinates cannot resolve the requested tolerance",
100            ));
101        }
102        edges.push((edge, index));
103    }
104    regions.push(Region { rule });
105    Ok(index)
106}
107
108impl VectorTree {
109    /// Shares a single concurrent preparation at 0.0025 tree-unit Wang tolerance. The chord error bound is
110    /// met with equality, so the inscribed polygon can lose up to about 2/3 of it times the perimeter in area.
111    pub fn surface(&self) -> Result<Arc<VectorSurface>, ValidationError> {
112        self.data
113            .surface
114            .get_or_init(|| self.surface_with_tolerance(0.0025).map(Arc::new))
115            .clone()
116    }
117
118    /// Prepares a separate continuous geometric triangulation with an explicit
119    /// positive finite curve tolerance. No rasterization or random rejection loop.
120    pub fn surface_with_tolerance(&self, tolerance: f64) -> Result<VectorSurface, ValidationError> {
121        fn masked(group: &VectorGroup) -> bool {
122            group.mask.is_some()
123                || group
124                    .children
125                    .iter()
126                    .any(|item| matches!(item,VectorItem::Group(child) if masked(child)))
127        }
128        if masked(self.root()) {
129            return Err(ValidationError::unsupported(
130                "vector_tree.surface",
131                "mask",
132                "Alpha/luminance-masked artwork is not a binary filled surface",
133            ));
134        }
135        if !tolerance.is_finite() || tolerance <= 0.0 {
136            return Err(invalid(
137                "tolerance",
138                "Surface tolerance must be finite and positive",
139            ));
140        }
141        let mut edges = Vec::new();
142        let mut regions = Vec::new();
143        let mut domains = Vec::new();
144        let mut paints = Vec::new();
145        let mut spent = 0;
146        for leaf in self.paint_leaves() {
147            if leaf.opacity == 0.0 {
148                continue;
149            }
150            let VectorItem::Path(path) = leaf.item else {
151                continue;
152            };
153            let Some((paint, rule)) = &path.fill else {
154                continue;
155            };
156            let mut ids = vec![add_region(
157                &path.path,
158                leaf.transform,
159                *rule,
160                tolerance,
161                &mut edges,
162                &mut regions,
163                &mut spent,
164            )?];
165            for clip in &leaf.clips {
166                ids.push(add_region(
167                    &clip.path,
168                    clip.transform,
169                    clip.rule,
170                    tolerance,
171                    &mut edges,
172                    &mut regions,
173                    &mut spent,
174                )?);
175            }
176            domains.push(Domain { regions: ids });
177            paints.push(SurfacePaint {
178                paint: paint.clone(),
179                transform: leaf.transform,
180                bounds: path.path.bounding_box(),
181                opacity: leaf.opacity,
182            });
183        }
184        let mut cuts = Vec::new();
185        for (edge, _) in &edges {
186            cuts.extend([edge.top.y, edge.bottom.y]);
187        }
188        for (index, (first, _)) in edges.iter().enumerate() {
189            for (second, _) in &edges[index + 1..] {
190                work(&mut spent, 1)?;
191                let top = first.top.y.max(second.top.y);
192                let bottom = first.bottom.y.min(second.bottom.y);
193                if top >= bottom {
194                    continue;
195                }
196                let top_difference = first.at(top) - second.at(top);
197                let bottom_difference = first.at(bottom) - second.at(bottom);
198                if top_difference != 0.0
199                    && bottom_difference != 0.0
200                    && top_difference.is_sign_positive() != bottom_difference.is_sign_positive()
201                {
202                    let cut = top
203                        + (bottom - top) * (top_difference / (top_difference - bottom_difference));
204                    if !cut.is_finite() {
205                        return Err(invalid("precision", "Surface intersection overflow"));
206                    }
207                    if cut > top && cut < bottom {
208                        cuts.push(cut);
209                    }
210                    if cuts.len() > MAX_EVENTS {
211                        return Err(limited("Surface intersection allowance exhausted"));
212                    }
213                }
214            }
215        }
216        work(&mut spent, cuts.len() * cuts.len().max(1).ilog2() as usize)?;
217        cuts.sort_by(f64::total_cmp);
218        cuts.dedup();
219        let mut dependencies = vec![Vec::new(); regions.len()];
220        for (index, domain) in domains.iter().enumerate() {
221            work(&mut spent, domain.regions.len())?;
222            for region in &domain.regions {
223                dependencies[*region].push(index);
224            }
225        }
226        let mut triangles = Vec::new();
227        let mut paint_stacks = Vec::new();
228        let mut paint_references = 0usize;
229        let mut ends = Vec::new();
230        let mut area = 0.0;
231        for band in cuts.windows(2) {
232            let top = band[0];
233            let bottom = band[1];
234            work(&mut spent, edges.len() + regions.len() * 2 + domains.len())?;
235            let mut active = edges
236                .iter()
237                .enumerate()
238                .filter(|(_, (edge, _))| edge.top.y <= top && edge.bottom.y >= bottom)
239                .map(|(index, (edge, _))| (index, edge.at(top) * 0.5 + edge.at(bottom) * 0.5))
240                .collect::<Vec<_>>();
241            work(
242                &mut spent,
243                active.len() * active.len().max(1).ilog2() as usize,
244            )?;
245            active.sort_by(|left, right| left.1.total_cmp(&right.1).then(left.0.cmp(&right.0)));
246            let mut winding = vec![0i32; regions.len()];
247            let mut filled = vec![false; regions.len()];
248            let mut inside = vec![false; domains.len()];
249            let mut count = 0usize;
250            for pair in active.windows(2) {
251                let (edge, owner) = edges[pair[0].0];
252                winding[owner] += edge.winding;
253                let now = match regions[owner].rule {
254                    FillRule::NonZero => winding[owner] != 0,
255                    FillRule::EvenOdd => winding[owner] % 2 != 0,
256                };
257                if now != filled[owner] {
258                    filled[owner] = now;
259                    for domain in &dependencies[owner] {
260                        work(&mut spent, domains[*domain].regions.len())?;
261                        let next = domains[*domain].regions.iter().all(|index| filled[*index]);
262                        if next != inside[*domain] {
263                            if next {
264                                count += 1
265                            } else {
266                                count -= 1
267                            }
268                            inside[*domain] = next;
269                        }
270                    }
271                }
272                if count == 0 || pair[0].1 == pair[1].1 {
273                    continue;
274                }
275                let left = edge;
276                let right = edges[pair[1].0].0;
277                let left_top = Point::new(left.at(top), top);
278                let right_top = Point::new(right.at(top), top);
279                let right_bottom = Point::new(right.at(bottom), bottom);
280                let left_bottom = Point::new(left.at(bottom), bottom);
281                work(&mut spent, inside.len())?;
282                let stack = inside
283                    .iter()
284                    .enumerate()
285                    .filter_map(|(index, active)| active.then_some(index))
286                    .collect::<Vec<_>>();
287                paint_references += stack.len();
288                if paint_stacks.len() >= MAX_TRIANGLES || paint_references > 1_048_576 {
289                    return Err(limited("Surface paint stack allowance exhausted"));
290                }
291                let paint_index = paint_stacks.len();
292                paint_stacks.push(Arc::<[usize]>::from(stack));
293                for vertices in [
294                    [left_top, right_top, right_bottom],
295                    [left_top, right_bottom, left_bottom],
296                ] {
297                    let [first, second, third] = vertices;
298                    let amount = ((second.x - first.x) * (third.y - first.y)
299                        - (second.y - first.y) * (third.x - first.x))
300                        .abs()
301                        * 0.5;
302                    if !amount.is_finite() {
303                        return Err(invalid("area", "Surface area overflow"));
304                    }
305                    if amount == 0.0 {
306                        continue;
307                    }
308                    if triangles.len() == MAX_TRIANGLES {
309                        return Err(limited("Surface exceeds 65536 triangles"));
310                    }
311                    triangles.push(SurfaceTriangle {
312                        vertices,
313                        area: amount,
314                        paint_index,
315                    });
316                }
317            }
318        }
319        work(
320            &mut spent,
321            triangles.len() * triangles.len().max(1).ilog2() as usize,
322        )?;
323        triangles.sort_by(|left, right| left.area.total_cmp(&right.area));
324        for triangle in &triangles {
325            let next = area + triangle.area;
326            if !next.is_finite() {
327                return Err(invalid("area", "Surface area overflow"));
328            }
329            if next == area {
330                return Err(invalid(
331                    "precision",
332                    "Surface area weights cannot represent a nonzero triangle",
333                ));
334            }
335            area = next;
336            ends.push(area);
337        }
338        Ok(VectorSurface {
339            triangles,
340            paints,
341            paint_stacks,
342            ends,
343            area,
344            tolerance,
345        })
346    }
347
348    /// Samples the filled geometric union by triangle area and seeded Halton
349    /// points. At most one million points; clones share cached preparation.
350    pub fn sample_points(&self, count: usize, seed: u64) -> Result<Vec<Point>, ValidationError> {
351        if count > 1_000_000 {
352            return Err(limited(
353                "Surface sampling admits at most one million points",
354            ));
355        }
356        if count == 0 {
357            return Ok(Vec::new());
358        }
359        self.surface()?.sample_points(count, seed)
360    }
361
362    /// Area-uniform points with ordered fill color; paint alpha is never a sampling density.
363    pub fn sample_colored_points(
364        &self,
365        count: usize,
366        seed: u64,
367    ) -> Result<Vec<(Point, [f32; 4])>, ValidationError> {
368        self.surface()?.sample_colored_points(count, seed)
369    }
370}
371
372fn halton(mut index: u64, base: u64) -> f64 {
373    let mut factor = 1.0;
374    let mut value = 0.0;
375    while index > 0 {
376        factor /= base as f64;
377        value += factor * (index % base) as f64;
378        index /= base;
379    }
380    value
381}
382
383fn shift(mut seed: u64) -> f64 {
384    seed = (seed ^ (seed >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
385    seed = (seed ^ (seed >> 27)).wrapping_mul(0x94d049bb133111eb);
386    ((seed ^ (seed >> 31)) >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
387}
388
389impl VectorSurface {
390    /// Positive area of the union (overlap counted once).
391    pub fn area(&self) -> f64 {
392        self.area
393    }
394
395    /// Read-only continuous triangles, including clipped/holed decomposition.
396    pub fn triangles(&self) -> &[SurfaceTriangle] {
397        &self.triangles
398    }
399
400    /// Requested maximum Wang curve-deviation tolerance in tree coordinates.
401    pub fn tolerance(&self) -> f64 {
402        self.tolerance
403    }
404
405    /// Pure deterministic area selection and barycentric Halton sampling.
406    pub fn sample_points(&self, count: usize, seed: u64) -> Result<Vec<Point>, ValidationError> {
407        if count > 1_000_000 {
408            return Err(limited(
409                "Surface sampling admits at most one million points",
410            ));
411        }
412        if count > 0 && self.area == 0.0 {
413            return Err(invalid(
414                "area",
415                "Cannot sample a surface without filled area",
416            ));
417        }
418        let offsets = [
419            shift(seed),
420            shift(seed.wrapping_add(0x9e3779b97f4a7c15)),
421            shift(seed.wrapping_add(0x3c6ef372fe94f82a)),
422        ];
423        let unit = |index, base, offset| {
424            let value = halton(index, base) + offset;
425            value - libm::floor(value)
426        };
427        let mut points = Vec::with_capacity(count);
428        for index in 1..=count as u64 {
429            let choice = unit(index, 2, offsets[0]) * self.area;
430            let triangle = &self.triangles[self
431                .ends
432                .partition_point(|end| *end <= choice)
433                .min(self.triangles.len() - 1)];
434            let root = libm::sqrt(unit(index, 3, offsets[1]));
435            let other = unit(index, 5, offsets[2]);
436            let weights = [1.0 - root, root * (1.0 - other), root * other];
437            let [first, second, third] = triangle.vertices;
438            points.push(Point::new(
439                first.x * weights[0] + second.x * weights[1] + third.x * weights[2],
440                first.y * weights[0] + second.y * weights[1] + third.y * weights[2],
441            ));
442        }
443        Ok(points)
444    }
445
446    /// Samples the same geometric points as sample_points and composites their retained fill stacks.
447    pub fn sample_colored_points(
448        &self,
449        count: usize,
450        seed: u64,
451    ) -> Result<Vec<(Point, [f32; 4])>, ValidationError> {
452        let points = self.sample_points(count, seed)?;
453        let mut output = Vec::new();
454        output
455            .try_reserve_exact(points.len())
456            .map_err(|_| limited("Colored point allocation failed"))?;
457        let mut spent = 0;
458        for (index, point) in points.into_iter().enumerate() {
459            let choice = (halton(index as u64 + 1, 2) + shift(seed)).fract() * self.area;
460            let triangle = &self.triangles[self
461                .ends
462                .partition_point(|end| *end <= choice)
463                .min(self.triangles.len() - 1)];
464            let mut color = [0.0_f64; 4];
465            for paint in self.paint_stacks[triangle.paint_index].iter() {
466                work(&mut spent, 16)?;
467                let paint = &self.paints[*paint];
468                let inverse = paint
469                    .transform
470                    .inverse()
471                    .ok_or_else(|| invalid("paint", "Singular surface paint transform"))?;
472                let position = inverse.apply(point);
473                let sample = match &paint.paint {
474                    Paint::Solid(color) => color.to_linear_premultiplied(),
475                    Paint::Linear(gradient) => gradient.sample(position, paint.bounds)?,
476                    Paint::Radial(gradient) => gradient.sample(position, paint.bounds)?,
477                    Paint::Sweep(gradient) => gradient.sample(position, paint.bounds)?,
478                    _ => {
479                        return Err(ValidationError::unsupported(
480                            "vector_tree.surface",
481                            "paint",
482                            "Colored surface emission requires a solid or gradient fill",
483                        ));
484                    }
485                }
486                .map(|value| value * paint.opacity);
487                color = std::array::from_fn(|channel| {
488                    sample[channel] + color[channel] * (1.0 - sample[3])
489                });
490            }
491            output.push((point, color.map(|value| value as f32)));
492        }
493        Ok(output)
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use motion_core::Color;
501
502    fn tree(path: Path2D, rule: FillRule) -> Result<VectorTree, ValidationError> {
503        let mut path = VectorPath::filled(path, Color::WHITE);
504        path.fill.as_mut().unwrap().1 = rule;
505        VectorTree::new(
506            Rect::new(0.0, 0.0, 30.0, 10.0),
507            VectorGroup {
508                children: vec![VectorItem::Path(path)],
509                ..Default::default()
510            },
511        )
512    }
513
514    #[test]
515    fn surfaces_respect_holes_clips_overlap_and_area_weighted_deterministic_points()
516    -> motion_core::Result<()> {
517        let hole = tree(
518            Path2D::parse_svg("M0 0h10v10h-10z M3 3h4v4h-4z")?,
519            FillRule::EvenOdd,
520        )?;
521        assert_eq!(hole.surface()?.area(), 84.0);
522        let points = hole.sample_points(4096, 42)?;
523        assert!(points.iter().all(|p| p.x >= 0.0
524            && p.x <= 10.0
525            && p.y >= 0.0
526            && p.y <= 10.0
527            && !(p.x > 3.0 && p.x < 7.0 && p.y > 3.0 && p.y < 7.0)));
528        assert_eq!(points, hole.sample_points(4096, 42)?);
529        assert_ne!(points, hole.sample_points(4096, 43)?);
530        let mut group = hole.root().clone();
531        group.transform =
532            Transform::scale_non_uniform(2.0, 1.0).then(Transform::translate(10.0, -2.0));
533        group.clip = Some(Path2D::from_rect(Rect::new(1.0, 1.0, 9.0, 9.0))?.into());
534        let clipped = VectorTree::new(hole.view_box(), group)?;
535        assert_eq!(clipped.surface()?.area(), 96.0);
536        assert!(
537            clipped
538                .sample_points(100, 0)?
539                .iter()
540                .all(|p| p.x >= 12.0 && p.x <= 28.0 && p.y >= -1.0 && p.y <= 7.0)
541        );
542        let mut group = VectorGroup::default();
543        for left in [0.0, 5.0] {
544            group.children.push(VectorItem::Path(VectorPath::filled(
545                Path2D::from_rect(Rect::new(left, 0.0, left + 10.0, 10.0))?,
546                Color::WHITE,
547            )));
548        }
549        let union = VectorTree::new(hole.view_box(), group)?;
550        assert_eq!(union.surface()?.area(), 150.0);
551        let bow = tree(
552            Path2D::parse_svg("M0 0L10 10L0 10L10 0Z")?,
553            FillRule::EvenOdd,
554        )?;
555        assert_eq!(bow.surface()?.area(), 50.0);
556        let nonzero = tree(
557            Path2D::parse_svg("M0 0h10v10h-10z M3 3v4h4v-4z")?,
558            FillRule::NonZero,
559        )?;
560        assert_eq!(nonzero.surface()?.area(), 84.0);
561        let mut group = VectorGroup::default();
562        for (left, width) in [(0.0, 1.0), (10.0, 3.0)] {
563            group.children.push(VectorItem::Path(VectorPath::filled(
564                Path2D::from_rect(Rect::new(left, 0.0, left + width, 1.0))?,
565                Color::WHITE,
566            )));
567        }
568        let weighted = VectorTree::new(hole.view_box(), group)?;
569        let points = weighted.sample_points(8192, 31)?;
570        let first = points.iter().filter(|point| point.x < 2.0).count();
571        assert!((first as f64 / 8192.0 - 0.25).abs() < 0.01);
572        for _ in 0..3 {
573            let copy = hole.clone();
574            let reference = hole.surface()?;
575            std::thread::spawn(move || {
576                assert!(Arc::ptr_eq(&reference, &copy.surface().unwrap()));
577                assert_eq!(copy.sample_points(4096, 42).unwrap(), points_for(&copy));
578            })
579            .join()
580            .unwrap();
581        }
582        assert!(hole.sample_points(1_000_001, 0).is_err());
583        assert!(hole.surface_with_tolerance(f64::NAN).is_err());
584        Ok(())
585    }
586
587    fn points_for(tree: &VectorTree) -> Vec<Point> {
588        tree.sample_points(4096, 42).unwrap()
589    }
590
591    #[test]
592    fn curved_and_straight_shapes_are_sampled_in_proportion_to_their_area()
593    -> motion_core::Result<()> {
594        use motion_core::Shape;
595        let mut group = VectorGroup::default();
596        group.children.push(VectorItem::Path(VectorPath::filled(
597            motion_core::Circle::new(Point::new(5.0, 5.0), 3.0)?.path()?,
598            Color::WHITE,
599        )));
600        group.children.push(VectorItem::Path(VectorPath::filled(
601            Path2D::from_rect(Rect::new(10.0, 0.0, 16.0, 6.0))?,
602            Color::WHITE,
603        )));
604        let tree = VectorTree::new(Rect::new(0.0, 0.0, 30.0, 10.0), group)?;
605        let circle = std::f64::consts::PI * 9.0;
606        let expected = circle / (circle + 36.0);
607        let count = 40_000;
608        let points = tree.sample_points(count, 7)?;
609        let inside = points.iter().filter(|point| point.x < 8.5).count() as f64 / count as f64;
610        // Five standard deviations of the binomial estimate, far above the tolerance bias.
611        assert!(
612            (inside - expected).abs() < 5.0 * (expected * (1.0 - expected) / count as f64).sqrt(),
613            "{inside} against {expected}"
614        );
615        let quadrants = |center: f64| {
616            points
617                .iter()
618                .filter(|point| point.x < 8.5 && point.x < center)
619                .count() as f64
620                / points.iter().filter(|point| point.x < 8.5).count() as f64
621        };
622        assert!((quadrants(5.0) - 0.5).abs() < 0.02);
623        Ok(())
624    }
625
626    #[test]
627    fn curves_and_resource_refusals_do_not_turn_into_raster_or_unbounded_work()
628    -> motion_core::Result<()> {
629        use motion_core::Shape;
630        let circle = tree(
631            motion_core::Circle::new(Point::new(5.0, 5.0), 3.0)?.path()?,
632            FillRule::NonZero,
633        )?;
634        assert!((circle.surface()?.area() - std::f64::consts::PI * 9.0).abs() < 0.03);
635        assert!(
636            circle
637                .sample_points(1000, 9)?
638                .iter()
639                .all(|p| (p.x - 5.0) * (p.x - 5.0) + (p.y - 5.0) * (p.y - 5.0) < 9.01)
640        );
641        assert!(circle.surface_with_tolerance(1e-16).is_err());
642        assert!(circle.surface()?.area() > 28.0);
643        let next = f64::from_bits(1.0f64.to_bits() + 1);
644        let thin = tree(
645            Path2D::from_rect(Rect::new(0.0, 1.0, 2.0, next))?,
646            FillRule::NonZero,
647        )?;
648        assert_eq!(thin.surface()?.area(), 2.0 * (next - 1.0));
649        let empty = VectorTree::new(circle.view_box(), VectorGroup::default())?;
650        assert!(empty.sample_points(1, 0).is_err());
651        assert!(empty.sample_points(0, 0)?.is_empty());
652        let mut deep = VectorGroup::default();
653        for _ in 0..50 {
654            deep = VectorGroup {
655                children: vec![VectorItem::Group(deep)],
656                ..Default::default()
657            };
658        }
659        assert!(VectorTree::new(circle.view_box(), deep).is_err());
660        Ok(())
661    }
662}