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
//! Retained curve identity, domain, trim, and cache evidence.
//!
//! CAD curve imports need a way to carry source identity and parameter-domain
//! facts before a topology kernel is allowed to consume the curve.  The types
//! in this module are intentionally small evidence records: they do not sample
//! curves, and they do not imply native topology.  That follows Yap's exact
//! geometric computation model, where exact objects and predicates remain
//! replayable until a certified operation consumes them; see Yap, "Towards
//! Exact Geometric Computation," *Computational Geometry* 7(1-2), 3-23 (1997).

use hyperreal::Real;

use crate::classify::compare_reals;
use crate::{
    Classification, CurveError, CurvePolicy, CurveResult, Point2, RetainedTopologyStatus,
    UncertaintyReason,
};

/// Curve family carried by retained curve metadata.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetainedCurveFamily2 {
    /// Polynomial B-spline carrier.
    PolynomialBSpline,
    /// Degree-two rational B-spline/NURBS carrier.
    RationalQuadraticBSpline,
    /// Degree-two-or-higher rational B-spline/NURBS carrier.
    RationalBSpline,
}

/// Stable source identity for a retained curve.
///
/// `source_index` is deliberately opaque to `hypercurve`: an importer can map
/// it to a STEP entity id, DXF handle table, or application-local source row
/// without changing the exact curve evidence.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetainedCurveIdentity2 {
    family: RetainedCurveFamily2,
    source_index: u64,
}

/// Exact one-dimensional parameter domain.
#[derive(Clone, Debug, PartialEq)]
pub struct RetainedParameterDomain1 {
    start: Real,
    end: Real,
}

/// Direction of a trim interval relative to its parameter domain.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetainedTrimDirection {
    /// Trim runs from low parameter to high parameter.
    Forward,
    /// Trim runs from high parameter to low parameter.
    Reversed,
}

/// Exact trim interval on a retained curve domain.
#[derive(Clone, Debug, PartialEq)]
pub struct RetainedTrimInterval1 {
    start: Real,
    end: Real,
    direction: RetainedTrimDirection,
}

/// Periodicity evidence for a retained curve.
#[derive(Clone, Debug, PartialEq)]
pub enum RetainedCurvePeriodicity1 {
    /// The carrier is non-periodic in the retained parameter domain.
    NonPeriodic,
    /// The carrier has an exact positive parameter period.
    Periodic { period: Box<Real> },
}

/// Exact endpoint evidence at the active retained domain boundaries.
#[derive(Clone, Debug, PartialEq)]
pub struct RetainedEndpointEvidence2 {
    start_parameter: Real,
    end_parameter: Real,
    start_point: Point2,
    end_point: Point2,
}

/// Prepared-cache shape summary for a retained curve.
///
/// This summary is not topology by itself. It is an audit trail for how much
/// exact construction evidence is available locally: controls, knots, Bezier
/// spans, and how many spans are native versus retained evidence.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RetainedCurveCacheSummary2 {
    control_count: usize,
    knot_count: usize,
    span_count: usize,
    native_span_count: usize,
    retained_span_count: usize,
}

/// Retained curve profile combining identity, domain, trim, endpoints, and cache facts.
#[derive(Clone, Debug, PartialEq)]
pub struct RetainedCurveProfile2 {
    identity: RetainedCurveIdentity2,
    domain: RetainedParameterDomain1,
    trim: RetainedTrimInterval1,
    periodicity: RetainedCurvePeriodicity1,
    topology_status: RetainedTopologyStatus,
    endpoints: RetainedEndpointEvidence2,
    cache_summary: RetainedCurveCacheSummary2,
}

impl RetainedCurveIdentity2 {
    /// Constructs a retained curve identity.
    pub const fn new(family: RetainedCurveFamily2, source_index: u64) -> Self {
        Self {
            family,
            source_index,
        }
    }

    /// Returns the retained curve family.
    pub const fn family(self) -> RetainedCurveFamily2 {
        self.family
    }

    /// Returns the opaque source index.
    pub const fn source_index(self) -> u64 {
        self.source_index
    }
}

impl RetainedParameterDomain1 {
    /// Constructs an exact ordered parameter domain.
    pub fn try_new(
        start: Real,
        end: Real,
        policy: &CurvePolicy,
    ) -> CurveResult<Classification<Self>> {
        match compare_reals(&start, &end, policy) {
            Some(std::cmp::Ordering::Less) => Ok(Classification::Decided(Self { start, end })),
            Some(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {
                Err(CurveError::InvalidBezierRange)
            }
            None => Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
        }
    }

    /// Returns the low end of the domain.
    pub const fn start(&self) -> &Real {
        &self.start
    }

    /// Returns the high end of the domain.
    pub const fn end(&self) -> &Real {
        &self.end
    }

    /// Certifies whether a parameter lies inside the closed domain.
    pub fn contains(&self, parameter: &Real, policy: &CurvePolicy) -> Classification<bool> {
        let lower = compare_reals(&self.start, parameter, policy);
        let upper = compare_reals(parameter, &self.end, policy);
        match (lower, upper) {
            (
                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal),
                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal),
            ) => Classification::Decided(true),
            (Some(_), Some(_)) => Classification::Decided(false),
            _ => Classification::Uncertain(UncertaintyReason::Ordering),
        }
    }
}

