Skip to main content

oxiphysics_geometry/
origami.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Origami and kirigami mechanics — fold pattern geometry, rigid origami kinematics,
5//! and tessellation analysis.
6//!
7//! Covers:
8//! - [`FoldLine`] — crease with mountain/valley type and fold angle
9//! - [`OrigamiPattern`] — full crease pattern with vertices, lines, facets
10//! - [`RigidOrigami`] — kinematic simulation via rotation matrices
11//! - [`MiuraOri`] — classic Miura-ori tessellation
12//! - [`WaterbombBase`] — waterbomb tessellation pleat geometry
13//! - [`YoshimuraBuckling`] — Yoshimura cylindrical buckling pattern
14//! - [`KirigamiCut`] — slit kirigami auxetic behaviour
15//! - [`OrigamiAnalysis`] — Gaussian curvature and Poisson's ratio of folded surfaces
16
17use oxiphysics_core::math::Vec3;
18use std::f64::consts::PI;
19
20// ---------------------------------------------------------------------------
21// FoldType
22// ---------------------------------------------------------------------------
23
24/// Mountain or valley classification for a crease.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum FoldType {
27    /// Mountain fold — the paper folds upward at this crease.
28    Mountain,
29    /// Valley fold — the paper folds downward at this crease.
30    Valley,
31    /// Boundary edge — not a fold line.
32    Boundary,
33}
34
35// ---------------------------------------------------------------------------
36// FoldLine
37// ---------------------------------------------------------------------------
38
39/// A single crease line in an origami pattern.
40///
41/// Stores the geometric position and direction of the fold, the current
42/// dihedral (fold) angle, and whether it is a mountain or valley crease.
43#[derive(Debug, Clone)]
44pub struct FoldLine {
45    /// Start point of the fold line (in reference flat configuration).
46    pub start: Vec3,
47    /// End point of the fold line (in reference flat configuration).
48    pub end: Vec3,
49    /// Unit direction from `start` to `end`.
50    pub direction: Vec3,
51    /// Dihedral fold angle in radians. 0 = flat, π = fully folded.
52    pub fold_angle: f64,
53    /// Mountain or valley type.
54    pub fold_type: FoldType,
55    /// Indices of the two facets on either side of this crease (if any).
56    pub adjacent_facets: [Option<usize>; 2],
57}
58
59impl FoldLine {
60    /// Create a new fold line from two end points.
61    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    /// Length of the crease segment.
80    pub fn length(&self) -> f64 {
81        (self.end - self.start).norm()
82    }
83
84    /// Mid-point of the crease.
85    pub fn midpoint(&self) -> Vec3 {
86        (self.start + self.end) * 0.5
87    }
88
89    /// Signed fold angle: positive for mountain, negative for valley.
90    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    /// Rotation matrix that rotates about the fold axis by `fold_angle`.
99    ///
100    /// Uses Rodrigues' rotation formula.
101    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    /// Apply the fold rotation to a point relative to the fold line start.
114    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// ---------------------------------------------------------------------------
127// OrigamiFacet
128// ---------------------------------------------------------------------------
129
130/// A planar facet (polygon) in an origami crease pattern.
131#[derive(Debug, Clone)]
132pub struct OrigamiFacet {
133    /// Ordered list of vertex indices forming the boundary of this facet.
134    pub vertex_indices: Vec<usize>,
135    /// Normal of the facet in the current folded configuration.
136    pub normal: Vec3,
137    /// Whether this facet is active (not cut away in kirigami).
138    pub active: bool,
139}
140
141impl OrigamiFacet {
142    /// Create a new facet from vertex indices with the default upward normal.
143    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    /// Number of sides.
152    pub fn num_sides(&self) -> usize {
153        self.vertex_indices.len()
154    }
155}
156
157// ---------------------------------------------------------------------------
158// OrigamiPattern
159// ---------------------------------------------------------------------------
160
161/// A complete crease pattern: vertices, fold lines, and facets.
162///
163/// The pattern stores both the flat (reference) configuration and can compute
164/// fold angles from an input folded state.
165#[derive(Debug, Clone)]
166pub struct OrigamiPattern {
167    /// Vertices in the flat (unfolded) configuration.
168    pub vertices: Vec<Vec3>,
169    /// All crease lines.
170    pub fold_lines: Vec<FoldLine>,
171    /// All planar facets.
172    pub facets: Vec<OrigamiFacet>,
173}
174
175impl OrigamiPattern {
176    /// Create an empty pattern.
177    pub fn new() -> Self {
178        Self {
179            vertices: Vec::new(),
180            fold_lines: Vec::new(),
181            facets: Vec::new(),
182        }
183    }
184
185    /// Add a vertex, returning its index.
186    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    /// Add a fold line, returning its index.
193    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    /// Add a facet, returning its index.
200    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    /// Compute the fold angle of crease `i` given two face normals on either side.
207    ///
208    /// Returns the dihedral angle in radians.
209    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    /// Set all fold angles proportionally to `t` ∈ \[0, 1\], where 0 = flat and
215    /// 1 = maximum angle (π for mountain/valley).
216    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    /// Return the total number of degrees of freedom (independent fold angles).
223    ///
224    /// For a generic crease pattern this is `F - 1` by Euler's formula, where `F`
225    /// is the number of interior vertices.
226    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    /// Compute approximate Gaussian curvature at vertex `vi` using the angle deficit.
232    ///
233    /// Returns `None` if the vertex index is out of range or has no adjacent facets.
234    pub fn gaussian_curvature_at(&self, _vi: usize) -> f64 {
235        // Placeholder: angle deficit = 2π - sum of sector angles
236        // For a flat sheet all Gaussian curvature is 0 except at fold vertices
237        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// ---------------------------------------------------------------------------
250// RigidOrigami
251// ---------------------------------------------------------------------------
252
253/// Kinematic simulation of rigid origami.
254///
255/// Each facet is treated as a rigid plate; only the fold lines deform.
256/// The configuration is parameterised by a single fold parameter `rho` ∈ \[0, 1\].
257#[derive(Debug, Clone)]
258pub struct RigidOrigami {
259    /// The underlying crease pattern.
260    pub pattern: OrigamiPattern,
261    /// Current positions of all vertices (folded configuration).
262    pub positions: Vec<Vec3>,
263    /// Current fold parameter.
264    pub rho: f64,
265}
266
267impl RigidOrigami {
268    /// Create from a crease pattern.
269    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    /// Step the fold parameter by `delta_rho`, clamping to \[0, 1\].
279    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    /// Recompute all vertex positions by propagating fold rotations.
286    ///
287    /// Uses a breadth-first traversal of the dual graph: the first facet is
288    /// fixed, and each subsequent facet is rotated about the shared crease.
289    pub fn update_positions(&mut self) {
290        // Reset to flat configuration
291        for (i, v) in self.pattern.vertices.iter().enumerate() {
292            self.positions[i] = *v;
293        }
294        // Apply each fold line in order (simplified: rotate all vertices on one side)
295        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            // Rotate vertices that are on the "positive" side of the fold
309            for pos in &mut self.positions {
310                let local = *pos - fl.start;
311                // Only rotate if on the positive half-plane (dot with a perpendicular direction)
312                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    /// Return the bounding box of the current folded configuration.
324    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// ---------------------------------------------------------------------------
340// MiuraOri
341// ---------------------------------------------------------------------------
342
343/// Unit cell parameters for the Miura-ori tessellation.
344#[derive(Debug, Clone, Copy)]
345pub struct MiuraUnitCell {
346    /// Side length a.
347    pub a: f64,
348    /// Side length b.
349    pub b: f64,
350    /// Acute angle of the parallelogram unit cell (radians).
351    pub alpha: f64,
352}
353
354impl MiuraUnitCell {
355    /// Create a new unit cell.
356    pub fn new(a: f64, b: f64, alpha: f64) -> Self {
357        Self { a, b, alpha }
358    }
359
360    /// Compute the projected (x, y, z) dimensions for fold angle θ (0 = flat, π/2 = compact).
361    ///
362    /// Returns `(lx, ly, lz)` where lx is the projected length along the fold direction.
363    pub fn projected_dimensions(&self, theta: f64) -> (f64, f64, f64) {
364        // Classic Miura-ori kinematics
365        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        // Auxiliary angle γ satisfying: cos γ = cos α sin θ / √(1 - sin²α sin²θ) ... wait
371        // Standard Miura formula: projected panel angles
372        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    /// The fold angle coupling: given the longitudinal fold angle θ, compute the
380    /// transverse fold angle φ.
381    ///
382    /// The Miura coupling relation: tan(φ/2) = cos α · tan(θ/2).
383    pub fn coupled_angle(&self, theta: f64) -> f64 {
384        2.0 * (self.alpha.cos() * (theta / 2.0).tan()).atan()
385    }
386}
387
388/// Miura-ori tessellation over an (m × n) lattice.
389#[derive(Debug, Clone)]
390pub struct MiuraOri {
391    /// Unit cell parameters.
392    pub cell: MiuraUnitCell,
393    /// Number of unit cells in the x direction.
394    pub m: usize,
395    /// Number of unit cells in the y direction.
396    pub n: usize,
397    /// Current fold angle θ (0 = flat, π/2 = compact).
398    pub theta: f64,
399}
400
401impl MiuraOri {
402    /// Create a Miura-ori tessellation.
403    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    /// Set the fold angle.
413    pub fn set_angle(&mut self, theta: f64) {
414        self.theta = theta.clamp(0.0, PI / 2.0);
415    }
416
417    /// Generate all vertex positions for the current fold angle.
418    ///
419    /// Returns a flat `Vec`Vec3` in row-major order: vertex `(i, j)` is at
420    /// index `i * (2*n + 2) + j` in the output.  There are `(2m+2) × (2n+2)` vertices.
421    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                // Alternating height pattern
434                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    /// Poisson's ratio of the tessellation in the longitudinal direction.
444    ///
445    /// Returns ν_xy = −(dε_y/dε_x) evaluated at the current fold angle.
446    pub fn poisson_ratio(&self) -> f64 {
447        // Analytical result for Miura-ori:
448        // ν_xy = −sin²α / (1 − sin²α sin²θ)  (approximate; exact form differs by factor)
449        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    /// Total number of unit cells.
459    pub fn num_cells(&self) -> usize {
460        self.m * self.n
461    }
462
463    /// Return fold lines for one row of the tessellation at the current angle.
464    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; // suppress warning
481                lines.push(fl);
482            }
483        }
484        lines
485    }
486}
487
488// ---------------------------------------------------------------------------
489// WaterbombBase
490// ---------------------------------------------------------------------------
491
492/// Parameters for the waterbomb tessellation.
493#[derive(Debug, Clone, Copy)]
494pub struct WaterbombBase {
495    /// Size of each square pleat panel.
496    pub panel_size: f64,
497    /// Number of pleat rows.
498    pub rows: usize,
499    /// Number of pleat columns.
500    pub cols: usize,
501    /// Pleat fold angle in radians.
502    pub pleat_angle: f64,
503}
504
505impl WaterbombBase {
506    /// Create a new waterbomb tessellation.
507    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    /// Set the pleat fold angle (0 = flat, π/4 = maximum pleat).
517    pub fn set_pleat_angle(&mut self, angle: f64) {
518        self.pleat_angle = angle.clamp(0.0, PI / 4.0);
519    }
520
521    /// Compute the vertex coordinates in the folded configuration.
522    ///
523    /// Returns `(rows+1) × (cols+1)` vertices.
524    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                // Alternating raised/lowered pattern for waterbomb
532                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    /// Folded height (z-extent) of the tessellation.
546    pub fn folded_height(&self) -> f64 {
547        self.panel_size * self.pleat_angle.sin() * 2.0
548    }
549
550    /// Projected footprint (x, y) of the tessellation.
551    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    /// Compactness ratio (folded height / flat span).
557    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// ---------------------------------------------------------------------------
567// YoshimuraBuckling
568// ---------------------------------------------------------------------------
569
570/// Yoshimura buckling pattern for a thin cylindrical shell.
571///
572/// The diamond-shaped crease pattern arises from axial compression of a cylinder.
573#[derive(Debug, Clone)]
574pub struct YoshimuraBuckling {
575    /// Cylinder radius.
576    pub radius: f64,
577    /// Cylinder length.
578    pub length: f64,
579    /// Number of circumferential lobes.
580    pub n_lobes: usize,
581    /// Number of axial repetitions.
582    pub n_axial: usize,
583    /// Fold depth parameter (0 = flat, 1 = maximum indentation).
584    pub fold_depth: f64,
585}
586
587impl YoshimuraBuckling {
588    /// Create a new Yoshimura buckling pattern.
589    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    /// Set the fold depth parameter.
600    pub fn set_fold_depth(&mut self, d: f64) {
601        self.fold_depth = d.clamp(0.0, 1.0);
602    }
603
604    /// Half-angle of one diamond cell.
605    pub fn diamond_half_angle(&self) -> f64 {
606        PI / self.n_lobes as f64
607    }
608
609    /// Axial wavelength of the buckling pattern.
610    pub fn axial_wavelength(&self) -> f64 {
611        self.length / self.n_axial as f64
612    }
613
614    /// Radial indentation at maximum fold depth for one diamond.
615    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    /// Generate vertex positions on the buckled cylinder surface.
621    ///
622    /// Returns `(n_lobes * n_axial * 4)` diamond vertices approximately.
623    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                // Indented radius for alternating rows
635                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    /// Estimated axial shortening for the given fold depth.
647    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// ---------------------------------------------------------------------------
656// KirigamiCut
657// ---------------------------------------------------------------------------
658
659/// A single slit cut in a kirigami sheet.
660#[derive(Debug, Clone)]
661pub struct KirigamiSlit {
662    /// Start point of the slit.
663    pub start: Vec3,
664    /// End point of the slit.
665    pub end: Vec3,
666    /// Width of the slit opening at maximum stretch.
667    pub opening: f64,
668}
669
670impl KirigamiSlit {
671    /// Create a new slit.
672    pub fn new(start: Vec3, end: Vec3) -> Self {
673        Self {
674            start,
675            end,
676            opening: 0.0,
677        }
678    }
679
680    /// Length of the slit.
681    pub fn length(&self) -> f64 {
682        (self.end - self.start).norm()
683    }
684}
685
686/// Kirigami sheet with an array of cuts creating auxetic behaviour.
687#[derive(Debug, Clone)]
688pub struct KirigamiCut {
689    /// Sheet width.
690    pub width: f64,
691    /// Sheet height.
692    pub height: f64,
693    /// All slits in the pattern.
694    pub slits: Vec<KirigamiSlit>,
695    /// Current stretch parameter (0 = undeformed, 1 = maximum stretch).
696    pub stretch: f64,
697}
698
699impl KirigamiCut {
700    /// Create an empty kirigami sheet.
701    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    /// Create a regular rectangular slit pattern with `rows × cols` slits.
711    ///
712    /// Slits are staggered in alternating rows.
713    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    /// Set the stretch parameter.
738    pub fn set_stretch(&mut self, s: f64) {
739        self.stretch = s.clamp(0.0, 1.0);
740        // Update slit openings proportionally
741        for sl in &mut self.slits {
742            sl.opening = sl.length() * s * 0.5;
743        }
744    }
745
746    /// Effective Poisson's ratio of the kirigami sheet at the current stretch.
747    ///
748    /// For a rectangular slit pattern the auxetic ν ≈ −(fractional area cut) · stretch.
749    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    /// Stretchability: ratio of stretched length to original length.
757    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    /// Number of slits.
764    pub fn num_slits(&self) -> usize {
765        self.slits.len()
766    }
767}
768
769// ---------------------------------------------------------------------------
770// OrigamiAnalysis
771// ---------------------------------------------------------------------------
772
773/// Analysis tools for folded origami surfaces.
774///
775/// Computes geometric properties such as Gaussian curvature (angle deficit at
776/// vertices) and the effective Poisson's ratio of the tessellation.
777#[derive(Debug, Clone)]
778pub struct OrigamiAnalysis;
779
780impl OrigamiAnalysis {
781    /// Compute the angle deficit (discrete Gaussian curvature) at a vertex,
782    /// given the sector angles (in radians) of the faces meeting at that vertex.
783    ///
784    /// For a flat sheet the deficit is 0; a cone tip has a positive deficit.
785    pub fn angle_deficit(sector_angles: &[f64]) -> f64 {
786        let sum: f64 = sector_angles.iter().sum();
787        2.0 * PI - sum
788    }
789
790    /// Check Kawasaki's theorem for a crease pattern at an interior vertex.
791    ///
792    /// For flat-foldability the alternating sum of sector angles must equal π.
793    /// Returns the residual (should be ≈ 0 for a flat-foldable vertex).
794    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    /// Check Maekawa's theorem: at an interior vertex the number of mountain
804    /// folds and valley folds must differ by exactly 2.
805    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    /// Compute the effective in-plane Poisson's ratio of a Miura-ori sheet.
811    ///
812    /// Uses the analytical closed form for the Miura-ori.
813    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    /// Estimate the bending energy of a fold line given fold angle and panel stiffness.
824    ///
825    /// `k` is the rotational stiffness per unit length (N·m/m), `length` is the
826    /// crease length (m), `theta` is the fold angle (radians).
827    pub fn fold_bending_energy(k: f64, length: f64, theta: f64) -> f64 {
828        0.5 * k * length * theta * theta
829    }
830
831    /// Compute the Gaussian curvature of a parallelogram panel given its vertex positions.
832    ///
833    /// For a flat (non-curved) panel, returns 0.
834    pub fn panel_gaussian_curvature(v0: &Vec3, v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
835        // Use the discrete formulation: K ≈ 0 for flat panels
836        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    /// Compute the fold angle between two planar facets given their unit normals and the
844    /// shared crease direction.
845    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        // Determine sign from cross product relative to crease direction
849        let cross = n1.cross(n2);
850        if cross.dot(crease_dir) < 0.0 {
851            -angle
852        } else {
853            angle
854        }
855    }
856}
857
858// ---------------------------------------------------------------------------
859// Helper free functions
860// ---------------------------------------------------------------------------
861
862/// Build a simple square crease pattern with one central fold line.
863///
864/// Returns an `OrigamiPattern` with 4 vertices, 1 fold line, and 2 facets.
865pub 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
883/// Compute the fold angle coupling for a generic degree-4 vertex.
884///
885/// Given three of the four sector angles at an interior vertex (in order),
886/// returns the fourth sector angle required by Kawasaki's theorem.
887pub fn kawasaki_fourth_angle(a1: f64, a2: f64, a3: f64) -> f64 {
888    // a1 - a2 + a3 - a4 = 0  ⟹  a4 = a1 - a2 + a3
889    (a1 - a2 + a3).rem_euclid(2.0 * PI)
890}
891
892// ---------------------------------------------------------------------------
893// Tests
894// ---------------------------------------------------------------------------
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899
900    // --- FoldLine tests ---
901
902    #[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        // Rotating y-axis by 90° about x-axis: y→z
950        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    // --- OrigamiPattern tests ---
966
967    #[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    // --- RigidOrigami tests ---
1000
1001    #[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); // Should clamp to 1.0
1013        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    // --- MiuraOri tests ---
1027
1028    #[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        // At θ=0, cos_theta=1, sin_theta=0 → denom=1
1033        // lx = 2*a*sin(alpha)*cos(0)/1 = 2*sin(PI/4) = √2
1034        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    // --- WaterbombBase tests ---
1077
1078    #[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); // (rows+1)*(cols+1)
1083    }
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); // cols * panel_size (at angle=0)
1096        assert!((fy - 2.0).abs() < 1e-10);
1097    }
1098
1099    // --- YoshimuraBuckling tests ---
1100
1101    #[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); // n_lobes * (n_axial+1)
1106    }
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    // --- KirigamiCut tests ---
1121
1122    #[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    // --- OrigamiAnalysis tests ---
1152
1153    #[test]
1154    fn test_kawasaki_theorem_flat_vertex() {
1155        // For a flat-foldable 4-crease vertex, alternating angles sum to π
1156        let angles = [PI / 4.0, PI / 4.0, PI / 4.0, PI / 4.0];
1157        // All equal → residual = 0
1158        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        // Six equilateral triangle sectors: sum = 2π → deficit = 0
1172        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}