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