hypercurve 0.3.0

Hyperreal-backed planar curves, contours, and regions for CAD topology
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
//! Finite polyline projection adapters for native hyper curves.
//!
//! Projection is an IO/rendering boundary, not a topology kernel. The methods
//! in this module preserve line segments exactly, approximate circular arcs by
//! a chord-error budget, and return primitive `f64` coordinates only after the
//! source [`Real`](hyperreal::Real) coordinates can be exported finitely. This
//! follows Yap, "Towards Exact Geometric Computation," *Computational
//! Geometry* 7(1-2), 1997 (<https://doi.org/10.1016/0925-7721(95)00040-2>):
//! exact objects own CAD/topology; finite samples are boundary products.
//! Boundary and containment decisions should continue to use the exact
//! contour/region APIs surveyed by Hormann and Agathos, "The Point in Polygon
//! Problem for Arbitrary Polygons," *Computational Geometry* 20(3), 2001
//! (<https://doi.org/10.1016/S0925-7721(01)00012-8>).

use std::f64::consts::PI;

use crate::{
    CircularArc2, Classification, Contour2, CurveError, CurvePolicy, CurveResult, CurveString2,
    Point2, Region2, RegionContourProfile, RegionView2, Segment2,
};

/// Options for projecting native curves to finite `f64` polylines.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FiniteProjectionOptions {
    arc_chord_error: f64,
}

/// Finite `f64` polyline emitted from a native curve object.
#[derive(Clone, Debug, PartialEq)]
pub struct FinitePolyline2 {
    points: Vec<[f64; 2]>,
    arc_chord_error: f64,
    closed: bool,
}

/// Finite `f64` projection of a region with material and hole roles retained.
///
/// This is an IO/display object. Exact containment, area, and boolean topology
/// remain on [`Region2`] / [`RegionView2`].
#[derive(Clone, Debug, PartialEq)]
pub struct FiniteRegionProjection2 {
    material_rings: Vec<FinitePolyline2>,
    hole_rings: Vec<FinitePolyline2>,
}

/// A finite material ring and the finite hole rings owned by it.
///
/// This is the projected counterpart to [`RegionContourProfile`]. Ownership is
/// still decided in exact hypercurve topology before any finite ring is
/// emitted; this type only carries the boundary result.
#[derive(Clone, Debug, PartialEq)]
pub struct FiniteRegionProfile2 {
    material: FinitePolyline2,
    holes: Vec<FinitePolyline2>,
}

impl FiniteProjectionOptions {
    /// Constructs projection options with a positive finite arc chord-error budget.
    pub fn try_new(arc_chord_error: f64) -> CurveResult<Self> {
        if arc_chord_error.is_finite() && arc_chord_error > 0.0 {
            Ok(Self { arc_chord_error })
        } else {
            Err(CurveError::InvalidFiniteProjectionOptions)
        }
    }

    /// Returns the maximum requested circular-arc chord error.
    pub const fn arc_chord_error(&self) -> f64 {
        self.arc_chord_error
    }
}

impl FinitePolyline2 {
    fn new(points: Vec<[f64; 2]>, arc_chord_error: f64, closed: bool) -> Self {
        Self {
            points,
            arc_chord_error,
            closed,
        }
    }

    /// Returns the finite projected vertices.
    pub fn points(&self) -> &[[f64; 2]] {
        &self.points
    }

    /// Consumes the projection and returns finite vertices.
    pub fn into_points(self) -> Vec<[f64; 2]> {
        self.points
    }

    /// Returns the arc chord-error budget requested for this projection.
    pub const fn arc_chord_error(&self) -> f64 {
        self.arc_chord_error
    }

    /// Returns true when this polyline was explicitly closed for a contour.
    pub const fn is_closed(&self) -> bool {
        self.closed
    }

