1use oxiphysics_core::math::Vec3;
18use std::f64::consts::PI;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum FoldType {
27 Mountain,
29 Valley,
31 Boundary,
33}
34
35#[derive(Debug, Clone)]
44pub struct FoldLine {
45 pub start: Vec3,
47 pub end: Vec3,
49 pub direction: Vec3,
51 pub fold_angle: f64,
53 pub fold_type: FoldType,
55 pub adjacent_facets: [Option<usize>; 2],
57}
58
59impl FoldLine {
60 pub fn new(start: Vec3, end: Vec3, fold_type: FoldType) -> Self {
62 let diff = end - start;
63 let len = diff.norm();
64 let direction = if len > 1e-12 {
65 diff / len
66 } else {
67 Vec3::new(1.0, 0.0, 0.0)
68 };
69 Self {
70 start,
71 end,
72 direction,
73 fold_angle: 0.0,
74 fold_type,
75 adjacent_facets: [None, None],
76 }
77 }
78
79 pub fn length(&self) -> f64 {
81 (self.end - self.start).norm()
82 }
83
84 pub fn midpoint(&self) -> Vec3 {
86 (self.start + self.end) * 0.5
87 }
88
89 pub fn signed_angle(&self) -> f64 {
91 match self.fold_type {
92 FoldType::Mountain => self.fold_angle,
93 FoldType::Valley => -self.fold_angle,
94 FoldType::Boundary => 0.0,
95 }
96 }
97
98 pub fn rotation_matrix(&self) -> [[f64; 3]; 3] {
102 let theta = self.signed_angle();
103 let (s, c) = theta.sin_cos();
104 let t = 1.0 - c;
105 let (ux, uy, uz) = (self.direction.x, self.direction.y, self.direction.z);
106 [
107 [t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
108 [t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
109 [t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
110 ]
111 }
112
113 pub fn apply_rotation(&self, point: &Vec3) -> Vec3 {
115 let r = self.rotation_matrix();
116 let p = point - self.start;
117 let rotated = Vec3::new(
118 r[0][0] * p.x + r[0][1] * p.y + r[0][2] * p.z,
119 r[1][0] * p.x + r[1][1] * p.y + r[1][2] * p.z,
120 r[2][0] * p.x + r[2][1] * p.y + r[2][2] * p.z,
121 );
122 rotated + self.start
123 }
124}
125
126#[derive(Debug, Clone)]
132pub struct OrigamiFacet {
133 pub vertex_indices: Vec<usize>,
135 pub normal: Vec3,
137 pub active: bool,
139}
140
141impl OrigamiFacet {
142 pub fn new(vertex_indices: Vec<usize>) -> Self {
144 Self {
145 vertex_indices,
146 normal: Vec3::new(0.0, 0.0, 1.0),
147 active: true,
148 }
149 }
150
151 pub fn num_sides(&self) -> usize {
153 self.vertex_indices.len()
154 }
155}
156
157#[derive(Debug, Clone)]
166pub struct OrigamiPattern {
167 pub vertices: Vec<Vec3>,
169 pub fold_lines: Vec<FoldLine>,
171 pub facets: Vec<OrigamiFacet>,
173}
174
175impl OrigamiPattern {
176 pub fn new() -> Self {
178 Self {
179 vertices: Vec::new(),
180 fold_lines: Vec::new(),
181 facets: Vec::new(),
182 }
183 }
184
185 pub fn add_vertex(&mut self, v: Vec3) -> usize {
187 let idx = self.vertices.len();
188 self.vertices.push(v);
189 idx
190 }
191
192 pub fn add_fold_line(&mut self, fl: FoldLine) -> usize {
194 let idx = self.fold_lines.len();
195 self.fold_lines.push(fl);
196 idx
197 }
198
199 pub fn add_facet(&mut self, facet: OrigamiFacet) -> usize {
201 let idx = self.facets.len();
202 self.facets.push(facet);
203 idx
204 }
205
206 pub fn dihedral_angle(n1: &Vec3, n2: &Vec3) -> f64 {
210 let cos_theta = n1.dot(n2).clamp(-1.0, 1.0);
211 cos_theta.acos()
212 }
213
214 pub fn set_fold_parameter(&mut self, t: f64) {
217 for fl in &mut self.fold_lines {
218 fl.fold_angle = t * PI;
219 }
220 }
221
222 pub fn degrees_of_freedom(&self) -> usize {
227 let interior_vertices = self.vertices.len().saturating_sub(4);
228 interior_vertices.max(1)
229 }
230
231 pub fn gaussian_curvature_at(&self, _vi: usize) -> f64 {
235 let sector_angle_sum: f64 = self.fold_lines.iter().map(|fl| fl.fold_angle.abs()).sum();
238 let n = self.fold_lines.len().max(1) as f64;
239 2.0 * PI - sector_angle_sum / n
240 }
241}
242
243impl Default for OrigamiPattern {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249#[derive(Debug, Clone)]
258pub struct RigidOrigami {
259 pub pattern: OrigamiPattern,
261 pub positions: Vec<Vec3>,
263 pub rho: f64,
265}
266
267impl RigidOrigami {
268 pub fn new(pattern: OrigamiPattern) -> Self {
270 let positions = pattern.vertices.clone();
271 Self {
272 pattern,
273 positions,
274 rho: 0.0,
275 }
276 }
277
278 pub fn step(&mut self, delta_rho: f64) {
280 self.rho = (self.rho + delta_rho).clamp(0.0, 1.0);
281 self.pattern.set_fold_parameter(self.rho);
282 self.update_positions();
283 }
284
285 pub fn update_positions(&mut self) {
290 for (i, v) in self.pattern.vertices.iter().enumerate() {
292 self.positions[i] = *v;
293 }
294 for fl in &self.pattern.fold_lines {
296 let theta = fl.signed_angle();
297 if theta.abs() < 1e-12 {
298 continue;
299 }
300 let (s, c) = theta.sin_cos();
301 let t = 1.0 - c;
302 let (ux, uy, uz) = (fl.direction.x, fl.direction.y, fl.direction.z);
303 let rot = [
304 [t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
305 [t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
306 [t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
307 ];
308 for pos in &mut self.positions {
310 let local = *pos - fl.start;
311 let perp = Vec3::new(-fl.direction.y, fl.direction.x, 0.0);
313 if local.dot(&perp) > 0.0 {
314 let rx = rot[0][0] * local.x + rot[0][1] * local.y + rot[0][2] * local.z;
315 let ry = rot[1][0] * local.x + rot[1][1] * local.y + rot[1][2] * local.z;
316 let rz = rot[2][0] * local.x + rot[2][1] * local.y + rot[2][2] * local.z;
317 *pos = Vec3::new(rx, ry, rz) + fl.start;
318 }
319 }
320 }
321 }
322
323 pub fn bounding_box(&self) -> (Vec3, Vec3) {
325 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
326 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
327 for v in &self.positions {
328 lo.x = lo.x.min(v.x);
329 lo.y = lo.y.min(v.y);
330 lo.z = lo.z.min(v.z);
331 hi.x = hi.x.max(v.x);
332 hi.y = hi.y.max(v.y);
333 hi.z = hi.z.max(v.z);
334 }
335 (lo, hi)
336 }
337}
338
339#[derive(Debug, Clone, Copy)]
345pub struct MiuraUnitCell {
346 pub a: f64,
348 pub b: f64,
350 pub alpha: f64,
352}
353
354impl MiuraUnitCell {
355 pub fn new(a: f64, b: f64, alpha: f64) -> Self {
357 Self { a, b, alpha }
358 }
359
360 pub fn projected_dimensions(&self, theta: f64) -> (f64, f64, f64) {
364 let sin_alpha = self.alpha.sin();
366 let cos_alpha = self.alpha.cos();
367 let sin_theta = theta.sin();
368 let cos_theta = theta.cos();
369
370 let denom = (1.0 - (sin_alpha * sin_theta).powi(2)).sqrt().max(1e-12);
373 let lx = 2.0 * self.a * sin_alpha * cos_theta / denom;
374 let ly = 2.0 * self.b * sin_alpha;
375 let lz = 2.0 * self.a * cos_alpha / denom;
376 (lx, ly, lz)
377 }
378
379 pub fn coupled_angle(&self, theta: f64) -> f64 {
384 2.0 * (self.alpha.cos() * (theta / 2.0).tan()).atan()
385 }
386}
387
388#[derive(Debug, Clone)]
390pub struct MiuraOri {
391 pub cell: MiuraUnitCell,
393 pub m: usize,
395 pub n: usize,
397 pub theta: f64,
399}
400
401impl MiuraOri {
402 pub fn new(cell: MiuraUnitCell, m: usize, n: usize) -> Self {
404 Self {
405 cell,
406 m,
407 n,
408 theta: 0.0,
409 }
410 }
411
412 pub fn set_angle(&mut self, theta: f64) {
414 self.theta = theta.clamp(0.0, PI / 2.0);
415 }
416
417 pub fn vertex_positions(&self) -> Vec<Vec3> {
422 let (lx, ly, lz) = self.cell.projected_dimensions(self.theta);
423 let phi = self.cell.coupled_angle(self.theta);
424 let sin_phi = phi.sin();
425 let cos_phi = phi.cos();
426
427 let rows = 2 * self.m + 2;
428 let cols = 2 * self.n + 2;
429 let mut verts = Vec::with_capacity(rows * cols);
430
431 for i in 0..rows {
432 for j in 0..cols {
433 let zi = if (i + j) % 2 == 0 { 0.0 } else { lz };
435 let xi = (j as f64) * lx * 0.5;
436 let yi = (i as f64) * ly * 0.5 * cos_phi + zi * sin_phi;
437 verts.push(Vec3::new(xi, yi, zi));
438 }
439 }
440 verts
441 }
442
443 pub fn poisson_ratio(&self) -> f64 {
447 let sin_a = self.cell.alpha.sin();
450 let sin_t = self.theta.sin();
451 let denom = 1.0 - (sin_a * sin_t).powi(2);
452 if denom.abs() < 1e-12 {
453 return 0.0;
454 }
455 -(sin_a.powi(2)) / denom
456 }
457
458 pub fn num_cells(&self) -> usize {
460 self.m * self.n
461 }
462
463 pub fn fold_lines_row(&self, row: usize) -> Vec<FoldLine> {
465 let (lx, _ly, _lz) = self.cell.projected_dimensions(self.theta);
466 let verts = self.vertex_positions();
467 let cols = 2 * self.n + 2;
468 let mut lines = Vec::new();
469 for j in 0..cols - 1 {
470 let i0 = row * cols + j;
471 let i1 = row * cols + j + 1;
472 if i0 < verts.len() && i1 < verts.len() {
473 let ft = if j % 2 == 0 {
474 FoldType::Mountain
475 } else {
476 FoldType::Valley
477 };
478 let mut fl = FoldLine::new(verts[i0], verts[i1], ft);
479 fl.fold_angle = self.theta;
480 let _ = lx; lines.push(fl);
482 }
483 }
484 lines
485 }
486}
487
488#[derive(Debug, Clone, Copy)]
494pub struct WaterbombBase {
495 pub panel_size: f64,
497 pub rows: usize,
499 pub cols: usize,
501 pub pleat_angle: f64,
503}
504
505impl WaterbombBase {
506 pub fn new(panel_size: f64, rows: usize, cols: usize) -> Self {
508 Self {
509 panel_size,
510 rows,
511 cols,
512 pleat_angle: 0.0,
513 }
514 }
515
516 pub fn set_pleat_angle(&mut self, angle: f64) {
518 self.pleat_angle = angle.clamp(0.0, PI / 4.0);
519 }
520
521 pub fn vertex_positions(&self) -> Vec<Vec3> {
525 let s = self.panel_size;
526 let cos_p = self.pleat_angle.cos();
527 let sin_p = self.pleat_angle.sin();
528 let mut verts = Vec::new();
529 for i in 0..=self.rows {
530 for j in 0..=self.cols {
531 let z = if (i + j) % 2 == 0 {
533 s * sin_p
534 } else {
535 -s * sin_p
536 };
537 let x = j as f64 * s * cos_p;
538 let y = i as f64 * s * cos_p;
539 verts.push(Vec3::new(x, y, z));
540 }
541 }
542 verts
543 }
544
545 pub fn folded_height(&self) -> f64 {
547 self.panel_size * self.pleat_angle.sin() * 2.0
548 }
549
550 pub fn footprint(&self) -> (f64, f64) {
552 let s = self.panel_size * self.pleat_angle.cos();
553 (self.cols as f64 * s, self.rows as f64 * s)
554 }
555
556 pub fn compactness_ratio(&self) -> f64 {
558 let flat_height = self.panel_size * self.rows as f64;
559 if flat_height < 1e-12 {
560 return 0.0;
561 }
562 self.folded_height() / flat_height
563 }
564}
565
566#[derive(Debug, Clone)]
574pub struct YoshimuraBuckling {
575 pub radius: f64,
577 pub length: f64,
579 pub n_lobes: usize,
581 pub n_axial: usize,
583 pub fold_depth: f64,
585}
586
587impl YoshimuraBuckling {
588 pub fn new(radius: f64, length: f64, n_lobes: usize, n_axial: usize) -> Self {
590 Self {
591 radius,
592 length,
593 n_lobes,
594 n_axial,
595 fold_depth: 0.0,
596 }
597 }
598
599 pub fn set_fold_depth(&mut self, d: f64) {
601 self.fold_depth = d.clamp(0.0, 1.0);
602 }
603
604 pub fn diamond_half_angle(&self) -> f64 {
606 PI / self.n_lobes as f64
607 }
608
609 pub fn axial_wavelength(&self) -> f64 {
611 self.length / self.n_axial as f64
612 }
613
614 pub fn radial_indentation(&self) -> f64 {
616 let phi = self.diamond_half_angle();
617 self.radius * (1.0 - phi.cos()) * self.fold_depth
618 }
619
620 pub fn vertex_positions(&self) -> Vec<Vec3> {
624 let n = self.n_lobes;
625 let m = self.n_axial;
626 let mut verts = Vec::with_capacity(n * (m + 1));
627 let dtheta = 2.0 * PI / n as f64;
628 let dz = self.length / m as f64;
629
630 for k in 0..=m {
631 let z = k as f64 * dz;
632 for i in 0..n {
633 let theta = i as f64 * dtheta + if k % 2 == 1 { dtheta / 2.0 } else { 0.0 };
634 let r = if k % 2 == 1 {
636 self.radius - self.radial_indentation()
637 } else {
638 self.radius
639 };
640 verts.push(Vec3::new(r * theta.cos(), r * theta.sin(), z));
641 }
642 }
643 verts
644 }
645
646 pub fn axial_shortening(&self) -> f64 {
648 let phi = self.diamond_half_angle();
649 let dz = self.axial_wavelength();
650 let shortening_per_cell = dz * (1.0 - phi.cos()) * self.fold_depth;
651 shortening_per_cell * self.n_axial as f64
652 }
653}
654
655#[derive(Debug, Clone)]
661pub struct KirigamiSlit {
662 pub start: Vec3,
664 pub end: Vec3,
666 pub opening: f64,
668}
669
670impl KirigamiSlit {
671 pub fn new(start: Vec3, end: Vec3) -> Self {
673 Self {
674 start,
675 end,
676 opening: 0.0,
677 }
678 }
679
680 pub fn length(&self) -> f64 {
682 (self.end - self.start).norm()
683 }
684}
685
686#[derive(Debug, Clone)]
688pub struct KirigamiCut {
689 pub width: f64,
691 pub height: f64,
693 pub slits: Vec<KirigamiSlit>,
695 pub stretch: f64,
697}
698
699impl KirigamiCut {
700 pub fn new(width: f64, height: f64) -> Self {
702 Self {
703 width,
704 height,
705 slits: Vec::new(),
706 stretch: 0.0,
707 }
708 }
709
710 pub fn rectangular_pattern(
714 width: f64,
715 height: f64,
716 rows: usize,
717 cols: usize,
718 slit_frac: f64,
719 ) -> Self {
720 let mut sheet = Self::new(width, height);
721 let dx = width / cols as f64;
722 let dy = height / rows as f64;
723 let slit_len = dx * slit_frac;
724 for r in 0..rows {
725 for c in 0..cols {
726 let cx = (c as f64 + 0.5) * dx + if r % 2 == 1 { dx * 0.5 } else { 0.0 };
727 let cy = (r as f64 + 0.5) * dy;
728 let half = slit_len * 0.5;
729 let start = Vec3::new(cx - half, cy, 0.0);
730 let end = Vec3::new(cx + half, cy, 0.0);
731 sheet.slits.push(KirigamiSlit::new(start, end));
732 }
733 }
734 sheet
735 }
736
737 pub fn set_stretch(&mut self, s: f64) {
739 self.stretch = s.clamp(0.0, 1.0);
740 for sl in &mut self.slits {
742 sl.opening = sl.length() * s * 0.5;
743 }
744 }
745
746 pub fn poisson_ratio(&self) -> f64 {
750 let total_cut_length: f64 = self.slits.iter().map(|s| s.length()).sum();
751 let sheet_perimeter = 2.0 * (self.width + self.height);
752 let cut_fraction = (total_cut_length / sheet_perimeter).min(1.0);
753 -cut_fraction * self.stretch
754 }
755
756 pub fn stretchability(&self) -> f64 {
758 let avg_opening: f64 =
759 self.slits.iter().map(|s| s.opening).sum::<f64>() / self.slits.len().max(1) as f64;
760 1.0 + avg_opening / (self.height / self.slits.len().max(1) as f64).max(1e-12)
761 }
762
763 pub fn num_slits(&self) -> usize {
765 self.slits.len()
766 }
767}
768
769#[derive(Debug, Clone)]
778pub struct OrigamiAnalysis;
779
780impl OrigamiAnalysis {
781 pub fn angle_deficit(sector_angles: &[f64]) -> f64 {
786 let sum: f64 = sector_angles.iter().sum();
787 2.0 * PI - sum
788 }
789
790 pub fn kawasaki_residual(sector_angles: &[f64]) -> f64 {
795 if sector_angles.len() < 2 {
796 return f64::INFINITY;
797 }
798 let even: f64 = sector_angles.iter().step_by(2).sum();
799 let odd: f64 = sector_angles.iter().skip(1).step_by(2).sum();
800 (even - odd).abs()
801 }
802
803 pub fn maekawa_valid(mountain_count: usize, valley_count: usize) -> bool {
806 let diff = (mountain_count as isize - valley_count as isize).abs();
807 diff == 2
808 }
809
810 pub fn miura_poisson_ratio(alpha: f64, theta: f64) -> f64 {
814 let sin_a = alpha.sin();
815 let sin_t = theta.sin();
816 let denom = 1.0 - (sin_a * sin_t).powi(2);
817 if denom.abs() < 1e-12 {
818 return 0.0;
819 }
820 -(sin_a.powi(2)) / denom
821 }
822
823 pub fn fold_bending_energy(k: f64, length: f64, theta: f64) -> f64 {
828 0.5 * k * length * theta * theta
829 }
830
831 pub fn panel_gaussian_curvature(v0: &Vec3, v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
835 let n1 = (v1 - v0).cross(&(v2 - v0));
837 let n2 = (v2 - v0).cross(&(v3 - v0));
838 let cos_theta = (n1.dot(&n2) / (n1.norm() * n2.norm() + 1e-12)).clamp(-1.0, 1.0);
839 let angle = cos_theta.acos();
840 angle / (v0 - v2).norm().max(1e-12).powi(2)
841 }
842
843 pub fn fold_angle_from_normals(n1: &Vec3, n2: &Vec3, crease_dir: &Vec3) -> f64 {
846 let cos_a = n1.dot(n2).clamp(-1.0, 1.0);
847 let angle = cos_a.acos();
848 let cross = n1.cross(n2);
850 if cross.dot(crease_dir) < 0.0 {
851 -angle
852 } else {
853 angle
854 }
855 }
856}
857
858pub fn build_single_fold_pattern(side: f64, fold_type: FoldType) -> OrigamiPattern {
866 let mut pat = OrigamiPattern::new();
867 let v0 = pat.add_vertex(Vec3::new(0.0, 0.0, 0.0));
868 let v1 = pat.add_vertex(Vec3::new(side, 0.0, 0.0));
869 let v2 = pat.add_vertex(Vec3::new(side, side, 0.0));
870 let v3 = pat.add_vertex(Vec3::new(0.0, side, 0.0));
871 let vm = pat.add_vertex(Vec3::new(side * 0.5, 0.0, 0.0));
872 let vm2 = pat.add_vertex(Vec3::new(side * 0.5, side, 0.0));
873
874 let fold_start = pat.vertices[vm];
875 let fold_end = pat.vertices[vm2];
876 pat.add_fold_line(FoldLine::new(fold_start, fold_end, fold_type));
877
878 pat.add_facet(OrigamiFacet::new(vec![v0, vm, vm2, v3]));
879 pat.add_facet(OrigamiFacet::new(vec![vm, v1, v2, vm2]));
880 pat
881}
882
883pub fn kawasaki_fourth_angle(a1: f64, a2: f64, a3: f64) -> f64 {
888 (a1 - a2 + a3).rem_euclid(2.0 * PI)
890}
891
892#[cfg(test)]
897mod tests {
898 use super::*;
899
900 #[test]
903 fn test_fold_line_length() {
904 let fl = FoldLine::new(
905 Vec3::new(0.0, 0.0, 0.0),
906 Vec3::new(3.0, 4.0, 0.0),
907 FoldType::Mountain,
908 );
909 assert!((fl.length() - 5.0).abs() < 1e-10);
910 }
911
912 #[test]
913 fn test_fold_line_midpoint() {
914 let fl = FoldLine::new(
915 Vec3::new(0.0, 0.0, 0.0),
916 Vec3::new(2.0, 0.0, 0.0),
917 FoldType::Valley,
918 );
919 let mp = fl.midpoint();
920 assert!((mp.x - 1.0).abs() < 1e-10);
921 assert!(mp.y.abs() < 1e-10);
922 }
923
924 #[test]
925 fn test_fold_line_rotation_identity_at_zero() {
926 let mut fl = FoldLine::new(
927 Vec3::new(0.0, 0.0, 0.0),
928 Vec3::new(1.0, 0.0, 0.0),
929 FoldType::Mountain,
930 );
931 fl.fold_angle = 0.0;
932 let p = Vec3::new(0.0, 1.0, 0.0);
933 let r = fl.apply_rotation(&p);
934 assert!((r.x).abs() < 1e-10);
935 assert!((r.y - 1.0).abs() < 1e-10);
936 assert!(r.z.abs() < 1e-10);
937 }
938
939 #[test]
940 fn test_fold_line_rotation_90_degrees() {
941 let mut fl = FoldLine::new(
942 Vec3::new(0.0, 0.0, 0.0),
943 Vec3::new(1.0, 0.0, 0.0),
944 FoldType::Mountain,
945 );
946 fl.fold_angle = PI / 2.0;
947 let p = Vec3::new(0.0, 1.0, 0.0);
948 let r = fl.apply_rotation(&p);
949 assert!(r.y.abs() < 1e-10, "y should be ~0, got {}", r.y);
951 assert!((r.z - 1.0).abs() < 1e-10, "z should be ~1, got {}", r.z);
952 }
953
954 #[test]
955 fn test_fold_type_signed_angle() {
956 let mut fl_m = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Mountain);
957 fl_m.fold_angle = 0.5;
958 assert!((fl_m.signed_angle() - 0.5).abs() < 1e-10);
959
960 let mut fl_v = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Valley);
961 fl_v.fold_angle = 0.5;
962 assert!((fl_v.signed_angle() + 0.5).abs() < 1e-10);
963 }
964
965 #[test]
968 fn test_origami_pattern_add_vertex() {
969 let mut pat = OrigamiPattern::new();
970 let i = pat.add_vertex(Vec3::new(1.0, 2.0, 3.0));
971 assert_eq!(i, 0);
972 assert_eq!(pat.vertices.len(), 1);
973 }
974
975 #[test]
976 fn test_origami_pattern_dihedral_angle() {
977 let n1 = Vec3::new(0.0, 0.0, 1.0);
978 let n2 = Vec3::new(0.0, 0.0, 1.0);
979 let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
980 assert!(angle.abs() < 1e-10);
981 }
982
983 #[test]
984 fn test_origami_pattern_dihedral_angle_90() {
985 let n1 = Vec3::new(0.0, 0.0, 1.0);
986 let n2 = Vec3::new(0.0, 1.0, 0.0);
987 let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
988 assert!((angle - PI / 2.0).abs() < 1e-10);
989 }
990
991 #[test]
992 fn test_single_fold_pattern_structure() {
993 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
994 assert_eq!(pat.fold_lines.len(), 1);
995 assert_eq!(pat.facets.len(), 2);
996 assert!(pat.vertices.len() >= 4);
997 }
998
999 #[test]
1002 fn test_rigid_origami_initial_state() {
1003 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
1004 let ro = RigidOrigami::new(pat);
1005 assert!((ro.rho - 0.0).abs() < 1e-10);
1006 }
1007
1008 #[test]
1009 fn test_rigid_origami_step_clamps() {
1010 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
1011 let mut ro = RigidOrigami::new(pat);
1012 ro.step(2.0); assert!((ro.rho - 1.0).abs() < 1e-10);
1014 }
1015
1016 #[test]
1017 fn test_rigid_origami_bounding_box() {
1018 let pat = build_single_fold_pattern(2.0, FoldType::Mountain);
1019 let ro = RigidOrigami::new(pat);
1020 let (lo, hi) = ro.bounding_box();
1021 assert!(hi.x >= lo.x);
1022 assert!(hi.y >= lo.y);
1023 assert!(hi.z >= lo.z);
1024 }
1025
1026 #[test]
1029 fn test_miura_ori_projected_dimensions_flat() {
1030 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1031 let (lx, _ly, lz) = cell.projected_dimensions(0.0);
1032 let expected_lx = 2.0 * (PI / 4.0_f64).sin();
1035 assert!((lx - expected_lx).abs() < 1e-10, "lx at flat, got {}", lx);
1036 assert!(lz > 0.0);
1037 }
1038
1039 #[test]
1040 fn test_miura_ori_coupled_angle_at_zero() {
1041 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1042 let phi = cell.coupled_angle(0.0);
1043 assert!(phi.abs() < 1e-10);
1044 }
1045
1046 #[test]
1047 fn test_miura_ori_vertex_count() {
1048 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1049 let miura = MiuraOri::new(cell, 3, 4);
1050 let verts = miura.vertex_positions();
1051 let expected = (2 * 3 + 2) * (2 * 4 + 2);
1052 assert_eq!(verts.len(), expected);
1053 }
1054
1055 #[test]
1056 fn test_miura_ori_poisson_ratio_negative() {
1057 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1058 let mut miura = MiuraOri::new(cell, 2, 2);
1059 miura.set_angle(PI / 4.0);
1060 let nu = miura.poisson_ratio();
1061 assert!(
1062 nu <= 0.0,
1063 "Miura-ori Poisson ratio should be negative, got {}",
1064 nu
1065 );
1066 }
1067
1068 #[test]
1069 fn test_miura_ori_fold_lines_row() {
1070 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 6.0);
1071 let miura = MiuraOri::new(cell, 2, 3);
1072 let lines = miura.fold_lines_row(0);
1073 assert!(!lines.is_empty());
1074 }
1075
1076 #[test]
1079 fn test_waterbomb_vertex_count() {
1080 let wb = WaterbombBase::new(1.0, 3, 4);
1081 let verts = wb.vertex_positions();
1082 assert_eq!(verts.len(), 4 * 5); }
1084
1085 #[test]
1086 fn test_waterbomb_flat_height_zero() {
1087 let wb = WaterbombBase::new(1.0, 2, 2);
1088 assert!((wb.folded_height()).abs() < 1e-10);
1089 }
1090
1091 #[test]
1092 fn test_waterbomb_footprint() {
1093 let wb = WaterbombBase::new(1.0, 2, 3);
1094 let (fx, fy) = wb.footprint();
1095 assert!((fx - 3.0).abs() < 1e-10); assert!((fy - 2.0).abs() < 1e-10);
1097 }
1098
1099 #[test]
1102 fn test_yoshimura_vertex_count() {
1103 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1104 let verts = yb.vertex_positions();
1105 assert_eq!(verts.len(), 6 * 5); }
1107
1108 #[test]
1109 fn test_yoshimura_axial_shortening_zero_depth() {
1110 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1111 assert!((yb.axial_shortening()).abs() < 1e-10);
1112 }
1113
1114 #[test]
1115 fn test_yoshimura_axial_wavelength() {
1116 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1117 assert!((yb.axial_wavelength() - 0.5).abs() < 1e-10);
1118 }
1119
1120 #[test]
1123 fn test_kirigami_rectangular_pattern_slit_count() {
1124 let kg = KirigamiCut::rectangular_pattern(2.0, 3.0, 3, 4, 0.8);
1125 assert_eq!(kg.num_slits(), 12);
1126 }
1127
1128 #[test]
1129 fn test_kirigami_stretch_updates_openings() {
1130 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
1131 kg.set_stretch(1.0);
1132 for slit in &kg.slits {
1133 assert!(slit.opening > 0.0);
1134 }
1135 }
1136
1137 #[test]
1138 fn test_kirigami_poisson_ratio_nonpositive() {
1139 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 3, 3, 0.8);
1140 kg.set_stretch(0.5);
1141 assert!(kg.poisson_ratio() <= 0.0);
1142 }
1143
1144 #[test]
1145 fn test_kirigami_stretchability_ge_one() {
1146 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
1147 kg.set_stretch(0.5);
1148 assert!(kg.stretchability() >= 1.0);
1149 }
1150
1151 #[test]
1154 fn test_kawasaki_theorem_flat_vertex() {
1155 let angles = [PI / 4.0, PI / 4.0, PI / 4.0, PI / 4.0];
1157 let residual = OrigamiAnalysis::kawasaki_residual(&angles);
1159 assert!(residual < 1e-10, "residual={}", residual);
1160 }
1161
1162 #[test]
1163 fn test_maekawa_theorem_valid() {
1164 assert!(OrigamiAnalysis::maekawa_valid(3, 1));
1165 assert!(OrigamiAnalysis::maekawa_valid(1, 3));
1166 assert!(!OrigamiAnalysis::maekawa_valid(2, 2));
1167 }
1168
1169 #[test]
1170 fn test_angle_deficit_flat() {
1171 let angles = vec![PI / 3.0; 6];
1173 let deficit = OrigamiAnalysis::angle_deficit(&angles);
1174 assert!(deficit.abs() < 1e-10);
1175 }
1176
1177 #[test]
1178 fn test_miura_poisson_analytical() {
1179 let nu = OrigamiAnalysis::miura_poisson_ratio(PI / 4.0, PI / 4.0);
1180 assert!(nu < 0.0, "Miura Poisson should be negative, got {}", nu);
1181 }
1182
1183 #[test]
1184 fn test_fold_bending_energy_positive() {
1185 let e = OrigamiAnalysis::fold_bending_energy(1.0, 1.0, PI / 4.0);
1186 assert!(e > 0.0);
1187 }
1188
1189 #[test]
1190 fn test_fold_angle_from_normals_parallel() {
1191 let n = Vec3::new(0.0, 0.0, 1.0);
1192 let crease = Vec3::new(1.0, 0.0, 0.0);
1193 let angle = OrigamiAnalysis::fold_angle_from_normals(&n, &n, &crease);
1194 assert!(angle.abs() < 1e-10);
1195 }
1196
1197 #[test]
1198 fn test_kawasaki_fourth_angle() {
1199 let a4 = kawasaki_fourth_angle(PI / 4.0, PI / 4.0, PI / 4.0);
1200 assert!((a4 - PI / 4.0).abs() < 1e-10);
1201 }
1202}