openigtlink_rust/protocol/types/
point.rs

1//! POINT message type implementation
2//!
3//! The POINT message type is used to transfer information about fiducials,
4//! which are often used in surgical planning and navigation.
5//!
6//! # Use Cases
7//!
8//! - **Surgical Navigation** - Fiducial markers for patient-to-image registration
9//! - **Biopsy Planning** - Target points for needle insertion
10//! - **Tumor Localization** - Marking tumor boundaries in pre-operative images
11//! - **Anatomical Landmarks** - Identifying critical structures (nerves, vessels)
12//! - **Treatment Verification** - Comparing planned vs. actual positions
13//!
14//! # Point Attributes
15//!
16//! Each point contains:
17//! - **3D Position (x, y, z)** - Coordinates in mm
18//! - **Name** - Identifier (e.g., "Fiducial_1", "TumorCenter")
19//! - **Group** - Logical grouping (e.g., "Fiducials", "Targets")
20//! - **Color (RGBA)** - Visualization color
21//! - **Diameter** - Size for rendering (mm)
22//! - **Owner** - Associated image/coordinate frame
23//!
24//! # Examples
25//!
26//! ## Registering Fiducial Points for Navigation
27//!
28//! ```no_run
29//! use openigtlink_rust::protocol::types::{PointMessage, PointElement};
30//! use openigtlink_rust::io::IgtlClient;
31//!
32//! let mut client = IgtlClient::connect("127.0.0.1:18944")?;
33//!
34//! let mut point_msg = PointMessage::new();
35//! point_msg.set_device_name("NavigationSystem");
36//!
37//! // Fiducial 1: Nasion (nose bridge)
38//! let mut fid1 = PointElement::new();
39//! fid1.name = "Nasion".to_string();
40//! fid1.group_name = "Fiducials".to_string();
41//! fid1.position = [0.0, 85.0, -30.0];  // x, y, z in mm
42//! fid1.rgba = [255, 0, 0, 255];        // Red
43//! fid1.diameter = 5.0;                 // 5mm sphere
44//! fid1.owner = "CTImage".to_string();
45//!
46//! // Fiducial 2: Left ear
47//! let mut fid2 = PointElement::new();
48//! fid2.name = "LeftEar".to_string();
49//! fid2.group_name = "Fiducials".to_string();
50//! fid2.position = [-75.0, 0.0, -20.0];
51//! fid2.rgba = [0, 255, 0, 255];        // Green
52//! fid2.diameter = 5.0;
53//! fid2.owner = "CTImage".to_string();
54//!
55//! // Fiducial 3: Right ear
56//! let mut fid3 = PointElement::new();
57//! fid3.name = "RightEar".to_string();
58//! fid3.group_name = "Fiducials".to_string();
59//! fid3.position = [75.0, 0.0, -20.0];
60//! fid3.rgba = [0, 0, 255, 255];        // Blue
61//! fid3.diameter = 5.0;
62//! fid3.owner = "CTImage".to_string();
63//!
64//! point_msg.add_point(fid1);
65//! point_msg.add_point(fid2);
66//! point_msg.add_point(fid3);
67//!
68//! client.send(&point_msg)?;
69//! # Ok::<(), openigtlink_rust::IgtlError>(())
70//! ```
71//!
72//! ## Receiving Biopsy Target Points
73//!
74//! ```no_run
75//! use openigtlink_rust::io::IgtlServer;
76//! use openigtlink_rust::protocol::types::PointMessage;
77//! use openigtlink_rust::protocol::message::Message;
78//!
79//! let server = IgtlServer::bind("0.0.0.0:18944")?;
80//! let mut client_conn = server.accept()?;
81//!
82//! let message = client_conn.receive()?;
83//!
84//! if message.header.message_type == "POINT" {
85//!     let points = PointMessage::from_bytes(&message.body)?;
86//!
87//!     println!("Received {} points", points.elements.len());
88//!
89//!     for (i, point) in points.elements.iter().enumerate() {
90//!         println!("\nPoint {}: {}", i + 1, point.name);
91//!         println!("  Group: {}", point.group_name);
92//!         println!("  Position: ({:.2}, {:.2}, {:.2}) mm",
93//!                  point.position[0], point.position[1], point.position[2]);
94//!         println!("  Color: RGB({}, {}, {})",
95//!                  point.rgba[0], point.rgba[1], point.rgba[2]);
96//!         println!("  Diameter: {:.2} mm", point.diameter);
97//!     }
98//! }
99//! # Ok::<(), openigtlink_rust::IgtlError>(())
100//! ```
101
102use crate::protocol::message::Message;
103use crate::error::{IgtlError, Result};
104use bytes::{Buf, BufMut};
105
106/// Point/fiducial data element
107#[derive(Debug, Clone, PartialEq)]
108pub struct PointElement {
109    /// Name or description of the point (max 64 chars)
110    pub name: String,
111    /// Group name (e.g., "Labeled Point", "Landmark", "Fiducial") (max 32 chars)
112    pub group: String,
113    /// Color in RGBA (0-255)
114    pub rgba: [u8; 4],
115    /// Coordinate of the point in millimeters
116    pub position: [f32; 3],
117    /// Diameter of the point in millimeters (can be 0)
118    pub diameter: f32,
119    /// ID of the owner image/sliceset (max 20 chars)
120    pub owner: String,
121}
122
123impl PointElement {
124    /// Create a new point element
125    pub fn new(
126        name: impl Into<String>,
127        group: impl Into<String>,
128        position: [f32; 3],
129    ) -> Self {
130        PointElement {
131            name: name.into(),
132            group: group.into(),
133            rgba: [255, 255, 255, 255], // White, fully opaque
134            position,
135            diameter: 0.0,
136            owner: String::new(),
137        }
138    }
139
140    /// Create a point with color
141    pub fn with_color(
142        name: impl Into<String>,
143        group: impl Into<String>,
144        rgba: [u8; 4],
145        position: [f32; 3],
146    ) -> Self {
147        PointElement {
148            name: name.into(),
149            group: group.into(),
150            rgba,
151            position,
152            diameter: 0.0,
153            owner: String::new(),
154        }
155    }
156
157    /// Create a point with all fields
158    pub fn with_details(
159        name: impl Into<String>,
160        group: impl Into<String>,
161        rgba: [u8; 4],
162        position: [f32; 3],
163        diameter: f32,
164        owner: impl Into<String>,
165    ) -> Self {
166        PointElement {
167            name: name.into(),
168            group: group.into(),
169            rgba,
170            position,
171            diameter,
172            owner: owner.into(),
173        }
174    }
175}
176
177/// POINT message containing multiple fiducial points
178///
179/// # OpenIGTLink Specification
180/// - Message type: "POINT"
181/// - Each element: NAME (char[64]) + GROUP (char[32]) + RGBA (uint8[4]) + XYZ (float32[3]) + DIAMETER (float32) + OWNER (char[20])
182/// - Element size: 64 + 32 + 4 + 12 + 4 + 20 = 136 bytes
183#[derive(Debug, Clone, PartialEq)]
184pub struct PointMessage {
185    /// List of point elements
186    pub points: Vec<PointElement>,
187}
188
189impl PointMessage {
190    /// Create a new POINT message with points
191    pub fn new(points: Vec<PointElement>) -> Self {
192        PointMessage { points }
193    }
194
195    /// Create an empty POINT message
196    pub fn empty() -> Self {
197        PointMessage { points: Vec::new() }
198    }
199
200    /// Add a point element
201    pub fn add_point(&mut self, point: PointElement) {
202        self.points.push(point);
203    }
204
205    /// Get number of points
206    pub fn len(&self) -> usize {
207        self.points.len()
208    }
209
210    /// Check if message has no points
211    pub fn is_empty(&self) -> bool {
212        self.points.is_empty()
213    }
214}
215
216impl Message for PointMessage {
217    fn message_type() -> &'static str {
218        "POINT"
219    }
220
221    fn encode_content(&self) -> Result<Vec<u8>> {
222        let mut buf = Vec::with_capacity(self.points.len() * 136);
223
224        for point in &self.points {
225            // Encode NAME (char[64])
226            let mut name_bytes = [0u8; 64];
227            let name_str = point.name.as_bytes();
228            let copy_len = name_str.len().min(63);
229            name_bytes[..copy_len].copy_from_slice(&name_str[..copy_len]);
230            buf.extend_from_slice(&name_bytes);
231
232            // Encode GROUP (char[32])
233            let mut group_bytes = [0u8; 32];
234            let group_str = point.group.as_bytes();
235            let copy_len = group_str.len().min(31);
236            group_bytes[..copy_len].copy_from_slice(&group_str[..copy_len]);
237            buf.extend_from_slice(&group_bytes);
238
239            // Encode RGBA (uint8[4])
240            buf.extend_from_slice(&point.rgba);
241
242            // Encode XYZ (float32[3])
243            for &coord in &point.position {
244                buf.put_f32(coord);
245            }
246
247            // Encode DIAMETER (float32)
248            buf.put_f32(point.diameter);
249
250            // Encode OWNER (char[20])
251            let mut owner_bytes = [0u8; 20];
252            let owner_str = point.owner.as_bytes();
253            let copy_len = owner_str.len().min(19);
254            owner_bytes[..copy_len].copy_from_slice(&owner_str[..copy_len]);
255            buf.extend_from_slice(&owner_bytes);
256        }
257
258        Ok(buf)
259    }
260
261    fn decode_content(mut data: &[u8]) -> Result<Self> {
262        let mut points = Vec::new();
263
264        while data.len() >= 136 {
265            // Decode NAME (char[64])
266            let name_bytes = &data[..64];
267            data.advance(64);
268            let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(64);
269            let name = String::from_utf8(name_bytes[..name_len].to_vec())?;
270
271            // Decode GROUP (char[32])
272            let group_bytes = &data[..32];
273            data.advance(32);
274            let group_len = group_bytes.iter().position(|&b| b == 0).unwrap_or(32);
275            let group = String::from_utf8(group_bytes[..group_len].to_vec())?;
276
277            // Decode RGBA (uint8[4])
278            let rgba = [data.get_u8(), data.get_u8(), data.get_u8(), data.get_u8()];
279
280            // Decode XYZ (float32[3])
281            let position = [data.get_f32(), data.get_f32(), data.get_f32()];
282
283            // Decode DIAMETER (float32)
284            let diameter = data.get_f32();
285
286            // Decode OWNER (char[20])
287            let owner_bytes = &data[..20];
288            data.advance(20);
289            let owner_len = owner_bytes.iter().position(|&b| b == 0).unwrap_or(20);
290            let owner = String::from_utf8(owner_bytes[..owner_len].to_vec())?;
291
292            points.push(PointElement {
293                name,
294                group,
295                rgba,
296                position,
297                diameter,
298                owner,
299            });
300        }
301
302        if !data.is_empty() {
303            return Err(IgtlError::InvalidSize {
304                expected: 0,
305                actual: data.len(),
306            });
307        }
308
309        Ok(PointMessage { points })
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_message_type() {
319        assert_eq!(PointMessage::message_type(), "POINT");
320    }
321
322    #[test]
323    fn test_empty() {
324        let msg = PointMessage::empty();
325        assert!(msg.is_empty());
326        assert_eq!(msg.len(), 0);
327    }
328
329    #[test]
330    fn test_new_point() {
331        let point = PointElement::new("Fiducial1", "Landmark", [10.0, 20.0, 30.0]);
332        assert_eq!(point.name, "Fiducial1");
333        assert_eq!(point.group, "Landmark");
334        assert_eq!(point.position, [10.0, 20.0, 30.0]);
335        assert_eq!(point.rgba, [255, 255, 255, 255]);
336    }
337
338    #[test]
339    fn test_point_with_color() {
340        let point = PointElement::with_color(
341            "Point1",
342            "Fiducial",
343            [255, 0, 0, 255],
344            [1.0, 2.0, 3.0],
345        );
346        assert_eq!(point.rgba, [255, 0, 0, 255]);
347    }
348
349    #[test]
350    fn test_add_point() {
351        let mut msg = PointMessage::empty();
352        msg.add_point(PointElement::new("P1", "Landmark", [0.0, 0.0, 0.0]));
353        assert_eq!(msg.len(), 1);
354    }
355
356    #[test]
357    fn test_encode_single_point() {
358        let point = PointElement::new("Test", "Fiducial", [1.0, 2.0, 3.0]);
359        let msg = PointMessage::new(vec![point]);
360        let encoded = msg.encode_content().unwrap();
361
362        assert_eq!(encoded.len(), 136);
363    }
364
365    #[test]
366    fn test_roundtrip_single() {
367        let original = PointMessage::new(vec![PointElement::with_details(
368            "Fiducial1",
369            "Landmark",
370            [255, 128, 64, 255],
371            [100.5, 200.5, 300.5],
372            5.0,
373            "Image1",
374        )]);
375
376        let encoded = original.encode_content().unwrap();
377        let decoded = PointMessage::decode_content(&encoded).unwrap();
378
379        assert_eq!(decoded.points.len(), 1);
380        assert_eq!(decoded.points[0].name, "Fiducial1");
381        assert_eq!(decoded.points[0].group, "Landmark");
382        assert_eq!(decoded.points[0].rgba, [255, 128, 64, 255]);
383        assert_eq!(decoded.points[0].position, [100.5, 200.5, 300.5]);
384        assert_eq!(decoded.points[0].diameter, 5.0);
385        assert_eq!(decoded.points[0].owner, "Image1");
386    }
387
388    #[test]
389    fn test_roundtrip_multiple() {
390        let original = PointMessage::new(vec![
391            PointElement::new("P1", "Landmark", [1.0, 2.0, 3.0]),
392            PointElement::new("P2", "Fiducial", [4.0, 5.0, 6.0]),
393            PointElement::new("P3", "Target", [7.0, 8.0, 9.0]),
394        ]);
395
396        let encoded = original.encode_content().unwrap();
397        let decoded = PointMessage::decode_content(&encoded).unwrap();
398
399        assert_eq!(decoded.points.len(), 3);
400        assert_eq!(decoded.points[0].name, "P1");
401        assert_eq!(decoded.points[1].name, "P2");
402        assert_eq!(decoded.points[2].name, "P3");
403    }
404
405    #[test]
406    fn test_name_truncation() {
407        let long_name = "A".repeat(100);
408        let point = PointElement::new(&long_name, "Group", [0.0, 0.0, 0.0]);
409        let msg = PointMessage::new(vec![point]);
410
411        let encoded = msg.encode_content().unwrap();
412        let decoded = PointMessage::decode_content(&encoded).unwrap();
413
414        assert!(decoded.points[0].name.len() <= 63);
415    }
416
417    #[test]
418    fn test_empty_message() {
419        let msg = PointMessage::empty();
420        let encoded = msg.encode_content().unwrap();
421        let decoded = PointMessage::decode_content(&encoded).unwrap();
422
423        assert_eq!(decoded.points.len(), 0);
424        assert_eq!(encoded.len(), 0);
425    }
426
427    #[test]
428    fn test_decode_invalid_size() {
429        let data = vec![0u8; 135]; // One byte short
430        let result = PointMessage::decode_content(&data);
431        assert!(result.is_err());
432    }
433
434    #[test]
435    fn test_color_values() {
436        let point = PointElement::with_color(
437            "ColorTest",
438            "Test",
439            [128, 64, 32, 200],
440            [0.0, 0.0, 0.0],
441        );
442        let msg = PointMessage::new(vec![point]);
443
444        let encoded = msg.encode_content().unwrap();
445        let decoded = PointMessage::decode_content(&encoded).unwrap();
446
447        assert_eq!(decoded.points[0].rgba, [128, 64, 32, 200]);
448    }
449}