//! 3D Geometry types for thermal ablation simulation use serde::{Deserialize, Serialize}; /// 3D point with vector operations #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)] pub struct Point3D { pub x: f32, pub y: f32, pub z: f32, } impl Point3D { /// Create a new 3D point #[must_use] pub const fn new(x: f32, y: f32, z: f32) -> Self { Self { x, y, z } } /// Create a point at the origin #[must_use] pub const fn origin() -> Self { Self::new(0.0, 0.0, 0.0) } /// Distance to another point #[must_use] pub fn distance(&self, other: &Self) -> f32 { self.distance_sq(other).sqrt() } /// Squared distance (faster, no sqrt) #[must_use] pub fn distance_sq(&self, other: &Self) -> f32 { let dx = self.x - other.x; let dy = self.y - other.y; let dz = self.z - other.z; dx * dx + dy * dy + dz * dz } /// Magnitude (length) of the vector from origin #[must_use] pub fn magnitude(&self) -> f32 { (self.x * self.x + self.y * self.y + self.z * self.z).sqrt() } /// Normalize to unit vector #[must_use] pub fn normalize(&self) -> Self { let mag = self.magnitude(); if mag > 1e-10 { Self::new(self.x / mag, self.y / mag, self.z / mag) } else { Self::new(0.0, 0.0, 1.0) // Default direction } } /// Dot product with another vector #[must_use] pub fn dot(&self, other: &Self) -> f32 { self.x * other.x + self.y * other.y + self.z * other.z } /// Cross product with another vector #[must_use] pub fn cross(&self, other: &Self) -> Self { Self::new( self.y * other.z - self.z * other.y, self.z * other.x - self.x * other.z, self.x * other.y - self.y * other.x, ) } /// Add two points/vectors #[must_use] pub fn add(&self, other: &Self) -> Self { Self::new(self.x + other.x, self.y + other.y, self.z + other.z) } /// Subtract another point/vector #[must_use] pub fn sub(&self, other: &Self) -> Self { Self::new(self.x - other.x, self.y - other.y, self.z - other.z) } /// Scale by a scalar value #[must_use] pub fn scale(&self, s: f32) -> Self { Self::new(self.x * s, self.y * s, self.z * s) } /// Convert to array #[must_use] pub fn to_array(&self) -> [f32; 3] { [self.x, self.y, self.z] } /// Create from array #[must_use] pub fn from_array(arr: [f32; 3]) -> Self { Self::new(arr[0], arr[1], arr[2]) } } /// 3D axis-aligned bounding box #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub struct BoundingBox3D { /// Minimum corner (x_min, y_min, z_min) pub min: Point3D, /// Maximum corner (x_max, y_max, z_max) pub max: Point3D, } impl BoundingBox3D { /// Create a new bounding box #[must_use] pub fn new(min: Point3D, max: Point3D) -> Self { Self { min, max } } /// Create a centered cube with given half-size #[must_use] pub fn centered_cube(center: Point3D, half_size: f32) -> Self { Self { min: Point3D::new( center.x - half_size, center.y - half_size, center.z - half_size, ), max: Point3D::new( center.x + half_size, center.y + half_size, center.z + half_size, ), } } /// Create a box from dimensions centered at origin #[must_use] pub fn from_dimensions(width: f32, height: f32, depth: f32) -> Self { Self { min: Point3D::new(-width / 2.0, -height / 2.0, -depth / 2.0), max: Point3D::new(width / 2.0, height / 2.0, depth / 2.0), } } /// Get the size of the box in each dimension #[must_use] pub fn size(&self) -> Point3D { self.max.sub(&self.min) } /// Get the center of the box #[must_use] pub fn center(&self) -> Point3D { Point3D::new( f32::midpoint(self.min.x, self.max.x), f32::midpoint(self.min.y, self.max.y), f32::midpoint(self.min.z, self.max.z), ) } /// Check if a point is inside the box #[must_use] pub fn contains(&self, point: &Point3D) -> bool { point.x >= self.min.x && point.x <= self.max.x && point.y >= self.min.y && point.y <= self.max.y && point.z >= self.min.z && point.z <= self.max.z } /// Get volume of the box #[must_use] pub fn volume(&self) -> f32 { let s = self.size(); s.x * s.y * s.z } } impl Default for BoundingBox3D { fn default() -> Self { // Default 10cm x 10cm x 10cm box (typical liver section) Self::from_dimensions(0.1, 0.1, 0.1) } } /// Geometry of the ablation probe (e.g., RF needle, laser fiber) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ProbeGeometry { /// Position of the probe tip in meters pub position: Point3D, /// Direction vector of the probe axis (normalized) pub direction: [f32; 3], /// Active heating length in meters (the part that generates heat) pub active_length: f32, /// Probe radius in meters pub radius: f32, } impl ProbeGeometry { /// Create a new probe geometry #[must_use] pub fn new(position: Point3D, direction: [f32; 3], active_length: f32, radius: f32) -> Self { // Normalize direction let mag = (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]) .sqrt(); let normalized = if mag > 1e-10 { [direction[0] / mag, direction[1] / mag, direction[2] / mag] } else { [0.0, 0.0, 1.0] // Default to Z-axis }; Self { position, direction: normalized, active_length, radius, } } /// Create a typical RF ablation needle #[must_use] pub fn rf_needle(position: Point3D) -> Self { Self::new( position, [0.0, 0.0, 1.0], // Aligned with Z-axis 0.03, // 3cm active length 0.0009, // ~17 gauge needle (0.9mm radius) ) } /// Create a typical microwave ablation antenna #[must_use] pub fn microwave_antenna(position: Point3D) -> Self { Self::new( position, [0.0, 0.0, 1.0], 0.04, // 4cm active length 0.0012, // ~14 gauge (1.2mm radius) ) } /// Create a laser fiber probe #[must_use] pub fn laser_fiber(position: Point3D) -> Self { Self::new( position, [0.0, 0.0, 1.0], 0.01, // 1cm diffusing tip 0.0003, // 600μm fiber (0.3mm radius) ) } /// Get the direction as a Point3D vector #[must_use] pub fn direction_vec(&self) -> Point3D { Point3D::from_array(self.direction) } /// Get the end point of the active region #[must_use] pub fn active_end(&self) -> Point3D { let dir = self.direction_vec(); self.position.add(&dir.scale(self.active_length)) } /// Calculate distance from a point to the probe axis (cylindrical distance) /// Returns (axial_distance, radial_distance) where: /// - axial_distance is distance along the probe axis from tip (negative if before tip) /// - radial_distance is perpendicular distance from axis #[must_use] pub fn distance_to_axis(&self, point: &Point3D) -> (f32, f32) { let dir = self.direction_vec(); let to_point = point.sub(&self.position); // Project onto axis let axial = to_point.dot(&dir); // Perpendicular component let axial_component = dir.scale(axial); let perpendicular = to_point.sub(&axial_component); let radial = perpendicular.magnitude(); (axial, radial) } /// Check if a point is within the active heating region #[must_use] pub fn is_in_active_region(&self, point: &Point3D) -> bool { let (axial, radial) = self.distance_to_axis(point); axial >= 0.0 && axial <= self.active_length && radial <= self.radius } } impl Default for ProbeGeometry { fn default() -> Self { Self::rf_needle(Point3D::origin()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_point3d_distance() { let p1 = Point3D::origin(); let p2 = Point3D::new(3.0, 4.0, 0.0); assert!((p1.distance(&p2) - 5.0).abs() < 1e-6); } #[test] fn test_point3d_normalize() { let p = Point3D::new(3.0, 4.0, 0.0); let n = p.normalize(); assert!((n.magnitude() - 1.0).abs() < 1e-6); assert!((n.x - 0.6).abs() < 1e-6); assert!((n.y - 0.8).abs() < 1e-6); } #[test] fn test_point3d_cross() { let x = Point3D::new(1.0, 0.0, 0.0); let y = Point3D::new(0.0, 1.0, 0.0); let z = x.cross(&y); assert!((z.x).abs() < 1e-6); assert!((z.y).abs() < 1e-6); assert!((z.z - 1.0).abs() < 1e-6); } #[test] fn test_bounding_box_contains() { let bbox = BoundingBox3D::centered_cube(Point3D::origin(), 1.0); assert!(bbox.contains(&Point3D::origin())); assert!(bbox.contains(&Point3D::new(0.5, 0.5, 0.5))); assert!(!bbox.contains(&Point3D::new(2.0, 0.0, 0.0))); } #[test] fn test_probe_distance_to_axis() { let probe = ProbeGeometry::rf_needle(Point3D::origin()); // Point on the axis let (axial, radial) = probe.distance_to_axis(&Point3D::new(0.0, 0.0, 0.01)); assert!((axial - 0.01).abs() < 1e-6); assert!(radial.abs() < 1e-6); // Point off the axis let (axial, radial) = probe.distance_to_axis(&Point3D::new(0.01, 0.0, 0.01)); assert!((axial - 0.01).abs() < 1e-6); assert!((radial - 0.01).abs() < 1e-6); } #[test] fn test_probe_active_region() { let probe = ProbeGeometry::new( Point3D::origin(), [0.0, 0.0, 1.0], 0.03, // 3cm active length 0.001, // 1mm radius ); // Inside active region assert!(probe.is_in_active_region(&Point3D::new(0.0, 0.0, 0.015))); // Outside (beyond active length) assert!(!probe.is_in_active_region(&Point3D::new(0.0, 0.0, 0.04))); // Outside (too far radially) assert!(!probe.is_in_active_region(&Point3D::new(0.01, 0.0, 0.015))); } #[test] fn test_serialize_probe() { let probe = ProbeGeometry::rf_needle(Point3D::new(0.05, 0.05, 0.0)); let json = serde_json::to_string(&probe).unwrap(); let deserialized: ProbeGeometry = serde_json::from_str(&json).unwrap(); assert_eq!(probe.position, deserialized.position); } }