    /// Returns the finite signed shoelace area when this polyline is treated as
    /// a ring.
    ///
    /// This is only a boundary/product measurement of projected vertices. Exact
    /// contour area stays on [`Contour2::signed_area`] and
    /// [`crate::Region2::filled_area`].
    pub fn signed_ring_area(&self) -> f64 {
        finite_ring_signed_area(&self.points)
    }

    /// Returns the checked finite signed shoelace area of this projected ring.
    pub fn try_signed_ring_area(&self) -> CurveResult<f64> {
        try_finite_ring_signed_area(&self.points)
    }

    /// Returns the arithmetic centroid of this finite projected polyline.
    ///
    /// This is a boundary-product measurement over emitted finite vertices, not
    /// an exact centroid of the native curve or filled area. A repeated closing
    /// vertex is ignored. Keeping this helper on the projected polyline type
    /// prevents downstream crates from reimplementing small finite adapters
    /// around hypercurve output. The exact-object/boundary split follows Yap,
    /// "Towards Exact Geometric Computation," *Computational Geometry* 7(1-2),
    /// 1997 (<https://doi.org/10.1016/0925-7721(95)00040-2>).
    pub fn vertex_centroid(&self) -> Option<[f64; 2]> {
        finite_polyline_vertex_centroid(&self.points)
    }

    /// Returns the checked arithmetic centroid of this projected polyline.
    pub fn try_vertex_centroid(&self) -> CurveResult<Option<[f64; 2]>> {
        try_finite_polyline_vertex_centroid(&self.points)
    }
}

impl FiniteRegionProjection2 {
    fn new(material_rings: Vec<FinitePolyline2>, hole_rings: Vec<FinitePolyline2>) -> Self {
        Self {
            material_rings,
            hole_rings,
        }
    }

    /// Returns projected material rings.
    pub fn material_rings(&self) -> &[FinitePolyline2] {
        &self.material_rings
    }

    /// Returns projected hole rings.
    pub fn hole_rings(&self) -> &[FinitePolyline2] {
        &self.hole_rings
    }
}

impl FiniteRegionProfile2 {
    fn new(material: FinitePolyline2, holes: Vec<FinitePolyline2>) -> Self {
        Self { material, holes }
    }

    /// Returns the projected material ring.
    pub const fn material(&self) -> &FinitePolyline2 {
        &self.material
    }

    /// Returns the projected hole rings owned by the material ring.
    pub fn holes(&self) -> &[FinitePolyline2] {
        &self.holes
    }

    /// Returns the finite projected material-minus-hole area.
    ///
    /// Hole ownership has already been decided by native region topology before
    /// this projected profile exists, so this method does not infer roles from
    /// winding. It only measures the finite output rings with the shoelace
    /// formula. Exact CAD area should use [`Region2::filled_area`]; this helper
    /// exists for IO, diagnostics, and tests at the projection boundary.
    pub fn projected_filled_area(&self) -> f64 {
        let material = self.material.signed_ring_area().abs();
        let holes = self
            .holes
            .iter()
            .map(|hole| hole.signed_ring_area().abs())
            .sum::<f64>();
        material - holes
    }

    /// Returns the checked finite projected material-minus-hole area.
    pub fn try_projected_filled_area(&self) -> CurveResult<f64> {
        let material = self.material.try_signed_ring_area()?.abs();
        let holes = self.holes.iter().try_fold(0.0, |sum, hole| {
            let next = sum + hole.try_signed_ring_area()?.abs();
            next.is_finite()
                .then_some(next)
                .ok_or(CurveError::NonFiniteProjectionPoint)
        })?;
        let area = material - holes;
        area.is_finite()
            .then_some(area)
            .ok_or(CurveError::NonFiniteProjectionPoint)
    }
}

