1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
18#![allow(clippy::too_many_arguments)]
19
20use std::f64::consts::PI;
21
22#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct V3 {
29 pub x: f64,
31 pub y: f64,
33 pub z: f64,
35}
36
37impl V3 {
38 pub fn new(x: f64, y: f64, z: f64) -> Self {
40 Self { x, y, z }
41 }
42
43 pub fn zero() -> Self {
45 Self::new(0.0, 0.0, 0.0)
46 }
47
48 pub fn norm(&self) -> f64 {
50 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
51 }
52
53 pub fn normalize(&self) -> Self {
55 let n = self.norm();
56 if n < 1e-15 {
57 Self::zero()
58 } else {
59 Self::new(self.x / n, self.y / n, self.z / n)
60 }
61 }
62
63 pub fn dot(&self, other: &Self) -> f64 {
65 self.x * other.x + self.y * other.y + self.z * other.z
66 }
67
68 pub fn cross(&self, other: &Self) -> Self {
70 Self::new(
71 self.y * other.z - self.z * other.y,
72 self.z * other.x - self.x * other.z,
73 self.x * other.y - self.y * other.x,
74 )
75 }
76
77 pub fn add(&self, other: &Self) -> Self {
79 Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
80 }
81
82 pub fn sub(&self, other: &Self) -> Self {
84 Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
85 }
86
87 pub fn scale(&self, s: f64) -> Self {
89 Self::new(self.x * s, self.y * s, self.z * s)
90 }
91}
92
93#[derive(Debug, Clone)]
104pub struct GridShell {
105 pub rows: usize,
107 pub cols: usize,
109 pub nodes: Vec<V3>,
111 pub members: Vec<(usize, usize)>,
113 pub rest_lengths: Vec<f64>,
115 pub stiffnesses: Vec<f64>,
117 pub fixed_nodes: Vec<usize>,
119 pub nodal_mass: Vec<f64>,
121}
122
123impl GridShell {
124 pub fn parabolic(rows: usize, cols: usize, span_x: f64, span_y: f64, sag: f64) -> Self {
130 assert!(rows >= 2 && cols >= 2);
131 let mut nodes = Vec::with_capacity(rows * cols);
132 for i in 0..rows {
133 let u = i as f64 / (rows - 1) as f64; for j in 0..cols {
135 let v = j as f64 / (cols - 1) as f64;
136 let x = u * span_x;
137 let y = v * span_y;
138 let z = 4.0 * sag * u * (1.0 - u) * v * (1.0 - v);
140 nodes.push(V3::new(x, y, z));
141 }
142 }
143 let mut members = Vec::new();
144 for i in 0..rows {
146 for j in 0..cols {
147 let idx = i * cols + j;
148 if j + 1 < cols {
149 members.push((idx, idx + 1));
150 }
151 if i + 1 < rows {
152 members.push((idx, idx + cols));
153 }
154 }
155 }
156 for i in 0..rows - 1 {
158 for j in 0..cols - 1 {
159 let a = i * cols + j;
160 let b = (i + 1) * cols + (j + 1);
161 members.push((a, b));
162 let c = i * cols + (j + 1);
163 let d = (i + 1) * cols + j;
164 members.push((c, d));
165 }
166 }
167 let rest_lengths: Vec<f64> = members
168 .iter()
169 .map(|(a, b)| nodes[*a].sub(&nodes[*b]).norm())
170 .collect();
171 let stiffnesses = vec![1.0e6_f64; members.len()]; let nodal_mass = vec![10.0_f64; nodes.len()];
173 let mut fixed_nodes = Vec::new();
175 for i in 0..rows {
176 for j in 0..cols {
177 if i == 0 || i == rows - 1 || j == 0 || j == cols - 1 {
178 fixed_nodes.push(i * cols + j);
179 }
180 }
181 }
182 Self {
183 rows,
184 cols,
185 nodes,
186 members,
187 rest_lengths,
188 stiffnesses,
189 fixed_nodes,
190 nodal_mass,
191 }
192 }
193
194 pub fn dynamic_relaxation(&mut self, max_iter: usize, dt: f64, damping: f64, tol: f64) -> f64 {
199 let n = self.nodes.len();
200 let fixed: std::collections::HashSet<usize> = self.fixed_nodes.iter().copied().collect();
201 let mut velocities = vec![V3::zero(); n];
202 let mut max_residual = f64::MAX;
203
204 for _iter in 0..max_iter {
205 let mut forces = vec![V3::zero(); n];
206
207 for (m_idx, (a, b)) in self.members.iter().enumerate() {
209 let pa = &self.nodes[*a];
210 let pb = &self.nodes[*b];
211 let delta = pb.sub(pa);
212 let length = delta.norm();
213 if length < 1e-15 {
214 continue;
215 }
216 let strain = (length - self.rest_lengths[m_idx]) / self.rest_lengths[m_idx];
217 let f_mag = self.stiffnesses[m_idx] * strain;
218 let dir = delta.normalize();
219 let force = dir.scale(f_mag);
220 forces[*a] = forces[*a].add(&force);
221 forces[*b] = forces[*b].sub(&force);
222 }
223
224 max_residual = 0.0;
226 for i in 0..n {
227 if fixed.contains(&i) {
228 velocities[i] = V3::zero();
229 continue;
230 }
231 let f_norm = forces[i].norm();
232 if f_norm > max_residual {
233 max_residual = f_norm;
234 }
235 let acc = forces[i].scale(1.0 / self.nodal_mass[i]);
237 velocities[i] = velocities[i].scale(1.0 - damping).add(&acc.scale(dt));
238 self.nodes[i] = self.nodes[i].add(&velocities[i].scale(dt));
239 }
240
241 if max_residual < tol {
242 break;
243 }
244 }
245 max_residual
246 }
247
248 pub fn max_member_force(&self) -> f64 {
250 self.members
251 .iter()
252 .enumerate()
253 .map(|(m_idx, (a, b))| {
254 let length = self.nodes[*a].sub(&self.nodes[*b]).norm();
255 let strain = (length - self.rest_lengths[m_idx]) / self.rest_lengths[m_idx];
256 (self.stiffnesses[m_idx] * strain).abs()
257 })
258 .fold(0.0_f64, f64::max)
259 }
260
261 pub fn node_count(&self) -> usize {
263 self.nodes.len()
264 }
265
266 pub fn member_count(&self) -> usize {
268 self.members.len()
269 }
270}
271
272#[derive(Debug, Clone)]
281pub struct PanelizationResult {
282 pub quad_panels: Vec<[usize; 4]>,
284 pub tri_panels: Vec<[usize; 3]>,
286 pub vertices: Vec<V3>,
288 pub planarity_errors: Vec<f64>,
291 pub planarity_tolerance: f64,
293 pub gap_stats: (f64, f64),
295}
296
297impl PanelizationResult {
298 pub fn quad_planarity_error(pts: &[V3; 4]) -> f64 {
303 let ab = pts[1].sub(&pts[0]);
304 let ac = pts[2].sub(&pts[0]);
305 let normal = ab.cross(&ac);
306 let n_len = normal.norm();
307 if n_len < 1e-15 {
308 return 0.0;
309 }
310 let n_unit = normal.normalize();
311 let ad = pts[3].sub(&pts[0]);
312 ad.dot(&n_unit).abs()
313 }
314
315 pub fn out_of_tolerance_count(&self) -> usize {
317 self.planarity_errors
318 .iter()
319 .filter(|&&e| e > self.planarity_tolerance)
320 .count()
321 }
322
323 pub fn mean_planarity_error(&self) -> f64 {
325 if self.planarity_errors.is_empty() {
326 return 0.0;
327 }
328 let sum: f64 = self.planarity_errors.iter().sum();
329 sum / self.planarity_errors.len() as f64
330 }
331
332 pub fn max_planarity_error(&self) -> f64 {
334 self.planarity_errors
335 .iter()
336 .cloned()
337 .fold(0.0_f64, f64::max)
338 }
339}
340
341#[derive(Debug, Clone)]
350pub struct PlanarQuadMesh {
351 pub nu: usize,
353 pub nv: usize,
355 pub vertices: Vec<V3>,
357 pub faces: Vec<[usize; 4]>,
359 pub planarity_errors: Vec<f64>,
361}
362
363impl PlanarQuadMesh {
364 pub fn from_surface<F>(nu: usize, nv: usize, surface_fn: F) -> Self
369 where
370 F: Fn(f64, f64) -> V3,
371 {
372 assert!(nu >= 2 && nv >= 2);
373 let mut vertices = Vec::with_capacity(nu * nv);
374 for i in 0..nu {
375 let u = i as f64 / (nu - 1) as f64;
376 for j in 0..nv {
377 let v = j as f64 / (nv - 1) as f64;
378 vertices.push(surface_fn(u, v));
379 }
380 }
381 let mut faces = Vec::new();
382 for i in 0..nu - 1 {
383 for j in 0..nv - 1 {
384 let a = i * nv + j;
385 let b = i * nv + j + 1;
386 let c = (i + 1) * nv + j + 1;
387 let d = (i + 1) * nv + j;
388 faces.push([a, b, c, d]);
389 }
390 }
391 let planarity_errors: Vec<f64> = faces
392 .iter()
393 .map(|[a, b, c, d]| {
394 let pts = [vertices[*a], vertices[*b], vertices[*c], vertices[*d]];
395 PanelizationResult::quad_planarity_error(&pts)
396 })
397 .collect();
398 Self {
399 nu,
400 nv,
401 vertices,
402 faces,
403 planarity_errors,
404 }
405 }
406
407 pub fn mean_planarity_error(&self) -> f64 {
409 if self.planarity_errors.is_empty() {
410 return 0.0;
411 }
412 self.planarity_errors.iter().sum::<f64>() / self.planarity_errors.len() as f64
413 }
414
415 pub fn max_planarity_error(&self) -> f64 {
417 self.planarity_errors
418 .iter()
419 .cloned()
420 .fold(0.0_f64, f64::max)
421 }
422
423 pub fn planarity_compliance_ratio(&self, tol: f64) -> f64 {
425 let compliant = self.planarity_errors.iter().filter(|&&e| e <= tol).count();
426 compliant as f64 / self.planarity_errors.len().max(1) as f64
427 }
428
429 pub fn gaussian_curvature_at(&self, i: usize, j: usize) -> f64 {
434 if i == 0 || i >= self.nu - 1 || j == 0 || j >= self.nv - 1 {
435 return 0.0; }
437 let idx = |ii: usize, jj: usize| ii * self.nv + jj;
438 let p = self.vertices[idx(i, j)];
439 let pn = self.vertices[idx(i - 1, j)];
441 let ps = self.vertices[idx(i + 1, j)];
442 let pe = self.vertices[idx(i, j + 1)];
443 let pw = self.vertices[idx(i, j - 1)];
444 let angle = |a: V3, centre: V3, b: V3| -> f64 {
446 let u = a.sub(¢re).normalize();
447 let v = b.sub(¢re).normalize();
448 u.dot(&v).clamp(-1.0, 1.0).acos()
449 };
450 let theta_sum = angle(pn, p, pe) + angle(pe, p, ps) + angle(ps, p, pw) + angle(pw, p, pn);
451 let area_approx = {
452 let d1 = pn.sub(&p).norm();
453 let d2 = pe.sub(&p).norm();
454 d1 * d2
455 };
456 if area_approx < 1e-20 {
457 return 0.0;
458 }
459 (2.0 * PI - theta_sum) / area_approx
460 }
461
462 pub fn face_count(&self) -> usize {
464 self.faces.len()
465 }
466}
467
468#[derive(Debug, Clone)]
478pub struct TensileStructure {
479 pub nodes: Vec<V3>,
481 pub cables: Vec<(usize, usize)>,
483 pub prestress: Vec<f64>,
485 pub anchors: Vec<usize>,
487 pub trib_areas: Vec<f64>,
489}
490
491impl TensileStructure {
492 pub fn flat_net(nx: usize, ny: usize, width: f64, height: f64, p0: f64) -> Self {
497 assert!(nx >= 2 && ny >= 2);
498 let mut nodes = Vec::with_capacity(nx * ny);
499 for i in 0..nx {
500 let x = i as f64 / (nx - 1) as f64 * width;
501 for j in 0..ny {
502 let y = j as f64 / (ny - 1) as f64 * height;
503 nodes.push(V3::new(x, y, 0.0));
504 }
505 }
506 let mut cables = Vec::new();
507 for i in 0..nx {
508 for j in 0..ny {
509 let idx = i * ny + j;
510 if j + 1 < ny {
511 cables.push((idx, idx + 1));
512 }
513 if i + 1 < nx {
514 cables.push((idx, idx + ny));
515 }
516 }
517 }
518 let prestress = vec![p0; cables.len()];
519 let mut anchors = Vec::new();
520 for i in 0..nx {
521 for j in 0..ny {
522 if i == 0 || i == nx - 1 || j == 0 || j == ny - 1 {
523 anchors.push(i * ny + j);
524 }
525 }
526 }
527 let trib_areas = vec![(width / (nx - 1) as f64) * (height / (ny - 1) as f64); nodes.len()];
528 Self {
529 nodes,
530 cables,
531 prestress,
532 anchors,
533 trib_areas,
534 }
535 }
536
537 pub fn form_find(
541 &mut self,
542 pressure: f64,
543 max_iter: usize,
544 dt: f64,
545 damping: f64,
546 tol: f64,
547 ) -> f64 {
548 let n = self.nodes.len();
549 let anchors: std::collections::HashSet<usize> = self.anchors.iter().copied().collect();
550 let mut vel = vec![V3::zero(); n];
551 let mass = 1.0_f64; for _iter in 0..max_iter {
554 let mut forces = vec![V3::zero(); n];
555 for (m_idx, (a, b)) in self.cables.iter().enumerate() {
557 let pa = self.nodes[*a];
558 let pb = self.nodes[*b];
559 let delta = pb.sub(&pa);
560 let length = delta.norm();
561 if length < 1e-15 {
562 continue;
563 }
564 let dir = delta.normalize();
565 let f_density = self.prestress[m_idx] / length;
567 let fv = dir.scale(f_density);
568 forces[*a] = forces[*a].add(&fv);
569 forces[*b] = forces[*b].sub(&fv);
570 }
571 for i in 0..n {
573 if !anchors.contains(&i) {
574 let fz = pressure * self.trib_areas[i];
575 forces[i].z -= fz;
576 }
577 }
578 let mut max_res = 0.0_f64;
580 for i in 0..n {
581 if anchors.contains(&i) {
582 vel[i] = V3::zero();
583 continue;
584 }
585 let f_norm = forces[i].norm();
586 if f_norm > max_res {
587 max_res = f_norm;
588 }
589 let acc = forces[i].scale(1.0 / mass);
590 vel[i] = vel[i].scale(1.0 - damping).add(&acc.scale(dt));
591 self.nodes[i] = self.nodes[i].add(&vel[i].scale(dt));
592 }
593 if max_res < tol {
594 break;
595 }
596 }
597 self.nodes
599 .iter()
600 .enumerate()
601 .filter(|(i, _)| !anchors.contains(i))
602 .map(|(_, p)| p.z.abs())
603 .fold(0.0_f64, f64::max)
604 }
605
606 pub fn prestress_energy(&self) -> f64 {
611 self.cables
612 .iter()
613 .enumerate()
614 .map(|(m, (a, b))| {
615 let l = self.nodes[*a].sub(&self.nodes[*b]).norm();
616 let t = self.prestress[m];
617 let dl = l - t / 1e3; t / l * dl * dl * 0.5
620 })
621 .sum()
622 }
623
624 pub fn mean_cable_tension(&self) -> f64 {
626 if self.prestress.is_empty() {
627 return 0.0;
628 }
629 self.prestress.iter().sum::<f64>() / self.prestress.len() as f64
630 }
631}
632
633#[derive(Debug, Clone)]
643pub struct GeodesicDome {
644 pub frequency: usize,
646 pub radius: f64,
648 pub vertices: Vec<V3>,
650 pub struts: Vec<(usize, usize)>,
652 pub hemisphere: bool,
654}
655
656impl GeodesicDome {
657 fn icosahedron_vertices(radius: f64) -> Vec<V3> {
659 let phi = (1.0 + 5.0_f64.sqrt()) / 2.0; let s = radius / (1.0 + phi * phi).sqrt();
661 let t = phi * s;
662 vec![
663 V3::new(0.0, s, t),
664 V3::new(0.0, -s, t),
665 V3::new(0.0, s, -t),
666 V3::new(0.0, -s, -t),
667 V3::new(s, t, 0.0),
668 V3::new(-s, t, 0.0),
669 V3::new(s, -t, 0.0),
670 V3::new(-s, -t, 0.0),
671 V3::new(t, 0.0, s),
672 V3::new(-t, 0.0, s),
673 V3::new(t, 0.0, -s),
674 V3::new(-t, 0.0, -s),
675 ]
676 }
677
678 fn icosahedron_faces() -> Vec<[usize; 3]> {
680 vec![
681 [0, 1, 8],
682 [0, 8, 4],
683 [0, 4, 5],
684 [0, 5, 9],
685 [0, 9, 1],
686 [1, 6, 8],
687 [8, 6, 10],
688 [8, 10, 4],
689 [4, 10, 2],
690 [4, 2, 5],
691 [5, 2, 11],
692 [5, 11, 9],
693 [9, 11, 7],
694 [9, 7, 1],
695 [1, 7, 6],
696 [3, 10, 6],
697 [3, 6, 7],
698 [3, 7, 11],
699 [3, 11, 2],
700 [3, 2, 10],
701 ]
702 }
703
704 pub fn new(frequency: usize, radius: f64, hemisphere: bool) -> Self {
708 assert!((1..=8).contains(&frequency), "frequency 1–8 supported");
709 let base_verts = Self::icosahedron_vertices(radius);
710 let base_faces = Self::icosahedron_faces();
711
712 if frequency == 1 {
713 let vertices: Vec<V3> = base_verts
715 .iter()
716 .map(|v| v.normalize().scale(radius))
717 .collect();
718 let mut struts = Vec::new();
719 for face in &base_faces {
720 for k in 0..3 {
721 let a = face[k];
722 let b = face[(k + 1) % 3];
723 let edge = if a < b { (a, b) } else { (b, a) };
724 if !struts.contains(&edge) {
725 struts.push(edge);
726 }
727 }
728 }
729 return Self {
730 frequency,
731 radius,
732 vertices,
733 struts,
734 hemisphere,
735 };
736 }
737
738 let mut all_points: Vec<V3> = Vec::new();
740 let mut all_struts: Vec<(usize, usize)> = Vec::new();
741
742 let find_or_insert = |pts: &mut Vec<V3>, p: V3| -> usize {
743 let threshold = radius * 1e-6;
744 for (idx, existing) in pts.iter().enumerate() {
745 if existing.sub(&p).norm() < threshold {
746 return idx;
747 }
748 }
749 pts.push(p);
750 pts.len() - 1
751 };
752
753 for face in &base_faces {
754 let va = base_verts[face[0]].normalize().scale(radius);
755 let vb = base_verts[face[1]].normalize().scale(radius);
756 let vc = base_verts[face[2]].normalize().scale(radius);
757
758 let f = frequency as f64;
759 let mut local: Vec<Vec<V3>> = vec![vec![V3::zero(); frequency + 1]; frequency + 1];
761 for i in 0..=frequency {
762 for j in 0..=frequency - i {
763 let k = frequency - i - j;
764 let p = V3::new(
765 (i as f64 * va.x + j as f64 * vb.x + k as f64 * vc.x) / f,
766 (i as f64 * va.y + j as f64 * vb.y + k as f64 * vc.y) / f,
767 (i as f64 * va.z + j as f64 * vb.z + k as f64 * vc.z) / f,
768 );
769 local[i][j] = p.normalize().scale(radius);
771 }
772 }
773
774 for i in 0..=frequency {
776 for j in 0..=frequency - i {
777 let idx_a = find_or_insert(&mut all_points, local[i][j]);
778 if j < frequency - i {
779 let idx_b = find_or_insert(&mut all_points, local[i][j + 1]);
780 let e = if idx_a < idx_b {
781 (idx_a, idx_b)
782 } else {
783 (idx_b, idx_a)
784 };
785 if !all_struts.contains(&e) {
786 all_struts.push(e);
787 }
788 }
789 if i < frequency - j {
790 let idx_c = find_or_insert(&mut all_points, local[i + 1][j]);
791 let e = if idx_a < idx_c {
792 (idx_a, idx_c)
793 } else {
794 (idx_c, idx_a)
795 };
796 if !all_struts.contains(&e) {
797 all_struts.push(e);
798 }
799 }
800 if i < frequency - j && j < frequency - (i + 1) {
801 let idx_b = find_or_insert(&mut all_points, local[i][j + 1]);
802 let idx_c = find_or_insert(&mut all_points, local[i + 1][j]);
803 let e = if idx_b < idx_c {
804 (idx_b, idx_c)
805 } else {
806 (idx_c, idx_b)
807 };
808 if !all_struts.contains(&e) {
809 all_struts.push(e);
810 }
811 }
812 }
813 }
814 }
815
816 let vertices = all_points;
817 let struts = all_struts;
818
819 Self {
820 frequency,
821 radius,
822 vertices,
823 struts,
824 hemisphere,
825 }
826 }
827
828 pub fn expected_vertex_count(frequency: usize) -> usize {
832 10 * frequency * frequency + 2
833 }
834
835 pub fn expected_strut_count(frequency: usize) -> usize {
839 30 * frequency * frequency
840 }
841
842 pub fn strut_length_stats(&self) -> (f64, f64, f64) {
844 if self.struts.is_empty() {
845 return (0.0, 0.0, 0.0);
846 }
847 let lengths: Vec<f64> = self
848 .struts
849 .iter()
850 .map(|(a, b)| self.vertices[*a].sub(&self.vertices[*b]).norm())
851 .collect();
852 let min = lengths.iter().cloned().fold(f64::MAX, f64::min);
853 let max = lengths.iter().cloned().fold(0.0_f64, f64::max);
854 let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
855 (min, max, mean)
856 }
857
858 pub fn verify_sphericity(&self, tol: f64) -> bool {
860 self.vertices
861 .iter()
862 .all(|v| (v.norm() - self.radius).abs() < tol)
863 }
864}
865
866#[derive(Debug, Clone)]
874pub struct StructuralGlass {
875 pub width: f64,
877 pub height: f64,
879 pub thickness: f64,
881 pub e_glass: f64,
883 pub nu_glass: f64,
885 pub density: f64,
887 pub wind_pressure: f64,
889 pub delta_temp: f64,
891 pub alpha_cte: f64,
893 pub sealant_width: f64,
895 pub sealant_shear_strength: f64,
897}
898
899impl StructuralGlass {
900 pub fn monolithic_10mm() -> Self {
902 Self {
903 width: 3.0,
904 height: 5.0,
905 thickness: 0.01,
906 e_glass: 70.0e9,
907 nu_glass: 0.23,
908 density: 2500.0,
909 wind_pressure: 1200.0, delta_temp: 40.0,
911 alpha_cte: 9.0e-6,
912 sealant_width: 0.025,
913 sealant_shear_strength: 0.14e6,
914 }
915 }
916
917 pub fn self_weight_pressure(&self) -> f64 {
919 self.density * 9.81 * self.thickness
920 }
921
922 pub fn max_bending_stress_wind(&self) -> f64 {
928 let a = self.width.min(self.height);
929 let alpha_coeff = 0.287_f64; alpha_coeff * self.wind_pressure * a * a / (self.thickness * self.thickness)
931 }
932
933 pub fn thermal_stress(&self) -> f64 {
937 self.e_glass * self.alpha_cte * self.delta_temp
938 }
939
940 pub fn combined_stress(&self) -> f64 {
942 self.max_bending_stress_wind() + self.thermal_stress()
943 }
944
945 pub fn sealant_shear_capacity(&self) -> f64 {
950 self.sealant_shear_strength * self.sealant_width
951 }
952
953 pub fn edge_reaction_wind(&self) -> f64 {
956 let area = self.width * self.height;
957 let perimeter = 2.0 * (self.width + self.height);
959 self.wind_pressure * area / perimeter
960 }
961
962 pub fn centre_deflection_wind(&self) -> f64 {
967 let a = self.width.min(self.height);
968 let beta = 0.0443_f64;
969 beta * self.wind_pressure * a.powi(4) / (self.e_glass * self.thickness.powi(3))
970 }
971
972 pub fn span_deflection_ratio(&self) -> f64 {
974 let a = self.width.min(self.height);
975 let delta = self.centre_deflection_wind();
976 if delta < 1e-12 {
977 return f64::MAX;
978 }
979 a / delta
980 }
981
982 pub fn igu_self_weight_pressure(&self) -> f64 {
986 let cavity_mass_per_m2 = 1.784 * 0.016; (2.0 * self.density * self.thickness + cavity_mass_per_m2) * 9.81
988 }
989}
990
991#[derive(Debug, Clone)]
1000pub struct ParametricFacade {
1001 pub bays: usize,
1003 pub levels: usize,
1005 pub bay_width: f64,
1007 pub storey_height: f64,
1009 pub facade_azimuth: f64,
1011 pub latitude: f64,
1013 pub max_shade_depth: f64,
1015 pub target_solar_exposure: f64,
1017}
1018
1019impl ParametricFacade {
1020 pub fn tokyo_south() -> Self {
1022 Self {
1023 bays: 8,
1024 levels: 10,
1025 bay_width: 1.5,
1026 storey_height: 3.6,
1027 facade_azimuth: 180.0,
1028 latitude: 35.7,
1029 max_shade_depth: 0.8,
1030 target_solar_exposure: 400.0,
1031 }
1032 }
1033
1034 pub fn solar_altitude(&self, day_of_year: f64, hour: f64) -> f64 {
1038 let lat_rad = self.latitude.to_radians();
1039 let decl =
1041 23.45_f64.to_radians() * (360.0 / 365.0 * (day_of_year - 81.0)).to_radians().sin();
1042 let hour_angle = (hour - 12.0) * 15.0; let h_rad = hour_angle.to_radians();
1044 let sin_alt = lat_rad.sin() * decl.sin() + lat_rad.cos() * decl.cos() * h_rad.cos();
1045 sin_alt.clamp(-1.0, 1.0).asin().to_degrees()
1046 }
1047
1048 pub fn solar_azimuth(&self, day_of_year: f64, hour: f64) -> f64 {
1051 let lat_rad = self.latitude.to_radians();
1052 let decl =
1053 23.45_f64.to_radians() * (360.0 / 365.0 * (day_of_year - 81.0)).to_radians().sin();
1054 let hour_angle = (hour - 12.0) * 15.0;
1055 let h_rad = hour_angle.to_radians();
1056 let sin_alt = lat_rad.sin() * decl.sin() + lat_rad.cos() * decl.cos() * h_rad.cos();
1057 let cos_az = (decl.sin() - lat_rad.sin() * sin_alt)
1058 / (lat_rad.cos() * sin_alt.asin().cos().max(1e-10));
1059 let az_deg = cos_az.clamp(-1.0, 1.0).acos().to_degrees();
1060 if hour_angle > 0.0 { az_deg } else { -az_deg }
1061 }
1062
1063 pub fn incidence_angle(&self, alt_deg: f64, az_deg: f64) -> f64 {
1066 let facade_az = self.facade_azimuth - 180.0; let delta_az = (az_deg - facade_az).to_radians();
1068 let alt_rad = alt_deg.to_radians();
1069 let cos_inc = alt_rad.cos() * delta_az.cos();
1071 cos_inc.clamp(-1.0, 1.0).acos().to_degrees()
1072 }
1073
1074 pub fn required_shade_depth(&self, incidence_deg: f64) -> f64 {
1078 let inc_from_horiz = (90.0 - incidence_deg).abs().to_radians();
1079 let depth = self.storey_height * inc_from_horiz.tan();
1080 depth.min(self.max_shade_depth)
1081 }
1082
1083 pub fn shade_depth_matrix(&self) -> Vec<Vec<f64>> {
1087 let alt = self.solar_altitude(172.0, 12.0); let az = self.solar_azimuth(172.0, 12.0);
1089 let inc = self.incidence_angle(alt, az);
1090 let depth = self.required_shade_depth(inc);
1091 vec![vec![depth; self.bays]; self.levels]
1092 }
1093
1094 pub fn annual_solar_exposure(&self) -> f64 {
1099 let panel_area = self.bay_width * self.storey_height;
1100 let mut total_irradiance = 0.0_f64;
1101 let dni = 800.0_f64; let months = [
1103 17.0, 47.0, 75.0, 105.0, 135.0, 162.0, 198.0, 228.0, 258.0, 288.0, 318.0, 344.0_f64,
1104 ];
1105 for &day in &months {
1106 for h_int in 7..17 {
1107 let hour = h_int as f64 + 0.5;
1108 let alt = self.solar_altitude(day, hour);
1109 if alt <= 0.0 {
1110 continue;
1111 }
1112 let az = self.solar_azimuth(day, hour);
1113 let inc = self.incidence_angle(alt, az);
1114 let cos_inc = inc.to_radians().cos().max(0.0);
1115 total_irradiance += dni * cos_inc;
1116 }
1117 }
1118 total_irradiance / 1000.0 * panel_area
1120 }
1121
1122 pub fn total_panel_count(&self) -> usize {
1124 self.bays * self.levels
1125 }
1126
1127 pub fn total_facade_area(&self) -> f64 {
1129 self.bays as f64 * self.bay_width * self.levels as f64 * self.storey_height
1130 }
1131
1132 pub fn window_to_wall_ratio(&self) -> f64 {
1136 0.70
1137 }
1138
1139 pub fn shading_performance_index(&self) -> f64 {
1143 let matrix = self.shade_depth_matrix();
1144 let mean_depth: f64 =
1145 matrix.iter().flatten().sum::<f64>() / (self.bays * self.levels) as f64;
1146 (mean_depth / self.max_shade_depth).min(1.0)
1147 }
1148}
1149
1150pub fn triangle_area(a: V3, b: V3, c: V3) -> f64 {
1156 let ab = b.sub(&a);
1157 let ac = c.sub(&a);
1158 ab.cross(&ac).norm() * 0.5
1159}
1160
1161pub fn fit_plane(points: &[V3]) -> (V3, V3) {
1166 if points.len() < 3 {
1167 return (V3::zero(), V3::new(0.0, 0.0, 1.0));
1168 }
1169 let n = points.len() as f64;
1170 let cx = points.iter().map(|p| p.x).sum::<f64>() / n;
1171 let cy = points.iter().map(|p| p.y).sum::<f64>() / n;
1172 let cz = points.iter().map(|p| p.z).sum::<f64>() / n;
1173 let centroid = V3::new(cx, cy, cz);
1174
1175 let (mut cxx, mut cxy, mut cxz, mut cyy, mut cyz, mut _czz) =
1177 (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
1178 for p in points {
1179 let dx = p.x - cx;
1180 let dy = p.y - cy;
1181 let dz = p.z - cz;
1182 cxx += dx * dx;
1183 cxy += dx * dy;
1184 cxz += dx * dz;
1185 cyy += dy * dy;
1186 cyz += dy * dz;
1187 _czz += dz * dz;
1188 }
1189 let v1 = V3::new(cxx, cxy, cxz).normalize();
1193 let v2 = V3::new(cxy, cyy, cyz).normalize();
1194 let normal = v1.cross(&v2).normalize();
1195 (centroid, normal)
1196}
1197
1198pub fn point_to_plane_distance(p: V3, origin: V3, normal: V3) -> f64 {
1200 p.sub(&origin).dot(&normal).abs()
1201}
1202
1203pub fn perimeter_resample(pts: &[V3], n: usize) -> Vec<V3> {
1207 if pts.len() < 2 || n == 0 {
1208 return Vec::new();
1209 }
1210 let mut arcs = vec![0.0_f64; pts.len()];
1212 for i in 1..pts.len() {
1213 arcs[i] = arcs[i - 1] + pts[i].sub(&pts[i - 1]).norm();
1214 }
1215 let total = *arcs.last().expect("collection should not be empty");
1216 let mut result = Vec::with_capacity(n);
1217 for k in 0..n {
1218 let s = k as f64 / n as f64 * total;
1219 let seg = arcs.partition_point(|&a| a <= s).min(pts.len() - 1);
1221 let seg = seg.max(1);
1222 let t0 = arcs[seg - 1];
1223 let t1 = arcs[seg];
1224 let t = if (t1 - t0).abs() < 1e-15 {
1225 0.0
1226 } else {
1227 (s - t0) / (t1 - t0)
1228 };
1229 let p = pts[seg - 1].scale(1.0 - t).add(&pts[seg].scale(t));
1230 result.push(p);
1231 }
1232 result
1233}
1234
1235#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 #[test]
1246 fn test_v3_norm() {
1247 let v = V3::new(3.0, 4.0, 0.0);
1248 assert!((v.norm() - 5.0).abs() < 1e-10);
1249 }
1250
1251 #[test]
1252 fn test_v3_normalize() {
1253 let v = V3::new(1.0, 2.0, 2.0);
1254 let n = v.normalize();
1255 assert!((n.norm() - 1.0).abs() < 1e-10);
1256 }
1257
1258 #[test]
1259 fn test_v3_cross_orthogonal() {
1260 let a = V3::new(1.0, 0.0, 0.0);
1261 let b = V3::new(0.0, 1.0, 0.0);
1262 let c = a.cross(&b);
1263 assert!((c.z - 1.0).abs() < 1e-10);
1264 assert!(c.x.abs() < 1e-10 && c.y.abs() < 1e-10);
1265 }
1266
1267 #[test]
1268 fn test_v3_dot() {
1269 let a = V3::new(1.0, 2.0, 3.0);
1270 let b = V3::new(4.0, 5.0, 6.0);
1271 assert!((a.dot(&b) - 32.0).abs() < 1e-10);
1272 }
1273
1274 #[test]
1277 fn test_grid_shell_node_count() {
1278 let gs = GridShell::parabolic(5, 6, 20.0, 15.0, 3.0);
1279 assert_eq!(gs.node_count(), 30);
1280 }
1281
1282 #[test]
1283 fn test_grid_shell_dynamic_relaxation_converges() {
1284 let mut gs = GridShell::parabolic(5, 5, 10.0, 10.0, 2.0);
1285 let residual = gs.dynamic_relaxation(500, 0.001, 0.1, 1.0);
1286 assert!(residual < 1.0e6, "residual too high: {residual}");
1288 }
1289
1290 #[test]
1291 fn test_grid_shell_fixed_nodes_unchanged() {
1292 let mut gs = GridShell::parabolic(4, 4, 8.0, 8.0, 1.5);
1293 let initial_positions: Vec<V3> = gs.fixed_nodes.iter().map(|&i| gs.nodes[i]).collect();
1294 gs.dynamic_relaxation(100, 0.001, 0.1, 1.0);
1295 for (k, &idx) in gs.fixed_nodes.iter().enumerate() {
1296 let diff = gs.nodes[idx].sub(&initial_positions[k]).norm();
1297 assert!(diff < 1e-10, "fixed node {idx} moved by {diff}");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_grid_shell_member_count() {
1303 let gs = GridShell::parabolic(3, 3, 6.0, 6.0, 1.0);
1304 assert_eq!(gs.member_count(), 20);
1306 }
1307
1308 #[test]
1311 fn test_planar_quad_planarity_zero() {
1312 let pts = [
1314 V3::new(0.0, 0.0, 0.0),
1315 V3::new(1.0, 0.0, 0.0),
1316 V3::new(1.0, 1.0, 0.0),
1317 V3::new(0.0, 1.0, 0.0),
1318 ];
1319 let err = PanelizationResult::quad_planarity_error(&pts);
1320 assert!(err < 1e-12, "planarity error = {err}");
1321 }
1322
1323 #[test]
1324 fn test_planar_quad_planarity_nonzero() {
1325 let pts = [
1326 V3::new(0.0, 0.0, 0.0),
1327 V3::new(1.0, 0.0, 0.0),
1328 V3::new(1.0, 1.0, 0.0),
1329 V3::new(0.0, 1.0, 0.1), ];
1331 let err = PanelizationResult::quad_planarity_error(&pts);
1332 assert!(err > 0.05, "expected non-zero planarity error, got {err}");
1333 }
1334
1335 #[test]
1336 fn test_planar_quad_mesh_flat_surface_zero_error() {
1337 let mesh = PlanarQuadMesh::from_surface(5, 5, |u, v| V3::new(u, v, 0.0));
1338 assert!(mesh.max_planarity_error() < 1e-12);
1339 }
1340
1341 #[test]
1342 fn test_planar_quad_mesh_curved_surface_error_positive() {
1343 let mesh = PlanarQuadMesh::from_surface(10, 10, |u, v| {
1344 V3::new(u, v, (u * PI * 2.0).sin() * (v * PI * 2.0).cos() * 0.3)
1345 });
1346 assert!(mesh.max_planarity_error() >= 0.0);
1348 }
1349
1350 #[test]
1351 fn test_planar_quad_mesh_face_count() {
1352 let mesh = PlanarQuadMesh::from_surface(6, 8, |u, v| V3::new(u, v, 0.0));
1353 assert_eq!(mesh.face_count(), 5 * 7);
1354 }
1355
1356 #[test]
1357 fn test_planar_quad_mesh_compliance_ratio_flat() {
1358 let mesh = PlanarQuadMesh::from_surface(5, 5, |u, v| V3::new(u, v, 0.0));
1359 assert!((mesh.planarity_compliance_ratio(0.001) - 1.0).abs() < 1e-10);
1360 }
1361
1362 #[test]
1365 fn test_tensile_flat_net_node_count() {
1366 let net = TensileStructure::flat_net(5, 5, 10.0, 10.0, 5000.0);
1367 assert_eq!(net.nodes.len(), 25);
1368 }
1369
1370 #[test]
1371 fn test_tensile_form_find_sag_positive() {
1372 let mut net = TensileStructure::flat_net(5, 5, 10.0, 10.0, 10000.0);
1373 let sag = net.form_find(500.0, 1000, 0.001, 0.1, 0.01);
1374 assert!(sag >= 0.0, "sag should be non-negative, got {sag}");
1375 }
1376
1377 #[test]
1378 fn test_tensile_mean_cable_tension() {
1379 let net = TensileStructure::flat_net(4, 4, 8.0, 8.0, 3000.0);
1380 let mean = net.mean_cable_tension();
1381 assert!((mean - 3000.0).abs() < 1e-6);
1382 }
1383
1384 #[test]
1385 fn test_tensile_anchor_nodes_not_moved() {
1386 let mut net = TensileStructure::flat_net(4, 4, 6.0, 6.0, 5000.0);
1387 let initial: Vec<V3> = net.anchors.iter().map(|&i| net.nodes[i]).collect();
1388 net.form_find(200.0, 200, 0.001, 0.2, 0.1);
1389 for (k, &idx) in net.anchors.iter().enumerate() {
1390 let diff = net.nodes[idx].sub(&initial[k]).norm();
1391 assert!(diff < 1e-10, "anchor {idx} moved by {diff}");
1392 }
1393 }
1394
1395 #[test]
1398 fn test_geodesic_dome_freq1_vertex_count() {
1399 let dome = GeodesicDome::new(1, 5.0, false);
1400 assert_eq!(dome.vertices.len(), 12);
1402 }
1403
1404 #[test]
1405 fn test_geodesic_dome_freq2_expected_vertices() {
1406 let dome = GeodesicDome::new(2, 5.0, false);
1407 let expected = GeodesicDome::expected_vertex_count(2); let actual = dome.vertices.len();
1410 assert!(
1411 (actual as i64 - expected as i64).abs() <= 5,
1412 "expected ~{expected} vertices, got {actual}"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_geodesic_dome_all_on_sphere() {
1418 let dome = GeodesicDome::new(2, 7.0, false);
1419 assert!(
1420 dome.verify_sphericity(1e-6),
1421 "vertices not on sphere within tolerance"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_geodesic_dome_strut_lengths_similar() {
1427 let dome = GeodesicDome::new(2, 5.0, false);
1428 let (min, max, _mean) = dome.strut_length_stats();
1429 assert!(
1431 max / min.max(1e-10) < 1.20,
1432 "strut length ratio {:.3} too large",
1433 max / min
1434 );
1435 }
1436
1437 #[test]
1438 fn test_geodesic_dome_freq1_strut_count() {
1439 let dome = GeodesicDome::new(1, 5.0, false);
1440 assert_eq!(dome.struts.len(), 30);
1442 }
1443
1444 #[test]
1447 fn test_glass_self_weight_positive() {
1448 let g = StructuralGlass::monolithic_10mm();
1449 let sw = g.self_weight_pressure();
1450 assert!(sw > 0.0 && sw < 500.0, "self weight = {sw} Pa");
1451 }
1452
1453 #[test]
1454 fn test_glass_thermal_stress() {
1455 let g = StructuralGlass::monolithic_10mm();
1456 let ts = g.thermal_stress();
1457 assert!((ts - 25.2e6).abs() < 1.0e5);
1459 }
1460
1461 #[test]
1462 fn test_glass_wind_stress_positive() {
1463 let g = StructuralGlass::monolithic_10mm();
1464 let ws = g.max_bending_stress_wind();
1465 assert!(ws > 0.0);
1466 }
1467
1468 #[test]
1469 fn test_glass_combined_stress_greater_than_parts() {
1470 let g = StructuralGlass::monolithic_10mm();
1471 let combined = g.combined_stress();
1472 assert!(combined > g.thermal_stress());
1473 assert!(combined > g.max_bending_stress_wind());
1474 }
1475
1476 #[test]
1477 fn test_glass_centre_deflection() {
1478 let g = StructuralGlass::monolithic_10mm();
1479 let d = g.centre_deflection_wind();
1480 assert!(d > 0.0 && d < 0.2, "deflection = {d} m");
1481 }
1482
1483 #[test]
1484 fn test_glass_sealant_capacity_positive() {
1485 let g = StructuralGlass::monolithic_10mm();
1486 assert!(g.sealant_shear_capacity() > 0.0);
1487 }
1488
1489 #[test]
1492 fn test_facade_total_panel_count() {
1493 let f = ParametricFacade::tokyo_south();
1494 assert_eq!(f.total_panel_count(), 80); }
1496
1497 #[test]
1498 fn test_facade_solar_altitude_summer_positive() {
1499 let f = ParametricFacade::tokyo_south();
1500 let alt = f.solar_altitude(172.0, 12.0);
1501 assert!(alt > 0.0, "summer noon altitude should be positive: {alt}");
1502 }
1503
1504 #[test]
1505 fn test_facade_solar_altitude_night_negative() {
1506 let f = ParametricFacade::tokyo_south();
1507 let alt = f.solar_altitude(172.0, 0.0); assert!(alt < 0.0, "midnight altitude should be negative: {alt}");
1509 }
1510
1511 #[test]
1512 fn test_facade_shade_depth_matrix_dimensions() {
1513 let f = ParametricFacade::tokyo_south();
1514 let m = f.shade_depth_matrix();
1515 assert_eq!(m.len(), f.levels);
1516 assert_eq!(m[0].len(), f.bays);
1517 }
1518
1519 #[test]
1520 fn test_facade_shade_depth_bounded() {
1521 let f = ParametricFacade::tokyo_south();
1522 let m = f.shade_depth_matrix();
1523 for row in &m {
1524 for &d in row {
1525 assert!(d >= 0.0 && d <= f.max_shade_depth + 1e-10, "depth = {d}");
1526 }
1527 }
1528 }
1529
1530 #[test]
1531 fn test_facade_total_area() {
1532 let f = ParametricFacade::tokyo_south();
1533 let area = f.total_facade_area();
1534 assert!((area - 432.0).abs() < 1e-6);
1536 }
1537
1538 #[test]
1539 fn test_facade_annual_solar_exposure_positive() {
1540 let f = ParametricFacade::tokyo_south();
1541 let exp = f.annual_solar_exposure();
1542 assert!(
1543 exp >= 0.0,
1544 "annual solar exposure should be non-negative: {exp}"
1545 );
1546 }
1547
1548 #[test]
1551 fn test_triangle_area() {
1552 let a = V3::new(0.0, 0.0, 0.0);
1553 let b = V3::new(4.0, 0.0, 0.0);
1554 let c = V3::new(0.0, 3.0, 0.0);
1555 let area = triangle_area(a, b, c);
1556 assert!((area - 6.0).abs() < 1e-10);
1557 }
1558
1559 #[test]
1560 fn test_fit_plane_xy_plane() {
1561 let pts = vec![
1562 V3::new(0.0, 0.0, 0.0),
1563 V3::new(1.0, 0.0, 0.0),
1564 V3::new(0.0, 1.0, 0.0),
1565 V3::new(1.0, 1.0, 0.0),
1566 ];
1567 let (_centroid, normal) = fit_plane(&pts);
1568 assert!(normal.z.abs() > 0.5, "normal.z = {}", normal.z);
1570 }
1571
1572 #[test]
1573 fn test_perimeter_resample_count() {
1574 let pts = vec![
1575 V3::new(0.0, 0.0, 0.0),
1576 V3::new(1.0, 0.0, 0.0),
1577 V3::new(1.0, 1.0, 0.0),
1578 V3::new(0.0, 1.0, 0.0),
1579 ];
1580 let resampled = perimeter_resample(&pts, 8);
1581 assert_eq!(resampled.len(), 8);
1582 }
1583
1584 #[test]
1585 fn test_point_to_plane_distance() {
1586 let p = V3::new(0.0, 0.0, 5.0);
1587 let origin = V3::zero();
1588 let normal = V3::new(0.0, 0.0, 1.0);
1589 let d = point_to_plane_distance(p, origin, normal);
1590 assert!((d - 5.0).abs() < 1e-10);
1591 }
1592}