impl RetainedTrimInterval1 {
    /// Constructs a nondegenerate trim interval whose endpoints lie in `domain`.
    pub fn try_new(
        start: Real,
        end: Real,
        domain: &RetainedParameterDomain1,
        policy: &CurvePolicy,
    ) -> CurveResult<Classification<Self>> {
        for parameter in [&start, &end] {
            match domain.contains(parameter, policy) {
                Classification::Decided(true) => {}
                Classification::Decided(false) => return Err(CurveError::InvalidBezierRange),
                Classification::Uncertain(reason) => {
                    return Ok(Classification::Uncertain(reason));
                }
            }
        }
        let direction = match compare_reals(&start, &end, policy) {
            Some(std::cmp::Ordering::Less) => RetainedTrimDirection::Forward,
            Some(std::cmp::Ordering::Greater) => RetainedTrimDirection::Reversed,
            Some(std::cmp::Ordering::Equal) => return Err(CurveError::InvalidBezierRange),
            None => return Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
        };
        Ok(Classification::Decided(Self {
            start,
            end,
            direction,
        }))
    }

    /// Returns the authored trim start.
    pub const fn start(&self) -> &Real {
        &self.start
    }

    /// Returns the authored trim end.
    pub const fn end(&self) -> &Real {
        &self.end
    }

    /// Returns the exact trim direction.
    pub const fn direction(&self) -> RetainedTrimDirection {
        self.direction
    }
}

impl RetainedCurvePeriodicity1 {
    /// Constructs exact positive-period evidence.
    pub fn periodic(period: Real, policy: &CurvePolicy) -> CurveResult<Classification<Self>> {
        match compare_reals(&Real::zero(), &period, policy) {
            Some(std::cmp::Ordering::Less) => Ok(Classification::Decided(Self::Periodic {
                period: Box::new(period),
            })),
            Some(_) => Err(CurveError::InvalidBezierRange),
            None => Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
        }
    }
}

impl RetainedEndpointEvidence2 {
    /// Constructs exact endpoint evidence for a certified retained domain.
    pub fn new(domain: &RetainedParameterDomain1, start_point: Point2, end_point: Point2) -> Self {
        Self {
            start_parameter: domain.start().clone(),
            end_parameter: domain.end().clone(),
            start_point,
            end_point,
        }
    }

    /// Returns the start-domain parameter.
    pub const fn start_parameter(&self) -> &Real {
        &self.start_parameter
    }

    /// Returns the end-domain parameter.
    pub const fn end_parameter(&self) -> &Real {
        &self.end_parameter
    }

    /// Returns the retained start point.
    pub const fn start_point(&self) -> &Point2 {
        &self.start_point
    }

    /// Returns the retained end point.
    pub const fn end_point(&self) -> &Point2 {
        &self.end_point
    }
}

impl RetainedCurveCacheSummary2 {
    /// Constructs a retained cache summary.
    pub fn new(
        control_count: usize,
        knot_count: usize,
        span_count: usize,
        native_span_count: usize,
        retained_span_count: usize,
    ) -> CurveResult<Self> {
        validate_cache_summary_counts(
            control_count,
            knot_count,
            span_count,
            native_span_count,
            retained_span_count,
        )?;
        Ok(Self {
            control_count,
            knot_count,
            span_count,
            native_span_count,
            retained_span_count,
        })
    }

    /// Returns the number of retained controls.
    pub const fn control_count(&self) -> usize {
        self.control_count
    }

    /// Returns the number of retained knots.
    pub const fn knot_count(&self) -> usize {
        self.knot_count
    }

    /// Returns the number of extracted Bezier spans.
    pub const fn span_count(&self) -> usize {
        self.span_count
    }

    /// Returns the number of spans with exact native topology.
    pub const fn native_span_count(&self) -> usize {
        self.native_span_count
    }

    /// Returns the number of spans retained without native topology.
    pub const fn retained_span_count(&self) -> usize {
        self.retained_span_count
    }
}

impl RetainedCurveProfile2 {
    /// Constructs a retained curve profile.
    pub fn new(
        identity: RetainedCurveIdentity2,
        domain: RetainedParameterDomain1,
        trim: RetainedTrimInterval1,
        periodicity: RetainedCurvePeriodicity1,
        topology_status: RetainedTopologyStatus,
        endpoints: RetainedEndpointEvidence2,
        cache_summary: RetainedCurveCacheSummary2,
    ) -> CurveResult<Self> {
        validate_curve_profile_evidence(
            identity,
            &domain,
            &trim,
            topology_status,
            &endpoints,
            &cache_summary,
        )?;
        Ok(Self {
            identity,
            domain,
            trim,
            periodicity,
            topology_status,
            endpoints,
            cache_summary,
        })
    }

    /// Returns retained source identity.
    pub const fn identity(&self) -> RetainedCurveIdentity2 {
        self.identity
    }