/// Returns the finite signed shoelace area of projected ring vertices.
///
/// The closing edge is included even when the caller did not repeat the first
/// vertex. This is the familiar Green's-theorem polygon formula applied only
/// to finite boundary data; exact CAD area should use native contour/region
/// area APIs instead. The boundary split follows Yap, "Towards Exact Geometric
/// Computation," *Computational Geometry* 7(1-2), 1997
/// (<https://doi.org/10.1016/0925-7721(95)00040-2>).
pub fn finite_ring_signed_area(ring: &[[f64; 2]]) -> f64 {
    if ring.len() < 3 {
        return 0.0;
    }
    let mut area = 0.0;
    for edge in ring.windows(2) {
        area += edge[0][0] * edge[1][1] - edge[1][0] * edge[0][1];
    }
    if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
        area += last[0] * first[1] - first[0] * last[1];
    }
    0.5 * area
}

/// Returns the checked finite signed shoelace area of projected ring vertices.
///
/// Non-finite coordinates or arithmetic overflow are reported explicitly
/// instead of leaking `NaN`/`inf` through a finite-boundary measurement.
pub fn try_finite_ring_signed_area(ring: &[[f64; 2]]) -> CurveResult<f64> {
    let ring = normalize_finite_ring_vertices(ring)?;
    if ring.len() < 3 {
        return Ok(0.0);
    }

    let mut area = 0.0;
    for edge in ring.windows(2) {
        area = checked_shoelace_sum(area, edge[0], edge[1])?;
    }
    if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
        area = checked_shoelace_sum(area, *last, *first)?;
    }
    let area = 0.5 * area;
    area.is_finite()
        .then_some(area)
        .ok_or(CurveError::NonFiniteProjectionPoint)
}

/// Returns the arithmetic centroid of finite polyline vertices.
///
/// A repeated final closing point is ignored so closed-ring projections do not
/// overweight the first vertex. This is a finite boundary statistic only; exact
/// geometric centroids belong on native curve/region facts.
pub fn finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> Option<[f64; 2]> {
    let unique = points
        .iter()
        .copied()
        .enumerate()
        .filter_map(|(index, point)| {
            (index + 1 != points.len() || Some(&point) != points.first()).then_some(point)
        })
        .collect::<Vec<_>>();
    if unique.is_empty() {
        return None;
    }
    let count = unique.len() as f64;
    let (sum_x, sum_y) = unique
        .iter()
        .fold((0.0, 0.0), |(x, y), point| (x + point[0], y + point[1]));
    Some([sum_x / count, sum_y / count])
}

/// Returns the checked arithmetic centroid of finite polyline vertices.
///
/// A repeated final closing point is ignored. Non-finite coordinates or
/// arithmetic overflow are reported explicitly.
pub fn try_finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> CurveResult<Option<[f64; 2]>> {
    let unique = finite_unique_polyline_vertices(points)?;
    if unique.is_empty() {
        return Ok(None);
    }
    let count = unique.len() as f64;
    let (sum_x, sum_y) = unique.iter().try_fold((0.0, 0.0), |(x, y), point| {
        let next_x = x + point[0];
        let next_y = y + point[1];
        (next_x.is_finite() && next_y.is_finite())
            .then_some((next_x, next_y))
            .ok_or(CurveError::NonFiniteProjectionPoint)
    })?;
    let centroid = [sum_x / count, sum_y / count];
    centroid
        .iter()
        .all(|value| value.is_finite())
        .then_some(Some(centroid))
        .ok_or(CurveError::NonFiniteProjectionPoint)
}

pub(crate) fn normalize_finite_ring_vertices(ring: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
    let mut normalized = Vec::with_capacity(ring.len());
    for point in finite_unique_polyline_vertices(ring)? {
        if normalized.last() == Some(&point) {
            continue;
        }
        normalized.push(point);
    }
    if normalized.len() > 1 && normalized.first() == normalized.last() {
        normalized.pop();
    }
    Ok(normalized)
}

