ifc_lite_geometry/
contour_composition.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4//! One source-qualified integer lattice for a sequence of planar booleans.
5//! Source groups are quantized together once; intermediate coordinates never
6//! return through a floating adapter. IDs are local to this composition only.
7use crate::{BooleanOp2D, ContourFillRule, ContourSet, Ring2D};
8use i_overlay::{
9    core::{fill_rule::FillRule, overlay::Overlay, overlay_rule::OverlayRule},
10    i_float::{adapter::FloatPointAdapter, float::rect::FloatRect, int::point::IntPoint},
11};
12type Ring = Vec<IntPoint<i64>>;
13struct Group {
14    rings: Vec<Ring>,
15    shape_offsets: Vec<usize>,
16    classified: bool,
17}
18/// Bounded sequential fixed-grid authoring. The caller charges worst-case work
19/// before construction and each overlay; this context independently caps sizes.
20pub struct FixedGridComposition {
21    adapter: FloatPointAdapter<[f64; 2], i64>,
22    groups: Vec<Group>,
23    vertices: usize,
24}
25impl FixedGridComposition {
26    /// Input group IDs are their indices; `groups.len()` is a built-in empty set.
27    /// Original endpoints and topology are qualified jointly before conversion.
28    pub fn new(groups: &[Vec<Ring2D>], grid: f64) -> Result<Self, String> {
29        if groups.len() > 257 || !grid.is_finite() || grid <= 0. || !(1. / grid).is_finite() {
30            return Err("Fixed-grid composition has invalid group count or grid".into());
31        }
32        let n = groups
33            .iter()
34            .flatten()
35            .try_fold(0usize, |n, r| n.checked_add(r.len()))
36            .ok_or("Fixed-grid source vertex overflow")?;
37        if n > 1024
38            || groups
39                .iter()
40                .flatten()
41                .flatten()
42                .flatten()
43                .any(|v| !v.is_finite() || v.abs() > 1e12)
44        {
45            return Err("Fixed-grid composition exceeds 1024 finite source vertices".into());
46        }
47        let rings: Vec<_> = groups.iter().flatten().cloned().collect();
48        let adapter =
49            FloatPointAdapter::with_iter_and_scale_checked(rings.iter().flatten(), 1. / grid)
50                .map_err(|e| format!("Fixed-grid composition numeric range: {e:?}"))?;
51        crate::contour_grid_guard::validate_with_adapter(&rings, &[], grid, &adapter)
52            .map_err(|e| format!("PDF original lattice qualification: {e}"))?;
53        let mut result = Self {
54            adapter,
55            groups: vec![],
56            vertices: n,
57        };
58        for group in groups {
59            let sanitized = crate::contour_bool2d::sanitize_fixed_grid(group)?;
60            let rings: Vec<Ring> = sanitized
61                .iter()
62                .map(|r| r.iter().map(|p| result.adapter.float_to_int(p)).collect())
63                .collect();
64            exact_range(&rings)?;
65            result.groups.push(Group {
66                rings,
67                shape_offsets: vec![],
68                classified: false,
69            });
70        }
71        result.groups.push(Group {
72            rings: vec![],
73            shape_offsets: vec![],
74            classified: true,
75        });
76        Ok(result)
77    }
78    /// Vertex count supports the caller's shared work precharge.
79    pub fn vertex_count(&self, id: usize) -> Result<usize, String> {
80        Ok(self.group(id)?.rings.iter().map(Vec::len).sum())
81    }
82    fn group(&self, id: usize) -> Result<&Group, String> {
83        self.groups
84            .get(id)
85            .ok_or_else(|| "Unknown fixed-grid composition group".into())
86    }
87    /// Returns a new local group ID, preserving all islands and holes.
88    pub fn overlay(
89        &mut self,
90        a: usize,
91        b: usize,
92        op: BooleanOp2D,
93        fill: ContourFillRule,
94    ) -> Result<usize, String> {
95        let a = self.group(a)?;
96        let b = self.group(b)?;
97        let n = a.rings.iter().chain(&b.rings).map(Vec::len).sum::<usize>();
98        if n > 1024 || self.groups.len() >= 2048 {
99            return Err("Fixed-grid composition stage exceeds bounds".into());
100        }
101        // Integer coordinates are exactly representable under exact_range.
102        // Qualification runs in integer units using a zero-centred unit adapter,
103        // so it cannot introduce another rounding phase or floating near-contact.
104        let floats = |rings: &[Ring]| {
105            rings
106                .iter()
107                .map(|r| r.iter().map(|p| [p.x as f64, p.y as f64]).collect())
108                .collect::<Vec<Ring2D>>()
109        };
110        let af = floats(&a.rings);
111        let bf = floats(&b.rings);
112        let radius = (1_u64 << 51) as f64;
113        let unit =
114            FloatPointAdapter::try_with_scale(FloatRect::new(-radius, radius, -radius, radius), 1.)
115                .map_err(|e| format!("Fixed-grid composition unit range: {e:?}"))?;
116        crate::contour_grid_guard::validate_with_adapter(&af, &bf, 1., &unit)
117            .map_err(|e| format!("PDF integer stage qualification: {e}"))?;
118        let rule = match op {
119            BooleanOp2D::Union => OverlayRule::Union,
120            BooleanOp2D::Difference => OverlayRule::Difference,
121            BooleanOp2D::Intersection => OverlayRule::Intersect,
122        };
123        let fill = match fill {
124            ContourFillRule::NonZero => FillRule::NonZero,
125            ContourFillRule::EvenOdd => FillRule::EvenOdd,
126        };
127        let shapes = Overlay::with_contours(&a.rings, &b.rings).overlay(rule, fill);
128        let mut group = Group {
129            rings: vec![],
130            shape_offsets: vec![],
131            classified: true,
132        };
133        for shape in shapes {
134            if shape.first().is_none_or(|r| r.len() < 3) {
135                continue;
136            }
137            group.shape_offsets.push(group.rings.len());
138            group
139                .rings
140                .extend(shape.into_iter().filter(|r| r.len() >= 3));
141        }
142        exact_range(&group.rings)?;
143        let output = group.rings.iter().map(Vec::len).sum::<usize>();
144        self.vertices = self
145            .vertices
146            .checked_add(output)
147            .ok_or("Fixed-grid composition vertex overflow")?;
148        if self.vertices > 16_384 {
149            return Err("Fixed-grid composition exceeds cumulative vertex budget".into());
150        }
151        let id = self.groups.len();
152        self.groups.push(group);
153        Ok(id)
154    }
155    /// Convert only a completed output to model coordinates. Raw input groups
156    /// are unclassified and cannot be exported as shapes.
157    pub fn contours(&self, id: usize) -> Result<ContourSet, String> {
158        let group = self.group(id)?;
159        if !group.classified {
160            return Err("Cannot export an unclassified fixed-grid source group".into());
161        }
162        Ok(ContourSet {
163            rings: group
164                .rings
165                .iter()
166                .map(|r| r.iter().map(|p| self.adapter.int_to_float(p)).collect())
167                .collect(),
168            shape_offsets: group.shape_offsets.clone(),
169        })
170    }
171}
172fn exact_range(rings: &[Ring]) -> Result<(), String> {
173    if rings
174        .iter()
175        .flatten()
176        .any(|p| p.x.unsigned_abs() > (1_u64 << 50) || p.y.unsigned_abs() > (1_u64 << 50))
177    {
178        return Err("Fixed-grid composition exceeds exact diagnostic coordinate range".into());
179    }
180    Ok(())
181}
182
183#[cfg(test)]
184#[path = "contour_composition_tests.rs"]
185mod tests;