1use 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#[derive(Clone, Copy, Debug)]
16pub struct SurfaceTriangle {
17 pub vertices: [Point; 3],
19 pub area: f64,
21 pub paint_index: usize,
23}
24
25#[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 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 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 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 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 pub fn area(&self) -> f64 {
392 self.area
393 }
394
395 pub fn triangles(&self) -> &[SurfaceTriangle] {
397 &self.triangles
398 }
399
400 pub fn tolerance(&self) -> f64 {
402 self.tolerance
403 }
404
405 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 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, ©.surface().unwrap()));
577 assert_eq!(copy.sample_points(4096, 42).unwrap(), points_for(©));
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 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}