Skip to main content

combs_mesh/engine/
builder.rs

1//! [`Emoji`] — an ordered set of typed blocks — and the fluent
2//! [`EmojiBuilder`] matching the spec quick start:
3//!
4//! ```no_run
5//! use combs_mesh::EmojiBuilder;
6//! let emoji = EmojiBuilder::new("my-emoji")
7//!     .description("...")
8//!     .add_todo("task1", "Build the thing")
9//!     .add_image_rgba(64, 64, vec![0u8; 64 * 64 * 4])
10//!     .with_agent_lifecycle()
11//!     .build();
12//! ```
13
14use crate::blocks::{
15    Block, EncryptionBlock, ImageBlock, LifecycleBlock, LifecycleState, LifecycleTransition,
16    SpriteAtlas, TextBlock, TodoBlock, TodoItem, TodoStatus,
17};
18
19/// An emoji: a name plus an ordered list of typed blocks.
20///
21/// The binary/unicode containers carry only the blocks; `name` round-trips
22/// through the text block (every builder-made emoji has one).
23#[derive(Debug, Clone, PartialEq)]
24pub struct Emoji {
25    /// Emoji name (mirrored into the text block by [`EmojiBuilder`]).
26    pub name: String,
27    /// The blocks, in insertion order.
28    pub blocks: Vec<Block>,
29}
30
31impl Emoji {
32    /// The first text block, if any.
33    #[must_use]
34    pub fn get_text(&self) -> Option<&TextBlock> {
35        self.blocks.iter().find_map(|b| match b {
36            Block::Txt(t) => Some(t),
37            _ => None,
38        })
39    }
40
41    /// The first image block, if any.
42    #[must_use]
43    pub fn get_image(&self) -> Option<&ImageBlock> {
44        self.blocks.iter().find_map(|b| match b {
45            Block::Img(i) => Some(i),
46            _ => None,
47        })
48    }
49
50    /// The first encryption directive, if any.
51    #[must_use]
52    pub fn get_encryption(&self) -> Option<&EncryptionBlock> {
53        self.blocks.iter().find_map(Block::as_encryption)
54    }
55
56    /// Iterates over all blocks.
57    pub fn iter(&self) -> impl Iterator<Item = &Block> {
58        self.blocks.iter()
59    }
60
61    /// Rebuilds an emoji from decoded blocks, deriving the name from the
62    /// text block (empty when there is none).
63    pub(crate) fn from_blocks(blocks: Vec<Block>) -> Emoji {
64        let name = blocks
65            .iter()
66            .find_map(|b| match b {
67                Block::Txt(t) => Some(t.name.clone()),
68                _ => None,
69            })
70            .unwrap_or_default();
71        Emoji { name, blocks }
72    }
73}
74
75/// Fluent builder. `build()` is infallible: validation happens in the
76/// `add_*` methods, which repair inconsistent input (documented per
77/// method) instead of panicking.
78#[derive(Debug, Clone)]
79pub struct EmojiBuilder {
80    emoji: Emoji,
81}
82
83impl EmojiBuilder {
84    /// Starts a builder; every emoji carries a text block so the name
85    /// round-trips through the binary/unicode containers.
86    #[must_use]
87    pub fn new(name: &str) -> Self {
88        EmojiBuilder {
89            emoji: Emoji {
90                name: name.to_string(),
91                blocks: vec![Block::Txt(TextBlock {
92                    name: name.to_string(),
93                    description: String::new(),
94                    specs: Vec::new(),
95                })],
96            },
97        }
98    }
99
100    /// Sets the description on the text block.
101    #[must_use]
102    pub fn description(mut self, description: &str) -> Self {
103        if let Some(Block::Txt(t)) = self
104            .emoji
105            .blocks
106            .iter_mut()
107            .find(|b| matches!(b, Block::Txt(_)))
108        {
109            t.description = description.to_string();
110        }
111        self
112    }
113
114    /// Adds a todo item (status `Pending`), appending to an existing todo
115    /// block when present.
116    #[must_use]
117    pub fn add_todo(mut self, key: &str, value: &str) -> Self {
118        let item = TodoItem {
119            key: key.to_string(),
120            value: value.to_string(),
121            status: TodoStatus::Pending,
122            depends_on: Vec::new(),
123        };
124        if let Some(Block::Tdo(t)) = self
125            .emoji
126            .blocks
127            .iter_mut()
128            .find(|b| matches!(b, Block::Tdo(_)))
129        {
130            t.items.push(item);
131        } else {
132            self.emoji.blocks.push(Block::Tdo(TodoBlock {
133                items: vec![item],
134            }));
135        }
136        self
137    }
138
139    /// Adds a single-frame image block from raw RGBA8 pixels. If `rgba`
140    /// does not match `width * height * 4` it is zero-padded/truncated to
141    /// fit (the builder never panics; strict validation lives in
142    /// [`SpriteAtlas::validate`]).
143    #[must_use]
144    pub fn add_image_rgba(mut self, width: u32, height: u32, mut rgba: Vec<u8>) -> Self {
145        rgba.resize(width as usize * height as usize * 4, 0);
146        self.emoji.blocks.push(Block::Img(ImageBlock {
147            name: String::new(),
148            atlas: SpriteAtlas {
149                width,
150                height,
151                frame_width: width.max(1),
152                frame_height: height.max(1),
153                frame_count: 1,
154                rgba,
155            },
156        }));
157        self
158    }
159
160    /// Adds the default agent lifecycle: `idle` (initial), `active`,
161    /// `sleeping`, with `wake`/`sleep`/`activate`/`deactivate` transitions.
162    #[must_use]
163    pub fn with_agent_lifecycle(mut self) -> Self {
164        self.emoji.blocks.push(Block::Lfc(LifecycleBlock {
165            states: vec![
166                LifecycleState {
167                    name: "idle".into(),
168                    initial: true,
169                },
170                LifecycleState {
171                    name: "active".into(),
172                    initial: false,
173                },
174                LifecycleState {
175                    name: "sleeping".into(),
176                    initial: false,
177                },
178            ],
179            transitions: vec![
180                LifecycleTransition {
181                    from: "idle".into(),
182                    to: "active".into(),
183                    event: "activate".into(),
184                },
185                LifecycleTransition {
186                    from: "active".into(),
187                    to: "idle".into(),
188                    event: "deactivate".into(),
189                },
190                LifecycleTransition {
191                    from: "active".into(),
192                    to: "sleeping".into(),
193                    event: "sleep".into(),
194                },
195                LifecycleTransition {
196                    from: "sleeping".into(),
197                    to: "idle".into(),
198                    event: "wake".into(),
199                },
200            ],
201        }));
202        self
203    }
204
205    /// Adds an encryption directive (which algorithm, which block types to
206    /// encrypt at rest).
207    #[must_use]
208    pub fn encryption(mut self, encryption: EncryptionBlock) -> Self {
209        self.emoji.blocks.push(Block::Enc(encryption));
210        self
211    }
212
213    /// Appends any block.
214    #[must_use]
215    pub fn add_block(mut self, block: Block) -> Self {
216        self.emoji.blocks.push(block);
217        self
218    }
219
220    /// Finishes building. Infallible by design (see type docs).
221    #[must_use]
222    pub fn build(self) -> Emoji {
223        self.emoji
224    }
225}