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::error::{IgtlError, Result};
100use crate::protocol::message::Message;
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(name: impl Into<String>, group: impl Into<String>, position: [f32; 3]) -> Self {
123        PointElement {
124            name: name.into(),
125            group: group.into(),
126            rgba: [255, 255, 255, 255], // White, fully opaque
127            position,
128            diameter: 0.0,
129            owner: String::new(),
130        }
131    }
132
133    /// Create a point with color
134    pub fn with_color(
135        name: impl Into<String>,
136        group: impl Into<String>,
137        rgba: [u8; 4],
138        position: [f32; 3],
139    ) -> Self {
140        PointElement {
141            name: name.into(),
142            group: group.into(),
143            rgba,
144            position,
145            diameter: 0.0,
146            owner: String::new(),
147        }
148    }
149
150    /// Create a point with all fields
151    pub fn with_details(
152        name: impl Into<String>,
153        group: impl Into<String>,
154        rgba: [u8; 4],
155        position: [f32; 3],
156        diameter: f32,
157        owner: impl Into<String>,
158    ) -> Self {
159        PointElement {
160            name: name.into(),
161            group: group.into(),
162            rgba,
163            position,
164            diameter,
165            owner: owner.into(),
166        }
167    }
168}
169
170/// POINT message containing multiple fiducial points
171///
172/// # OpenIGTLink Specification
173/// - Message type: "POINT"
174/// - Each element: NAME (`char[64]`) + GROUP (`char[32]`) + RGBA (`uint8[4]`) + XYZ (`float32[3]`) + DIAMETER (float32) + OWNER (`char[20]`)
175/// - Element size: 64 + 32 + 4 + 12 + 4 + 20 = 136 bytes
176#[derive(Debug, Clone, PartialEq)]
177pub struct PointMessage {
178    /// List of point elements
179    pub points: Vec<PointElement>,
180}
181
182impl PointMessage {
183    /// Create a new POINT message with points
184    pub fn new(points: Vec<PointElement>) -> Self {
185        PointMessage { points }
186    }
187
188    /// Create an empty POINT message
189    pub fn empty() -> Self {
190        PointMessage { points: Vec::new() }
191    }
192
193    /// Add a point element
194    pub fn add_point(&mut self, point: PointElement) {
195        self.points.push(point);
196    }
197
198    /// Get number of points
199    pub fn len(&self) -> usize {
200        self.points.len()
201    }
202
203    /// Check if message has no points
204    pub fn is_empty(&self) -> bool {
205        self.points.is_empty()
206    }
207}
208
209impl Message for PointMessage {
210    fn message_type() -> &'static str {
211        "POINT"
212    }
213
214    fn encode_content(&self) -> Result<Vec<u8>> {
215        let mut buf = Vec::with_capacity(self.points.len() * 136);
216
217        for point in &self.points {
218            // Encode NAME (`char[64]`)
219            let mut name_bytes = [0u8; 64];
220            let name_str = point.name.as_bytes();
221            let copy_len = name_str.len().min(63);
222            name_bytes[..copy_len].copy_from_slice(&name_str[..copy_len]);
223            buf.extend_from_slice(&name_bytes);
224
225            // Encode GROUP (`char[32]`)
226            let mut group_bytes = [0u8; 32];
227            let group_str = point.group.as_bytes();
228            let copy_len = group_str.len().min(31);
229            group_bytes[..copy_len].copy_from_slice(&group_str[..copy_len]);
230            buf.extend_from_slice(&group_bytes);
231
232            // Encode RGBA (`uint8[4]`)
233            buf.extend_from_slice(&point.rgba);
234
235            // Encode XYZ (`float32[3]`)
236            for &coord in &point.position {
237                buf.put_f32(coord);
238            }
239
240            // Encode DIAMETER (float32)
241            buf.put_f32(point.diameter);
242
243            // Encode OWNER (`char[20]`)
244            let mut owner_bytes = [0u8; 20];
245            let owner_str = point.owner.as_bytes();
246            let copy_len = owner_str.len().min(19);
247            owner_bytes[..copy_len].copy_from_slice(&owner_str[..copy_len]);
248            buf.extend_from_slice(&owner_bytes);
249        }
250
251        Ok(buf)
252    }
253
254    fn decode_content(mut data: &[u8]) -> Result<Self> {
255        let mut points = Vec::new();
256
257        while data.len() >= 136 {
258            // Decode NAME (`char[64]`)
259            let name_bytes = &data[..64];
260            data.advance(64);
261            let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(64);
262            let name = String::from_utf8(name_bytes[..name_len].to_vec())?;
263
264            // Decode GROUP (`char[32]`)
265            let group_bytes = &data[..32];
266            data.advance(32);
267            let group_len = group_bytes.iter().position(|&b| b == 0).unwrap_or(32);
268            let group = String::from_utf8(group_bytes[..group_len].to_vec())?;
269
270            // Decode RGBA (`uint8[4]`)
271            let rgba = [data.get_u8(), data.get_u8(), data.get_u8(), data.get_u8()];
272
273            // Decode XYZ (`float32[3]`)
274            let position = [data.get_f32(), data.get_f32(), data.get_f32()];
275
276            // Decode DIAMETER (float32)
277            let diameter = data.get_f32();
278
279            // Decode OWNER (`char[20]`)
280            let owner_bytes = &data[..20];
281            data.advance(20);
282            let owner_len = owner_bytes.iter().position(|&b| b == 0).unwrap_or(20);
283            let owner = String::from_utf8(owner_bytes[..owner_len].to_vec())?;
284
285            points.push(PointElement {
286                name,
287                group,
288                rgba,
289                position,
290                diameter,
291                owner,
292            });
293        }
294
295        if !data.is_empty() {
296            return Err(IgtlError::InvalidSize {
297                expected: 0,
298                actual: data.len(),
299            });
300        }
301
302        Ok(PointMessage { points })
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_message_type() {
312        assert_eq!(PointMessage::message_type(), "POINT");
313    }
314
315    #[test]
316    fn test_empty() {
317        let msg = PointMessage::empty();
318        assert!(msg.is_empty());
319        assert_eq!(msg.len(), 0);
320    }
321
322    #[test]
323    fn test_new_point() {
324        let point = PointElement::new("Fiducial1", "Landmark", [10.0, 20.0, 30.0]);
325        assert_eq!(point.name, "Fiducial1");
326        assert_eq!(point.group, "Landmark");
327        assert_eq!(point.position, [10.0, 20.0, 30.0]);
328        assert_eq!(point.rgba, [255, 255, 255, 255]);
329    }
330
331    #[test]
332    fn test_point_with_color() {
333        let point =
334            PointElement::with_color("Point1", "Fiducial", [255, 0, 0, 255], [1.0, 2.0, 3.0]);
335        assert_eq!(point.rgba, [255, 0, 0, 255]);
336    }
337
338    #[test]
339    fn test_add_point() {
340        let mut msg = PointMessage::empty();
341        msg.add_point(PointElement::new("P1", "Landmark", [0.0, 0.0, 0.0]));
342        assert_eq!(msg.len(), 1);
343    }
344
345    #[test]
346    fn test_encode_single_point() {
347        let point = PointElement::new("Test", "Fiducial", [1.0, 2.0, 3.0]);
348        let msg = PointMessage::new(vec![point]);
349        let encoded = msg.encode_content().unwrap();
350
351        assert_eq!(encoded.len(), 136);
352    }
353
354    #[test]
355    fn test_roundtrip_single() {
356        let original = PointMessage::new(vec![PointElement::with_details(
357            "Fiducial1",
358            "Landmark",
359            [255, 128, 64, 255],
360            [100.5, 200.5, 300.5],
361            5.0,
362            "Image1",
363        )]);
364
365        let encoded = original.encode_content().unwrap();
366        let decoded = PointMessage::decode_content(&encoded).unwrap();
367
368        assert_eq!(decoded.points.len(), 1);
369        assert_eq!(decoded.points[0].name, "Fiducial1");
370        assert_eq!(decoded.points[0].group, "Landmark");
371        assert_eq!(decoded.points[0].rgba, [255, 128, 64, 255]);
372        assert_eq!(decoded.points[0].position, [100.5, 200.5, 300.5]);
373        assert_eq!(decoded.points[0].diameter, 5.0);
374        assert_eq!(decoded.points[0].owner, "Image1");
375    }
376
377    #[test]
378    fn test_roundtrip_multiple() {
379        let original = PointMessage::new(vec![
380            PointElement::new("P1", "Landmark", [1.0, 2.0, 3.0]),
381            PointElement::new("P2", "Fiducial", [4.0, 5.0, 6.0]),
382            PointElement::new("P3", "Target", [7.0, 8.0, 9.0]),
383        ]);
384
385        let encoded = original.encode_content().unwrap();
386        let decoded = PointMessage::decode_content(&encoded).unwrap();
387
388        assert_eq!(decoded.points.len(), 3);
389        assert_eq!(decoded.points[0].name, "P1");
390        assert_eq!(decoded.points[1].name, "P2");
391        assert_eq!(decoded.points[2].name, "P3");
392    }
393
394    #[test]
395    fn test_name_truncation() {
396        let long_name = "A".repeat(100);
397        let point = PointElement::new(&long_name, "Group", [0.0, 0.0, 0.0]);
398        let msg = PointMessage::new(vec![point]);
399
400        let encoded = msg.encode_content().unwrap();
401        let decoded = PointMessage::decode_content(&encoded).unwrap();
402
403        assert!(decoded.points[0].name.len() <= 63);
404    }
405
406    #[test]
407    fn test_empty_message() {
408        let msg = PointMessage::empty();
409        let encoded = msg.encode_content().unwrap();
410        let decoded = PointMessage::decode_content(&encoded).unwrap();
411
412        assert_eq!(decoded.points.len(), 0);
413        assert_eq!(encoded.len(), 0);
414    }
415
416    #[test]
417    fn test_decode_invalid_size() {
418        let data = vec![0u8; 135]; // One byte short
419        let result = PointMessage::decode_content(&data);
420        assert!(result.is_err());
421    }
422
423    #[test]
424    fn test_color_values() {
425        let point =
426            PointElement::with_color("ColorTest", "Test", [128, 64, 32, 200], [0.0, 0.0, 0.0]);
427        let msg = PointMessage::new(vec![point]);
428
429        let encoded = msg.encode_content().unwrap();
430        let decoded = PointMessage::decode_content(&encoded).unwrap();
431
432        assert_eq!(decoded.points[0].rgba, [128, 64, 32, 200]);
433    }
434}