385 lines
11 KiB
Rust
385 lines
11 KiB
Rust
//! 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);
|
|
}
|
|
}
|