use dxf::entities::EntityType as DxfEntityType;
use dxf::Drawing;
use std::f64::consts::PI;
use std::path::Path;
use thiserror::Error;
use vtkio::model::{DataSet, Piece};
use vtkio::Vtk;
#[derive(Error, Debug)]
pub enum DeckEdgeLoadError {
#[error("Failed to read file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse DXF file: {0}")]
DxfParseError(#[from] dxf::DxfError),
#[error("Failed to parse VTK file: {0}")]
VtkParseError(#[from] vtkio::Error),
#[error("No valid geometry found in file")]
NoGeometry,
#[error("Unsupported file format: {0}")]
UnsupportedFormat(String),
}
#[derive(Clone, Debug, PartialEq)]
pub enum DeckEdgeSide {
Port,
Starboard,
Both,
}
#[derive(Clone, Debug)]
pub struct DeckEdge {
name: String,
points: Vec<[f64; 3]>,
side: DeckEdgeSide,
}
impl DeckEdge {
pub fn new(name: &str, points: Vec<[f64; 3]>, side: DeckEdgeSide) -> Self {
Self {
name: name.to_string(),
points,
side,
}
}
pub fn from_file(name: &str, path: &Path) -> Result<Self, DeckEdgeLoadError> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_default();
let points = match ext.as_str() {
"dxf" => Self::load_dxf(path)?,
"vtk" | "vtp" => Self::load_vtk(path)?,
_ => {
return Err(DeckEdgeLoadError::UnsupportedFormat(format!(
"Unsupported extension: {}",
ext
)));
}
};
if points.is_empty() {
return Err(DeckEdgeLoadError::NoGeometry);
}
let side = Self::detect_side(&points);
Ok(Self {
name: name.to_string(),
points,
side,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
pub fn points(&self) -> &[[f64; 3]] {
&self.points
}
pub fn side(&self) -> &DeckEdgeSide {
&self.side
}
pub fn set_side(&mut self, side: DeckEdgeSide) {
self.side = side;
}
pub fn get_min_z_at_heel(&self, heel: f64, trim: f64, pivot: [f64; 3]) -> f64 {
self.points
.iter()
.map(|p| Self::rotate_point(*p, heel, trim, pivot)[2])
.fold(f64::INFINITY, f64::min)
}
pub fn get_freeboard(&self, heel: f64, trim: f64, pivot: [f64; 3], waterline_z: f64) -> f64 {
self.get_min_z_at_heel(heel, trim, pivot) - waterline_z
}
fn rotate_point(point: [f64; 3], heel: f64, trim: f64, pivot: [f64; 3]) -> [f64; 3] {
let heel_rad = heel * PI / 180.0;
let trim_rad = trim * PI / 180.0;
let x = point[0] - pivot[0];
let y = point[1] - pivot[1];
let z = point[2] - pivot[2];
let cos_h = heel_rad.cos();
let sin_h = heel_rad.sin();
let y1 = y * cos_h - z * sin_h;
let z1 = y * sin_h + z * cos_h;
let cos_t = trim_rad.cos();
let sin_t = trim_rad.sin();
let x2 = x * cos_t + z1 * sin_t;
let z2 = -x * sin_t + z1 * cos_t;
[x2 + pivot[0], y1 + pivot[1], z2 + pivot[2]]
}
fn detect_side(points: &[[f64; 3]]) -> DeckEdgeSide {
let has_positive_y = points.iter().any(|p| p[1] > 0.01);
let has_negative_y = points.iter().any(|p| p[1] < -0.01);
match (has_positive_y, has_negative_y) {
(true, true) => DeckEdgeSide::Both,
(true, false) => DeckEdgeSide::Starboard,
(false, true) => DeckEdgeSide::Port,
(false, false) => DeckEdgeSide::Both, }
}
fn load_dxf(path: &Path) -> Result<Vec<[f64; 3]>, DeckEdgeLoadError> {
let drawing = Drawing::load_file(path)?;
let mut points = Vec::new();
for entity in drawing.entities() {
match &entity.specific {
DxfEntityType::Polyline(polyline) => {
for vertex in polyline.vertices() {
points.push([vertex.location.x, vertex.location.y, vertex.location.z]);
}
}
DxfEntityType::LwPolyline(lwpoly) => {
let z = lwpoly.extrusion_direction.z;
for vertex in &lwpoly.vertices {
points.push([vertex.x, vertex.y, z]);
}
}
DxfEntityType::Line(line) => {
points.push([line.p1.x, line.p1.y, line.p1.z]);
points.push([line.p2.x, line.p2.y, line.p2.z]);
}
DxfEntityType::Spline(spline) => {
for cp in &spline.control_points {
points.push([cp.x, cp.y, cp.z]);
}
}
_ => {}
}
}
Ok(points)
}
fn load_vtk(path: &Path) -> Result<Vec<[f64; 3]>, DeckEdgeLoadError> {
let vtk = Vtk::import(path)?;
fn extract_points(data: &DataSet) -> Result<Vec<[f64; 3]>, DeckEdgeLoadError> {
match data {
DataSet::PolyData { pieces, .. } => {
for piece in pieces {
if let Piece::Inline(piece_data) = piece {
let buffer = &piece_data.points;
return extract_coords(buffer);
}
}
Err(DeckEdgeLoadError::NoGeometry)
}
DataSet::UnstructuredGrid { pieces, .. } => {
for piece in pieces {
if let Piece::Inline(piece_data) = piece {
let buffer = &piece_data.points;
return extract_coords(buffer);
}
}
Err(DeckEdgeLoadError::NoGeometry)
}
_ => Err(DeckEdgeLoadError::NoGeometry),
}
}
fn extract_coords(buffer: &vtkio::IOBuffer) -> Result<Vec<[f64; 3]>, DeckEdgeLoadError> {
let coords: Vec<f64> = match buffer {
vtkio::IOBuffer::F64(v) => v.clone(),
vtkio::IOBuffer::F32(v) => v.iter().map(|x| *x as f64).collect(),
_ => return Err(DeckEdgeLoadError::NoGeometry),
};
Ok(coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect())
}
extract_points(&vtk.data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deck_edge_creation() {
let points = vec![[0.0, 5.0, 10.0], [50.0, 5.0, 10.0], [100.0, 5.0, 10.0]];
let edge = DeckEdge::new("main_deck", points.clone(), DeckEdgeSide::Starboard);
assert_eq!(edge.name(), "main_deck");
assert_eq!(edge.points().len(), 3);
assert_eq!(*edge.side(), DeckEdgeSide::Starboard);
}
#[test]
fn test_freeboard_level() {
let points = vec![[0.0, 5.0, 10.0], [50.0, 5.0, 10.0], [100.0, 5.0, 10.0]];
let edge = DeckEdge::new("deck", points, DeckEdgeSide::Starboard);
let min_z = edge.get_min_z_at_heel(0.0, 0.0, [50.0, 0.0, 5.0]);
assert!((min_z - 10.0).abs() < 1e-6);
let freeboard = edge.get_freeboard(0.0, 0.0, [50.0, 0.0, 5.0], 5.0);
assert!((freeboard - 5.0).abs() < 1e-6);
}
#[test]
fn test_freeboard_heeled() {
let points = vec![
[50.0, 5.0, 10.0], ];
let edge = DeckEdge::new("deck", points, DeckEdgeSide::Starboard);
let min_z = edge.get_min_z_at_heel(30.0, 0.0, [50.0, 0.0, 0.0]);
assert!(min_z > 10.0); }
#[test]
fn test_side_detection() {
let pts_stbd = vec![[0.0, 5.0, 10.0]];
assert_eq!(DeckEdge::detect_side(&pts_stbd), DeckEdgeSide::Starboard);
let pts_port = vec![[0.0, -5.0, 10.0]];
assert_eq!(DeckEdge::detect_side(&pts_port), DeckEdgeSide::Port);
let pts_both = vec![[0.0, 5.0, 10.0], [0.0, -5.0, 10.0]];
assert_eq!(DeckEdge::detect_side(&pts_both), DeckEdgeSide::Both);
}
}