    /// Returns the active parameter domain.
    pub const fn domain(&self) -> &RetainedParameterDomain1 {
        &self.domain
    }

    /// Returns the active trim interval.
    pub const fn trim(&self) -> &RetainedTrimInterval1 {
        &self.trim
    }

    /// Returns periodicity evidence.
    pub const fn periodicity(&self) -> &RetainedCurvePeriodicity1 {
        &self.periodicity
    }

    /// Returns the topology-readiness status for the whole retained curve.
    pub const fn topology_status(&self) -> RetainedTopologyStatus {
        self.topology_status
    }

    /// Returns endpoint evidence at the active domain boundaries.
    pub const fn endpoints(&self) -> &RetainedEndpointEvidence2 {
        &self.endpoints
    }

    /// Returns prepared-cache shape evidence.
    pub const fn cache_summary(&self) -> &RetainedCurveCacheSummary2 {
        &self.cache_summary
    }
}

fn validate_cache_summary_counts(
    control_count: usize,
    knot_count: usize,
    span_count: usize,
    native_span_count: usize,
    retained_span_count: usize,
) -> CurveResult<()> {
    if control_count == 0 || knot_count == 0 || span_count == 0 {
        return Err(CurveError::Topology(
            "retained curve cache summary must carry nonempty controls, knots, and spans".into(),
        ));
    }
    if knot_count <= control_count {
        return Err(CurveError::Topology(
            "retained B-spline cache summary must carry more knots than controls".into(),
        ));
    }
    if span_count
        .checked_add(2)
        .is_none_or(|minimum_control_count| control_count < minimum_control_count)
    {
        return Err(CurveError::Topology(
            "retained B-spline cache summary must carry at least two more controls than spans"
                .into(),
        ));
    }
    let Some(order) = knot_count.checked_sub(control_count) else {
        return Err(CurveError::Topology(
            "retained B-spline cache summary knot/control counts are inconsistent".into(),
        ));
    };
    if order < 3 || control_count < order {
        return Err(CurveError::Topology(
            "retained B-spline cache summary must carry a supported degree shape".into(),
        ));
    }
    let degree = order - 1;
    if span_count > control_count - degree {
        return Err(CurveError::Topology(
            "retained B-spline cache summary span count exceeds the degree-implied maximum".into(),
        ));
    }
    if native_span_count
        .checked_add(retained_span_count)
        .is_none_or(|count| count != span_count)
    {
        return Err(CurveError::Topology(
            "retained curve cache summary span decomposition does not match span count".into(),
        ));
    }
    Ok(())
}

fn validate_curve_profile_evidence(
    identity: RetainedCurveIdentity2,
    domain: &RetainedParameterDomain1,
    trim: &RetainedTrimInterval1,
    topology_status: RetainedTopologyStatus,
    endpoints: &RetainedEndpointEvidence2,
    cache_summary: &RetainedCurveCacheSummary2,
) -> CurveResult<()> {
    validate_profile_family_shape(identity, cache_summary)?;
    let policy = CurvePolicy::certified();
    for parameter in [trim.start(), trim.end()] {
        if domain.contains(parameter, &policy) != Classification::Decided(true) {
            return Err(CurveError::Topology(
                "retained curve trim evidence must lie inside the active parameter domain".into(),
            ));
        }
    }
    if endpoints.start_parameter() != domain.start() || endpoints.end_parameter() != domain.end() {
        return Err(CurveError::Topology(
            "retained curve endpoint evidence must match the active parameter domain".into(),
        ));
    }
    if topology_status.is_native_exact() && cache_summary.retained_span_count() != 0 {
        return Err(CurveError::Topology(
            "native retained curve profile must not report retained unsupported spans".into(),
        ));
    }
    if !topology_status.is_native_exact() && cache_summary.retained_span_count() == 0 {
        return Err(CurveError::Topology(
            "non-native retained curve profile must report retained evidence spans".into(),
        ));
    }
    if !topology_status.is_native_exact() && !topology_status.is_retained_evidence() {
        return Err(CurveError::Topology(
            "retained curve profile must carry exact native or retained evidence status".into(),
        ));
    }
    Ok(())
}

fn validate_profile_family_shape(
    identity: RetainedCurveIdentity2,
    cache_summary: &RetainedCurveCacheSummary2,
) -> CurveResult<()> {
    let degree = cache_summary
        .knot_count()
        .checked_sub(cache_summary.control_count())
        .and_then(|order| order.checked_sub(1))
        .ok_or_else(|| {
            CurveError::Topology(
                "retained curve profile cache summary has no certified B-spline degree".into(),
            )
        })?;
    match identity.family() {
        RetainedCurveFamily2::PolynomialBSpline if !(2..=3).contains(&degree) => {
            Err(CurveError::Topology(
                "polynomial B-spline profile must carry quadratic or cubic cache evidence".into(),
            ))
        }
        RetainedCurveFamily2::RationalQuadraticBSpline if degree != 2 => Err(CurveError::Topology(
            "rational quadratic B-spline profile must carry quadratic cache evidence".into(),
        )),
        _ => Ok(()),
    }
}