use nalgebra::Point3;
use parry3d_f64::shape::TriMesh;
#[derive(Debug, Clone)]
pub struct WaterplaneProperties {
pub area: f64,
pub centroid: [f64; 2],
pub i_transverse: f64,
pub i_longitudinal: f64,
pub min_x: f64,
pub max_x: f64,
pub min_y: f64,
pub max_y: f64,
}
pub fn calculate_waterplane_properties(mesh: &TriMesh, draft: f64) -> Option<WaterplaneProperties> {
let contours = extract_waterline_contours(mesh, draft)?;
if contours.is_empty() {
return None;
}
let mut total_area = 0.0;
let mut moment_x = 0.0; let mut moment_y = 0.0; let mut i_xx = 0.0; let mut i_yy = 0.0;
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for contour in &contours {
if contour.len() < 3 {
continue;
}
for i in 0..contour.len() {
let p0 = contour[i];
let p1 = contour[(i + 1) % contour.len()];
min_x = min_x.min(p0.x);
max_x = max_x.max(p0.x);
min_y = min_y.min(p0.y);
max_y = max_y.max(p0.y);
let cp = p0.x * p1.y - p1.x * p0.y;
total_area += cp;
moment_y += (p0.y + p1.y) * cp; moment_x += (p0.x + p1.x) * cp;
i_xx += (p0.y * p0.y + p0.y * p1.y + p1.y * p1.y) * cp;
i_yy += (p0.x * p0.x + p0.x * p1.x + p1.x * p1.x) * cp;
}
}
total_area *= 0.5;
if total_area.abs() <= 1e-9 {
return None;
}
let my_val = moment_x / 6.0; let mx_val = moment_y / 6.0;
let lcf = my_val / total_area;
let tcf = mx_val / total_area;
let i_xx_origin = i_xx / 12.0;
let i_yy_origin = i_yy / 12.0;
let _sign = total_area.signum();
let area_final = total_area.abs();
let i_transverse_signed = i_xx_origin - total_area * tcf * tcf;
let i_longitudinal_signed = i_yy_origin - total_area * lcf * lcf;
Some(WaterplaneProperties {
area: area_final,
centroid: [lcf, tcf],
i_transverse: i_transverse_signed.abs(),
i_longitudinal: i_longitudinal_signed.abs(),
min_x,
max_x,
min_y,
max_y,
})
}
fn extract_waterline_contours(mesh: &TriMesh, draft: f64) -> Option<Vec<Vec<Point3<f64>>>> {
let vertices = mesh.vertices();
let indices = mesh.indices();
if vertices.is_empty() || indices.is_empty() {
return None;
}
let tolerance = 1e-6;
let mut segments: Vec<(Point3<f64>, Point3<f64>)> = Vec::new();
for tri_indices in indices {
let v0 = vertices[tri_indices[0] as usize];
let v1 = vertices[tri_indices[1] as usize];
let v2 = vertices[tri_indices[2] as usize];
let tri_vs = [v0, v1, v2];
let mut intersections = Vec::with_capacity(2);
for i in 0..3 {
let p_start = tri_vs[i];
let p_end = tri_vs[(i + 1) % 3];
let d1 = p_start.z - draft;
let d2 = p_end.z - draft;
if d1 * d2 < 0.0 {
let t = d1 / (d1 - d2); let pt = Point3::new(
p_start.x + (p_end.x - p_start.x) * t,
p_start.y + (p_end.y - p_start.y) * t,
draft,
);
intersections.push((i, pt));
}
}
if intersections.len() == 2 {
let (idx1, p1) = intersections[0];
let (idx2, p2) = intersections[1];
let get_status = |idx: usize| {
let start = tri_vs[idx].z;
let end = tri_vs[(idx + 1) % 3].z;
if start > draft && end < draft {
1
}
else if start < draft && end > draft {
-1
}
else {
0
}
};
let s1 = get_status(idx1);
let s2 = get_status(idx2);
if s1 == -1 && s2 == 1 {
segments.push((p1, p2));
} else if s1 == 1 && s2 == -1 {
segments.push((p2, p1));
}
}
}
chain_segments(segments, tolerance)
}
fn chain_segments(
segments: Vec<(Point3<f64>, Point3<f64>)>,
tolerance: f64,
) -> Option<Vec<Vec<Point3<f64>>>> {
if segments.is_empty() {
return Some(vec![]);
}
#[derive(Clone, Copy)]
struct Segment {
p_start: Point3<f64>,
p_end: Point3<f64>,
original_idx: usize,
}
let mut indexed_segments: Vec<Segment> = segments
.iter()
.enumerate()
.map(|(i, &(s, e))| Segment {
p_start: s,
p_end: e,
original_idx: i,
})
.collect();
indexed_segments.sort_by(|a, b| {
a.p_start
.x
.partial_cmp(&b.p_start.x)
.unwrap()
.then(a.p_start.y.partial_cmp(&b.p_start.y).unwrap())
});
let find_next = |p: Point3<f64>, used: &[bool]| -> Option<usize> {
let found = indexed_segments.binary_search_by(|seg| {
if seg.p_start.x < p.x - tolerance {
std::cmp::Ordering::Less
} else if seg.p_start.x > p.x + tolerance {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
});
let start_idx = match found {
Ok(i) => i,
Err(i) => i,
};
let mut i = start_idx;
while i > 0 && indexed_segments[i - 1].p_start.x >= p.x - tolerance {
i -= 1;
}
while i < indexed_segments.len() && indexed_segments[i].p_start.x <= p.x + tolerance {
let seg = &indexed_segments[i];
if !used[seg.original_idx] {
if (seg.p_start - p).norm_squared() < tolerance * tolerance {
return Some(i);
}
}
i += 1;
}
None
};
let count = segments.len();
let mut used = vec![false; count];
let mut contours = Vec::new();
for i in 0..count {
let root_seg_idx_in_sorted = i;
let root_seg = &indexed_segments[root_seg_idx_in_sorted];
if used[root_seg.original_idx] {
continue;
}
let mut contour = Vec::new();
let mut current_seg_idx_sorted = root_seg_idx_in_sorted;
loop {
let seg = &indexed_segments[current_seg_idx_sorted];
used[seg.original_idx] = true;
contour.push(seg.p_start);
let next_pt = seg.p_end;
if let Some(next_idx) = find_next(next_pt, &used) {
current_seg_idx_sorted = next_idx;
} else {
if (next_pt - contour[0]).norm_squared() < tolerance * tolerance {
break;
} else {
contour.push(next_pt);
break;
}
}
}
if contour.len() >= 3 {
contours.push(contour);
}
}
Some(contours)
}
#[cfg(test)]
mod tests {
use super::*;
use parry3d_f64::math::Point;
use parry3d_f64::shape::TriMesh;
fn create_box_mesh(loa: f64, boa: f64, depth: f64) -> TriMesh {
let hb = boa / 2.0;
let vertices = vec![
Point::new(0.0, -hb, 0.0),
Point::new(loa, -hb, 0.0),
Point::new(loa, hb, 0.0),
Point::new(0.0, hb, 0.0),
Point::new(0.0, -hb, depth),
Point::new(loa, -hb, depth),
Point::new(loa, hb, depth),
Point::new(0.0, hb, depth),
];
let indices = vec![
[0, 2, 1],
[0, 3, 2],
[4, 5, 6],
[4, 6, 7],
[0, 1, 5],
[0, 5, 4],
[2, 3, 7],
[2, 7, 6],
[0, 4, 7],
[0, 7, 3],
[1, 2, 6],
[1, 6, 5],
];
TriMesh::new(vertices, indices).unwrap()
}
#[test]
fn test_box_waterplane_area() {
let mesh = create_box_mesh(10.0, 10.0, 10.0);
let draft = 5.0;
let wp = calculate_waterplane_properties(&mesh, draft).unwrap();
let expected_area = 100.0;
assert!(
(wp.area - expected_area).abs() < 1.0,
"Area: got {:.2}, expected {:.2}",
wp.area,
expected_area
);
}
#[test]
fn test_box_waterplane_centroid() {
let mesh = create_box_mesh(10.0, 10.0, 10.0);
let draft = 5.0;
let wp = calculate_waterplane_properties(&mesh, draft).unwrap();
assert!(
(wp.centroid[0] - 5.0).abs() < 0.1,
"LCF: got {:.2}, expected 5.0",
wp.centroid[0]
);
assert!(
wp.centroid[1].abs() < 0.1,
"TCF: got {:.2}, expected 0.0",
wp.centroid[1]
);
}
}