1pub mod polygon;
8pub mod vec3;
9
10use vec3::{cross, unit};
11
12pub const VERTICAL_TOL_MM: f64 = 1.0;
14
15pub const MEMBER_AXIS_TOL_MM: f64 = 10.0;
22
23pub const LEVEL_TOL_MM: f64 = 1.0;
30
31pub fn is_vertical_pair(a: [f64; 3], b: [f64; 3]) -> bool {
37 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt() < VERTICAL_TOL_MM
38}
39
40pub const VERTICAL_COS_TOL: f64 = 0.707;
42
43pub fn is_vertical_axis(a: [f64; 3], b: [f64; 3]) -> bool {
53 vec3::unit_from(a, b).is_some_and(|d| axis_dominates(d, 2))
54}
55
56pub const AXIS_COS_TOL: f64 = VERTICAL_COS_TOL;
59
60pub fn axis_dominates(dir: [f64; 3], axis: usize) -> bool {
66 dir.get(axis).is_some_and(|c| c.abs() > AXIS_COS_TOL)
67}
68
69pub const ORTHOGONAL_DOT_MAX: f64 = 0.707;
74
75pub const MEMBER_COLUMN_EZ_MIN: f64 = 0.8;
84
85pub const MEMBER_BEAM_EZ_MAX: f64 = 0.2;
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum MemberAxisClass {
94 Column,
96 Beam,
98 Diagonal,
100}
101
102pub fn classify_member_ez(ez: f64) -> MemberAxisClass {
104 if ez >= MEMBER_COLUMN_EZ_MIN {
105 MemberAxisClass::Column
106 } else if ez <= MEMBER_BEAM_EZ_MAX {
107 MemberAxisClass::Beam
108 } else {
109 MemberAxisClass::Diagonal
110 }
111}
112
113pub fn default_local_ref_vector(vertical: bool) -> [f64; 3] {
119 if vertical {
120 [1.0, 0.0, 0.0]
121 } else {
122 [0.0, 0.0, 1.0]
123 }
124}
125
126pub fn default_local_ref_vector_for_pair(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
128 default_local_ref_vector(is_vertical_pair(a, b))
129}
130
131pub fn element_axis(model: &crate::model::Model, e: &crate::model::ElementData) -> [f64; 3] {
138 if e.nodes.len() < 2 {
139 return [0.0, 0.0, 0.0];
140 }
141 let (Some(n0), Some(n1)) = (
142 model.nodes.get(e.nodes[0].index()),
143 model.nodes.get(e.nodes[e.nodes.len() - 1].index()),
144 ) else {
145 return [0.0, 0.0, 0.0];
146 };
147 vec3::unit_from(n0.coord, n1.coord).unwrap_or([0.0, 0.0, 0.0])
148}
149
150pub fn abs_lerp_integral(h0: f64, h1: f64, t0: f64, t1: f64) -> f64 {
157 let t0 = t0.clamp(0.0, 1.0);
158 let t1 = t1.clamp(0.0, 1.0);
159 if t1 <= t0 {
160 return 0.0;
161 }
162 let h = |t: f64| h0 + (h1 - h0) * t;
163 let a = h(t0);
164 let b = h(t1);
165 if a * b >= 0.0 || a.abs() <= 1e-15 || b.abs() <= 1e-15 {
166 return (a.abs() + b.abs()) * 0.5 * (t1 - t0);
167 }
168 let denom = h1 - h0;
169 if denom.abs() <= 1e-15 {
170 return a.abs() * (t1 - t0);
171 }
172 let tz = (-h0 / denom).clamp(t0, t1);
173 abs_lerp_integral(h0, h1, t0, tz) + abs_lerp_integral(h0, h1, tz, t1)
174}
175
176pub fn abs_lerp_centroid(h0: f64, h1: f64) -> f64 {
179 let area = abs_lerp_integral(h0, h1, 0.0, 1.0);
180 if area <= 1e-15 {
181 return 0.5;
182 }
183 fn moment(h0: f64, h1: f64, t0: f64, t1: f64) -> f64 {
184 if t1 <= t0 {
185 return 0.0;
186 }
187 let h = |t: f64| h0 + (h1 - h0) * t;
188 let a = h(t0);
189 let b = h(t1);
190 if a * b < 0.0 && a.abs() > 1e-15 && b.abs() > 1e-15 {
191 let tz = (-h0 / (h1 - h0)).clamp(t0, t1);
192 if tz > t0 && tz < t1 {
193 return moment(h0, h1, t0, tz) + moment(h0, h1, tz, t1);
194 }
195 }
196 let dt = t1 - t0;
197 let ha = a.abs();
198 let hb = b.abs();
199 dt * (t0 * ha + t0 * (hb - ha) * 0.5 + dt * ha * 0.5 + dt * (hb - ha) / 3.0)
200 }
201 moment(h0, h1, 0.0, 1.0) / area
202}
203
204pub fn best_fit_plane_normal(pts: &[[f64; 3]]) -> Option<[f64; 3]> {
217 if pts.len() < 3 {
218 return None;
219 }
220 let n = pts.len() as f64;
221 let c = [
222 pts.iter().map(|p| p[0]).sum::<f64>() / n,
223 pts.iter().map(|p| p[1]).sum::<f64>() / n,
224 pts.iter().map(|p| p[2]).sum::<f64>() / n,
225 ];
226 let mut cov = [[0.0f64; 3]; 3];
227 for p in pts {
228 let d = [p[0] - c[0], p[1] - c[1], p[2] - c[2]];
229 for (i, di) in d.iter().enumerate() {
230 for (j, dj) in d.iter().enumerate() {
231 cov[i][j] += di * dj;
232 }
233 }
234 }
235 let (vals, vecs) = jacobi_eigen_sym3(cov);
236 let mut order = [0usize, 1, 2];
238 order.sort_by(|&a, &b| vals[b].total_cmp(&vals[a]));
239 let (l0, l1) = (vals[order[0]], vals[order[1]]);
240 if l0 <= 1e-12 {
241 return None;
243 }
244 if l1 <= 1e-9 * l0 {
246 let line = [vecs[0][order[0]], vecs[1][order[0]], vecs[2][order[0]]];
247 return unit(cross(line, [0.0, 0.0, 1.0])).or(Some([1.0, 0.0, 0.0]));
249 }
250 let normal = [vecs[0][order[2]], vecs[1][order[2]], vecs[2][order[2]]];
251 unit(normal)
252}
253
254fn jacobi_eigen_sym3(mut a: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
260 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
261 for _ in 0..24 {
262 let (mut p, mut q, mut max) = (0usize, 1usize, 0.0f64);
264 for (i, j) in [(0usize, 1usize), (0, 2), (1, 2)] {
265 if a[i][j].abs() > max {
266 max = a[i][j].abs();
267 p = i;
268 q = j;
269 }
270 }
271 if max < 1e-14 {
272 break;
273 }
274 let theta = 0.5 * (2.0 * a[p][q]).atan2(a[p][p] - a[q][q]);
275 let (s, c) = theta.sin_cos();
276 let mut b = a;
277 for k in 0..3 {
278 b[p][k] = c * a[p][k] + s * a[q][k];
279 b[q][k] = -s * a[p][k] + c * a[q][k];
280 }
281 let mut d = b;
282 for k in 0..3 {
283 d[k][p] = c * b[k][p] + s * b[k][q];
284 d[k][q] = -s * b[k][p] + c * b[k][q];
285 }
286 a = d;
287 let mut nv = v;
288 for k in 0..3 {
289 nv[k][p] = c * v[k][p] + s * v[k][q];
290 nv[k][q] = -s * v[k][p] + c * v[k][q];
291 }
292 v = nv;
293 }
294 ([a[0][0], a[1][1], a[2][2]], v)
295}
296
297#[cfg(test)]
298mod tests;