Skip to main content

fd_core/
id.rs

1use lasso::{Spur, ThreadedRodeo};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4use std::sync::LazyLock;
5
6/// Global string interner for node IDs — fast comparisons, low memory.
7static INTERNER: LazyLock<ThreadedRodeo> = LazyLock::new(ThreadedRodeo::default);
8
9/// A lightweight, interned identifier for nodes in the scene graph.
10/// Internally a `Spur` index — 4 bytes, Copy, Eq, Hash in O(1).
11#[derive(Clone, Copy, PartialEq, Eq, Hash)]
12pub struct NodeId(Spur);
13
14impl NodeId {
15    /// Intern a new string as a NodeId, or return existing if already interned.
16    pub fn intern(s: &str) -> Self {
17        NodeId(INTERNER.get_or_intern(s))
18    }
19
20    /// Resolve back to a string slice.
21    pub fn as_str(&self) -> &str {
22        INTERNER.resolve(&self.0)
23    }
24
25    /// Generate a unique anonymous ID (for nodes without explicit @id).
26    pub fn anonymous() -> Self {
27        Self::with_prefix("_anon")
28    }
29
30    /// Generate a unique ID with a type prefix (e.g. `rect_1`, `ellipse_2`).
31    pub fn with_prefix(prefix: &str) -> Self {
32        use std::sync::atomic::{AtomicU64, Ordering};
33        static COUNTER: AtomicU64 = AtomicU64::new(0);
34        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
35        Self::intern(&format!("{prefix}_{n}"))
36    }
37}
38
39impl fmt::Debug for NodeId {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "@{}", self.as_str())
42    }
43}
44
45impl fmt::Display for NodeId {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "@{}", self.as_str())
48    }
49}
50
51impl Serialize for NodeId {
52    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
53        serializer.serialize_str(self.as_str())
54    }
55}
56
57impl<'de> Deserialize<'de> for NodeId {
58    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
59        let s = String::deserialize(deserializer)?;
60        Ok(NodeId::intern(&s))
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn interning_roundtrip() {
70        let a = NodeId::intern("login_form");
71        let b = NodeId::intern("login_form");
72        assert_eq!(a, b);
73        assert_eq!(a.as_str(), "login_form");
74    }
75
76    #[test]
77    fn anonymous_ids_are_unique() {
78        let a = NodeId::anonymous();
79        let b = NodeId::anonymous();
80        assert_ne!(a, b);
81    }
82}