1use crate::curve::parameter_line;
40use crate::topology::{
41 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
42};
43use crate::{make_line, NurbsSurface, Vec3, Vec4};
44use serde_json::Value;
45
46pub const CAGE_DEGREE: usize = 3;
49pub const MIN_SEGMENTS: usize = 4;
51pub const MIN_RINGS: usize = 2;
53pub const MAX_SEGMENTS: usize = 64;
55pub const MAX_RINGS: usize = 64;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum CagePrimitive {
60 Sphere,
62 Cylinder,
65 Box,
67}
68
69impl CagePrimitive {
70 pub const OPTIONS: [&'static str; 3] = ["BOX", "CYLINDER", "SPHERE"];
72
73 pub fn parse(name: &str) -> Option<Self> {
74 match name.trim().to_ascii_uppercase().as_str() {
75 "SPHERE" | "ELLIPSOID" => Some(Self::Sphere),
76 "CYLINDER" => Some(Self::Cylinder),
77 "BOX" | "CUBE" => Some(Self::Box),
78 _ => None,
79 }
80 }
81
82 pub fn name(self) -> &'static str {
83 match self {
84 Self::Sphere => "SPHERE",
85 Self::Cylinder => "CYLINDER",
86 Self::Box => "BOX",
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum CagePoint {
94 Pole(usize),
96 Ring { ring: usize, column: usize },
98}
99
100#[derive(Clone, Debug)]
102pub struct ControlCage {
103 pub knots_u: Vec<f64>,
105 pub knots_v: Vec<f64>,
107 pub poles: [Vec3; 2],
109 pub rings: Vec<Vec<Vec3>>,
111}
112
113const KNOT_TOL: f64 = 1e-9;
115
116impl ControlCage {
117 pub fn primitive(
124 kind: CagePrimitive,
125 size: [f64; 3],
126 segments: usize,
127 rings: usize,
128 ) -> Result<Self, String> {
129 if !(MIN_SEGMENTS..=MAX_SEGMENTS).contains(&segments) {
130 return Err(format!(
131 "control cage: segments must be between {MIN_SEGMENTS} and {MAX_SEGMENTS}, got {segments}"
132 ));
133 }
134 if !(MIN_RINGS..=MAX_RINGS).contains(&rings) {
135 return Err(format!(
136 "control cage: rings must be between {MIN_RINGS} and {MAX_RINGS}, got {rings}"
137 ));
138 }
139 let half = [size[0].abs() * 0.5, size[1].abs() * 0.5, size[2].abs() * 0.5];
140 if half.iter().any(|h| !h.is_finite() || *h <= 1e-9) {
141 return Err(format!("control cage: every size must be positive, got {size:?}"));
142 }
143 let n = segments;
144 let m = rings;
145 let mut out = Vec::with_capacity(m);
146 for r in 0..m {
147 let fraction = if m == 1 { 0.5 } else { r as f64 / (m - 1) as f64 };
151 let ring: Vec<Vec3> = (0..n)
152 .map(|c| {
153 let phi = std::f64::consts::TAU * c as f64 / n as f64;
154 let height = -half[1] + 2.0 * half[1] * fraction;
155 match kind {
156 CagePrimitive::Sphere => {
157 let theta = std::f64::consts::PI * (r + 1) as f64 / (m + 1) as f64;
158 Vec3::new(
159 half[0] * theta.sin() * phi.cos(),
160 -half[1] * theta.cos(),
161 -half[2] * theta.sin() * phi.sin(),
162 )
163 }
164 CagePrimitive::Cylinder => {
165 Vec3::new(half[0] * phi.cos(), height, -half[2] * phi.sin())
166 }
167 CagePrimitive::Box => {
168 let angle = phi + std::f64::consts::FRAC_PI_4;
172 let (s, c) = angle.sin_cos();
173 let scale = 1.0 / c.abs().max(s.abs());
174 Vec3::new(half[0] * c * scale, height, -half[2] * s * scale)
175 }
176 }
177 })
178 .collect();
179 out.push(ring);
180 }
181 let poles = [Vec3::new(0.0, -half[1], 0.0), Vec3::new(0.0, half[1], 0.0)];
182 let knots_u = (0..=n).map(|i| i as f64 / n as f64).collect();
183 let knots_v = crate::uniform_clamped_knots(m + 2, CAGE_DEGREE)?;
184 let cage = Self {
185 knots_u,
186 knots_v,
187 poles,
188 rings: out,
189 };
190 cage.validate()?;
191 Ok(cage)
192 }
193
194 pub fn validate(&self) -> Result<(), String> {
197 let n = self.segments();
198 let m = self.rings.len();
199 if n < MIN_SEGMENTS {
200 return Err(format!("control cage: a ring needs at least {MIN_SEGMENTS} points, got {n}"));
201 }
202 if m < MIN_RINGS {
203 return Err(format!("control cage: needs at least {MIN_RINGS} rings, got {m}"));
204 }
205 if self.rings.iter().any(|ring| ring.len() != n) {
206 return Err("control cage: every ring must have the same number of points".into());
207 }
208 if self.knots_u.len() != n + 1 {
209 return Err(format!(
210 "control cage: expected {} periodic knots for {n} segments, got {}",
211 n + 1,
212 self.knots_u.len()
213 ));
214 }
215 if self.knots_v.len() != m + 2 + CAGE_DEGREE + 1 {
216 return Err(format!(
217 "control cage: expected {} clamped knots for {m} rings, got {}",
218 m + 2 + CAGE_DEGREE + 1,
219 self.knots_v.len()
220 ));
221 }
222 let finite = |knots: &[f64]| knots.iter().all(|k| k.is_finite());
223 if !finite(&self.knots_u) || !finite(&self.knots_v) {
224 return Err("control cage: knots must be finite".into());
225 }
226 if self.knots_u[0].abs() > KNOT_TOL || self.period() <= KNOT_TOL {
227 return Err("control cage: periodic knots must start at 0 and span a positive period".into());
228 }
229 if self.knots_u.windows(2).any(|w| w[1] <= w[0] + KNOT_TOL * self.period()) {
230 return Err("control cage: periodic knots must be strictly increasing".into());
231 }
232 if self.knots_v.windows(2).any(|w| w[1] < w[0] - KNOT_TOL) {
233 return Err("control cage: ring knots must be non-decreasing".into());
234 }
235 let [v0, v1] = self.domain_v();
236 if v1 - v0 <= KNOT_TOL {
237 return Err("control cage: the ring knots span no interval".into());
238 }
239 for index in 0..=CAGE_DEGREE {
240 let last = self.knots_v.len() - 1 - index;
241 if (self.knots_v[index] - self.knots_v[0]).abs() > KNOT_TOL
242 || (self.knots_v[last] - self.knots_v[self.knots_v.len() - 1]).abs() > KNOT_TOL
243 {
244 return Err("control cage: ring knots must be clamped".into());
245 }
246 }
247 let mut run = 1;
249 for w in self.knots_v[CAGE_DEGREE + 1..self.knots_v.len() - CAGE_DEGREE - 1].windows(2) {
250 run = if (w[1] - w[0]).abs() <= KNOT_TOL { run + 1 } else { 1 };
251 if run > CAGE_DEGREE {
252 return Err("control cage: an interior ring knot repeats more than the degree".into());
253 }
254 }
255 let finite_point = |p: &Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
256 if !self.poles.iter().all(finite_point) || !self.rings.iter().flatten().all(finite_point) {
257 return Err("control cage: every point must be finite".into());
258 }
259 Ok(())
260 }
261
262 pub fn segments(&self) -> usize {
266 self.rings.first().map_or(0, Vec::len)
267 }
268
269 pub fn ring_count(&self) -> usize {
271 self.rings.len()
272 }
273
274 pub fn point_count(&self) -> usize {
276 self.rings.len() * self.segments() + 2
277 }
278
279 pub fn period(&self) -> f64 {
281 self.knots_u[self.knots_u.len() - 1]
282 }
283
284 pub fn domain_v(&self) -> [f64; 2] {
286 [self.knots_v[CAGE_DEGREE], self.knots_v[self.knots_v.len() - 1 - CAGE_DEGREE]]
287 }
288
289 pub fn locate(&self, index: usize) -> Option<CagePoint> {
291 let n = self.segments();
292 let m = self.rings.len();
293 if index == 0 {
294 return Some(CagePoint::Pole(0));
295 }
296 if index == 1 + m * n {
297 return Some(CagePoint::Pole(1));
298 }
299 if index > 1 + m * n {
300 return None;
301 }
302 let offset = index - 1;
303 Some(CagePoint::Ring {
304 ring: offset / n,
305 column: offset % n,
306 })
307 }
308
309 pub fn index_of(&self, ring: usize, column: usize) -> usize {
311 1 + ring * self.segments() + column
312 }
313
314 pub fn pole_index(&self, which: usize) -> usize {
316 if which == 0 {
317 0
318 } else {
319 1 + self.rings.len() * self.segments()
320 }
321 }
322
323 pub fn point(&self, index: usize) -> Option<Vec3> {
324 match self.locate(index)? {
325 CagePoint::Pole(which) => Some(self.poles[which]),
326 CagePoint::Ring { ring, column } => Some(self.rings[ring][column]),
327 }
328 }
329
330 pub fn set_point(&mut self, index: usize, point: Vec3) -> Result<(), String> {
331 match self
332 .locate(index)
333 .ok_or_else(|| format!("control cage: no point {index}"))?
334 {
335 CagePoint::Pole(which) => self.poles[which] = point,
336 CagePoint::Ring { ring, column } => self.rings[ring][column] = point,
337 }
338 Ok(())
339 }
340
341 pub fn points(&self) -> Vec<Vec3> {
343 let mut out = Vec::with_capacity(self.point_count());
344 out.push(self.poles[0]);
345 for ring in &self.rings {
346 out.extend_from_slice(ring);
347 }
348 out.push(self.poles[1]);
349 out
350 }
351
352 fn rows(&self) -> Vec<Vec<Vec3>> {
355 let n = self.segments();
356 let mut rows = Vec::with_capacity(self.rings.len() + 2);
357 rows.push(vec![self.poles[0]; n]);
358 rows.extend(self.rings.iter().cloned());
359 rows.push(vec![self.poles[1]; n]);
360 rows
361 }
362
363 fn knot_u(&self, i: isize) -> f64 {
365 let n = self.segments() as isize;
366 let q = i.div_euclid(n);
367 let r = i.rem_euclid(n) as usize;
368 self.knots_u[r] + q as f64 * self.period()
369 }
370
371 pub fn greville_u(&self, column: usize) -> f64 {
373 let c = column as isize;
374 (self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0
375 }
376
377 pub fn greville_v(&self, row: usize) -> f64 {
379 (self.knots_v[row + 1] + self.knots_v[row + 2] + self.knots_v[row + 3]) / 3.0
380 }
381
382 pub fn insert_ring_between(&mut self, row: usize) -> Result<usize, String> {
389 let m = self.rings.len();
390 if row > m {
391 return Err(format!("control cage: no row pair {row}/{} to insert between", row + 1));
392 }
393 let v = 0.5 * (self.greville_v(row) + self.greville_v(row + 1));
394 self.insert_ring_at(v)
395 }
396
397 pub fn insert_ring_at(&mut self, v: f64) -> Result<usize, String> {
399 let [v0, v1] = self.domain_v();
400 if !(v > v0 + KNOT_TOL && v < v1 - KNOT_TOL) {
401 return Err(format!("control cage: ring parameter {v} is outside ({v0}, {v1})"));
402 }
403 let n = self.segments();
404 let rows = self.rows();
405 let mut new_rows: Vec<Vec<Vec3>> = Vec::new();
406 let mut new_knots = self.knots_v.clone();
407 for column in 0..n {
408 let column_points: Vec<Vec3> = rows.iter().map(|row| row[column]).collect();
409 let (points, knots) = insert_knot_open(&column_points, &self.knots_v, CAGE_DEGREE, v)?;
410 if new_rows.is_empty() {
411 new_rows = vec![Vec::with_capacity(n); points.len()];
412 new_knots = knots;
413 }
414 for (row, point) in new_rows.iter_mut().zip(points) {
415 row.push(point);
416 }
417 }
418 let count = new_rows.len();
421 self.rings = new_rows[1..count - 1].to_vec();
422 self.knots_v = new_knots;
423 let nearest = (0..self.rings.len())
425 .min_by(|a, b| {
426 (self.greville_v(a + 1) - v)
427 .abs()
428 .total_cmp(&(self.greville_v(b + 1) - v).abs())
429 })
430 .unwrap_or(0);
431 self.validate()?;
432 Ok(nearest)
433 }
434
435 pub fn insert_column_between(&mut self, column: usize) -> Result<usize, String> {
439 let n = self.segments();
440 if column >= n {
441 return Err(format!("control cage: no column {column}"));
442 }
443 let c = column as isize;
444 let g0 = (self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0;
445 let g1 = (self.knot_u(c + 2) + self.knot_u(c + 3) + self.knot_u(c + 4)) / 3.0;
446 let u = 0.5 * (g0 + g1);
447 self.insert_column_at(u.rem_euclid(self.period()))
448 }
449
450 pub fn insert_column_at(&mut self, u: f64) -> Result<usize, String> {
452 let n = self.segments();
453 let period = self.period();
454 let mut u = u.rem_euclid(period);
455 if let Some(k) = self
458 .knots_u
459 .iter()
460 .position(|t| (t - u).abs() <= KNOT_TOL * period)
461 {
462 let next = self.knot_u(k as isize + 1);
463 u = 0.5 * (self.knots_u[k] + next);
464 if u >= period {
465 u -= period;
466 }
467 }
468 let k = (0..n)
470 .find(|&k| self.knots_u[k] <= u && u < self.knots_u[k + 1])
471 .ok_or_else(|| format!("control cage: column parameter {u} is outside the period"))?
472 as isize;
473 let new_len = n + 1;
474 let mut new_rings = Vec::with_capacity(self.rings.len());
475 for ring in &self.rings {
476 let p = |i: isize| ring[i.rem_euclid(n as isize) as usize];
477 let mut out = vec![Vec3::default(); new_len];
478 for j in (k - 2)..=(k - 2 + n as isize) {
479 let value = if j <= k {
480 let t_j = self.knot_u(j);
481 let alpha = (u - t_j) / (self.knot_u(j + 3) - t_j);
482 p(j).scale(alpha).add(p(j - 1).scale(1.0 - alpha))
483 } else {
484 p(j - 1)
485 };
486 out[j.rem_euclid(new_len as isize) as usize] = value;
487 }
488 new_rings.push(out);
489 }
490 let ku = k as usize;
491 let mut knots = Vec::with_capacity(n + 2);
492 knots.extend_from_slice(&self.knots_u[..=ku]);
493 knots.push(u);
494 knots.extend_from_slice(&self.knots_u[ku + 1..]);
495 self.rings = new_rings;
496 self.knots_u = knots;
497 self.validate()?;
498 let nearest = (0..new_len)
500 .min_by(|&a, &b| {
501 let da = cyclic_distance(self.greville_u(a), u, period);
502 let db = cyclic_distance(self.greville_u(b), u, period);
503 da.total_cmp(&db)
504 })
505 .unwrap_or(0);
506 Ok(nearest)
507 }
508
509 fn clamped_row(&self, row: &[Vec3]) -> Result<(Vec<Vec3>, Vec<f64>), String> {
515 let n = self.segments();
516 let p = CAGE_DEGREE;
517 let points: Vec<Vec3> = (0..n + p).map(|j| row[(j + n - p) % n]).collect();
519 let knots: Vec<f64> = (0..n + 2 * p + 1)
520 .map(|j| self.knot_u(j as isize - p as isize))
521 .collect();
522 let start = self.knot_u(0);
523 let end = self.knot_u(n as isize);
524 let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
525 let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
526 let (points, knots) = insert_knot_open(&points, &knots, p, end)?;
527 let (mut points, mut knots) = insert_knot_open(&points, &knots, p, end)?;
528 let drop = p - 1;
532 points.drain(..drop);
533 points.truncate(points.len() - drop);
534 knots.drain(..drop);
535 knots.truncate(knots.len() - drop);
536 knots[0] = start;
537 let last = knots.len() - 1;
538 knots[last] = end;
539 debug_assert_eq!(points.len(), n + p);
540 debug_assert_eq!(knots.len(), n + 2 * p + 1);
541 Ok((points, knots))
542 }
543
544 pub fn surface(&self) -> Result<NurbsSurface, String> {
547 self.validate()?;
548 let n = self.segments();
549 let rows = self.rows();
550 let mut knots_u: Option<Vec<f64>> = None;
551 let mut columns: Vec<Vec<Vec4>> = vec![Vec::with_capacity(rows.len()); n + CAGE_DEGREE];
552 for row in &rows {
553 let (points, knots) = self.clamped_row(row)?;
554 if knots_u.is_none() {
555 let period = self.period();
556 knots_u = Some(knots.iter().map(|k| (k / period).clamp(0.0, 1.0)).collect());
557 }
558 for (column, point) in columns.iter_mut().zip(points) {
559 column.push(Vec4::from_point(point, 1.0));
560 }
561 }
562 let [v0, v1] = self.domain_v();
563 let knots_v: Vec<f64> = self
564 .knots_v
565 .iter()
566 .map(|k| ((k - v0) / (v1 - v0)).clamp(0.0, 1.0))
567 .collect();
568 NurbsSurface::new(
569 CAGE_DEGREE,
570 CAGE_DEGREE,
571 knots_u.expect("at least four rows"),
572 knots_v,
573 columns,
574 )
575 }
576
577 pub fn solid(&self, face_name: Option<&str>) -> Result<BrepSolid, String> {
581 let surface = self.surface()?;
582 let south = surface.evaluate(0.0, 0.0)?;
583 let north = surface.evaluate(0.0, 1.0)?;
584 let seam = surface.iso_curve_u(0.0)?;
585 let solid = BrepSolid {
586 mass_properties_cache: Default::default(),
587 id: 120,
588 vertices: vec![
589 VertexRecord { id: 1, point: south },
590 VertexRecord { id: 2, point: north },
591 ],
592 edges: vec![
593 EdgeRecord {
594 id: 10,
595 curve: seam,
596 t0: 0.0,
597 t1: 1.0,
598 start_vertex_id: 1,
599 end_vertex_id: 2,
600 degenerate: false,
601 name: None,
602 },
603 EdgeRecord {
604 id: 11,
605 curve: make_line(south, south)?,
606 t0: 0.0,
607 t1: 1.0,
608 start_vertex_id: 1,
609 end_vertex_id: 1,
610 degenerate: true,
611 name: None,
612 },
613 EdgeRecord {
614 id: 12,
615 curve: make_line(north, north)?,
616 t0: 0.0,
617 t1: 1.0,
618 start_vertex_id: 2,
619 end_vertex_id: 2,
620 degenerate: true,
621 name: None,
622 },
623 ],
624 shells: vec![ShellRecord {
625 id: 119,
626 faces: vec![FaceRecord {
627 id: 118,
628 surface,
629 same_sense: true,
630 loops: vec![LoopRecord {
631 id: 117,
632 coedges: vec![
633 CoedgeRecord {
634 id: 101,
635 edge_id: 11,
636 forward: true,
637 pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
638 },
639 CoedgeRecord {
640 id: 102,
641 edge_id: 10,
642 forward: true,
643 pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
644 },
645 CoedgeRecord {
646 id: 103,
647 edge_id: 12,
648 forward: true,
649 pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
650 },
651 CoedgeRecord {
652 id: 104,
653 edge_id: 10,
654 forward: false,
655 pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
656 },
657 ],
658 }],
659 name: face_name.map(str::to_string),
660 }],
661 }],
662 genus: 0,
663 kind: crate::BodyKind::Solid,
664 };
665 let issues = solid.validate();
666 if !issues.is_empty() {
667 return Err(format!("control cage solid has invalid topology: {issues:?}"));
668 }
669 Ok(solid)
670 }
671
672 pub fn to_json(&self) -> Value {
677 let point = |p: &Vec3| serde_json::json!([p.x, p.y, p.z]);
678 serde_json::json!({
679 "knotsU": self.knots_u,
680 "knotsV": self.knots_v,
681 "poles": [point(&self.poles[0]), point(&self.poles[1])],
682 "rings": self.rings.iter().map(|ring| ring.iter().map(point).collect::<Vec<_>>()).collect::<Vec<_>>(),
683 })
684 }
685
686 pub fn from_json(value: &Value) -> Result<Self, String> {
688 let numbers = |key: &str| -> Result<Vec<f64>, String> {
689 value
690 .get(key)
691 .and_then(Value::as_array)
692 .ok_or_else(|| format!("control cage: `{key}` must be an array"))?
693 .iter()
694 .map(|n| n.as_f64().ok_or_else(|| format!("control cage: `{key}` holds a non-number")))
695 .collect()
696 };
697 let point = |p: &Value, what: &str| -> Result<Vec3, String> {
698 let xyz = p
699 .as_array()
700 .filter(|a| a.len() == 3)
701 .ok_or_else(|| format!("control cage: {what} must be [x, y, z]"))?;
702 let read = |i: usize| xyz[i].as_f64().ok_or_else(|| format!("control cage: {what} holds a non-number"));
703 Ok(Vec3::new(read(0)?, read(1)?, read(2)?))
704 };
705 let poles = value
706 .get("poles")
707 .and_then(Value::as_array)
708 .filter(|a| a.len() == 2)
709 .ok_or("control cage: `poles` must hold two points")?;
710 let rings = value
711 .get("rings")
712 .and_then(Value::as_array)
713 .ok_or("control cage: `rings` must be an array")?
714 .iter()
715 .map(|ring| {
716 ring.as_array()
717 .ok_or_else(|| "control cage: a ring must be an array".to_string())?
718 .iter()
719 .map(|p| point(p, "a ring point"))
720 .collect::<Result<Vec<_>, _>>()
721 })
722 .collect::<Result<Vec<_>, _>>()?;
723 let cage = Self {
724 knots_u: numbers("knotsU")?,
725 knots_v: numbers("knotsV")?,
726 poles: [point(&poles[0], "a pole")?, point(&poles[1], "a pole")?],
727 rings,
728 };
729 cage.validate()?;
730 Ok(cage)
731 }
732}
733
734fn insert_knot_open(
739 points: &[Vec3],
740 knots: &[f64],
741 degree: usize,
742 u: f64,
743) -> Result<(Vec<Vec3>, Vec<f64>), String> {
744 let n = points.len();
745 let p = degree;
746 if knots.len() != n + p + 1 {
747 return Err(format!(
748 "insert_knot: {} knots do not fit {n} points of degree {p}",
749 knots.len()
750 ));
751 }
752 let [start, end] = [knots[p], knots[n]];
753 if u < start - KNOT_TOL || u > end + KNOT_TOL {
754 return Err(format!("insert_knot: {u} is outside the domain [{start}, {end}]"));
755 }
756 let mut k = p;
758 for (i, pair) in knots.windows(2).enumerate().take(n).skip(p) {
759 if pair[0] <= u {
760 k = i;
761 }
762 }
763 let mut out = Vec::with_capacity(n + 1);
764 for i in 0..=n {
765 let value = if i + p <= k {
766 points[i]
767 } else if i <= k {
768 let denominator = knots[i + p] - knots[i];
769 let alpha = if denominator.abs() <= f64::EPSILON { 0.0 } else { (u - knots[i]) / denominator };
770 points[i].scale(alpha).add(points[i - 1].scale(1.0 - alpha))
771 } else {
772 points[i - 1]
773 };
774 out.push(value);
775 }
776 let mut new_knots = Vec::with_capacity(knots.len() + 1);
777 new_knots.extend_from_slice(&knots[..=k]);
778 new_knots.push(u);
779 new_knots.extend_from_slice(&knots[k + 1..]);
780 Ok((out, new_knots))
781}
782
783fn cyclic_distance(a: f64, b: f64, period: f64) -> f64 {
784 let d = (a - b).rem_euclid(period);
785 d.min(period - d)
786}
787