use oxiphysics_core::math::Vec3;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FoldType {
Mountain,
Valley,
Boundary,
}
#[derive(Debug, Clone)]
pub struct FoldLine {
pub start: Vec3,
pub end: Vec3,
pub direction: Vec3,
pub fold_angle: f64,
pub fold_type: FoldType,
pub adjacent_facets: [Option<usize>; 2],
}
impl FoldLine {
pub fn new(start: Vec3, end: Vec3, fold_type: FoldType) -> Self {
let diff = end - start;
let len = diff.norm();
let direction = if len > 1e-12 {
diff / len
} else {
Vec3::new(1.0, 0.0, 0.0)
};
Self {
start,
end,
direction,
fold_angle: 0.0,
fold_type,
adjacent_facets: [None, None],
}
}
pub fn length(&self) -> f64 {
(self.end - self.start).norm()
}
pub fn midpoint(&self) -> Vec3 {
(self.start + self.end) * 0.5
}
pub fn signed_angle(&self) -> f64 {
match self.fold_type {
FoldType::Mountain => self.fold_angle,
FoldType::Valley => -self.fold_angle,
FoldType::Boundary => 0.0,
}
}
pub fn rotation_matrix(&self) -> [[f64; 3]; 3] {
let theta = self.signed_angle();
let (s, c) = theta.sin_cos();
let t = 1.0 - c;
let (ux, uy, uz) = (self.direction.x, self.direction.y, self.direction.z);
[
[t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
[t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
[t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
]
}
pub fn apply_rotation(&self, point: &Vec3) -> Vec3 {
let r = self.rotation_matrix();
let p = point - self.start;
let rotated = Vec3::new(
r[0][0] * p.x + r[0][1] * p.y + r[0][2] * p.z,
r[1][0] * p.x + r[1][1] * p.y + r[1][2] * p.z,
r[2][0] * p.x + r[2][1] * p.y + r[2][2] * p.z,
);
rotated + self.start
}
}
#[derive(Debug, Clone)]
pub struct OrigamiFacet {
pub vertex_indices: Vec<usize>,
pub normal: Vec3,
pub active: bool,
}
impl OrigamiFacet {
pub fn new(vertex_indices: Vec<usize>) -> Self {
Self {
vertex_indices,
normal: Vec3::new(0.0, 0.0, 1.0),
active: true,
}
}
pub fn num_sides(&self) -> usize {
self.vertex_indices.len()
}
}
#[derive(Debug, Clone)]
pub struct OrigamiPattern {
pub vertices: Vec<Vec3>,
pub fold_lines: Vec<FoldLine>,
pub facets: Vec<OrigamiFacet>,
}
impl OrigamiPattern {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
fold_lines: Vec::new(),
facets: Vec::new(),
}
}
pub fn add_vertex(&mut self, v: Vec3) -> usize {
let idx = self.vertices.len();
self.vertices.push(v);
idx
}
pub fn add_fold_line(&mut self, fl: FoldLine) -> usize {
let idx = self.fold_lines.len();
self.fold_lines.push(fl);
idx
}
pub fn add_facet(&mut self, facet: OrigamiFacet) -> usize {
let idx = self.facets.len();
self.facets.push(facet);
idx
}
pub fn dihedral_angle(n1: &Vec3, n2: &Vec3) -> f64 {
let cos_theta = n1.dot(n2).clamp(-1.0, 1.0);
cos_theta.acos()
}
pub fn set_fold_parameter(&mut self, t: f64) {
for fl in &mut self.fold_lines {
fl.fold_angle = t * PI;
}
}
pub fn degrees_of_freedom(&self) -> usize {
let interior_vertices = self.vertices.len().saturating_sub(4);
interior_vertices.max(1)
}
pub fn gaussian_curvature_at(&self, _vi: usize) -> f64 {
let sector_angle_sum: f64 = self.fold_lines.iter().map(|fl| fl.fold_angle.abs()).sum();
let n = self.fold_lines.len().max(1) as f64;
2.0 * PI - sector_angle_sum / n
}
}
impl Default for OrigamiPattern {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RigidOrigami {
pub pattern: OrigamiPattern,
pub positions: Vec<Vec3>,
pub rho: f64,
}
impl RigidOrigami {
pub fn new(pattern: OrigamiPattern) -> Self {
let positions = pattern.vertices.clone();
Self {
pattern,
positions,
rho: 0.0,
}
}
pub fn step(&mut self, delta_rho: f64) {
self.rho = (self.rho + delta_rho).clamp(0.0, 1.0);
self.pattern.set_fold_parameter(self.rho);
self.update_positions();
}
pub fn update_positions(&mut self) {
for (i, v) in self.pattern.vertices.iter().enumerate() {
self.positions[i] = *v;
}
for fl in &self.pattern.fold_lines {
let theta = fl.signed_angle();
if theta.abs() < 1e-12 {
continue;
}
let (s, c) = theta.sin_cos();
let t = 1.0 - c;
let (ux, uy, uz) = (fl.direction.x, fl.direction.y, fl.direction.z);
let rot = [
[t * ux * ux + c, t * ux * uy - s * uz, t * ux * uz + s * uy],
[t * ux * uy + s * uz, t * uy * uy + c, t * uy * uz - s * ux],
[t * ux * uz - s * uy, t * uy * uz + s * ux, t * uz * uz + c],
];
for pos in &mut self.positions {
let local = *pos - fl.start;
let perp = Vec3::new(-fl.direction.y, fl.direction.x, 0.0);
if local.dot(&perp) > 0.0 {
let rx = rot[0][0] * local.x + rot[0][1] * local.y + rot[0][2] * local.z;
let ry = rot[1][0] * local.x + rot[1][1] * local.y + rot[1][2] * local.z;
let rz = rot[2][0] * local.x + rot[2][1] * local.y + rot[2][2] * local.z;
*pos = Vec3::new(rx, ry, rz) + fl.start;
}
}
}
}
pub fn bounding_box(&self) -> (Vec3, Vec3) {
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for v in &self.positions {
lo.x = lo.x.min(v.x);
lo.y = lo.y.min(v.y);
lo.z = lo.z.min(v.z);
hi.x = hi.x.max(v.x);
hi.y = hi.y.max(v.y);
hi.z = hi.z.max(v.z);
}
(lo, hi)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MiuraUnitCell {
pub a: f64,
pub b: f64,
pub alpha: f64,
}
impl MiuraUnitCell {
pub fn new(a: f64, b: f64, alpha: f64) -> Self {
Self { a, b, alpha }
}
pub fn projected_dimensions(&self, theta: f64) -> (f64, f64, f64) {
let sin_alpha = self.alpha.sin();
let cos_alpha = self.alpha.cos();
let sin_theta = theta.sin();
let cos_theta = theta.cos();
let denom = (1.0 - (sin_alpha * sin_theta).powi(2)).sqrt().max(1e-12);
let lx = 2.0 * self.a * sin_alpha * cos_theta / denom;
let ly = 2.0 * self.b * sin_alpha;
let lz = 2.0 * self.a * cos_alpha / denom;
(lx, ly, lz)
}
pub fn coupled_angle(&self, theta: f64) -> f64 {
2.0 * (self.alpha.cos() * (theta / 2.0).tan()).atan()
}
}
#[derive(Debug, Clone)]
pub struct MiuraOri {
pub cell: MiuraUnitCell,
pub m: usize,
pub n: usize,
pub theta: f64,
}
impl MiuraOri {
pub fn new(cell: MiuraUnitCell, m: usize, n: usize) -> Self {
Self {
cell,
m,
n,
theta: 0.0,
}
}
pub fn set_angle(&mut self, theta: f64) {
self.theta = theta.clamp(0.0, PI / 2.0);
}
pub fn vertex_positions(&self) -> Vec<Vec3> {
let (lx, ly, lz) = self.cell.projected_dimensions(self.theta);
let phi = self.cell.coupled_angle(self.theta);
let sin_phi = phi.sin();
let cos_phi = phi.cos();
let rows = 2 * self.m + 2;
let cols = 2 * self.n + 2;
let mut verts = Vec::with_capacity(rows * cols);
for i in 0..rows {
for j in 0..cols {
let zi = if (i + j) % 2 == 0 { 0.0 } else { lz };
let xi = (j as f64) * lx * 0.5;
let yi = (i as f64) * ly * 0.5 * cos_phi + zi * sin_phi;
verts.push(Vec3::new(xi, yi, zi));
}
}
verts
}
pub fn poisson_ratio(&self) -> f64 {
let sin_a = self.cell.alpha.sin();
let sin_t = self.theta.sin();
let denom = 1.0 - (sin_a * sin_t).powi(2);
if denom.abs() < 1e-12 {
return 0.0;
}
-(sin_a.powi(2)) / denom
}
pub fn num_cells(&self) -> usize {
self.m * self.n
}
pub fn fold_lines_row(&self, row: usize) -> Vec<FoldLine> {
let (lx, _ly, _lz) = self.cell.projected_dimensions(self.theta);
let verts = self.vertex_positions();
let cols = 2 * self.n + 2;
let mut lines = Vec::new();
for j in 0..cols - 1 {
let i0 = row * cols + j;
let i1 = row * cols + j + 1;
if i0 < verts.len() && i1 < verts.len() {
let ft = if j % 2 == 0 {
FoldType::Mountain
} else {
FoldType::Valley
};
let mut fl = FoldLine::new(verts[i0], verts[i1], ft);
fl.fold_angle = self.theta;
let _ = lx; lines.push(fl);
}
}
lines
}
}
#[derive(Debug, Clone, Copy)]
pub struct WaterbombBase {
pub panel_size: f64,
pub rows: usize,
pub cols: usize,
pub pleat_angle: f64,
}
impl WaterbombBase {
pub fn new(panel_size: f64, rows: usize, cols: usize) -> Self {
Self {
panel_size,
rows,
cols,
pleat_angle: 0.0,
}
}
pub fn set_pleat_angle(&mut self, angle: f64) {
self.pleat_angle = angle.clamp(0.0, PI / 4.0);
}
pub fn vertex_positions(&self) -> Vec<Vec3> {
let s = self.panel_size;
let cos_p = self.pleat_angle.cos();
let sin_p = self.pleat_angle.sin();
let mut verts = Vec::new();
for i in 0..=self.rows {
for j in 0..=self.cols {
let z = if (i + j) % 2 == 0 {
s * sin_p
} else {
-s * sin_p
};
let x = j as f64 * s * cos_p;
let y = i as f64 * s * cos_p;
verts.push(Vec3::new(x, y, z));
}
}
verts
}
pub fn folded_height(&self) -> f64 {
self.panel_size * self.pleat_angle.sin() * 2.0
}
pub fn footprint(&self) -> (f64, f64) {
let s = self.panel_size * self.pleat_angle.cos();
(self.cols as f64 * s, self.rows as f64 * s)
}
pub fn compactness_ratio(&self) -> f64 {
let flat_height = self.panel_size * self.rows as f64;
if flat_height < 1e-12 {
return 0.0;
}
self.folded_height() / flat_height
}
}
#[derive(Debug, Clone)]
pub struct YoshimuraBuckling {
pub radius: f64,
pub length: f64,
pub n_lobes: usize,
pub n_axial: usize,
pub fold_depth: f64,
}
impl YoshimuraBuckling {
pub fn new(radius: f64, length: f64, n_lobes: usize, n_axial: usize) -> Self {
Self {
radius,
length,
n_lobes,
n_axial,
fold_depth: 0.0,
}
}
pub fn set_fold_depth(&mut self, d: f64) {
self.fold_depth = d.clamp(0.0, 1.0);
}
pub fn diamond_half_angle(&self) -> f64 {
PI / self.n_lobes as f64
}
pub fn axial_wavelength(&self) -> f64 {
self.length / self.n_axial as f64
}
pub fn radial_indentation(&self) -> f64 {
let phi = self.diamond_half_angle();
self.radius * (1.0 - phi.cos()) * self.fold_depth
}
pub fn vertex_positions(&self) -> Vec<Vec3> {
let n = self.n_lobes;
let m = self.n_axial;
let mut verts = Vec::with_capacity(n * (m + 1));
let dtheta = 2.0 * PI / n as f64;
let dz = self.length / m as f64;
for k in 0..=m {
let z = k as f64 * dz;
for i in 0..n {
let theta = i as f64 * dtheta + if k % 2 == 1 { dtheta / 2.0 } else { 0.0 };
let r = if k % 2 == 1 {
self.radius - self.radial_indentation()
} else {
self.radius
};
verts.push(Vec3::new(r * theta.cos(), r * theta.sin(), z));
}
}
verts
}
pub fn axial_shortening(&self) -> f64 {
let phi = self.diamond_half_angle();
let dz = self.axial_wavelength();
let shortening_per_cell = dz * (1.0 - phi.cos()) * self.fold_depth;
shortening_per_cell * self.n_axial as f64
}
}
#[derive(Debug, Clone)]
pub struct KirigamiSlit {
pub start: Vec3,
pub end: Vec3,
pub opening: f64,
}
impl KirigamiSlit {
pub fn new(start: Vec3, end: Vec3) -> Self {
Self {
start,
end,
opening: 0.0,
}
}
pub fn length(&self) -> f64 {
(self.end - self.start).norm()
}
}
#[derive(Debug, Clone)]
pub struct KirigamiCut {
pub width: f64,
pub height: f64,
pub slits: Vec<KirigamiSlit>,
pub stretch: f64,
}
impl KirigamiCut {
pub fn new(width: f64, height: f64) -> Self {
Self {
width,
height,
slits: Vec::new(),
stretch: 0.0,
}
}
pub fn rectangular_pattern(
width: f64,
height: f64,
rows: usize,
cols: usize,
slit_frac: f64,
) -> Self {
let mut sheet = Self::new(width, height);
let dx = width / cols as f64;
let dy = height / rows as f64;
let slit_len = dx * slit_frac;
for r in 0..rows {
for c in 0..cols {
let cx = (c as f64 + 0.5) * dx + if r % 2 == 1 { dx * 0.5 } else { 0.0 };
let cy = (r as f64 + 0.5) * dy;
let half = slit_len * 0.5;
let start = Vec3::new(cx - half, cy, 0.0);
let end = Vec3::new(cx + half, cy, 0.0);
sheet.slits.push(KirigamiSlit::new(start, end));
}
}
sheet
}
pub fn set_stretch(&mut self, s: f64) {
self.stretch = s.clamp(0.0, 1.0);
for sl in &mut self.slits {
sl.opening = sl.length() * s * 0.5;
}
}
pub fn poisson_ratio(&self) -> f64 {
let total_cut_length: f64 = self.slits.iter().map(|s| s.length()).sum();
let sheet_perimeter = 2.0 * (self.width + self.height);
let cut_fraction = (total_cut_length / sheet_perimeter).min(1.0);
-cut_fraction * self.stretch
}
pub fn stretchability(&self) -> f64 {
let avg_opening: f64 =
self.slits.iter().map(|s| s.opening).sum::<f64>() / self.slits.len().max(1) as f64;
1.0 + avg_opening / (self.height / self.slits.len().max(1) as f64).max(1e-12)
}
pub fn num_slits(&self) -> usize {
self.slits.len()
}
}
#[derive(Debug, Clone)]
pub struct OrigamiAnalysis;
impl OrigamiAnalysis {
pub fn angle_deficit(sector_angles: &[f64]) -> f64 {
let sum: f64 = sector_angles.iter().sum();
2.0 * PI - sum
}
pub fn kawasaki_residual(sector_angles: &[f64]) -> f64 {
if sector_angles.len() < 2 {
return f64::INFINITY;
}
let even: f64 = sector_angles.iter().step_by(2).sum();
let odd: f64 = sector_angles.iter().skip(1).step_by(2).sum();
(even - odd).abs()
}
pub fn maekawa_valid(mountain_count: usize, valley_count: usize) -> bool {
let diff = (mountain_count as isize - valley_count as isize).abs();
diff == 2
}
pub fn miura_poisson_ratio(alpha: f64, theta: f64) -> f64 {
let sin_a = alpha.sin();
let sin_t = theta.sin();
let denom = 1.0 - (sin_a * sin_t).powi(2);
if denom.abs() < 1e-12 {
return 0.0;
}
-(sin_a.powi(2)) / denom
}
pub fn fold_bending_energy(k: f64, length: f64, theta: f64) -> f64 {
0.5 * k * length * theta * theta
}
pub fn panel_gaussian_curvature(v0: &Vec3, v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
let n1 = (v1 - v0).cross(&(v2 - v0));
let n2 = (v2 - v0).cross(&(v3 - v0));
let cos_theta = (n1.dot(&n2) / (n1.norm() * n2.norm() + 1e-12)).clamp(-1.0, 1.0);
let angle = cos_theta.acos();
angle / (v0 - v2).norm().max(1e-12).powi(2)
}
pub fn fold_angle_from_normals(n1: &Vec3, n2: &Vec3, crease_dir: &Vec3) -> f64 {
let cos_a = n1.dot(n2).clamp(-1.0, 1.0);
let angle = cos_a.acos();
let cross = n1.cross(n2);
if cross.dot(crease_dir) < 0.0 {
-angle
} else {
angle
}
}
}
pub fn build_single_fold_pattern(side: f64, fold_type: FoldType) -> OrigamiPattern {
let mut pat = OrigamiPattern::new();
let v0 = pat.add_vertex(Vec3::new(0.0, 0.0, 0.0));
let v1 = pat.add_vertex(Vec3::new(side, 0.0, 0.0));
let v2 = pat.add_vertex(Vec3::new(side, side, 0.0));
let v3 = pat.add_vertex(Vec3::new(0.0, side, 0.0));
let vm = pat.add_vertex(Vec3::new(side * 0.5, 0.0, 0.0));
let vm2 = pat.add_vertex(Vec3::new(side * 0.5, side, 0.0));
let fold_start = pat.vertices[vm];
let fold_end = pat.vertices[vm2];
pat.add_fold_line(FoldLine::new(fold_start, fold_end, fold_type));
pat.add_facet(OrigamiFacet::new(vec![v0, vm, vm2, v3]));
pat.add_facet(OrigamiFacet::new(vec![vm, v1, v2, vm2]));
pat
}
pub fn kawasaki_fourth_angle(a1: f64, a2: f64, a3: f64) -> f64 {
(a1 - a2 + a3).rem_euclid(2.0 * PI)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fold_line_length() {
let fl = FoldLine::new(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(3.0, 4.0, 0.0),
FoldType::Mountain,
);
assert!((fl.length() - 5.0).abs() < 1e-10);
}
#[test]
fn test_fold_line_midpoint() {
let fl = FoldLine::new(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
FoldType::Valley,
);
let mp = fl.midpoint();
assert!((mp.x - 1.0).abs() < 1e-10);
assert!(mp.y.abs() < 1e-10);
}
#[test]
fn test_fold_line_rotation_identity_at_zero() {
let mut fl = FoldLine::new(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
FoldType::Mountain,
);
fl.fold_angle = 0.0;
let p = Vec3::new(0.0, 1.0, 0.0);
let r = fl.apply_rotation(&p);
assert!((r.x).abs() < 1e-10);
assert!((r.y - 1.0).abs() < 1e-10);
assert!(r.z.abs() < 1e-10);
}
#[test]
fn test_fold_line_rotation_90_degrees() {
let mut fl = FoldLine::new(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
FoldType::Mountain,
);
fl.fold_angle = PI / 2.0;
let p = Vec3::new(0.0, 1.0, 0.0);
let r = fl.apply_rotation(&p);
assert!(r.y.abs() < 1e-10, "y should be ~0, got {}", r.y);
assert!((r.z - 1.0).abs() < 1e-10, "z should be ~1, got {}", r.z);
}
#[test]
fn test_fold_type_signed_angle() {
let mut fl_m = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Mountain);
fl_m.fold_angle = 0.5;
assert!((fl_m.signed_angle() - 0.5).abs() < 1e-10);
let mut fl_v = FoldLine::new(Vec3::zeros(), Vec3::new(1.0, 0.0, 0.0), FoldType::Valley);
fl_v.fold_angle = 0.5;
assert!((fl_v.signed_angle() + 0.5).abs() < 1e-10);
}
#[test]
fn test_origami_pattern_add_vertex() {
let mut pat = OrigamiPattern::new();
let i = pat.add_vertex(Vec3::new(1.0, 2.0, 3.0));
assert_eq!(i, 0);
assert_eq!(pat.vertices.len(), 1);
}
#[test]
fn test_origami_pattern_dihedral_angle() {
let n1 = Vec3::new(0.0, 0.0, 1.0);
let n2 = Vec3::new(0.0, 0.0, 1.0);
let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
assert!(angle.abs() < 1e-10);
}
#[test]
fn test_origami_pattern_dihedral_angle_90() {
let n1 = Vec3::new(0.0, 0.0, 1.0);
let n2 = Vec3::new(0.0, 1.0, 0.0);
let angle = OrigamiPattern::dihedral_angle(&n1, &n2);
assert!((angle - PI / 2.0).abs() < 1e-10);
}
#[test]
fn test_single_fold_pattern_structure() {
let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
assert_eq!(pat.fold_lines.len(), 1);
assert_eq!(pat.facets.len(), 2);
assert!(pat.vertices.len() >= 4);
}
#[test]
fn test_rigid_origami_initial_state() {
let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
let ro = RigidOrigami::new(pat);
assert!((ro.rho - 0.0).abs() < 1e-10);
}
#[test]
fn test_rigid_origami_step_clamps() {
let pat = build_single_fold_pattern(1.0, FoldType::Mountain);
let mut ro = RigidOrigami::new(pat);
ro.step(2.0); assert!((ro.rho - 1.0).abs() < 1e-10);
}
#[test]
fn test_rigid_origami_bounding_box() {
let pat = build_single_fold_pattern(2.0, FoldType::Mountain);
let ro = RigidOrigami::new(pat);
let (lo, hi) = ro.bounding_box();
assert!(hi.x >= lo.x);
assert!(hi.y >= lo.y);
assert!(hi.z >= lo.z);
}
#[test]
fn test_miura_ori_projected_dimensions_flat() {
let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
let (lx, _ly, lz) = cell.projected_dimensions(0.0);
let expected_lx = 2.0 * (PI / 4.0_f64).sin();
assert!((lx - expected_lx).abs() < 1e-10, "lx at flat, got {}", lx);
assert!(lz > 0.0);
}
#[test]
fn test_miura_ori_coupled_angle_at_zero() {
let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
let phi = cell.coupled_angle(0.0);
assert!(phi.abs() < 1e-10);
}
#[test]
fn test_miura_ori_vertex_count() {
let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
let miura = MiuraOri::new(cell, 3, 4);
let verts = miura.vertex_positions();
let expected = (2 * 3 + 2) * (2 * 4 + 2);
assert_eq!(verts.len(), expected);
}
#[test]
fn test_miura_ori_poisson_ratio_negative() {
let cell = MiuraUnitCell::new(1.0, 1.0, PI / 4.0);
let mut miura = MiuraOri::new(cell, 2, 2);
miura.set_angle(PI / 4.0);
let nu = miura.poisson_ratio();
assert!(
nu <= 0.0,
"Miura-ori Poisson ratio should be negative, got {}",
nu
);
}
#[test]
fn test_miura_ori_fold_lines_row() {
let cell = MiuraUnitCell::new(1.0, 1.0, PI / 6.0);
let miura = MiuraOri::new(cell, 2, 3);
let lines = miura.fold_lines_row(0);
assert!(!lines.is_empty());
}
#[test]
fn test_waterbomb_vertex_count() {
let wb = WaterbombBase::new(1.0, 3, 4);
let verts = wb.vertex_positions();
assert_eq!(verts.len(), 4 * 5); }
#[test]
fn test_waterbomb_flat_height_zero() {
let wb = WaterbombBase::new(1.0, 2, 2);
assert!((wb.folded_height()).abs() < 1e-10);
}
#[test]
fn test_waterbomb_footprint() {
let wb = WaterbombBase::new(1.0, 2, 3);
let (fx, fy) = wb.footprint();
assert!((fx - 3.0).abs() < 1e-10); assert!((fy - 2.0).abs() < 1e-10);
}
#[test]
fn test_yoshimura_vertex_count() {
let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
let verts = yb.vertex_positions();
assert_eq!(verts.len(), 6 * 5); }
#[test]
fn test_yoshimura_axial_shortening_zero_depth() {
let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
assert!((yb.axial_shortening()).abs() < 1e-10);
}
#[test]
fn test_yoshimura_axial_wavelength() {
let yb = YoshimuraBuckling::new(1.0, 2.0, 6, 4);
assert!((yb.axial_wavelength() - 0.5).abs() < 1e-10);
}
#[test]
fn test_kirigami_rectangular_pattern_slit_count() {
let kg = KirigamiCut::rectangular_pattern(2.0, 3.0, 3, 4, 0.8);
assert_eq!(kg.num_slits(), 12);
}
#[test]
fn test_kirigami_stretch_updates_openings() {
let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
kg.set_stretch(1.0);
for slit in &kg.slits {
assert!(slit.opening > 0.0);
}
}
#[test]
fn test_kirigami_poisson_ratio_nonpositive() {
let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 3, 3, 0.8);
kg.set_stretch(0.5);
assert!(kg.poisson_ratio() <= 0.0);
}
#[test]
fn test_kirigami_stretchability_ge_one() {
let mut kg = KirigamiCut::rectangular_pattern(2.0, 2.0, 2, 2, 0.9);
kg.set_stretch(0.5);
assert!(kg.stretchability() >= 1.0);
}
#[test]
fn test_kawasaki_theorem_flat_vertex() {
let angles = [PI / 4.0, PI / 4.0, PI / 4.0, PI / 4.0];
let residual = OrigamiAnalysis::kawasaki_residual(&angles);
assert!(residual < 1e-10, "residual={}", residual);
}
#[test]
fn test_maekawa_theorem_valid() {
assert!(OrigamiAnalysis::maekawa_valid(3, 1));
assert!(OrigamiAnalysis::maekawa_valid(1, 3));
assert!(!OrigamiAnalysis::maekawa_valid(2, 2));
}
#[test]
fn test_angle_deficit_flat() {
let angles = vec![PI / 3.0; 6];
let deficit = OrigamiAnalysis::angle_deficit(&angles);
assert!(deficit.abs() < 1e-10);
}
#[test]
fn test_miura_poisson_analytical() {
let nu = OrigamiAnalysis::miura_poisson_ratio(PI / 4.0, PI / 4.0);
assert!(nu < 0.0, "Miura Poisson should be negative, got {}", nu);
}
#[test]
fn test_fold_bending_energy_positive() {
let e = OrigamiAnalysis::fold_bending_energy(1.0, 1.0, PI / 4.0);
assert!(e > 0.0);
}
#[test]
fn test_fold_angle_from_normals_parallel() {
let n = Vec3::new(0.0, 0.0, 1.0);
let crease = Vec3::new(1.0, 0.0, 0.0);
let angle = OrigamiAnalysis::fold_angle_from_normals(&n, &n, &crease);
assert!(angle.abs() < 1e-10);
}
#[test]
fn test_kawasaki_fourth_angle() {
let a4 = kawasaki_fourth_angle(PI / 4.0, PI / 4.0, PI / 4.0);
assert!((a4 - PI / 4.0).abs() < 1e-10);
}
}