use crate::appendage::Appendage;
use crate::deckedge::DeckEdge;
use crate::downflooding::DownfloodingOpening;
use crate::hull::Hull;
use crate::silhouette::Silhouette;
use crate::tanks::Tank;
#[derive(Clone)]
pub struct Vessel {
hulls: Vec<Hull>,
tanks: Vec<Tank>,
appendages: Vec<Appendage>,
deck_edges: Vec<DeckEdge>,
ap: Option<f64>,
fp: Option<f64>,
silhouettes: Vec<Silhouette>,
downflooding_openings: Vec<DownfloodingOpening>,
}
impl Vessel {
pub fn new(hull: Hull) -> Self {
Self {
hulls: vec![hull],
tanks: Vec::new(),
appendages: Vec::new(),
deck_edges: Vec::new(),
ap: None,
fp: None,
silhouettes: Vec::new(),
downflooding_openings: Vec::new(),
}
}
pub fn new_multi(hulls: Vec<Hull>) -> Result<Self, &'static str> {
if hulls.is_empty() {
return Err("At least one hull must be provided");
}
Ok(Self {
hulls,
tanks: Vec::new(),
appendages: Vec::new(),
deck_edges: Vec::new(),
ap: None,
fp: None,
silhouettes: Vec::new(),
downflooding_openings: Vec::new(),
})
}
pub fn with_perpendiculars(hull: Hull, ap: f64, fp: f64) -> Self {
Self {
hulls: vec![hull],
tanks: Vec::new(),
appendages: Vec::new(),
deck_edges: Vec::new(),
ap: Some(ap),
fp: Some(fp),
silhouettes: Vec::new(),
downflooding_openings: Vec::new(),
}
}
pub fn hulls(&self) -> &[Hull] {
&self.hulls
}
pub fn hulls_mut(&mut self) -> &mut Vec<Hull> {
&mut self.hulls
}
pub fn is_multihull(&self) -> bool {
self.hulls.len() > 1
}
pub fn tanks(&self) -> &[Tank] {
&self.tanks
}
pub fn tanks_mut(&mut self) -> &mut Vec<Tank> {
&mut self.tanks
}
pub fn ap(&self) -> f64 {
self.ap.unwrap_or_else(|| self.get_bounds().0)
}
pub fn fp(&self) -> f64 {
self.fp.unwrap_or_else(|| self.get_bounds().1)
}
pub fn set_ap(&mut self, ap: f64) {
self.ap = Some(ap);
}
pub fn set_fp(&mut self, fp: f64) {
self.fp = Some(fp);
}
pub fn lbp(&self) -> f64 {
self.fp() - self.ap()
}
pub fn get_bounds(&self) -> (f64, f64, f64, f64, f64, f64) {
if self.hulls.len() == 1 {
return self.hulls[0].get_bounds();
}
let all_bounds: Vec<_> = self.hulls.iter().map(|h| h.get_bounds()).collect();
let xmin = all_bounds.iter().map(|b| b.0).fold(f64::INFINITY, f64::min);
let xmax = all_bounds
.iter()
.map(|b| b.1)
.fold(f64::NEG_INFINITY, f64::max);
let ymin = all_bounds.iter().map(|b| b.2).fold(f64::INFINITY, f64::min);
let ymax = all_bounds
.iter()
.map(|b| b.3)
.fold(f64::NEG_INFINITY, f64::max);
let zmin = all_bounds.iter().map(|b| b.4).fold(f64::INFINITY, f64::min);
let zmax = all_bounds
.iter()
.map(|b| b.5)
.fold(f64::NEG_INFINITY, f64::max);
(xmin, xmax, ymin, ymax, zmin, zmax)
}
pub fn add_tank(&mut self, tank: Tank) {
self.tanks.push(tank);
}
pub fn remove_tank(&mut self, index: usize) -> Option<Tank> {
if index < self.tanks.len() {
Some(self.tanks.remove(index))
} else {
None
}
}
pub fn get_tank_by_name(&self, name: &str) -> Option<&Tank> {
self.tanks.iter().find(|t| t.name() == name)
}
pub fn get_tank_by_name_mut(&mut self, name: &str) -> Option<&mut Tank> {
self.tanks.iter_mut().find(|t| t.name() == name)
}
pub fn get_total_tanks_mass(&self) -> f64 {
self.tanks.iter().map(|t| t.fluid_mass()).sum()
}
pub fn get_tanks_center_of_gravity(&self) -> [f64; 3] {
let total_mass = self.get_total_tanks_mass();
if total_mass <= 0.0 {
return [0.0, 0.0, 0.0];
}
let mut moment = [0.0, 0.0, 0.0];
for tank in &self.tanks {
if tank.fluid_mass() > 0.0 {
let cog = tank.center_of_gravity();
moment[0] += tank.fluid_mass() * cog[0];
moment[1] += tank.fluid_mass() * cog[1];
moment[2] += tank.fluid_mass() * cog[2];
}
}
[
moment[0] / total_mass,
moment[1] / total_mass,
moment[2] / total_mass,
]
}
pub fn get_total_free_surface_moment(&self) -> (f64, f64) {
let fsm_t: f64 = self.tanks.iter().map(|t| t.free_surface_moment_t()).sum();
let fsm_l: f64 = self.tanks.iter().map(|t| t.free_surface_moment_l()).sum();
(fsm_t, fsm_l)
}
pub fn get_total_free_surface_correction(&self) -> (f64, f64) {
let fsc_t: f64 = self
.tanks
.iter()
.map(|t| t.free_surface_correction_t())
.sum();
let fsc_l: f64 = self
.tanks
.iter()
.map(|t| t.free_surface_correction_l())
.sum();
(fsc_t, fsc_l)
}
pub fn add_silhouette(&mut self, silhouette: Silhouette) {
self.silhouettes.push(silhouette);
}
pub fn silhouettes(&self) -> &[Silhouette] {
&self.silhouettes
}
pub fn silhouettes_mut(&mut self) -> &mut Vec<Silhouette> {
&mut self.silhouettes
}
pub fn num_silhouettes(&self) -> usize {
self.silhouettes.len()
}
pub fn has_silhouettes(&self) -> bool {
!self.silhouettes.is_empty()
}
pub fn get_silhouette_by_name(&self, name: &str) -> Option<&Silhouette> {
self.silhouettes.iter().find(|s| s.name() == name)
}
pub fn remove_silhouette(&mut self, index: usize) -> Option<Silhouette> {
if index < self.silhouettes.len() {
Some(self.silhouettes.remove(index))
} else {
None
}
}
pub fn clear_silhouettes(&mut self) {
self.silhouettes.clear();
}
pub fn get_total_emerged_area(&self, waterline_z: f64) -> f64 {
self.silhouettes
.iter()
.map(|s| s.get_emerged_area(waterline_z))
.sum()
}
pub fn get_combined_emerged_centroid(&self, waterline_z: f64) -> [f64; 2] {
let total_area = self.get_total_emerged_area(waterline_z);
if total_area < 1e-9 {
return [0.0, 0.0];
}
let mut cx = 0.0;
let mut cz = 0.0;
for s in &self.silhouettes {
let area = s.get_emerged_area(waterline_z);
if area > 1e-9 {
let centroid = s.get_emerged_centroid(waterline_z);
cx += centroid[0] * area;
cz += centroid[1] * area;
}
}
[cx / total_area, cz / total_area]
}
pub fn add_appendage(&mut self, appendage: Appendage) {
self.appendages.push(appendage);
}
pub fn appendages(&self) -> &[Appendage] {
&self.appendages
}
pub fn appendages_mut(&mut self) -> &mut Vec<Appendage> {
&mut self.appendages
}
pub fn num_appendages(&self) -> usize {
self.appendages.len()
}
pub fn delete_appendage(&mut self, index: usize) -> Option<Appendage> {
if index < self.appendages.len() {
Some(self.appendages.remove(index))
} else {
None
}
}
pub fn delete_appendage_by_name(&mut self, name: &str) -> Option<Appendage> {
if let Some(idx) = self.appendages.iter().position(|a| a.name() == name) {
Some(self.appendages.remove(idx))
} else {
None
}
}
pub fn get_appendage_by_name(&self, name: &str) -> Option<&Appendage> {
self.appendages.iter().find(|a| a.name() == name)
}
pub fn get_appendage_by_name_mut(&mut self, name: &str) -> Option<&mut Appendage> {
self.appendages.iter_mut().find(|a| a.name() == name)
}
pub fn get_total_appendage_volume(&self) -> f64 {
self.appendages.iter().map(|a| a.volume()).sum()
}
pub fn get_total_appendage_wetted_surface(&self) -> f64 {
self.appendages
.iter()
.filter_map(|a| a.wetted_surface())
.sum()
}
pub fn clear_appendages(&mut self) {
self.appendages.clear();
}
pub fn add_deck_edge(&mut self, deck_edge: DeckEdge) {
self.deck_edges.push(deck_edge);
}
pub fn deck_edges(&self) -> &[DeckEdge] {
&self.deck_edges
}
pub fn deck_edges_mut(&mut self) -> &mut Vec<DeckEdge> {
&mut self.deck_edges
}
pub fn num_deck_edges(&self) -> usize {
self.deck_edges.len()
}
pub fn has_deck_edges(&self) -> bool {
!self.deck_edges.is_empty()
}
pub fn delete_deck_edge(&mut self, index: usize) -> Option<DeckEdge> {
if index < self.deck_edges.len() {
Some(self.deck_edges.remove(index))
} else {
None
}
}
pub fn get_deck_edge_by_name(&self, name: &str) -> Option<&DeckEdge> {
self.deck_edges.iter().find(|d| d.name() == name)
}
pub fn clear_deck_edges(&mut self) {
self.deck_edges.clear();
}
pub fn get_min_freeboard(&self, heel: f64, trim: f64, waterline_z: f64) -> Option<f64> {
if self.deck_edges.is_empty() {
return None;
}
let bounds = self.get_bounds();
let pivot = [
(self.ap() + self.fp()) / 2.0,
(bounds.2 + bounds.3) / 2.0,
waterline_z,
];
Some(
self.deck_edges
.iter()
.map(|de| de.get_freeboard(heel, trim, pivot, waterline_z))
.fold(f64::INFINITY, f64::min),
)
}
pub fn add_downflooding_opening(&mut self, opening: DownfloodingOpening) {
self.downflooding_openings.push(opening);
}
pub fn downflooding_openings(&self) -> &[DownfloodingOpening] {
&self.downflooding_openings
}
pub fn downflooding_openings_mut(&mut self) -> &mut Vec<DownfloodingOpening> {
&mut self.downflooding_openings
}
pub fn num_downflooding_openings(&self) -> usize {
self.downflooding_openings.len()
}
pub fn has_downflooding_openings(&self) -> bool {
!self.downflooding_openings.is_empty()
}
pub fn clear_downflooding_openings(&mut self) {
self.downflooding_openings.clear();
}
}
impl std::fmt::Debug for Vessel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bounds = self.get_bounds();
f.debug_struct("Vessel")
.field("hulls", &self.hulls.len())
.field("tanks", &self.tanks.len())
.field("ap", &self.ap())
.field("fp", &self.fp())
.field("lbp", &self.lbp())
.field("bounds", &bounds)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::Point3;
use parry3d_f64::shape::TriMesh;
fn create_test_hull() -> Hull {
let vertices = vec![
Point3::new(0.0, -5.0, 0.0),
Point3::new(100.0, -5.0, 0.0),
Point3::new(100.0, 5.0, 0.0),
Point3::new(0.0, 5.0, 0.0),
Point3::new(0.0, -5.0, 10.0),
Point3::new(100.0, -5.0, 10.0),
Point3::new(100.0, 5.0, 10.0),
Point3::new(0.0, 5.0, 10.0),
];
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],
];
let mesh = TriMesh::new(vertices, indices).unwrap();
Hull::from_mesh(mesh)
}
#[test]
fn test_vessel_bounds() {
let hull = create_test_hull();
let vessel = Vessel::new(hull);
let bounds = vessel.get_bounds();
assert!((bounds.0 - 0.0).abs() < 1e-6);
assert!((bounds.1 - 100.0).abs() < 1e-6);
}
#[test]
fn test_vessel_perpendiculars() {
let hull = create_test_hull();
let vessel = Vessel::new(hull);
assert!((vessel.ap() - 0.0).abs() < 1e-6);
assert!((vessel.fp() - 100.0).abs() < 1e-6);
assert!((vessel.lbp() - 100.0).abs() < 1e-6);
}
}