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