1use crate::budget::{Budget, RasterLimits};
3use crate::{PathVerb, PixelSize, Point, Rect, Transform, ValidationError};
4use kurbo::{CubicBez, ParamCurve, QuadBez};
5use std::mem::size_of;
6
7#[derive(Clone, Copy)]
8pub struct Edge {
10 pub top: Point,
12 pub bottom: Point,
14 pub winding: i32,
16}
17
18impl Edge {
19 pub fn at(self, vertical: f64) -> f64 {
21 if self.top.x == self.bottom.x {
22 return self.top.x;
23 }
24 let progress = ((vertical - self.top.y) / (self.bottom.y - self.top.y)).clamp(0.0, 1.0);
25 self.top.x * (1.0 - progress) + self.bottom.x * progress
26 }
27
28 pub fn check_projection(
31 self,
32 start: f64,
33 end: f64,
34 width: u32,
35 path: &str,
36 ) -> Result<(), ValidationError> {
37 let first = self.at(start);
38 let last = self.at(end);
39 let cancellation =
40 (self.top.x < 0.0 && self.bottom.x > 0.0) || (self.top.x > 0.0 && self.bottom.x < 0.0);
41 let error = self.top.x.abs().max(self.bottom.x.abs()) * (16.0 * f64::EPSILON);
42 if cancellation
43 && first.min(last) <= f64::from(width)
44 && first.max(last) >= 0.0
45 && error > 1.0e-7
46 {
47 return Err(ValidationError::invalid(
48 path,
49 "geometry",
50 "Visible edge cancellation exceeds the 1e-7 device-pixel precision guard",
51 ));
52 }
53 Ok(())
54 }
55}
56
57pub fn surface_edges(
60 source: &crate::Path2D,
61 transform: Transform,
62 tolerance: f64,
63 max_edges: usize,
64 max_work: u64,
65) -> Result<(Vec<Edge>, u64), ValidationError> {
66 if max_work == 0 {
67 return Err(ValidationError::limit(
68 "vector_tree.surface",
69 "max_work",
70 "Surface preparation work exhausted",
71 ));
72 }
73 let mut budget = Budget::new(
74 PixelSize::new(1, 1),
75 RasterLimits {
76 max_pixels: 1,
77 max_bytes: (max_edges as u64 + 1) * size_of::<Edge>() as u64,
78 max_work: max_work.min(4_000_000),
79 },
80 )?;
81 let edges = path_edges(
82 source.verbs().iter().copied(),
83 transform,
84 tolerance,
85 &mut budget,
86 "vector_tree.surface",
87 )?;
88 Ok((edges, budget.work_done()))
89}
90
91pub fn edge_bounds(edges: &[Edge]) -> Rect {
93 let mut bounds = Rect::new(
94 f64::INFINITY,
95 f64::INFINITY,
96 f64::NEG_INFINITY,
97 f64::NEG_INFINITY,
98 );
99 for edge in edges {
100 bounds.left = bounds.left.min(edge.top.x).min(edge.bottom.x);
101 bounds.right = bounds.right.max(edge.top.x).max(edge.bottom.x);
102 bounds.top = bounds.top.min(edge.top.y);
103 bounds.bottom = bounds.bottom.max(edge.bottom.y);
104 }
105 bounds
106}
107
108pub fn path_edges(
111 verbs: impl Iterator<Item = PathVerb>,
112 transform: Transform,
113 tolerance: f64,
114 budget: &mut Budget,
115 path: &str,
116) -> Result<Vec<Edge>, ValidationError> {
117 let mut edges = Vec::new();
118 let mut current = Point::ORIGIN;
119 let mut start = Point::ORIGIN;
120 for verb in verbs {
121 budget.work(1, path)?;
122 match verb {
123 PathVerb::MoveTo(point) => {
124 add_edge(&mut edges, current, start, budget, path)?;
125 start = transform.apply(point);
126 current = start;
127 }
128 PathVerb::LineTo(point) => {
129 let end = transform.apply(point);
130 add_edge(&mut edges, current, end, budget, path)?;
131 current = end;
132 }
133 PathVerb::Close => {
134 add_edge(&mut edges, current, start, budget, path)?;
135 current = start;
136 }
137 PathVerb::QuadraticTo(control, end) => {
138 let control = transform.apply(control);
139 let end = transform.apply(end);
140 let curve = QuadBez::new(
141 (current.x, current.y),
142 (control.x, control.y),
143 (end.x, end.y),
144 )
145 .raise();
146 flatten_curve(&mut edges, curve, tolerance, budget, path)?;
147 current = end;
148 }
149 PathVerb::CubicTo(first, second, end) => {
150 let first = transform.apply(first);
151 let second = transform.apply(second);
152 let end = transform.apply(end);
153 let curve = CubicBez::new(
154 (current.x, current.y),
155 (first.x, first.y),
156 (second.x, second.y),
157 (end.x, end.y),
158 );
159 flatten_curve(&mut edges, curve, tolerance, budget, path)?;
160 current = end;
161 }
162 }
163 }
164 add_edge(&mut edges, current, start, budget, path)?;
165 Ok(edges)
166}
167
168pub fn flatten_curve(
173 edges: &mut Vec<Edge>,
174 curve: CubicBez,
175 tolerance: f64,
176 budget: &mut Budget,
177 path: &str,
178) -> Result<(), ValidationError> {
179 budget.work(1, path)?;
180 if curve.p0 == curve.p1 && curve.p0 == curve.p2 && curve.p0 == curve.p3 {
181 return Ok(());
182 }
183 let magnitude = [curve.p0, curve.p1, curve.p2, curve.p3]
184 .iter()
185 .fold(0.0f64, |scale, point| {
186 scale.max(point.x.abs()).max(point.y.abs())
187 });
188 if magnitude * (128.0 * f64::EPSILON) > tolerance * 0.25 {
189 return Err(ValidationError::invalid(
190 path,
191 "geometry",
192 "Device-space curve arithmetic cannot resolve the requested tolerance",
193 ));
194 }
195 let first = (curve.p2 - curve.p1) - (curve.p1 - curve.p0);
196 let second = (curve.p3 - curve.p2) - (curve.p2 - curve.p1);
197 let curvature = libm::hypot(first.x, first.y).max(libm::hypot(second.x, second.y));
198 let count = if curvature == 0.0 {
199 1.0
200 } else {
201 (libm::ceil(libm::sqrt(0.75 * curvature / tolerance) * 0.5) * 2.0).max(2.0)
202 };
203 if !count.is_finite() || count > u64::MAX as f64 {
204 return Err(ValidationError::limit(
205 path,
206 "max_work",
207 "Wang subdivision count overflow",
208 ));
209 }
210 budget.work(count as u64, path)?;
211 let mut previous = Point::new(curve.p0.x, curve.p0.y);
212 for index in 1..=count as u64 {
213 let point = if index == count as u64 {
214 curve.p3
215 } else {
216 curve.eval(index as f64 / count)
217 };
218 let current = Point::new(point.x, point.y);
219 add_edge(edges, previous, current, budget, path)?;
220 previous = current;
221 }
222 Ok(())
223}
224
225pub fn add_edge(
227 edges: &mut Vec<Edge>,
228 start: Point,
229 end: Point,
230 budget: &mut Budget,
231 path: &str,
232) -> Result<(), ValidationError> {
233 if start.y != end.y {
234 let edge = if start.y < end.y {
235 Edge {
236 top: start,
237 bottom: end,
238 winding: 1,
239 }
240 } else {
241 Edge {
242 top: end,
243 bottom: start,
244 winding: -1,
245 }
246 };
247 budget.push(edges, edge, path)?;
248 }
249 Ok(())
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use kurbo::ParamCurveDeriv;
256
257 #[test]
258 fn wang_segments_follow_derivative_bound_and_device_tolerance() {
259 let curves = [
260 CubicBez::new((0.0, 0.0), (0.0, 8.0), (8.0, -8.0), (8.0, 0.0)),
261 CubicBez::new((1.0, 2.0), (1.0, 2.0), (1.0, 2.0), (8.0, 9.0)),
262 CubicBez::new((0.0, 0.0), (3.0, 1.0), (6.0, 2.0), (9.0, 3.0)),
263 QuadBez::new((0.0, 0.0), (4.0, 8.0), (8.0, 1.0)).raise(),
264 ];
265 for curve in curves {
266 for scale in [0.01, 1.0, 32.0] {
267 let curve = CubicBez::new(
268 (curve.p0.x * scale, curve.p0.y * scale),
269 (curve.p1.x * scale, curve.p1.y * scale),
270 (curve.p2.x * scale, curve.p2.y * scale),
271 (curve.p3.x * scale, curve.p3.y * scale),
272 );
273 let derivative = curve.deriv().deriv();
274 let bound = derivative
275 .eval(0.0)
276 .to_vec2()
277 .hypot()
278 .max(derivative.eval(1.0).to_vec2().hypot());
279 for tolerance in [0.001, 0.125, 0.5, 1.0] {
280 let count = if bound == 0.0 {
281 1
282 } else {
283 let required =
284 libm::ceil(libm::sqrt(bound / (8.0 * tolerance))).max(1.0) as u64;
285 required + required % 2
286 };
287 let mut budget =
288 Budget::new(PixelSize::new(1, 1), RasterLimits::default()).unwrap();
289 let mut actual = Vec::new();
290 flatten_curve(&mut actual, curve, tolerance, &mut budget, "test.wang").unwrap();
291 let mut expected = Vec::new();
292 for segment in 0..count {
293 let start = curve.eval(segment as f64 / count as f64);
294 let end = curve.eval((segment + 1) as f64 / count as f64);
295 if start.y < end.y {
296 expected.push((
297 Point::new(start.x, start.y),
298 Point::new(end.x, end.y),
299 1,
300 ));
301 } else if start.y > end.y {
302 expected.push((
303 Point::new(end.x, end.y),
304 Point::new(start.x, start.y),
305 -1,
306 ));
307 }
308 for sample in 0..=64 {
309 let progress = f64::from(sample) / 64.0;
310 let parameter = (segment as f64 + progress) / count as f64;
311 let chord = start.lerp(end, progress);
312 let error = (curve.eval(parameter) - chord).hypot();
313 assert!(
314 error <= tolerance * (1.0 + 1e-9),
315 "error {error}, tolerance {tolerance}, count {count}"
316 );
317 }
318 }
319 assert_eq!(
320 actual
321 .iter()
322 .map(|edge| (edge.top, edge.bottom, edge.winding))
323 .collect::<Vec<_>>(),
324 expected
325 );
326 }
327 }
328 }
329 }
330
331 #[test]
332 fn wang_reduction_preserves_precision_and_work_failures() {
333 let curve = CubicBez::new((0.0, 0.0), (0.0, 8.0), (8.0, -8.0), (8.0, 0.0));
334 let mut budget = Budget::new(
335 PixelSize::new(1, 1),
336 RasterLimits {
337 max_work: 1,
338 ..RasterLimits::default()
339 },
340 )
341 .unwrap();
342 let error =
343 flatten_curve(&mut Vec::new(), curve, 0.125, &mut budget, "test.wang").unwrap_err();
344 assert_eq!(error.field, "max_work");
345 let curve = CubicBez::new(
346 (1e13, 0.0),
347 (1e13, 8.0),
348 (1e13 + 8.0, -8.0),
349 (1e13 + 8.0, 0.0),
350 );
351 let mut budget = Budget::new(PixelSize::new(1, 1), RasterLimits::default()).unwrap();
352 let error =
353 flatten_curve(&mut Vec::new(), curve, 0.125, &mut budget, "test.wang").unwrap_err();
354 assert_eq!(error.field, "geometry");
355 }
356}