1#![allow(dead_code)]
18
19use oxiphysics_core::math::Vec3;
20use std::f64::consts::PI;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum FoldType {
29 Mountain,
31 Valley,
33 Boundary,
35}
36
37#[derive(Debug, Clone)]
46pub struct FoldLine {
47 pub start: Vec3,
49 pub end: Vec3,
51 pub direction: Vec3,
53 pub fold_angle: f64,
55 pub fold_type: FoldType,
57 pub adjacent_facets: [Option<usize>; 2],
59}
60
61impl FoldLine {
62 pub fn new(start: Vec3, end: Vec3, fold_type: FoldType) -> Self {
64 let diff = end - start;
65 let len = diff.norm();
66 let direction = if len > 1e-12 {
67 diff / len
68 } else {
69 Vec3::new(1.0, 0.0, 0.0)
70 };
71 Self {
72 start,
73 end,
74 direction,
75 fold_angle: 0.0,
76 fold_type,
77 adjacent_facets: [None, None],
78 }
79 }
80
81 pub fn length(&self) -> f64 {
83 (self.end - self.start).norm()
84 }
85
86 pub fn midpoint(&self) -> Vec3 {
88 (self.start + self.end) * 0.5
89 }
90
91 pub fn signed_angle(&self) -> f64 {
93 match self.fold_type {
94 FoldType::Mountain => self.fold_angle,
95 FoldType::Valley => -self.fold_angle,
96 FoldType::Boundary => 0.0,
97 }
98 }
99
100 pub fn rotation_matrix(&self) -> [[f64; 3]; 3] {
104 let theta = self.signed_angle();
105 let (s, c) = theta.sin_cos();
106 let t = 1.0 - c;
107 let (ux, uy, uz) = (self.direction.x, self.direction.y, self.direction.z);
108 [
109 [t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
110 [t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
111 [t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
112 ]
113 }
114
115 pub fn apply_rotation(&self, point: &Vec3) -> Vec3 {
117 let r = self.rotation_matrix();
118 let p = point - self.start;
119 let rotated = Vec3::new(
120 r[0][0] * p.x + r[0][1] * p.y + r[0][2] * p.z,
121 r[1][0] * p.x + r[1][1] * p.y + r[1][2] * p.z,
122 r[2][0] * p.x + r[2][1] * p.y + r[2][2] * p.z,
123 );
124 rotated + self.start
125 }
126}
127
128#[derive(Debug, Clone)]
134pub struct OrigamiFacet {
135 pub vertex_indices: Vec<usize>,
137 pub normal: Vec3,
139 pub active: bool,
141}
142
143impl OrigamiFacet {
144 pub fn new(vertex_indices: Vec<usize>) -> Self {
146 Self {
147 vertex_indices,
148 normal: Vec3::new(0.0, 0.0, 1.0),
149 active: true,
150 }
151 }
152
153 pub fn num_sides(&self) -> usize {
155 self.vertex_indices.len()
156 }
157}
158
159#[derive(Debug, Clone)]
168pub struct OrigamiPattern {
169 pub vertices: Vec<Vec3>,
171 pub fold_lines: Vec<FoldLine>,
173 pub facets: Vec<OrigamiFacet>,
175}
176
177impl OrigamiPattern {
178 pub fn new() -> Self {
180 Self {
181 vertices: Vec::new(),
182 fold_lines: Vec::new(),
183 facets: Vec::new(),
184 }
185 }
186
187 pub fn add_vertex(&mut self, v: Vec3) -> usize {
189 let idx = self.vertices.len();
190 self.vertices.push(v);
191 idx
192 }
193
194 pub fn add_fold_line(&mut self, fl: FoldLine) -> usize {
196 let idx = self.fold_lines.len();
197 self.fold_lines.push(fl);
198 idx
199 }
200
201 pub fn add_facet(&mut self, facet: OrigamiFacet) -> usize {
203 let idx = self.facets.len();
204 self.facets.push(facet);
205 idx
206 }
207
208 pub fn dihedral_angle(n1: &Vec3, n2: &Vec3) -> f64 {
212 let cos_theta = n1.dot(n2).clamp(-1.0, 1.0);
213 cos_theta.acos()
214 }
215
216 pub fn set_fold_parameter(&mut self, t: f64) {
219 for fl in &mut self.fold_lines {
220 fl.fold_angle = t * PI;
221 }
222 }
223
224 pub fn degrees_of_freedom(&self) -> usize {
229 let interior_vertices = self.vertices.len().saturating_sub(4);
230 interior_vertices.max(1)
231 }
232
233 pub fn gaussian_curvature_at(&self, _vi: usize) -> f64 {
237 let sector_angle_sum: f64 = self.fold_lines.iter().map(|fl| fl.fold_angle.abs()).sum();
240 let n = self.fold_lines.len().max(1) as f64;
241 2.0 * PI - sector_angle_sum / n
242 }
243}
244
245impl Default for OrigamiPattern {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251#[derive(Debug, Clone)]
260pub struct RigidOrigami {
261 pub pattern: OrigamiPattern,
263 pub positions: Vec<Vec3>,
265 pub rho: f64,
267}
268
269impl RigidOrigami {
270 pub fn new(pattern: OrigamiPattern) -> Self {
272 let positions = pattern.vertices.clone();
273 Self {
274 pattern,
275 positions,
276 rho: 0.0,
277 }
278 }
279
280 pub fn step(&mut self, delta_rho: f64) {
282 self.rho = (self.rho + delta_rho).clamp(0.0, 1.0);
283 self.pattern.set_fold_parameter(self.rho);
284 self.update_positions();
285 }
286
287 pub fn update_positions(&mut self) {
292 for (i, v) in self.pattern.vertices.iter().enumerate() {
294 self.positions[i] = *v;
295 }
296 for fl in &self.pattern.fold_lines {
298 let theta = fl.signed_angle();
299 if theta.abs() < 1e-12 {
300 continue;
301 }
302 let (s, c) = theta.sin_cos();
303 let t = 1.0 - c;
304 let (ux, uy, uz) = (fl.direction.x, fl.direction.y, fl.direction.z);
305 let rot = [
306 [t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
307 [t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
308 [t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
309 ];
310 for pos in &mut self.positions {
312 let local = *pos - fl.start;
313 let perp = Vec3::new(-fl.direction.y, fl.direction.x, 0.0);
315 if local.dot(&perp) > 0.0 {
316 let rx = rot[0][0] * local.x + rot[0][1] * local.y + rot[0][2] * local.z;
317 let ry = rot[1][0] * local.x + rot[1][1] * local.y + rot[1][2] * local.z;
318 let rz = rot[2][0] * local.x + rot[2][1] * local.y + rot[2][2] * local.z;
319 *pos = Vec3::new(rx, ry, rz) + fl.start;
320 }
321 }
322 }
323 }
324
325 pub fn bounding_box(&self) -> (Vec3, Vec3) {
327 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
328 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
329 for v in &self.positions {
330 lo.x = lo.x.min(v.x);
331 lo.y = lo.y.min(v.y);
332 lo.z = lo.z.min(v.z);
333 hi.x = hi.x.max(v.x);
334 hi.y = hi.y.max(v.y);
335 hi.z = hi.z.max(v.z);
336 }
337 (lo, hi)
338 }
339}
340
341#[derive(Debug, Clone, Copy)]
347pub struct MiuraUnitCell {
348 pub a: f64,
350 pub b: f64,
352 pub alpha: f64,
354}
355
356impl MiuraUnitCell {
357 pub fn new(a: f64, b: f64, alpha: f64) -> Self {
359 Self { a, b, alpha }
360 }
361
362 pub fn projected_dimensions(&self, theta: f64) -> (f64, f64, f64) {
366 let sin_alpha = self.alpha.sin();
368 let cos_alpha = self.alpha.cos();
369 let sin_theta = theta.sin();
370 let cos_theta = theta.cos();
371
372 let denom = (1.0 - (sin_alpha * sin_theta).powi(2)).sqrt().max(1e-12);
375 let lx = 2.0 * self.a * sin_alpha * cos_theta / denom;
376 let ly = 2.0 * self.b * sin_alpha;
377 let lz = 2.0 * self.a * cos_alpha / denom;
378 (lx, ly, lz)
379 }
380
381 pub fn coupled_angle(&self, theta: f64) -> f64 {
386 2.0 * (self.alpha.cos() * (theta / 2.0).tan()).atan()
387 }
388}
389
390#[derive(Debug, Clone)]
392pub struct MiuraOri {
393 pub cell: MiuraUnitCell,
395 pub m: usize,
397 pub n: usize,
399 pub theta: f64,
401}
402
403impl MiuraOri {
404 pub fn new(cell: MiuraUnitCell, m: usize, n: usize) -> Self {
406 Self {
407 cell,
408 m,
409 n,
410 theta: 0.0,
411 }
412 }
413
414 pub fn set_angle(&mut self, theta: f64) {
416 self.theta = theta.clamp(0.0, PI / 2.0);
417 }
418
419 pub fn vertex_positions(&self) -> Vec<Vec3> {
424 let (lx, ly, lz) = self.cell.projected_dimensions(self.theta);
425 let phi = self.cell.coupled_angle(self.theta);
426 let sin_phi = phi.sin();
427 let cos_phi = phi.cos();
428
429 let rows = 2 * self.m + 2;
430 let cols = 2 * self.n + 2;
431 let mut verts = Vec::with_capacity(rows * cols);
432
433 for i in 0..rows {
434 for j in 0..cols {
435 let zi = if (i + j) % 2 == 0 { 0.0 } else { lz };
437 let xi = (j as f64) * lx * 0.5;
438 let yi = (i as f64) * ly * 0.5 * cos_phi + zi * sin_phi;
439 verts.push(Vec3::new(xi, yi, zi));
440 }
441 }
442 verts
443 }
444
445 pub fn poisson_ratio(&self) -> f64 {
449 let sin_a = self.cell.alpha.sin();
452 let sin_t = self.theta.sin();
453 let denom = 1.0 - (sin_a * sin_t).powi(2);
454 if denom.abs() < 1e-12 {
455 return 0.0;
456 }
457 -(sin_a.powi(2)) / denom
458 }
459
460 pub fn num_cells(&self) -> usize {
462 self.m * self.n
463 }
464
465 pub fn fold_lines_row(&self, row: usize) -> Vec<FoldLine> {
467 let (lx, _ly, _lz) = self.cell.projected_dimensions(self.theta);
468 let verts = self.vertex_positions();
469 let cols = 2 * self.n + 2;
470 let mut lines = Vec::new();
471 for j in 0..cols - 1 {
472 let i0 = row * cols + j;
473 let i1 = row * cols + j + 1;
474 if i0 < verts.len() && i1 < verts.len() {
475 let ft = if j % 2 == 0 {
476 FoldType::Mountain
477 } else {
478 FoldType::Valley
479 };
480 let mut fl = FoldLine::new(verts[i0], verts[i1], ft);
481 fl.fold_angle = self.theta;
482 let _ = lx; lines.push(fl);
484 }
485 }
486 lines
487 }
488}
489
490#[derive(Debug, Clone, Copy)]
496pub struct WaterbombBase {
497 pub panel_size: f64,
499 pub rows: usize,
501 pub cols: usize,
503 pub pleat_angle: f64,
505}
506
507impl WaterbombBase {
508 pub fn new(panel_size: f64, rows: usize, cols: usize) -> Self {
510 Self {
511 panel_size,
512 rows,
513 cols,
514 pleat_angle: 0.0,
515 }
516 }
517
518 pub fn set_pleat_angle(&mut self, angle: f64) {
520 self.pleat_angle = angle.clamp(0.0, PI / 4.0);
521 }
522
523 pub fn vertex_positions(&self) -> Vec<Vec3> {
527 let s = self.panel_size;
528 let cos_p = self.pleat_angle.cos();
529 let sin_p = self.pleat_angle.sin();
530 let mut verts = Vec::new();
531 for i in 0..=self.rows {
532 for j in 0..=self.cols {
533 let z = if (i + j) % 2 == 0 {
535 s * sin_p
536 } else {
537 -s * sin_p
538 };
539 let x = j as f64 * s * cos_p;
540 let y = i as f64 * s * cos_p;
541 verts.push(Vec3::new(x, y, z));
542 }
543 }
544 verts
545 }
546
547 pub fn folded_height(&self) -> f64 {
549 self.panel_size * self.pleat_angle.sin() * 2.0
550 }
551
552 pub fn footprint(&self) -> (f64, f64) {
554 let s = self.panel_size * self.pleat_angle.cos();
555 (self.cols as f64 * s, self.rows as f64 * s)
556 }
557
558 pub fn compactness_ratio(&self) -> f64 {
560 let flat_height = self.panel_size * self.rows as f64;
561 if flat_height < 1e-12 {
562 return 0.0;
563 }
564 self.folded_height() / flat_height
565 }
566}
567
568#[derive(Debug, Clone)]
576pub struct YoshimuraBuckling {
577 pub radius: f64,
579 pub length: f64,
581 pub n_lobes: usize,
583 pub n_axial: usize,
585 pub fold_depth: f64,
587}
588
589impl YoshimuraBuckling {
590 pub fn new(radius: f64, length: f64, n_lobes: usize, n_axial: usize) -> Self {
592 Self {
593 radius,
594 length,
595 n_lobes,
596 n_axial,
597 fold_depth: 0.0,
598 }
599 }
600
601 pub fn set_fold_depth(&mut self, d: f64) {
603 self.fold_depth = d.clamp(0.0, 1.0);
604 }
605
606 pub fn diamond_half_angle(&self) -> f64 {
608 PI / self.n_lobes as f64
609 }
610
611 pub fn axial_wavelength(&self) -> f64 {
613 self.length / self.n_axial as f64
614 }
615
616 pub fn radial_indentation(&self) -> f64 {
618 let phi = self.diamond_half_angle();
619 self.radius * (1.0 - phi.cos()) * self.fold_depth
620 }
621
622 pub fn vertex_positions(&self) -> Vec<Vec3> {
626 let n = self.n_lobes;
627 let m = self.n_axial;
628 let mut verts = Vec::with_capacity(n * (m + 1));
629 let dtheta = 2.0 * PI / n as f64;
630 let dz = self.length / m as f64;
631
632 for k in 0..=m {
633 let z = k as f64 * dz;
634 for i in 0..n {
635 let theta = i as f64 * dtheta + if k % 2 == 1 { dtheta / 2.0 } else { 0.0 };
636 let r = if k % 2 == 1 {
638 self.radius - self.radial_indentation()
639 } else {
640 self.radius
641 };
642 verts.push(Vec3::new(r * theta.cos(), r * theta.sin(), z));
643 }
644 }
645 verts
646 }
647
648 pub fn axial_shortening(&self) -> f64 {
650 let phi = self.diamond_half_angle();
651 let dz = self.axial_wavelength();
652 let shortening_per_cell = dz * (1.0 - phi.cos()) * self.fold_depth;
653 shortening_per_cell * self.n_axial as f64
654 }
655}
656
657#[derive(Debug, Clone)]
663pub struct KirigamiSlit {
664 pub start: Vec3,
666 pub end: Vec3,
668 pub opening: f64,
670}
671
672impl KirigamiSlit {
673 pub fn new(start: Vec3, end: Vec3) -> Self {
675 Self {
676 start,
677 end,
678 opening: 0.0,
679 }
680 }
681
682 pub fn length(&self) -> f64 {
684 (self.end - self.start).norm()
685 }
686}
687
688#[derive(Debug, Clone)]
690pub struct KirigamiCut {
691 pub width: f64,
693 pub height: f64,
695 pub slits: Vec<KirigamiSlit>,
697 pub stretch: f64,
699}
700
701impl KirigamiCut {
702 pub fn new(width: f64, height: f64) -> Self {
704 Self {
705 width,
706 height,
707 slits: Vec::new(),
708 stretch: 0.0,
709 }
710 }
711
712 pub fn rectangular_pattern(
716 width: f64,
717 height: f64,
718 rows: usize,
719 cols: usize,
720 slit_frac: f64,
721 ) -> Self {
722 let mut sheet = Self::new(width, height);
723 let dx = width / cols as f64;
724 let dy = height / rows as f64;
725 let slit_len = dx * slit_frac;
726 for r in 0..rows {
727 for c in 0..cols {
728 let cx = (c as f64 + 0.5) * dx + if r % 2 == 1 { dx * 0.5 } else { 0.0 };
729 let cy = (r as f64 + 0.5) * dy;
730 let half = slit_len * 0.5;
731 let start = Vec3::new(cx - half, cy, 0.0);
732 let end = Vec3::new(cx + half, cy, 0.0);
733 sheet.slits.push(KirigamiSlit::new(start, end));
734 }
735 }
736 sheet
737 }
738
739 pub fn set_stretch(&mut self, s: f64) {
741 self.stretch = s.clamp(0.0, 1.0);
742 for sl in &mut self.slits {
744 sl.opening = sl.length() * s * 0.5;
745 }
746 }
747
748 pub fn poisson_ratio(&self) -> f64 {
752 let total_cut_length: f64 = self.slits.iter().map(|s| s.length()).sum();
753 let sheet_perimeter = 2.0 * (self.width + self.height);
754 let cut_fraction = (total_cut_length / sheet_perimeter).min(1.0);
755 -cut_fraction * self.stretch
756 }
757
758 pub fn stretchability(&self) -> f64 {
760 let avg_opening: f64 =
761 self.slits.iter().map(|s| s.opening).sum::<f64>() / self.slits.len().max(1) as f64;
762 1.0 + avg_opening / (self.height / self.slits.len().max(1) as f64).max(1e-12)
763 }
764
765 pub fn num_slits(&self) -> usize {
767 self.slits.len()
768 }
769}
770
771#[derive(Debug, Clone)]
780pub struct OrigamiAnalysis;
781
782impl OrigamiAnalysis {
783 pub fn angle_deficit(sector_angles: &[f64]) -> f64 {
788 let sum: f64 = sector_angles.iter().sum();
789 2.0 * PI - sum
790 }
791
792 pub fn kawasaki_residual(sector_angles: &[f64]) -> f64 {
797 if sector_angles.len() < 2 {
798 return f64::INFINITY;
799 }
800 let even: f64 = sector_angles.iter().step_by(2).sum();
801 let odd: f64 = sector_angles.iter().skip(1).step_by(2).sum();
802 (even - odd).abs()
803 }
804
805 pub fn maekawa_valid(mountain_count: usize, valley_count: usize) -> bool {
808 let diff = (mountain_count as isize - valley_count as isize).abs();
809 diff == 2
810 }
811
812 pub fn miura_poisson_ratio(alpha: f64, theta: f64) -> f64 {
816 let sin_a = alpha.sin();
817 let sin_t = theta.sin();
818 let denom = 1.0 - (sin_a * sin_t).powi(2);
819 if denom.abs() < 1e-12 {
820 return 0.0;
821 }
822 -(sin_a.powi(2)) / denom
823 }
824
825 pub fn fold_bending_energy(k: f64, length: f64, theta: f64) -> f64 {
830 0.5 * k * length * theta * theta
831 }
832
833 pub fn panel_gaussian_curvature(v0: &Vec3, v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
837 let n1 = (v1 - v0).cross(&(v2 - v0));
839 let n2 = (v2 - v0).cross(&(v3 - v0));
840 let cos_theta = (n1.dot(&n2) / (n1.norm() * n2.norm() + 1e-12)).clamp(-1.0, 1.0);
841 let angle = cos_theta.acos();
842 angle / (v0 - v2).norm().max(1e-12).powi(2)
843 }
844
845 pub fn fold_angle_from_normals(n1: &Vec3, n2: &Vec3, crease_dir: &Vec3) -> f64 {
848 let cos_a = n1.dot(n2).clamp(-1.0, 1.0);
849 let angle = cos_a.acos();
850 let cross = n1.cross(n2);
852 if cross.dot(crease_dir) < 0.0 {
853 -angle
854 } else {
855 angle
856 }
857 }
858}
859
860pub fn build_single_fold_pattern(side: f64, fold_type: FoldType) -> OrigamiPattern {
868 let mut pat = OrigamiPattern::new();
869 let v0 = pat.add_vertex(Vec3::new(0.0, 0.0, 0.0));
870 let v1 = pat.add_vertex(Vec3::new(side, 0.0, 0.0));
871 let v2 = pat.add_vertex(Vec3::new(side, side, 0.0));
872 let v3 = pat.add_vertex(Vec3::new(0.0, side, 0.0));
873 let vm = pat.add_vertex(Vec3::new(side * 0.5, 0.0, 0.0));
874 let vm2 = pat.add_vertex(Vec3::new(side * 0.5, side, 0.0));
875
876 let fold_start = pat.vertices[vm];
877 let fold_end = pat.vertices[vm2];
878 pat.add_fold_line(FoldLine::new(fold_start, fold_end, fold_type));
879
880 pat.add_facet(OrigamiFacet::new(vec![v0, vm, vm2, v3]));
881 pat.add_facet(OrigamiFacet::new(vec![vm, v1, v2, vm2]));
882 pat
883}
884
885pub fn kawasaki_fourth_angle(a1: f64, a2: f64, a3: f64) -> f64 {
890 (a1 - a2 + a3).rem_euclid(2.0 * PI)
892}
893
894#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
905 fn test_fold_line_length() {
906 let fl = FoldLine::new(
907 Vec3::new(0.0, 0.0, 0.0),
908 Vec3::new(3.0, 4.0, 0.0),
909 FoldType::Mountain,
910 );
911 assert!((fl.length() - 5.0).abs() < 1e-10);
912 }
913
914 #[test]
915 fn test_fold_line_midpoint() {
916 let fl = FoldLine::new(
917 Vec3::new(0.0, 0.0, 0.0),
918 Vec3::new(2.0, 0.0, 0.0),
919 FoldType::Valley,
920 );
921 let mp = fl.midpoint();
922 assert!((mp.x - 1.0).abs() < 1e-10);
923 assert!(mp.y.abs() < 1e-10);
924 }
925
926 #[test]
927 fn test_fold_line_rotation_identity_at_zero() {
928 let mut fl = FoldLine::new(
929 Vec3::new(0.0, 0.0, 0.0),
930 Vec3::new(1.0, 0.0, 0.0),
931 FoldType::Mountain,
932 );
933 fl.fold_angle = 0.0;
934 let p = Vec3::new(0.0, 1.0, 0.0);
935 let r = fl.apply_rotation(&p);
936 assert!((r.x).abs() < 1e-10);
937 assert!((r.y - 1.0).abs() < 1e-10);
938 assert!(r.z.abs() < 1e-10);
939 }
940
941 #[test]
942 fn test_fold_line_rotation_90_degrees() {
943 let mut fl = FoldLine::new(
944 Vec3::new(0.0, 0.0, 0.0),
945 Vec3::new(1.0, 0.0, 0.0),
946 FoldType::Mountain,
947 );
948 fl.fold_angle = PI / 2.0;
949 let p = Vec3::new(0.0, 1.0, 0.0);
950 let r = fl.apply_rotation(&p);
951 assert!(r.y.abs() < 1e-10, "y should be ~0, got {}", r.y);
953 assert!((r.z - 1.0).abs() < 1e-10, "z should be ~1, got {}", r.z);
954 }
955
956 #[test]
957 fn test_fold_type_signed_angle() {
958 let mut fl_m = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Mountain);
959 fl_m.fold_angle = 0.5;
960 assert!((fl_m.signed_angle() - 0.5).abs() < 1e-10);
961
962 let mut fl_v = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Valley);
963 fl_v.fold_angle = 0.5;
964 assert!((fl_v.signed_angle() + 0.5).abs() < 1e-10);
965 }
966
967 #[test]
970 fn test_origami_pattern_add_vertex() {
971 let mut pat = OrigamiPattern::new();
972 let i = pat.add_vertex(Vec3::new(1.0, 2.0, 3.0));
973 assert_eq!(i, 0);
974 assert_eq!(pat.vertices.len(), 1);
975 }
976
977 #[test]
978 fn test_origami_pattern_dihedral_angle() {
979 let n1 = Vec3::new(0.0, 0.0, 1.0);
980 let n2 = Vec3::new(0.0, 0.0, 1.0);
981 let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
982 assert!(angle.abs() < 1e-10);
983 }
984
985 #[test]
986 fn test_origami_pattern_dihedral_angle_90() {
987 let n1 = Vec3::new(0.0, 0.0, 1.0);
988 let n2 = Vec3::new(0.0, 1.0, 0.0);
989 let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
990 assert!((angle - PI / 2.0).abs() < 1e-10);
991 }
992
993 #[test]
994 fn test_single_fold_pattern_structure() {
995 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
996 assert_eq!(pat.fold_lines.len(), 1);
997 assert_eq!(pat.facets.len(), 2);
998 assert!(pat.vertices.len() >= 4);
999 }
1000
1001 #[test]
1004 fn test_rigid_origami_initial_state() {
1005 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
1006 let ro = RigidOrigami::new(pat);
1007 assert!((ro.rho - 0.0).abs() < 1e-10);
1008 }
1009
1010 #[test]
1011 fn test_rigid_origami_step_clamps() {
1012 let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
1013 let mut ro = RigidOrigami::new(pat);
1014 ro.step(2.0); assert!((ro.rho - 1.0).abs() < 1e-10);
1016 }
1017
1018 #[test]
1019 fn test_rigid_origami_bounding_box() {
1020 let pat = build_single_fold_pattern(2.0, FoldType::Mountain);
1021 let ro = RigidOrigami::new(pat);
1022 let (lo, hi) = ro.bounding_box();
1023 assert!(hi.x >= lo.x);
1024 assert!(hi.y >= lo.y);
1025 assert!(hi.z >= lo.z);
1026 }
1027
1028 #[test]
1031 fn test_miura_ori_projected_dimensions_flat() {
1032 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1033 let (lx, _ly, lz) = cell.projected_dimensions(0.0);
1034 let expected_lx = 2.0 * (PI / 4.0_f64).sin();
1037 assert!((lx - expected_lx).abs() < 1e-10, "lx at flat, got {}", lx);
1038 assert!(lz > 0.0);
1039 }
1040
1041 #[test]
1042 fn test_miura_ori_coupled_angle_at_zero() {
1043 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1044 let phi = cell.coupled_angle(0.0);
1045 assert!(phi.abs() < 1e-10);
1046 }
1047
1048 #[test]
1049 fn test_miura_ori_vertex_count() {
1050 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1051 let miura = MiuraOri::new(cell, 3, 4);
1052 let verts = miura.vertex_positions();
1053 let expected = (2 * 3 + 2) * (2 * 4 + 2);
1054 assert_eq!(verts.len(), expected);
1055 }
1056
1057 #[test]
1058 fn test_miura_ori_poisson_ratio_negative() {
1059 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
1060 let mut miura = MiuraOri::new(cell, 2, 2);
1061 miura.set_angle(PI / 4.0);
1062 let nu = miura.poisson_ratio();
1063 assert!(
1064 nu <= 0.0,
1065 "Miura-ori Poisson ratio should be negative, got {}",
1066 nu
1067 );
1068 }
1069
1070 #[test]
1071 fn test_miura_ori_fold_lines_row() {
1072 let cell = MiuraUnitCell::new(1.0, 1.0, PI / 6.0);
1073 let miura = MiuraOri::new(cell, 2, 3);
1074 let lines = miura.fold_lines_row(0);
1075 assert!(!lines.is_empty());
1076 }
1077
1078 #[test]
1081 fn test_waterbomb_vertex_count() {
1082 let wb = WaterbombBase::new(1.0, 3, 4);
1083 let verts = wb.vertex_positions();
1084 assert_eq!(verts.len(), 4 * 5); }
1086
1087 #[test]
1088 fn test_waterbomb_flat_height_zero() {
1089 let wb = WaterbombBase::new(1.0, 2, 2);
1090 assert!((wb.folded_height()).abs() < 1e-10);
1091 }
1092
1093 #[test]
1094 fn test_waterbomb_footprint() {
1095 let wb = WaterbombBase::new(1.0, 2, 3);
1096 let (fx, fy) = wb.footprint();
1097 assert!((fx - 3.0).abs() < 1e-10); assert!((fy - 2.0).abs() < 1e-10);
1099 }
1100
1101 #[test]
1104 fn test_yoshimura_vertex_count() {
1105 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1106 let verts = yb.vertex_positions();
1107 assert_eq!(verts.len(), 6 * 5); }
1109
1110 #[test]
1111 fn test_yoshimura_axial_shortening_zero_depth() {
1112 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1113 assert!((yb.axial_shortening()).abs() < 1e-10);
1114 }
1115
1116 #[test]
1117 fn test_yoshimura_axial_wavelength() {
1118 let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
1119 assert!((yb.axial_wavelength() - 0.5).abs() < 1e-10);
1120 }
1121
1122 #[test]
1125 fn test_kirigami_rectangular_pattern_slit_count() {
1126 let kg = KirigamiCut::rectangular_pattern(2.0, 3.0, 3, 4, 0.8);
1127 assert_eq!(kg.num_slits(), 12);
1128 }
1129
1130 #[test]
1131 fn test_kirigami_stretch_updates_openings() {
1132 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
1133 kg.set_stretch(1.0);
1134 for slit in &kg.slits {
1135 assert!(slit.opening > 0.0);
1136 }
1137 }
1138
1139 #[test]
1140 fn test_kirigami_poisson_ratio_nonpositive() {
1141 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 3, 3, 0.8);
1142 kg.set_stretch(0.5);
1143 assert!(kg.poisson_ratio() <= 0.0);
1144 }
1145
1146 #[test]
1147 fn test_kirigami_stretchability_ge_one() {
1148 let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
1149 kg.set_stretch(0.5);
1150 assert!(kg.stretchability() >= 1.0);
1151 }
1152
1153 #[test]
1156 fn test_kawasaki_theorem_flat_vertex() {
1157 let angles = [PI / 4.0, PI / 4.0, PI / 4.0, PI / 4.0];
1159 let residual = OrigamiAnalysis::kawasaki_residual(&angles);
1161 assert!(residual < 1e-10, "residual={}", residual);
1162 }
1163
1164 #[test]
1165 fn test_maekawa_theorem_valid() {
1166 assert!(OrigamiAnalysis::maekawa_valid(3, 1));
1167 assert!(OrigamiAnalysis::maekawa_valid(1, 3));
1168 assert!(!OrigamiAnalysis::maekawa_valid(2, 2));
1169 }
1170
1171 #[test]
1172 fn test_angle_deficit_flat() {
1173 let angles = vec![PI / 3.0; 6];
1175 let deficit = OrigamiAnalysis::angle_deficit(&angles);
1176 assert!(deficit.abs() < 1e-10);
1177 }
1178
1179 #[test]
1180 fn test_miura_poisson_analytical() {
1181 let nu = OrigamiAnalysis::miura_poisson_ratio(PI / 4.0, PI / 4.0);
1182 assert!(nu < 0.0, "Miura Poisson should be negative, got {}", nu);
1183 }
1184
1185 #[test]
1186 fn test_fold_bending_energy_positive() {
1187 let e = OrigamiAnalysis::fold_bending_energy(1.0, 1.0, PI / 4.0);
1188 assert!(e > 0.0);
1189 }
1190
1191 #[test]
1192 fn test_fold_angle_from_normals_parallel() {
1193 let n = Vec3::new(0.0, 0.0, 1.0);
1194 let crease = Vec3::new(1.0, 0.0, 0.0);
1195 let angle = OrigamiAnalysis::fold_angle_from_normals(&n, &n, &crease);
1196 assert!(angle.abs() < 1e-10);
1197 }
1198
1199 #[test]
1200 fn test_kawasaki_fourth_angle() {
1201 let a4 = kawasaki_fourth_angle(PI / 4.0, PI / 4.0, PI / 4.0);
1202 assert!((a4 - PI / 4.0).abs() < 1e-10);
1203 }
1204}