1use crate::{
8 axis::active_spans,
9 transform::{insert_knot2, insert_knot3, split2, split3},
10};
11use axiolid_contracts::{GeomError, GeomResult};
12use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec2, Vec3};
13use axiolid_curve::{BSplineCurve2, BSplineCurve3};
14use axiolid_evaluate::curve::{bspline_jet2, bspline_jet3, CurveJet};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17#[non_exhaustive]
18pub enum SeamContinuity {
20 Discontinuous,
22 Position,
24 FirstDerivative,
26 SecondDerivative,
28}
29
30#[derive(Debug, Clone, Copy)]
36pub struct PeriodicCurve2<'a> {
37 curve: &'a BSplineCurve2,
38 tolerance: Tolerance,
39 domain: (Scalar, Scalar),
40 continuity: SeamContinuity,
41}
42
43impl<'a> PeriodicCurve2<'a> {
44 pub fn try_new(curve: &'a BSplineCurve2, tolerance: Tolerance) -> GeomResult<Self> {
46 let domain = domain(
47 curve.degree,
48 curve.control_points.len(),
49 &curve.knots,
50 &curve.multiplicities,
51 )?;
52 let continuity = curve2_seam_continuity(curve, tolerance)?;
53 require_verified_seam(curve.closed, continuity)?;
54 Ok(Self {
55 curve,
56 tolerance,
57 domain,
58 continuity,
59 })
60 }
61
62 pub const fn domain(self) -> (Scalar, Scalar) {
64 self.domain
65 }
66
67 pub const fn seam_continuity(self) -> SeamContinuity {
69 self.continuity
70 }
71
72 pub fn wrap_parameter(self, parameter: Scalar) -> GeomResult<Scalar> {
77 wrap(parameter, self.domain)
78 }
79
80 pub fn evaluate(self, parameter: Scalar) -> GeomResult<Point2> {
82 Ok(self.jet(parameter)?.point)
83 }
84
85 pub fn jet(self, parameter: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
87 bspline_jet2(self.curve, self.wrap_parameter(parameter)?)
88 }
89
90 pub fn insert_knot(self, parameter: Scalar) -> GeomResult<BSplineCurve2> {
96 let native = interior_periodic_parameter(parameter, self.domain)?;
97 let edited = insert_knot2(self.curve, native)?;
98 PeriodicCurve2::try_new(&edited, self.tolerance)?;
99 Ok(edited)
100 }
101
102 pub fn split_at(self, parameter: Scalar) -> GeomResult<(BSplineCurve2, BSplineCurve2)> {
106 split2(
107 self.curve,
108 interior_periodic_parameter(parameter, self.domain)?,
109 )
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
118pub struct PeriodicCurve3<'a> {
119 curve: &'a BSplineCurve3,
120 tolerance: Tolerance,
121 domain: (Scalar, Scalar),
122 continuity: SeamContinuity,
123}
124
125impl<'a> PeriodicCurve3<'a> {
126 pub fn try_new(curve: &'a BSplineCurve3, tolerance: Tolerance) -> GeomResult<Self> {
128 let domain = domain(
129 curve.degree,
130 curve.control_points.len(),
131 &curve.knots,
132 &curve.multiplicities,
133 )?;
134 let continuity = curve3_seam_continuity(curve, tolerance)?;
135 require_verified_seam(curve.closed, continuity)?;
136 Ok(Self {
137 curve,
138 tolerance,
139 domain,
140 continuity,
141 })
142 }
143
144 pub const fn domain(self) -> (Scalar, Scalar) {
146 self.domain
147 }
148
149 pub const fn seam_continuity(self) -> SeamContinuity {
151 self.continuity
152 }
153
154 pub fn wrap_parameter(self, parameter: Scalar) -> GeomResult<Scalar> {
156 wrap(parameter, self.domain)
157 }
158
159 pub fn evaluate(self, parameter: Scalar) -> GeomResult<Point3> {
161 Ok(self.jet(parameter)?.point)
162 }
163
164 pub fn jet(self, parameter: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
166 bspline_jet3(self.curve, self.wrap_parameter(parameter)?)
167 }
168
169 pub fn insert_knot(self, parameter: Scalar) -> GeomResult<BSplineCurve3> {
171 let native = interior_periodic_parameter(parameter, self.domain)?;
172 let edited = insert_knot3(self.curve, native)?;
173 PeriodicCurve3::try_new(&edited, self.tolerance)?;
174 Ok(edited)
175 }
176
177 pub fn split_at(self, parameter: Scalar) -> GeomResult<(BSplineCurve3, BSplineCurve3)> {
179 split3(
180 self.curve,
181 interior_periodic_parameter(parameter, self.domain)?,
182 )
183 }
184}
185
186fn require_verified_seam(closed: bool, continuity: SeamContinuity) -> GeomResult<()> {
187 if !closed || continuity < SeamContinuity::Position {
188 return Err(GeomError::InvalidInput(
189 "curve is not a verified closed seam".to_owned(),
190 ));
191 }
192 Ok(())
193}
194
195fn interior_periodic_parameter(parameter: Scalar, domain: (Scalar, Scalar)) -> GeomResult<Scalar> {
196 let native = wrap(parameter, domain)?;
197 if native <= domain.0 || native >= domain.1 {
198 return Err(GeomError::InvalidInput(
199 "periodic edit parameter must not be seam-equivalent".to_owned(),
200 ));
201 }
202 Ok(native)
203}
204
205pub fn curve2_seam_continuity(
207 curve: &BSplineCurve2,
208 tolerance: Tolerance,
209) -> GeomResult<SeamContinuity> {
210 let (lo, hi) = domain(
211 curve.degree,
212 curve.control_points.len(),
213 &curve.knots,
214 &curve.multiplicities,
215 )?;
216 let a = bspline_jet2(curve, lo)?;
217 let b = bspline_jet2(curve, hi)?;
218 Ok(classify(a, b, tolerance))
219}
220
221pub fn curve3_seam_continuity(
223 curve: &BSplineCurve3,
224 tolerance: Tolerance,
225) -> GeomResult<SeamContinuity> {
226 let (lo, hi) = domain(
227 curve.degree,
228 curve.control_points.len(),
229 &curve.knots,
230 &curve.multiplicities,
231 )?;
232 let a = bspline_jet3(curve, lo)?;
233 let b = bspline_jet3(curve, hi)?;
234 Ok(classify(a, b, tolerance))
235}
236
237pub fn wrap_curve2_parameter(
243 curve: &BSplineCurve2,
244 parameter: Scalar,
245 tolerance: Tolerance,
246) -> GeomResult<Scalar> {
247 if !curve.closed || curve2_seam_continuity(curve, tolerance)? < SeamContinuity::Position {
248 return Err(GeomError::InvalidInput(
249 "curve is not a verified closed seam".to_owned(),
250 ));
251 }
252 wrap(
253 parameter,
254 domain(
255 curve.degree,
256 curve.control_points.len(),
257 &curve.knots,
258 &curve.multiplicities,
259 )?,
260 )
261}
262
263pub fn wrap_curve3_parameter(
267 curve: &BSplineCurve3,
268 parameter: Scalar,
269 tolerance: Tolerance,
270) -> GeomResult<Scalar> {
271 if !curve.closed || curve3_seam_continuity(curve, tolerance)? < SeamContinuity::Position {
272 return Err(GeomError::InvalidInput(
273 "curve is not a verified closed seam".to_owned(),
274 ));
275 }
276 wrap(
277 parameter,
278 domain(
279 curve.degree,
280 curve.control_points.len(),
281 &curve.knots,
282 &curve.multiplicities,
283 )?,
284 )
285}
286
287fn classify<P, D>(a: CurveJet<P, D>, b: CurveJet<P, D>, tolerance: Tolerance) -> SeamContinuity
288where
289 P: Copy + core::ops::Sub<P, Output = D>,
290 D: Copy + core::ops::Sub<D, Output = D> + Length,
291{
292 if (a.point - b.point).length() > tolerance.linear() {
293 return SeamContinuity::Discontinuous;
294 }
295 if !close_vector(a.first, b.first, tolerance) {
296 return SeamContinuity::Position;
297 }
298 if !close_vector(a.second, b.second, tolerance) {
299 return SeamContinuity::FirstDerivative;
300 }
301 SeamContinuity::SecondDerivative
302}
303
304fn close_vector<D: Copy + core::ops::Sub<D, Output = D> + Length>(
305 a: D,
306 b: D,
307 tolerance: Tolerance,
308) -> bool {
309 let scale = a.length().max(b.length()).max(1.0);
310 (a - b).length() <= tolerance.linear() + tolerance.angular() * scale
311}
312
313trait Length {
314 fn length(self) -> Scalar;
315}
316impl Length for axiolid_core::Vec2 {
317 fn length(self) -> Scalar {
318 self.length()
319 }
320}
321impl Length for axiolid_core::Vec3 {
322 fn length(self) -> Scalar {
323 self.length()
324 }
325}
326
327fn domain(
328 degree: u16,
329 count: usize,
330 knots: &[Scalar],
331 multiplicities: &[u32],
332) -> GeomResult<(Scalar, Scalar)> {
333 let spans = active_spans(knots, multiplicities, degree, count)?;
334 Ok((spans[0].0, spans[spans.len() - 1].1))
335}
336
337fn wrap(parameter: Scalar, (lo, hi): (Scalar, Scalar)) -> GeomResult<Scalar> {
338 if !parameter.is_finite() {
339 return Err(GeomError::InvalidInput(
340 "parameter must be finite".to_owned(),
341 ));
342 }
343 let period = hi - lo;
344 if !period.is_finite() || period <= 0.0 {
345 return Err(GeomError::InvalidInput(
346 "periodic domain must have finite positive length".to_owned(),
347 ));
348 }
349 if parameter >= lo && parameter <= hi {
350 return Ok(parameter);
351 }
352 let offset = parameter - lo;
353 if !offset.is_finite() {
354 return Err(GeomError::InvalidInput(
355 "parameter offset exceeds finite periodic arithmetic".to_owned(),
356 ));
357 }
358 let wrapped = lo + offset.rem_euclid(period);
359 if !wrapped.is_finite() || wrapped < lo || wrapped >= hi {
360 return Err(GeomError::InvalidInput(
361 "parameter could not be wrapped into the periodic domain".to_owned(),
362 ));
363 }
364 Ok(wrapped)
365}