fn finite_unique_polyline_vertices(points: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
    let mut unique = Vec::with_capacity(points.len());
    for (index, &[x, y]) in points.iter().enumerate() {
        if !x.is_finite() || !y.is_finite() {
            return Err(CurveError::NonFiniteProjectionPoint);
        }
        let point = [x, y];
        if index + 1 != points.len() || Some(&point) != points.first() {
            unique.push(point);
        }
    }
    Ok(unique)
}

fn checked_shoelace_sum(sum: f64, start: [f64; 2], end: [f64; 2]) -> CurveResult<f64> {
    let term = start[0] * end[1] - end[0] * start[1];
    let next = sum + term;
    next.is_finite()
        .then_some(next)
        .ok_or(CurveError::NonFiniteProjectionPoint)
}

impl CurveString2 {
    /// Projects this curve string to a finite polyline for IO and display.
    ///
    /// This is a lossy boundary view: circular arcs are sampled by chord error,
    /// and the returned `f64` vertices must not be used as the source of exact
    /// topology decisions.
    pub fn project_to_finite_polyline(
        &self,
        options: &FiniteProjectionOptions,
    ) -> CurveResult<FinitePolyline2> {
        project_curve_string(self, options, false)
    }
}

impl Contour2 {
    /// Projects this closed contour to a finite closed ring for IO and display.
    ///
    /// The contour itself remains authoritative for area, containment, and
    /// winding. This method only emits a finite boundary ring after all points
    /// can cross the API boundary as `f64`.
    pub fn project_to_finite_ring(
        &self,
        options: &FiniteProjectionOptions,
    ) -> CurveResult<FinitePolyline2> {
        project_curve_string(self.curve_string(), options, true)
    }
}

impl Region2 {
    /// Projects this region to finite material/hole rings for IO and display.
    ///
    /// Region roles are preserved, but the returned rings are boundary
    /// products only. Exact point classification and area should continue to
    /// use [`Region2::classify_point`] and [`Region2::filled_area`].
    pub fn project_to_finite_region(
        &self,
        options: &FiniteProjectionOptions,
    ) -> CurveResult<FiniteRegionProjection2> {
        self.as_view().project_to_finite_region(options)
    }

    /// Projects exact material/hole ownership profiles to finite rings.
    ///
    /// Ownership is classified before projection with
    /// [`Region2::contour_profiles`], so this method does not recover holes
    /// from sampled centroids or winding heuristics. The returned rings are
    /// still finite API-boundary products; exact topology remains in the
    /// region. This follows Yap's exact-object/API-boundary split and the
    /// boundary-first point-in-polygon structure surveyed by Hormann and
    /// Agathos, both cited on [`Region2::contour_profiles`].
    pub fn project_to_finite_profiles(
        &self,
        options: &FiniteProjectionOptions,
        policy: &CurvePolicy,
    ) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
        self.as_view().project_to_finite_profiles(options, policy)
    }
}

impl<'a> RegionView2<'a> {
    /// Projects this borrowed region view to finite material/hole rings.
    ///
    /// This method exists for export adapters that already work with borrowed
    /// topology. It clones only finite output vertices, not exact contours.
    pub fn project_to_finite_region(
        &self,
        options: &FiniteProjectionOptions,
    ) -> CurveResult<FiniteRegionProjection2> {
        let material_rings = project_contour_slice(self.material_contours(), options)?;
        let hole_rings = project_contour_slice(self.hole_contours(), options)?;
        Ok(FiniteRegionProjection2::new(material_rings, hole_rings))
    }

    /// Projects exact material/hole ownership profiles to finite rings.
    pub fn project_to_finite_profiles(
        &self,
        options: &FiniteProjectionOptions,
        policy: &CurvePolicy,
    ) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
        match self.contour_profiles(policy) {
            Classification::Decided(profiles) => profiles
                .iter()
                .map(|profile| project_region_profile(profile, options))
                .collect::<CurveResult<Vec<_>>>()
                .map(Classification::Decided),
            Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
        }
    }
}

