1use crate::budget::{Budget, RasterLimits};
3use crate::{PathVerb, PixelSize, Point, 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 path_edges(
94 verbs: impl Iterator<Item = PathVerb>,
95 transform: Transform,
96 tolerance: f64,
97 budget: &mut Budget,
98 path: &str,
99) -> Result<Vec<Edge>, ValidationError> {
100 let mut edges = Vec::new();
101 let mut current = Point::ORIGIN;
102 let mut start = Point::ORIGIN;
103 for verb in verbs {
104 budget.work(1, path)?;
105 match verb {
106 PathVerb::MoveTo(point) => {
107 add_edge(&mut edges, current, start, budget, path)?;
108 start = transform.apply(point);
109 current = start;
110 }
111 PathVerb::LineTo(point) => {
112 let end = transform.apply(point);
113 add_edge(&mut edges, current, end, budget, path)?;
114 current = end;
115 }
116 PathVerb::Close => {
117 add_edge(&mut edges, current, start, budget, path)?;
118 current = start;
119 }
120 PathVerb::QuadraticTo(control, end) => {
121 let control = transform.apply(control);
122 let end = transform.apply(end);
123 let curve = QuadBez::new(
124 (current.x, current.y),
125 (control.x, control.y),
126 (end.x, end.y),
127 )
128 .raise();
129 flatten_curve(&mut edges, curve, tolerance, budget, path)?;
130 current = end;
131 }
132 PathVerb::CubicTo(first, second, end) => {
133 let first = transform.apply(first);
134 let second = transform.apply(second);
135 let end = transform.apply(end);
136 let curve = CubicBez::new(
137 (current.x, current.y),
138 (first.x, first.y),
139 (second.x, second.y),
140 (end.x, end.y),
141 );
142 flatten_curve(&mut edges, curve, tolerance, budget, path)?;
143 current = end;
144 }
145 }
146 }
147 add_edge(&mut edges, current, start, budget, path)?;
148 Ok(edges)
149}
150
151pub fn flatten_curve(
156 edges: &mut Vec<Edge>,
157 curve: CubicBez,
158 tolerance: f64,
159 budget: &mut Budget,
160 path: &str,
161) -> Result<(), ValidationError> {
162 budget.work(1, path)?;
163 if curve.p0 == curve.p1 && curve.p0 == curve.p2 && curve.p0 == curve.p3 {
164 return Ok(());
165 }
166 let magnitude = [curve.p0, curve.p1, curve.p2, curve.p3]
167 .iter()
168 .fold(0.0f64, |scale, point| {
169 scale.max(point.x.abs()).max(point.y.abs())
170 });
171 if magnitude * (128.0 * f64::EPSILON) > tolerance * 0.25 {
172 return Err(ValidationError::invalid(
173 path,
174 "geometry",
175 "Device-space curve arithmetic cannot resolve the requested tolerance",
176 ));
177 }
178 let first = (curve.p2 - curve.p1) - (curve.p1 - curve.p0);
179 let second = (curve.p3 - curve.p2) - (curve.p2 - curve.p1);
180 let curvature = libm::hypot(first.x, first.y).max(libm::hypot(second.x, second.y));
181 let count = if curvature == 0.0 {
182 1.0
183 } else {
184 (libm::ceil(libm::sqrt(0.75 * curvature / tolerance) * 0.5) * 2.0).max(2.0)
185 };
186 if !count.is_finite() || count > u64::MAX as f64 {
187 return Err(ValidationError::limit(
188 path,
189 "max_work",
190 "Wang subdivision count overflow",
191 ));
192 }
193 budget.work(count as u64, path)?;
194 let mut previous = Point::new(curve.p0.x, curve.p0.y);
195 for index in 1..=count as u64 {
196 let point = if index == count as u64 {
197 curve.p3
198 } else {
199 curve.eval(index as f64 / count)
200 };
201 let current = Point::new(point.x, point.y);
202 add_edge(edges, previous, current, budget, path)?;
203 previous = current;
204 }
205 Ok(())
206}
207
208pub fn add_edge(
210 edges: &mut Vec<Edge>,
211 start: Point,
212 end: Point,
213 budget: &mut Budget,
214 path: &str,
215) -> Result<(), ValidationError> {
216 if start.y != end.y {
217 let edge = if start.y < end.y {
218 Edge {
219 top: start,
220 bottom: end,
221 winding: 1,
222 }
223 } else {
224 Edge {
225 top: end,
226 bottom: start,
227 winding: -1,
228 }
229 };
230 budget.push(edges, edge, path)?;
231 }
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use kurbo::ParamCurveDeriv;
239
240 #[test]
241 fn wang_segments_follow_derivative_bound_and_device_tolerance() {
242 let curves = [
243 CubicBez::new((0.0, 0.0), (0.0, 8.0), (8.0, -8.0), (8.0, 0.0)),
244 CubicBez::new((1.0, 2.0), (1.0, 2.0), (1.0, 2.0), (8.0, 9.0)),
245 CubicBez::new((0.0, 0.0), (3.0, 1.0), (6.0, 2.0), (9.0, 3.0)),
246 QuadBez::new((0.0, 0.0), (4.0, 8.0), (8.0, 1.0)).raise(),
247 ];
248 for curve in curves {
249 for scale in [0.01, 1.0, 32.0] {
250 let curve = CubicBez::new(
251 (curve.p0.x * scale, curve.p0.y * scale),
252 (curve.p1.x * scale, curve.p1.y * scale),
253 (curve.p2.x * scale, curve.p2.y * scale),
254 (curve.p3.x * scale, curve.p3.y * scale),
255 );
256 let derivative = curve.deriv().deriv();
257 let bound = derivative
258 .eval(0.0)
259 .to_vec2()
260 .hypot()
261 .max(derivative.eval(1.0).to_vec2().hypot());
262 for tolerance in [0.001, 0.125, 0.5, 1.0] {
263 let count = if bound == 0.0 {
264 1
265 } else {
266 let required =
267 libm::ceil(libm::sqrt(bound / (8.0 * tolerance))).max(1.0) as u64;
268 required + required % 2
269 };
270 let mut budget =
271 Budget::new(PixelSize::new(1, 1), RasterLimits::default()).unwrap();
272 let mut actual = Vec::new();
273 flatten_curve(&mut actual, curve, tolerance, &mut budget, "test.wang").unwrap();
274 let mut expected = Vec::new();
275 for segment in 0..count {
276 let start = curve.eval(segment as f64 / count as f64);
277 let end = curve.eval((segment + 1) as f64 / count as f64);
278 if start.y < end.y {
279 expected.push((
280 Point::new(start.x, start.y),
281 Point::new(end.x, end.y),
282 1,
283 ));
284 } else if start.y > end.y {
285 expected.push((
286 Point::new(end.x, end.y),
287 Point::new(start.x, start.y),
288 -1,
289 ));
290 }
291 for sample in 0..=64 {
292 let progress = f64::from(sample) / 64.0;
293 let parameter = (segment as f64 + progress) / count as f64;
294 let chord = start.lerp(end, progress);
295 let error = (curve.eval(parameter) - chord).hypot();
296 assert!(
297 error <= tolerance * (1.0 + 1e-9),
298 "error {error}, tolerance {tolerance}, count {count}"
299 );
300 }
301 }
302 assert_eq!(
303 actual
304 .iter()
305 .map(|edge| (edge.top, edge.bottom, edge.winding))
306 .collect::<Vec<_>>(),
307 expected
308 );
309 }
310 }
311 }
312 }
313
314 #[test]
315 fn wang_reduction_preserves_precision_and_work_failures() {
316 let curve = CubicBez::new((0.0, 0.0), (0.0, 8.0), (8.0, -8.0), (8.0, 0.0));
317 let mut budget = Budget::new(
318 PixelSize::new(1, 1),
319 RasterLimits {
320 max_work: 1,
321 ..RasterLimits::default()
322 },
323 )
324 .unwrap();
325 let error =
326 flatten_curve(&mut Vec::new(), curve, 0.125, &mut budget, "test.wang").unwrap_err();
327 assert_eq!(error.field, "max_work");
328 let curve = CubicBez::new(
329 (1e13, 0.0),
330 (1e13, 8.0),
331 (1e13 + 8.0, -8.0),
332 (1e13 + 8.0, 0.0),
333 );
334 let mut budget = Budget::new(PixelSize::new(1, 1), RasterLimits::default()).unwrap();
335 let error =
336 flatten_curve(&mut Vec::new(), curve, 0.125, &mut budget, "test.wang").unwrap_err();
337 assert_eq!(error.field, "geometry");
338 }
339}