Skip to main content

oxiphysics_io/medical_io/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use std::collections::HashMap;
9
10/// A set of anatomical landmarks.
11#[derive(Clone, Debug)]
12pub struct LandmarkSet {
13    /// Collection of landmarks.
14    pub landmarks: Vec<Landmark>,
15    /// Coordinate space name.
16    pub space: String,
17}
18impl LandmarkSet {
19    /// Create an empty landmark set.
20    pub fn new(space: &str) -> Self {
21        Self {
22            landmarks: Vec::new(),
23            space: space.to_string(),
24        }
25    }
26    /// Add a landmark.
27    pub fn add(&mut self, lm: Landmark) {
28        self.landmarks.push(lm);
29    }
30    /// Find landmark by name.
31    pub fn find(&self, name: &str) -> Option<&Landmark> {
32        self.landmarks.iter().find(|l| l.name == name)
33    }
34    /// Compute centroid of all landmarks.
35    pub fn centroid(&self) -> Option<[f64; 3]> {
36        if self.landmarks.is_empty() {
37            return None;
38        }
39        let n = self.landmarks.len() as f64;
40        let mut c = [0.0f64; 3];
41        for lm in &self.landmarks {
42            for d in 0..3 {
43                c[d] += lm.position[d];
44            }
45        }
46        for d in 0..3 {
47            c[d] /= n;
48        }
49        Some(c)
50    }
51}
52/// DICOM data element: tag + typed value.
53#[derive(Clone, Debug)]
54pub struct DicomElement {
55    /// The tag identifying this element.
56    pub tag: DicomTag,
57    /// The value representation and data.
58    pub vr: DicomVr,
59}
60impl DicomElement {
61    /// Create a new DICOM element.
62    pub fn new(tag: DicomTag, vr: DicomVr) -> Self {
63        Self { tag, vr }
64    }
65}
66/// VTK structured image data (vtkImageData equivalent).
67#[derive(Clone, Debug)]
68pub struct VtkImageData {
69    /// Grid dimensions \[nx, ny, nz\].
70    pub dimensions: [usize; 3],
71    /// Grid origin \[ox, oy, oz\].
72    pub origin: [f64; 3],
73    /// Voxel spacing \[dx, dy, dz\].
74    pub spacing: [f64; 3],
75    /// Named scalar arrays (one value per voxel).
76    pub scalar_arrays: HashMap<String, Vec<f64>>,
77    /// Named vector arrays (3 values per voxel).
78    pub vector_arrays: HashMap<String, Vec<[f64; 3]>>,
79}
80impl VtkImageData {
81    /// Create new VTK image data.
82    pub fn new(dimensions: [usize; 3], origin: [f64; 3], spacing: [f64; 3]) -> Self {
83        Self {
84            dimensions,
85            origin,
86            spacing,
87            scalar_arrays: HashMap::new(),
88            vector_arrays: HashMap::new(),
89        }
90    }
91    /// Total number of voxels.
92    pub fn n_voxels(&self) -> usize {
93        self.dimensions[0] * self.dimensions[1] * self.dimensions[2]
94    }
95    /// Add a scalar data array.
96    pub fn add_scalar(&mut self, name: &str, data: Vec<f64>) {
97        self.scalar_arrays.insert(name.to_string(), data);
98    }
99    /// Add a vector data array.
100    pub fn add_vector(&mut self, name: &str, data: Vec<[f64; 3]>) {
101        self.vector_arrays.insert(name.to_string(), data);
102    }
103    /// Convert voxel index to world coordinates.
104    pub fn index_to_world(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
105        [
106            self.origin[0] + i as f64 * self.spacing[0],
107            self.origin[1] + j as f64 * self.spacing[1],
108            self.origin[2] + k as f64 * self.spacing[2],
109        ]
110    }
111    /// Serialize to VTK legacy format string.
112    pub fn to_vtk_string(&self, scalar_name: &str) -> String {
113        let [nx, ny, nz] = self.dimensions;
114        let [ox, oy, oz] = self.origin;
115        let [dx, dy, dz] = self.spacing;
116        let n = self.n_voxels();
117        let mut s = format!(
118            "# vtk DataFile Version 3.0\nVTK ImageData\nASCII\nDATASET STRUCTURED_POINTS\n\
119             DIMENSIONS {} {} {}\nORIGIN {} {} {}\nSPACING {} {} {}\n\
120             POINT_DATA {}\n",
121            nx, ny, nz, ox, oy, oz, dx, dy, dz, n
122        );
123        if let Some(arr) = self.scalar_arrays.get(scalar_name) {
124            s.push_str(&format!(
125                "SCALARS {} float 1\nLOOKUP_TABLE default\n",
126                scalar_name
127            ));
128            for &v in arr.iter().take(10) {
129                s.push_str(&format!("{:.6} ", v));
130            }
131        }
132        s
133    }
134}
135/// NIfTI qform code.
136#[derive(Clone, Debug, PartialEq)]
137pub enum NiftiQformCode {
138    /// Unknown / not set.
139    Unknown,
140    /// Scanner anatomical coordinates.
141    ScannerAnat,
142    /// Aligned to reference.
143    AlignedAnat,
144    /// Talairach space.
145    Talairach,
146    /// MNI 152 standard space.
147    Mni152,
148}
149impl NiftiQformCode {
150    /// Return the integer code as stored in the NIfTI header byte.
151    pub fn as_code(&self) -> u16 {
152        match self {
153            NiftiQformCode::Unknown => 0,
154            NiftiQformCode::ScannerAnat => 1,
155            NiftiQformCode::AlignedAnat => 2,
156            NiftiQformCode::Talairach => 3,
157            NiftiQformCode::Mni152 => 4,
158        }
159    }
160    /// Create from integer code.
161    pub fn from_code(code: u16) -> Self {
162        match code {
163            1 => NiftiQformCode::ScannerAnat,
164            2 => NiftiQformCode::AlignedAnat,
165            3 => NiftiQformCode::Talairach,
166            4 => NiftiQformCode::Mni152,
167            _ => NiftiQformCode::Unknown,
168        }
169    }
170}
171/// NRRD (Nearly Raw Raster Data) data encoding.
172#[derive(Clone, Debug, PartialEq)]
173pub enum NrrdEncoding {
174    /// Raw binary (no compression).
175    Raw,
176    /// ASCII text.
177    Text,
178    /// Gzip compressed (mock: same as raw in unit tests).
179    Gzip,
180}
181/// DICOM header tag (group, element).
182#[derive(Clone, Debug, PartialEq, Eq, Hash)]
183pub struct DicomTag {
184    /// Tag group number.
185    pub group: u16,
186    /// Tag element number.
187    pub element: u16,
188}
189impl DicomTag {
190    /// Create a new DICOM tag.
191    pub fn new(group: u16, element: u16) -> Self {
192        Self { group, element }
193    }
194    /// Patient name tag (0010,0010).
195    pub fn patient_name() -> Self {
196        Self::new(0x0010, 0x0010)
197    }
198    /// Patient ID tag (0010,0020).
199    pub fn patient_id() -> Self {
200        Self::new(0x0010, 0x0020)
201    }
202    /// Modality tag (0008,0060).
203    pub fn modality() -> Self {
204        Self::new(0x0008, 0x0060)
205    }
206    /// Rows tag (0028,0010).
207    pub fn rows() -> Self {
208        Self::new(0x0028, 0x0010)
209    }
210    /// Columns tag (0028,0011).
211    pub fn columns() -> Self {
212        Self::new(0x0028, 0x0011)
213    }
214    /// Pixel spacing tag (0028,0030).
215    pub fn pixel_spacing() -> Self {
216        Self::new(0x0028, 0x0030)
217    }
218    /// Slice thickness tag (0050,0018).
219    pub fn slice_thickness() -> Self {
220        Self::new(0x0050, 0x0018)
221    }
222    /// Window center tag (0028,1050).
223    pub fn window_center() -> Self {
224        Self::new(0x0028, 0x1050)
225    }
226    /// Window width tag (0028,1051).
227    pub fn window_width() -> Self {
228        Self::new(0x0028, 0x1051)
229    }
230}
231/// NIfTI-1 header (.nii / .nii.gz reader mock).
232#[derive(Clone, Debug)]
233pub struct NiftiReader {
234    /// File path.
235    pub file_path: String,
236    /// Image dimensions \[nx, ny, nz, nt, ...\].
237    pub dim: [usize; 8],
238    /// Voxel sizes in mm.
239    pub pixdim: [f64; 8],
240    /// Data type.
241    pub datatype: NiftiDtype,
242    /// Affine transform (4x4, row-major).
243    pub affine: [f64; 16],
244    /// Description string.
245    pub descrip: String,
246    /// Voxel data (as f64 for convenience).
247    pub data: Vec<f64>,
248}
249impl NiftiReader {
250    /// Create a new NIfTI reader.
251    pub fn new(file_path: &str) -> Self {
252        let mut affine = [0.0f64; 16];
253        affine[0] = 1.0;
254        affine[5] = 1.0;
255        affine[10] = 1.0;
256        affine[15] = 1.0;
257        Self {
258            file_path: file_path.to_string(),
259            dim: [3, 64, 64, 32, 1, 1, 1, 1],
260            pixdim: [1.0; 8],
261            datatype: NiftiDtype::Float32,
262            affine,
263            descrip: String::new(),
264            data: Vec::new(),
265        }
266    }
267    /// Number of voxels = dim\[1\] * dim\[2\] * dim\[3\].
268    pub fn n_voxels(&self) -> usize {
269        self.dim[1] * self.dim[2] * self.dim[3]
270    }
271    /// Initialize data with zeros.
272    pub fn init_data(&mut self) {
273        self.data = vec![0.0; self.n_voxels()];
274    }
275    /// Apply affine to voxel index (i, j, k) to get world coordinates (mm).
276    pub fn voxel_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
277        let a = &self.affine;
278        [
279            a[0] * i + a[1] * j + a[2] * k + a[3],
280            a[4] * i + a[5] * j + a[6] * k + a[7],
281            a[8] * i + a[9] * j + a[10] * k + a[11],
282        ]
283    }
284    /// Get voxel value at (i, j, k).
285    pub fn get_voxel(&self, i: usize, j: usize, k: usize) -> Option<f64> {
286        let nx = self.dim[1];
287        let ny = self.dim[2];
288        let nz = self.dim[3];
289        if i < nx && j < ny && k < nz {
290            let idx = k * nx * ny + j * nx + i;
291            self.data.get(idx).copied()
292        } else {
293            None
294        }
295    }
296}
297/// An anatomical landmark.
298#[derive(Clone, Debug)]
299pub struct Landmark {
300    /// Landmark name (e.g., "AC", "PC").
301    pub name: String,
302    /// World coordinates in mm.
303    pub position: [f64; 3],
304    /// Uncertainty radius (mm).
305    pub uncertainty: f64,
306    /// Whether this landmark was manually placed.
307    pub manual: bool,
308}
309impl Landmark {
310    /// Create a new landmark.
311    pub fn new(name: &str, position: [f64; 3], uncertainty: f64) -> Self {
312        Self {
313            name: name.to_string(),
314            position,
315            uncertainty,
316            manual: true,
317        }
318    }
319}
320/// NIfTI sform/qform affine parameters.
321#[derive(Clone, Debug)]
322pub struct NiftiTransform {
323    /// qform code.
324    pub qform_code: NiftiQformCode,
325    /// sform code.
326    pub sform_code: u16,
327    /// Quaternion parameters (qb, qc, qd) for qform.
328    pub quatern: [f64; 3],
329    /// qoffset (qx, qy, qz).
330    pub qoffset: [f64; 3],
331    /// Pixel dimension sign (qfac: +1 or -1).
332    pub qfac: f64,
333    /// sform row vectors for the 3×4 affine (srow_x, srow_y, srow_z).
334    pub sform_matrix: [[f64; 4]; 3],
335}
336impl NiftiTransform {
337    /// Create an identity-like transform.
338    pub fn identity() -> Self {
339        Self {
340            qform_code: NiftiQformCode::Unknown,
341            sform_code: 0,
342            quatern: [0.0, 0.0, 0.0],
343            qoffset: [0.0, 0.0, 0.0],
344            qfac: 1.0,
345            sform_matrix: [
346                [1.0, 0.0, 0.0, 0.0],
347                [0.0, 1.0, 0.0, 0.0],
348                [0.0, 0.0, 1.0, 0.0],
349            ],
350        }
351    }
352    /// Convert voxel index (i,j,k) to world coordinates using the sform matrix.
353    pub fn sform_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
354        let m = &self.sform_matrix;
355        [
356            m[0][0] * i + m[0][1] * j + m[0][2] * k + m[0][3],
357            m[1][0] * i + m[1][1] * j + m[1][2] * k + m[1][3],
358            m[2][0] * i + m[2][1] * j + m[2][2] * k + m[2][3],
359        ]
360    }
361}
362/// NIfTI-1 data types.
363#[derive(Clone, Debug, PartialEq)]
364pub enum NiftiDtype {
365    /// Unsigned 8-bit integer.
366    Uint8,
367    /// Signed 16-bit integer.
368    Int16,
369    /// Signed 32-bit integer.
370    Int32,
371    /// 32-bit float.
372    Float32,
373    /// 64-bit float.
374    Float64,
375}
376/// A triangle in 3D (for STL output).
377#[derive(Clone, Debug)]
378pub struct StlTriangle {
379    /// Triangle normal (unit vector).
380    pub normal: [f32; 3],
381    /// Three vertex positions.
382    pub vertices: [[f32; 3]; 3],
383}
384impl StlTriangle {
385    /// Compute the face normal from vertex positions.
386    pub fn compute_normal(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> [f32; 3] {
387        let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
388        let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
389        let n = [
390            e1[1] * e2[2] - e1[2] * e2[1],
391            e1[2] * e2[0] - e1[0] * e2[2],
392            e1[0] * e2[1] - e1[1] * e2[0],
393        ];
394        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
395        [n[0] / len, n[1] / len, n[2] / len]
396    }
397}
398/// Triangulated surface mesh from medical images.
399#[derive(Clone, Debug)]
400pub struct MedicalMesh {
401    /// Vertex positions (world coordinates, mm).
402    pub vertices: Vec<[f64; 3]>,
403    /// Triangle connectivity (indices into vertices).
404    pub triangles: Vec<[usize; 3]>,
405    /// Vertex normals.
406    pub normals: Vec<[f64; 3]>,
407    /// Mesh name / label.
408    pub name: String,
409}
410impl MedicalMesh {
411    /// Create an empty mesh.
412    pub fn new(name: &str) -> Self {
413        Self {
414            vertices: Vec::new(),
415            triangles: Vec::new(),
416            normals: Vec::new(),
417            name: name.to_string(),
418        }
419    }
420    /// Add a triangle.
421    pub fn add_triangle(&mut self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) {
422        let i0 = self.vertices.len();
423        self.vertices.push(v0);
424        self.vertices.push(v1);
425        self.vertices.push(v2);
426        self.triangles.push([i0, i0 + 1, i0 + 2]);
427        let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
428        let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
429        let n = [
430            e1[1] * e2[2] - e1[2] * e2[1],
431            e1[2] * e2[0] - e1[0] * e2[2],
432            e1[0] * e2[1] - e1[1] * e2[0],
433        ];
434        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
435        let nn = [n[0] / len, n[1] / len, n[2] / len];
436        self.normals.push(nn);
437        self.normals.push(nn);
438        self.normals.push(nn);
439    }
440    /// Compute surface area.
441    pub fn surface_area(&self) -> f64 {
442        let mut area = 0.0;
443        for &[i0, i1, i2] in &self.triangles {
444            let v0 = self.vertices[i0];
445            let v1 = self.vertices[i1];
446            let v2 = self.vertices[i2];
447            let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
448            let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
449            let cx = e1[1] * e2[2] - e1[2] * e2[1];
450            let cy = e1[2] * e2[0] - e1[0] * e2[2];
451            let cz = e1[0] * e2[1] - e1[1] * e2[0];
452            area += 0.5 * (cx * cx + cy * cy + cz * cz).sqrt();
453        }
454        area
455    }
456    /// Number of triangles.
457    pub fn n_triangles(&self) -> usize {
458        self.triangles.len()
459    }
460}
461/// Reconstruct a 3D voxel volume from a stack of 2D slices.
462///
463/// Each slice is a `Vec`u16` of `rows × cols` pixels.  The function stacks
464/// them into a single flat buffer in slice-major order (slice fastest varies last).
465#[derive(Clone, Debug)]
466pub struct VolumeReconstructor {
467    /// Number of rows per slice.
468    pub rows: usize,
469    /// Number of columns per slice.
470    pub cols: usize,
471    /// Pixel spacing [row_mm, col_mm].
472    pub pixel_spacing: [f64; 2],
473    /// Slice thickness in mm.
474    pub slice_thickness: f64,
475    /// Assembled voxel data (slice-major).
476    pub data: Vec<u16>,
477    /// Number of slices assembled so far.
478    pub n_slices: usize,
479}
480impl VolumeReconstructor {
481    /// Create a new reconstructor for slices of size `rows × cols`.
482    pub fn new(rows: usize, cols: usize, pixel_spacing: [f64; 2], slice_thickness: f64) -> Self {
483        Self {
484            rows,
485            cols,
486            pixel_spacing,
487            slice_thickness,
488            data: Vec::new(),
489            n_slices: 0,
490        }
491    }
492    /// Append one slice of pixel data.
493    pub fn add_slice(&mut self, slice: &[u16]) {
494        assert_eq!(slice.len(), self.rows * self.cols, "slice size mismatch");
495        self.data.extend_from_slice(slice);
496        self.n_slices += 1;
497    }
498    /// Access voxel at (row, col, slice).
499    pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
500        if row < self.rows && col < self.cols && slice < self.n_slices {
501            Some(self.data[slice * self.rows * self.cols + row * self.cols + col])
502        } else {
503            None
504        }
505    }
506    /// Physical size in mm [rows, cols, slices].
507    pub fn physical_size(&self) -> [f64; 3] {
508        [
509            self.rows as f64 * self.pixel_spacing[0],
510            self.cols as f64 * self.pixel_spacing[1],
511            self.n_slices as f64 * self.slice_thickness,
512        ]
513    }
514}
515/// NRRD header and data container.
516#[derive(Clone, Debug)]
517pub struct NrrdReader {
518    /// Key-value pairs from the NRRD header.
519    pub fields: HashMap<String, String>,
520    /// Data encoding.
521    pub encoding: NrrdEncoding,
522    /// Flat f64 data buffer.
523    pub data: Vec<f64>,
524    /// Dimensions as parsed from the "sizes" field.
525    pub sizes: Vec<usize>,
526}
527impl NrrdReader {
528    /// Create an empty NRRD reader.
529    pub fn new() -> Self {
530        Self {
531            fields: HashMap::new(),
532            encoding: NrrdEncoding::Raw,
533            data: Vec::new(),
534            sizes: Vec::new(),
535        }
536    }
537    /// Parse an NRRD header string (lines of "key: value").
538    ///
539    /// Populates `fields`, `encoding`, and `sizes`.
540    pub fn parse_header(&mut self, header: &str) {
541        for line in header.lines() {
542            let line = line.trim();
543            if line.is_empty() || line.starts_with('#') {
544                continue;
545            }
546            if let Some((k, v)) = line.split_once(':') {
547                let key = k.trim().to_lowercase();
548                let val = v.trim().to_string();
549                if key == "encoding" {
550                    self.encoding = match val.as_str() {
551                        "raw" => NrrdEncoding::Raw,
552                        "text" | "ascii" => NrrdEncoding::Text,
553                        "gzip" | "gz" => NrrdEncoding::Gzip,
554                        _ => NrrdEncoding::Raw,
555                    };
556                }
557                if key == "sizes" {
558                    self.sizes = val
559                        .split_whitespace()
560                        .filter_map(|s| s.parse().ok())
561                        .collect();
562                }
563                self.fields.insert(key, val);
564            }
565        }
566    }
567    /// Load raw-binary f32 data from a byte slice.
568    pub fn load_raw_f32(&mut self, bytes: &[u8]) {
569        self.data = bytes
570            .chunks_exact(4)
571            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
572            .collect();
573    }
574    /// Load text-encoded data (space/newline-separated ASCII floats).
575    pub fn load_text(&mut self, text: &str) {
576        self.data = text
577            .split_whitespace()
578            .filter_map(|s| s.parse::<f64>().ok())
579            .collect();
580    }
581    /// Total number of voxels as the product of `sizes`.
582    pub fn n_voxels(&self) -> usize {
583        self.sizes.iter().product::<usize>()
584    }
585}
586/// MINC volume (mock in-memory representation).
587#[derive(Clone, Debug)]
588pub struct MincVolume {
589    /// Spatial dimensions (up to 4: x, y, z, t).
590    pub dimensions: Vec<MincDimension>,
591    /// Flat data buffer.
592    pub data: Vec<f64>,
593    /// Real-value valid range [min, max].
594    pub valid_range: [f64; 2],
595    /// Attribute map (e.g. history, patient info).
596    pub attributes: HashMap<String, String>,
597}
598impl MincVolume {
599    /// Create a new MINC volume.
600    pub fn new(dimensions: Vec<MincDimension>) -> Self {
601        let n: usize = dimensions.iter().map(|d| d.length).product();
602        Self {
603            dimensions,
604            data: vec![0.0; n],
605            valid_range: [0.0, 1.0],
606            attributes: HashMap::new(),
607        }
608    }
609    /// Total voxel count.
610    pub fn n_voxels(&self) -> usize {
611        self.dimensions.iter().map(|d| d.length).product()
612    }
613    /// Set attribute.
614    pub fn set_attr(&mut self, key: &str, value: &str) {
615        self.attributes.insert(key.to_string(), value.to_string());
616    }
617    /// Voxel index for (i, j, k) in a 3-D volume (last-index fastest = C order).
618    pub fn index3(&self, i: usize, j: usize, k: usize) -> Option<usize> {
619        if self.dimensions.len() < 3 {
620            return None;
621        }
622        let nx = self.dimensions[0].length;
623        let ny = self.dimensions[1].length;
624        let nz = self.dimensions[2].length;
625        if i < nx && j < ny && k < nz {
626            Some(i * ny * nz + j * nz + k)
627        } else {
628            None
629        }
630    }
631}
632/// MetaImage (.mha / .mhd) format.
633#[derive(Clone, Debug)]
634pub struct MhaMhdFormat {
635    /// Header key-value pairs.
636    pub header: HashMap<String, String>,
637    /// Raw voxel data.
638    pub data: Vec<f64>,
639    /// Whether the data is embedded (.mha) or external (.mhd).
640    pub is_mha: bool,
641}
642impl MhaMhdFormat {
643    /// Create a new MetaImage container.
644    pub fn new(is_mha: bool) -> Self {
645        let mut header = HashMap::new();
646        header.insert("ObjectType".to_string(), "Image".to_string());
647        header.insert("NDims".to_string(), "3".to_string());
648        header.insert("ElementType".to_string(), "MET_FLOAT".to_string());
649        Self {
650            header,
651            data: Vec::new(),
652            is_mha,
653        }
654    }
655    /// Set header field.
656    pub fn set(&mut self, key: &str, value: &str) {
657        self.header.insert(key.to_string(), value.to_string());
658    }
659    /// Get header field.
660    pub fn get(&self, key: &str) -> Option<&str> {
661        self.header.get(key).map(|s| s.as_str())
662    }
663    /// Parse dimensions from "DimSize" header field.
664    pub fn dimensions(&self) -> Option<Vec<usize>> {
665        self.get("DimSize").map(|s| {
666            s.split_whitespace()
667                .filter_map(|t| t.parse().ok())
668                .collect()
669        })
670    }
671    /// Serialize header to string.
672    pub fn serialize_header(&self) -> String {
673        let mut lines: Vec<String> = self
674            .header
675            .iter()
676            .map(|(k, v)| format!("{} = {}", k, v))
677            .collect();
678        lines.sort();
679        if self.is_mha {
680            lines.push("ElementDataFile = LOCAL".to_string());
681        }
682        lines.join("\n")
683    }
684}
685/// DICOM header reader (mock implementation).
686#[derive(Clone, Debug)]
687pub struct DicomReader {
688    /// File path (virtual).
689    pub file_path: String,
690    /// Header key-value pairs (string representation).
691    pub header: HashMap<DicomTag, String>,
692    /// Pixel data as u16 values (row-major).
693    pub pixel_data: Vec<u16>,
694    /// Number of rows.
695    pub rows: usize,
696    /// Number of columns.
697    pub columns: usize,
698}
699impl DicomReader {
700    /// Create a new DICOM reader.
701    pub fn new(file_path: &str) -> Self {
702        Self {
703            file_path: file_path.to_string(),
704            header: HashMap::new(),
705            pixel_data: Vec::new(),
706            rows: 0,
707            columns: 0,
708        }
709    }
710    /// Set a header tag value.
711    pub fn set_tag(&mut self, tag: DicomTag, value: &str) {
712        self.header.insert(tag, value.to_string());
713    }
714    /// Get a header tag value.
715    pub fn get_tag(&self, tag: &DicomTag) -> Option<&str> {
716        self.header.get(tag).map(|s| s.as_str())
717    }
718    /// Load synthetic pixel data for testing (checker pattern).
719    pub fn load_synthetic(&mut self, rows: usize, cols: usize) {
720        self.rows = rows;
721        self.columns = cols;
722        self.pixel_data = (0..(rows * cols))
723            .map(|i| {
724                let r = i / cols;
725                let c = i % cols;
726                if (r + c).is_multiple_of(2) {
727                    2048u16
728                } else {
729                    512u16
730                }
731            })
732            .collect();
733        self.set_tag(DicomTag::rows(), &rows.to_string());
734        self.set_tag(DicomTag::columns(), &cols.to_string());
735    }
736    /// Extract pixel at (row, col).
737    pub fn pixel(&self, row: usize, col: usize) -> Option<u16> {
738        if row < self.rows && col < self.columns {
739            Some(self.pixel_data[row * self.columns + col])
740        } else {
741            None
742        }
743    }
744    /// Get patient name from header.
745    pub fn patient_name(&self) -> Option<&str> {
746        self.get_tag(&DicomTag::patient_name())
747    }
748    /// Get modality from header.
749    pub fn modality(&self) -> Option<&str> {
750        self.get_tag(&DicomTag::modality())
751    }
752}
753/// NIfTI-1 writer.
754#[derive(Clone, Debug)]
755pub struct NiftiWriter {
756    /// Output path.
757    pub output_path: String,
758    /// Data scaling: slope.
759    pub scl_slope: f64,
760    /// Data scaling: intercept.
761    pub scl_inter: f64,
762    /// Use gzip compression.
763    pub gzip: bool,
764}
765impl NiftiWriter {
766    /// Create a new NIfTI writer.
767    pub fn new(output_path: &str) -> Self {
768        Self {
769            output_path: output_path.to_string(),
770            scl_slope: 1.0,
771            scl_inter: 0.0,
772            gzip: false,
773        }
774    }
775    /// Build NIfTI-1 header bytes (simplified, 348-byte mock).
776    pub fn build_header(&self, reader: &NiftiReader) -> Vec<u8> {
777        let mut header = vec![0u8; 348];
778        let sizeof_hdr = 348u32.to_le_bytes();
779        header[0] = sizeof_hdr[0];
780        header[1] = sizeof_hdr[1];
781        let ndim = reader.dim[0] as u16;
782        let bytes = ndim.to_le_bytes();
783        header[40] = bytes[0];
784        header[41] = bytes[1];
785        header[344] = b'n';
786        header[345] = b'+';
787        header[346] = b'1';
788        header[347] = 0;
789        header
790    }
791    /// Write NIfTI data to a buffer (mock: returns serialized bytes).
792    pub fn write_to_buffer(&self, reader: &NiftiReader) -> Vec<u8> {
793        let header = self.build_header(reader);
794        let mut buf = header;
795        for &v in &reader.data {
796            let scaled = (v * self.scl_slope + self.scl_inter) as f32;
797            buf.extend_from_slice(&scaled.to_le_bytes());
798        }
799        buf
800    }
801}
802/// A binary or multi-label 3D segmentation mask.
803#[derive(Clone, Debug)]
804pub struct SegmentationMask {
805    /// Label array (0 = background).
806    pub labels: Vec<u8>,
807    /// Dimensions [nx, ny, nz].
808    pub dimensions: [usize; 3],
809    /// Voxel spacing [dx, dy, dz] in mm.
810    pub spacing: [f64; 3],
811    /// Label names.
812    pub label_names: HashMap<u8, String>,
813}
814impl SegmentationMask {
815    /// Create a new empty segmentation mask.
816    pub fn new(dimensions: [usize; 3], spacing: [f64; 3]) -> Self {
817        let n = dimensions[0] * dimensions[1] * dimensions[2];
818        Self {
819            labels: vec![0; n],
820            dimensions,
821            spacing,
822            label_names: HashMap::new(),
823        }
824    }
825    /// Set a voxel label.
826    pub fn set_label(&mut self, i: usize, j: usize, k: usize, label: u8) {
827        let [nx, ny, _nz] = self.dimensions;
828        let idx = k * nx * ny + j * nx + i;
829        if idx < self.labels.len() {
830            self.labels[idx] = label;
831        }
832    }
833    /// Get a voxel label.
834    pub fn get_label(&self, i: usize, j: usize, k: usize) -> Option<u8> {
835        let [nx, ny, _nz] = self.dimensions;
836        let idx = k * nx * ny + j * nx + i;
837        self.labels.get(idx).copied()
838    }
839    /// Count voxels with a given label.
840    pub fn count_label(&self, label: u8) -> usize {
841        self.labels.iter().filter(|&&l| l == label).count()
842    }
843    /// Volume of labeled region in mm^3.
844    pub fn volume_mm3(&self, label: u8) -> f64 {
845        let n = self.count_label(label) as f64;
846        let voxel_vol = self.spacing[0] * self.spacing[1] * self.spacing[2];
847        n * voxel_vol
848    }
849    /// Bounding box of labeled region [min_i, max_i, min_j, max_j, min_k, max_k].
850    pub fn bounding_box(&self, label: u8) -> Option<[usize; 6]> {
851        let [nx, ny, nz] = self.dimensions;
852        let mut min_i = nx;
853        let mut max_i = 0;
854        let mut min_j = ny;
855        let mut max_j = 0;
856        let mut min_k = nz;
857        let mut max_k = 0;
858        let mut found = false;
859        for k in 0..nz {
860            for j in 0..ny {
861                for i in 0..nx {
862                    if self.get_label(i, j, k) == Some(label) {
863                        min_i = min_i.min(i);
864                        max_i = max_i.max(i);
865                        min_j = min_j.min(j);
866                        max_j = max_j.max(j);
867                        min_k = min_k.min(k);
868                        max_k = max_k.max(k);
869                        found = true;
870                    }
871                }
872            }
873        }
874        if found {
875            Some([min_i, max_i, min_j, max_j, min_k, max_k])
876        } else {
877            None
878        }
879    }
880    /// Register a label name.
881    pub fn name_label(&mut self, label: u8, name: &str) {
882        self.label_names.insert(label, name.to_string());
883    }
884}
885/// DICOM data set: an ordered collection of elements.
886#[derive(Clone, Debug, Default)]
887pub struct DicomDataSet {
888    /// Elements stored in tag order.
889    pub elements: Vec<DicomElement>,
890}
891impl DicomDataSet {
892    /// Create an empty data set.
893    pub fn new() -> Self {
894        Self {
895            elements: Vec::new(),
896        }
897    }
898    /// Insert or replace an element.
899    pub fn set(&mut self, element: DicomElement) {
900        if let Some(pos) = self.elements.iter().position(|e| e.tag == element.tag) {
901            self.elements[pos] = element;
902        } else {
903            self.elements.push(element);
904            self.elements.sort_by(|a, b| {
905                a.tag
906                    .group
907                    .cmp(&b.tag.group)
908                    .then(a.tag.element.cmp(&b.tag.element))
909            });
910        }
911    }
912    /// Look up an element by tag.
913    pub fn get(&self, tag: &DicomTag) -> Option<&DicomElement> {
914        self.elements.iter().find(|e| &e.tag == tag)
915    }
916    /// Convenience: get US value for a tag.
917    pub fn get_us(&self, tag: &DicomTag) -> Option<u16> {
918        self.get(tag)?.vr.as_us()
919    }
920    /// Convenience: get DS value for a tag.
921    pub fn get_ds(&self, tag: &DicomTag) -> Option<f64> {
922        self.get(tag)?.vr.as_ds()
923    }
924    /// Convenience: get string for LO/UI tag.
925    pub fn get_str(&self, tag: &DicomTag) -> Option<&str> {
926        self.get(tag)?.vr.as_str()
927    }
928    /// Parse a minimal DICOM-like byte stream (explicit VR, little-endian mock).
929    ///
930    /// Format: repeated records of `\[group:u16\]\[element:u16\]\[vr:2 bytes\]\[length:u16\]\[data\]`.
931    /// This is a simplified subset suitable for unit-test round-trips.
932    pub fn parse_bytes(bytes: &[u8]) -> Self {
933        let mut ds = DicomDataSet::new();
934        let mut pos = 0usize;
935        while pos + 8 <= bytes.len() {
936            let group = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
937            let element = u16::from_le_bytes([bytes[pos + 2], bytes[pos + 3]]);
938            let vr_code = [bytes[pos + 4], bytes[pos + 5]];
939            let length = u16::from_le_bytes([bytes[pos + 6], bytes[pos + 7]]) as usize;
940            pos += 8;
941            if pos + length > bytes.len() {
942                break;
943            }
944            let data = &bytes[pos..pos + length];
945            pos += length;
946            let tag = DicomTag::new(group, element);
947            let vr = match &vr_code {
948                b"US" if length == 2 => DicomVr::Us(u16::from_le_bytes([data[0], data[1]])),
949                b"DS" => {
950                    let s = std::str::from_utf8(data).unwrap_or("0").trim();
951                    DicomVr::Ds(s.parse::<f64>().unwrap_or(0.0))
952                }
953                b"LO" => DicomVr::Lo(String::from_utf8_lossy(data).trim().to_string()),
954                b"UI" => DicomVr::Ui(String::from_utf8_lossy(data).trim().to_string()),
955                b"OW" => DicomVr::OW(data.to_vec()),
956                _ => continue,
957            };
958            ds.set(DicomElement::new(tag, vr));
959        }
960        ds
961    }
962    /// Serialize the data set back to bytes (same simplified format as `parse_bytes`).
963    pub fn to_bytes(&self) -> Vec<u8> {
964        let mut out = Vec::new();
965        for elem in &self.elements {
966            let vr_code: &[u8; 2];
967            let data: Vec<u8>;
968            match &elem.vr {
969                DicomVr::Us(v) => {
970                    vr_code = b"US";
971                    data = v.to_le_bytes().to_vec();
972                }
973                DicomVr::Ds(v) => {
974                    vr_code = b"DS";
975                    data = format!("{:.6}", v).into_bytes();
976                }
977                DicomVr::Lo(s) => {
978                    vr_code = b"LO";
979                    data = s.as_bytes().to_vec();
980                }
981                DicomVr::Ui(s) => {
982                    vr_code = b"UI";
983                    data = s.as_bytes().to_vec();
984                }
985                DicomVr::OW(bytes) => {
986                    vr_code = b"OW";
987                    data = bytes.clone();
988                }
989                DicomVr::Sq(_) => continue,
990            }
991            let length = data.len() as u16;
992            out.extend_from_slice(&elem.tag.group.to_le_bytes());
993            out.extend_from_slice(&elem.tag.element.to_le_bytes());
994            out.extend_from_slice(vr_code);
995            out.extend_from_slice(&length.to_le_bytes());
996            out.extend_from_slice(&data);
997        }
998        out
999    }
1000}
1001/// A single tractography fiber (a polyline of 3D points).
1002#[derive(Clone, Debug)]
1003pub struct FiberTract {
1004    /// Ordered points along the fiber [x, y, z] in mm.
1005    pub points: Vec<[f64; 3]>,
1006    /// Optional per-point scalar (e.g. FA, MD).
1007    pub scalars: Vec<f64>,
1008}
1009impl FiberTract {
1010    /// Create a new fiber tract.
1011    pub fn new(points: Vec<[f64; 3]>) -> Self {
1012        let n = points.len();
1013        Self {
1014            points,
1015            scalars: vec![0.0; n],
1016        }
1017    }
1018    /// Arc length of the fiber in mm.
1019    pub fn arc_length(&self) -> f64 {
1020        if self.points.len() < 2 {
1021            return 0.0;
1022        }
1023        self.points
1024            .windows(2)
1025            .map(|w| {
1026                let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
1027                (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
1028            })
1029            .sum()
1030    }
1031    /// Number of points.
1032    pub fn n_points(&self) -> usize {
1033        self.points.len()
1034    }
1035}
1036/// Radiation therapy dose distribution.
1037#[derive(Clone, Debug)]
1038pub struct DoseVolume {
1039    /// Dose values in Gray (Gy).
1040    pub dose: Vec<f64>,
1041    /// Dimensions [nx, ny, nz].
1042    pub dimensions: [usize; 3],
1043    /// Voxel spacing [dx, dy, dz] in mm.
1044    pub spacing: [f64; 3],
1045    /// Prescription dose (Gy).
1046    pub prescription_dose: f64,
1047}
1048impl DoseVolume {
1049    /// Create a dose volume.
1050    pub fn new(dimensions: [usize; 3], spacing: [f64; 3], prescription_dose: f64) -> Self {
1051        let n = dimensions[0] * dimensions[1] * dimensions[2];
1052        Self {
1053            dose: vec![0.0; n],
1054            dimensions,
1055            spacing,
1056            prescription_dose,
1057        }
1058    }
1059    /// Get dose at voxel (i, j, k).
1060    pub fn get_dose(&self, i: usize, j: usize, k: usize) -> Option<f64> {
1061        let [nx, ny, _nz] = self.dimensions;
1062        let idx = k * nx * ny + j * nx + i;
1063        self.dose.get(idx).copied()
1064    }
1065    /// Set dose at voxel.
1066    pub fn set_dose(&mut self, i: usize, j: usize, k: usize, d: f64) {
1067        let [nx, ny, _nz] = self.dimensions;
1068        let idx = k * nx * ny + j * nx + i;
1069        if idx < self.dose.len() {
1070            self.dose[idx] = d;
1071        }
1072    }
1073    /// Maximum dose in the volume.
1074    pub fn max_dose(&self) -> f64 {
1075        self.dose.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
1076    }
1077    /// Mean dose over non-zero voxels.
1078    pub fn mean_dose(&self) -> f64 {
1079        let nonzero: Vec<f64> = self.dose.iter().cloned().filter(|&d| d > 0.0).collect();
1080        if nonzero.is_empty() {
1081            return 0.0;
1082        }
1083        nonzero.iter().sum::<f64>() / nonzero.len() as f64
1084    }
1085    /// Compute dose-volume histogram (DVH).
1086    ///
1087    /// Returns (dose_bins, volume_fraction) vectors.
1088    pub fn dvh(&self, n_bins: usize) -> (Vec<f64>, Vec<f64>) {
1089        let d_max = self.max_dose();
1090        if d_max <= 0.0 {
1091            return (vec![0.0; n_bins], vec![0.0; n_bins]);
1092        }
1093        let bin_width = d_max / n_bins as f64;
1094        let mut counts = vec![0usize; n_bins];
1095        let n_total = self.dose.len();
1096        for &d in &self.dose {
1097            let bin = ((d / d_max) * (n_bins - 1) as f64) as usize;
1098            counts[bin.min(n_bins - 1)] += 1;
1099        }
1100        let mut cumulative = vec![0usize; n_bins];
1101        let mut running = 0usize;
1102        for i in (0..n_bins).rev() {
1103            running += counts[i];
1104            cumulative[i] = running;
1105        }
1106        let dose_bins: Vec<f64> = (0..n_bins).map(|i| i as f64 * bin_width).collect();
1107        let vol_frac: Vec<f64> = cumulative
1108            .iter()
1109            .map(|&c| c as f64 / n_total as f64)
1110            .collect();
1111        (dose_bins, vol_frac)
1112    }
1113    /// V_n: volume fraction receiving at least `dose_threshold` Gy.
1114    pub fn v_dose(&self, dose_threshold: f64) -> f64 {
1115        let above = self.dose.iter().filter(|&&d| d >= dose_threshold).count();
1116        above as f64 / self.dose.len() as f64
1117    }
1118    /// D_n: dose received by at least `volume_fraction` of the volume.
1119    pub fn d_volume(&self, volume_fraction: f64) -> f64 {
1120        let mut sorted = self.dose.clone();
1121        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1122        let idx = ((1.0 - volume_fraction) * sorted.len() as f64) as usize;
1123        sorted[idx.min(sorted.len() - 1)]
1124    }
1125}
1126/// VTK PolyData writer for tractography fibers.
1127pub struct TractVtkWriter {
1128    /// Label for the output data set.
1129    pub label: String,
1130}
1131impl TractVtkWriter {
1132    /// Create a new VTK tract writer.
1133    pub fn new(label: &str) -> Self {
1134        Self {
1135            label: label.to_string(),
1136        }
1137    }
1138    /// Serialise fibers to VTK legacy ASCII PolyData format.
1139    pub fn to_vtk_string(&self, fibers: &[FiberTract]) -> String {
1140        let total_pts: usize = fibers.iter().map(|f| f.n_points()).sum();
1141        let total_lines: usize = fibers.len();
1142        let cell_size: usize = fibers.iter().map(|f| f.n_points() + 1).sum();
1143        let mut s = format!(
1144            "# vtk DataFile Version 3.0\n{}\nASCII\nDATASET POLYDATA\nPOINTS {} float\n",
1145            self.label, total_pts
1146        );
1147        for fiber in fibers {
1148            for p in &fiber.points {
1149                s.push_str(&format!("{:.4} {:.4} {:.4}\n", p[0], p[1], p[2]));
1150            }
1151        }
1152        s.push_str(&format!(
1153            "LINES {} {}\n",
1154            total_lines,
1155            total_lines + cell_size
1156        ));
1157        let mut offset = 0usize;
1158        for fiber in fibers {
1159            s.push_str(&format!("{}", fiber.n_points()));
1160            for k in 0..fiber.n_points() {
1161                s.push_str(&format!(" {}", offset + k));
1162            }
1163            s.push('\n');
1164            offset += fiber.n_points();
1165        }
1166        if total_pts > 0 {
1167            s.push_str(&format!(
1168                "POINT_DATA {}\nSCALARS scalars float 1\nLOOKUP_TABLE default\n",
1169                total_pts
1170            ));
1171            for fiber in fibers {
1172                for &sc in &fiber.scalars {
1173                    s.push_str(&format!("{:.6} ", sc));
1174                }
1175            }
1176        }
1177        s
1178    }
1179}
1180/// DICOM image data with physical spacing and window/level settings.
1181#[derive(Clone, Debug)]
1182pub struct DicomImageData {
1183    /// Pixel array (row-major, in-plane first, slice last).
1184    pub pixels: Vec<u16>,
1185    /// Number of rows.
1186    pub rows: usize,
1187    /// Number of columns.
1188    pub cols: usize,
1189    /// Number of slices.
1190    pub n_slices: usize,
1191    /// Pixel spacing in mm [row_spacing, col_spacing].
1192    pub pixel_spacing: [f64; 2],
1193    /// Slice thickness in mm.
1194    pub slice_thickness: f64,
1195    /// Image orientation cosines (6 values: row_cos + col_cos).
1196    pub orientation: [f64; 6],
1197    /// Window center for display.
1198    pub window_center: f64,
1199    /// Window width for display.
1200    pub window_width: f64,
1201    /// Rescale slope (Hounsfield units: HU = pixel * slope + intercept).
1202    pub rescale_slope: f64,
1203    /// Rescale intercept.
1204    pub rescale_intercept: f64,
1205}
1206impl DicomImageData {
1207    /// Create DICOM image data.
1208    pub fn new(rows: usize, cols: usize, n_slices: usize) -> Self {
1209        Self {
1210            pixels: vec![0; rows * cols * n_slices],
1211            rows,
1212            cols,
1213            n_slices,
1214            pixel_spacing: [1.0, 1.0],
1215            slice_thickness: 1.0,
1216            orientation: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
1217            window_center: 40.0,
1218            window_width: 400.0,
1219            rescale_slope: 1.0,
1220            rescale_intercept: -1024.0,
1221        }
1222    }
1223    /// Convert pixel value to Hounsfield Units.
1224    pub fn to_hu(&self, pixel: u16) -> f64 {
1225        pixel as f64 * self.rescale_slope + self.rescale_intercept
1226    }
1227    /// Apply window/level mapping to HU value -> display [0, 255].
1228    pub fn window_level(&self, hu: f64) -> u8 {
1229        let low = self.window_center - self.window_width / 2.0;
1230        let high = self.window_center + self.window_width / 2.0;
1231        let norm = (hu - low) / (high - low);
1232        (norm.clamp(0.0, 1.0) * 255.0) as u8
1233    }
1234    /// Physical size in mm.
1235    pub fn physical_size(&self) -> [f64; 3] {
1236        [
1237            self.rows as f64 * self.pixel_spacing[0],
1238            self.cols as f64 * self.pixel_spacing[1],
1239            self.n_slices as f64 * self.slice_thickness,
1240        ]
1241    }
1242    /// Get voxel at (row, col, slice).
1243    pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
1244        if row < self.rows && col < self.cols && slice < self.n_slices {
1245            Some(self.pixels[slice * self.rows * self.cols + row * self.cols + col])
1246        } else {
1247            None
1248        }
1249    }
1250}
1251/// STL binary exporter.
1252#[allow(dead_code)]
1253pub struct StlExporter;
1254impl StlExporter {
1255    /// Serialise a list of triangles to a binary STL buffer (80-byte header + triangles).
1256    pub fn to_binary(triangles: &[StlTriangle]) -> Vec<u8> {
1257        let mut buf = vec![0u8; 80];
1258        let count = triangles.len() as u32;
1259        buf.extend_from_slice(&count.to_le_bytes());
1260        for tri in triangles {
1261            for &v in &tri.normal {
1262                buf.extend_from_slice(&v.to_le_bytes());
1263            }
1264            for vtx in &tri.vertices {
1265                for &c in vtx {
1266                    buf.extend_from_slice(&c.to_le_bytes());
1267                }
1268            }
1269            buf.extend_from_slice(&0u16.to_le_bytes());
1270        }
1271        buf
1272    }
1273    /// Parse a binary STL buffer back into a vector of triangles.
1274    pub fn from_binary(data: &[u8]) -> Vec<StlTriangle> {
1275        if data.len() < 84 {
1276            return Vec::new();
1277        }
1278        let count = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
1279        let mut triangles = Vec::with_capacity(count);
1280        let mut pos = 84usize;
1281        for _ in 0..count {
1282            if pos + 50 > data.len() {
1283                break;
1284            }
1285            let read_f32 = |p: &mut usize| -> f32 {
1286                let v = f32::from_le_bytes([data[*p], data[*p + 1], data[*p + 2], data[*p + 3]]);
1287                *p += 4;
1288                v
1289            };
1290            let nx = read_f32(&mut pos);
1291            let ny = read_f32(&mut pos);
1292            let nz = read_f32(&mut pos);
1293            let mut verts = [[0f32; 3]; 3];
1294            for vtx in &mut verts {
1295                vtx[0] = read_f32(&mut pos);
1296                vtx[1] = read_f32(&mut pos);
1297                vtx[2] = read_f32(&mut pos);
1298            }
1299            pos += 2;
1300            triangles.push(StlTriangle {
1301                normal: [nx, ny, nz],
1302                vertices: verts,
1303            });
1304        }
1305        triangles
1306    }
1307}
1308/// DICOM Value Representation (VR) types.
1309#[derive(Clone, Debug, PartialEq)]
1310pub enum DicomVr {
1311    /// Unsigned Short (2 bytes).
1312    Us(u16),
1313    /// Decimal String (ASCII numeric).
1314    Ds(f64),
1315    /// Long String (64-char max).
1316    Lo(String),
1317    /// Unique Identifier (UID string).
1318    Ui(String),
1319    /// Sequence of items (nested data sets).
1320    Sq(Vec<HashMap<DicomTag, DicomVr>>),
1321    /// Pixel Data (raw bytes).
1322    OW(Vec<u8>),
1323}
1324impl DicomVr {
1325    /// Return the VR type name as a 2-character DICOM code.
1326    pub fn vr_code(&self) -> &'static str {
1327        match self {
1328            DicomVr::Us(_) => "US",
1329            DicomVr::Ds(_) => "DS",
1330            DicomVr::Lo(_) => "LO",
1331            DicomVr::Ui(_) => "UI",
1332            DicomVr::Sq(_) => "SQ",
1333            DicomVr::OW(_) => "OW",
1334        }
1335    }
1336    /// Return the unsigned-short value if this is a US VR.
1337    pub fn as_us(&self) -> Option<u16> {
1338        if let DicomVr::Us(v) = self {
1339            Some(*v)
1340        } else {
1341            None
1342        }
1343    }
1344    /// Return the decimal string value if this is a DS VR.
1345    pub fn as_ds(&self) -> Option<f64> {
1346        if let DicomVr::Ds(v) = self {
1347            Some(*v)
1348        } else {
1349            None
1350        }
1351    }
1352    /// Return the string value for LO or UI VRs.
1353    pub fn as_str(&self) -> Option<&str> {
1354        match self {
1355            DicomVr::Lo(s) | DicomVr::Ui(s) => Some(s.as_str()),
1356            _ => None,
1357        }
1358    }
1359}
1360/// MINC dimension descriptor (mirrors minc_volume dim structure).
1361#[derive(Clone, Debug)]
1362pub struct MincDimension {
1363    /// Dimension name (e.g. "xspace", "yspace", "zspace", "time").
1364    pub name: String,
1365    /// Number of voxels along this dimension.
1366    pub length: usize,
1367    /// Voxel step size in mm (can be negative for direction).
1368    pub step: f64,
1369    /// World-coordinate start value.
1370    pub start: f64,
1371}
1372impl MincDimension {
1373    /// Create a new MINC dimension.
1374    pub fn new(name: &str, length: usize, step: f64, start: f64) -> Self {
1375        Self {
1376            name: name.to_string(),
1377            length,
1378            step,
1379            start,
1380        }
1381    }
1382    /// Return the world coordinate of voxel index `i`.
1383    pub fn world_coord(&self, i: usize) -> f64 {
1384        self.start + i as f64 * self.step
1385    }
1386}