fn project_region_profile(
    profile: &RegionContourProfile<'_>,
    options: &FiniteProjectionOptions,
) -> CurveResult<FiniteRegionProfile2> {
    let material = profile.material.project_to_finite_ring(options)?;
    let holes = profile
        .holes
        .iter()
        .map(|hole| hole.project_to_finite_ring(options))
        .collect::<CurveResult<Vec<_>>>()?;
    Ok(FiniteRegionProfile2::new(material, holes))
}

fn project_contour_slice(
    contours: &[&Contour2],
    options: &FiniteProjectionOptions,
) -> CurveResult<Vec<FinitePolyline2>> {
    contours
        .iter()
        .map(|contour| contour.project_to_finite_ring(options))
        .collect()
}

fn project_curve_string(
    curve: &CurveString2,
    options: &FiniteProjectionOptions,
    close: bool,
) -> CurveResult<FinitePolyline2> {
    let first = curve.start().ok_or(CurveError::EmptyCurveString)?;
    let mut points = Vec::with_capacity(curve.len() + 1);
    push_if_new(&mut points, finite_point(first)?);

    for segment in curve.segments() {
        match segment {
            Segment2::Line(line) => {
                push_if_new(&mut points, finite_point(line.end())?);
            }
            Segment2::Arc(arc) => {
                append_arc_samples(&mut points, arc, options.arc_chord_error)?;
            }
        }
    }

    if close {
        close_ring(&mut points);
    }

    Ok(FinitePolyline2::new(points, options.arc_chord_error, close))
}

fn finite_point(point: &Point2) -> CurveResult<[f64; 2]> {
    let x = point
        .x()
        .to_f64_lossy()
        .filter(|value| value.is_finite())
        .ok_or(CurveError::NonFiniteProjectionPoint)?;
    let y = point
        .y()
        .to_f64_lossy()
        .filter(|value| value.is_finite())
        .ok_or(CurveError::NonFiniteProjectionPoint)?;
    Ok([x, y])
}

fn append_arc_samples(
    points: &mut Vec<[f64; 2]>,
    arc: &CircularArc2,
    chord_error: f64,
) -> CurveResult<usize> {
    let start = finite_point(arc.start())?;
    let end = finite_point(arc.end())?;
    let center = finite_point(arc.center())?;

    let radius = ((start[0] - center[0]).powi(2) + (start[1] - center[1]).powi(2)).sqrt();
    if !radius.is_finite() || radius <= f64::EPSILON {
        return Err(CurveError::NonFiniteProjectionPoint);
    }

    let a0 = (start[1] - center[1]).atan2(start[0] - center[0]);
    let a1 = (end[1] - center[1]).atan2(end[0] - center[0]);
    let mut sweep = a1 - a0;
    if arc.is_clockwise() {
        if sweep > 0.0 {
            sweep -= 2.0 * PI;
        }
    } else if sweep < 0.0 {
        sweep += 2.0 * PI;
    }

    let max_angle = (1.0 - (chord_error / radius).min(1.0)).acos().max(1e-3) * 2.0;
    let steps = ((sweep.abs() / max_angle).ceil() as usize).max(1);
    let before = points.len();
    for step in 1..=steps {
        let t = step as f64 / steps as f64;
        let angle = a0 + sweep * t;
        let point = [
            center[0] + radius * angle.cos(),
            center[1] + radius * angle.sin(),
        ];
        if !point[0].is_finite() || !point[1].is_finite() {
            return Err(CurveError::NonFiniteProjectionPoint);
        }
        push_if_new(points, point);
    }
    Ok(points.len() - before)
}

fn close_ring(points: &mut Vec<[f64; 2]>) {
    if points.len() >= 2 && points.first() != points.last() {
        points.push(points[0]);
    }
}

fn push_if_new(points: &mut Vec<[f64; 2]>, point: [f64; 2]) {
    if points.last().is_none_or(|last| *last != point) {
        points.push(point);
    }
}