orrery-core 0.2.0

Core types and definitions for Orrery diagrams
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! Note drawable for diagram annotations.
//!
//! This module provides a note drawable that renders as a rectangle with a "dog-eared"
//! (bent) top-right corner. Notes are commonly used for annotations and comments in diagrams.
//!
//! # Visual Appearance
//!
//! A note consists of three visual layers:
//! - **Main body**: A rectangle with the top-right corner cut at a 45° angle
//! - **Fold triangle**: A small triangle showing the folded corner (slightly darker)
//! - **Fold line**: A diagonal line emphasizing where the corner bends
//!
//! # Examples
//!
//! ```
//! # use orrery_core::draw::{Note, NoteDefinition};
//! # use orrery_core::geometry::Point;
//! # use orrery_core::draw::Drawable;
//! # use std::rc::Rc;
//! #
//! // Create a note with default styling
//! let definition = NoteDefinition::new();
//! let note = Note::new(Rc::new(definition), "This is a note".to_string());
//!
//! // Get the size
//! let size = note.size();
//!
//! // Render to layered output
//! let position = Point::new(100.0, 100.0);
//! let output = note.render_to_layers(position);
//! let svg_nodes = output.render();
//! ```
//!
//! # Customization
//!
//! ```
//! # use std::rc::Rc;
//! #
//! # use orrery_core::draw::{NoteDefinition, StrokeStyle};
//! # use orrery_core::color::Color;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut definition = NoteDefinition::new();
//!
//! // Customize background color
//! definition.set_background_color(Some(Color::new("#ffebcd")?));
//!
//! // Customize border stroke
//! let mut stroke = Rc::clone(&definition.stroke());
//! let mut stroke_mut = Rc::make_mut(&mut stroke);
//! stroke_mut.set_color(Color::new("blue")?);
//! stroke_mut.set_width(2.0);
//! stroke_mut.set_style(StrokeStyle::Dashed);
//! definition.set_stroke(stroke);
//! # Ok(())
//! # }
//! ```

use std::rc::Rc;

use svg::{self, node::element as svg_element};

use crate::{
    color::Color,
    draw::{Drawable, LayeredOutput, RenderLayer, StrokeDefinition, Text, TextDefinition},
    geometry::{Insets, Point, Size},
};

/// Fixed size for the dog-eared corner fold in pixels.
///
/// This constant defines the size of the cut corner at the top-right of the note.
/// The fold appears as a 12px x 12px triangle that has been "bent over" to create
/// the dog-eared effect.
const CORNER_FOLD_SIZE: f32 = 12.0;

/// Definition for note styling and appearance.
///
/// `NoteDefinition` is a configuration struct that defines how a note should be styled,
/// including its background color, border stroke, and text styling. Multiple notes can
/// share the same definition for consistent styling.
///
/// # Default Values
///
/// - **Background**: Light yellow (`#fffacd`)
/// - **Stroke**: Default stroke
/// - **Text**: Default text definition
///
/// # Examples
///
/// ```
/// # use std::rc::Rc;
/// #
/// # use orrery_core::draw::{NoteDefinition, StrokeDefinition};
/// # use orrery_core::color::Color;
/// #
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Create with defaults
/// let mut definition = NoteDefinition::new();
///
/// // Customize
/// definition.set_background_color(Some(Color::new("lightblue")?));
/// let mut stroke = Rc::clone(&definition.stroke());
/// let stroke_mut = Rc::make_mut(&mut stroke);
/// stroke_mut.set_color(Color::new("navy")?);
/// stroke_mut.set_width(2.0);
/// definition.set_stroke(stroke);
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct NoteDefinition {
    background_color: Option<Color>,
    stroke: Rc<StrokeDefinition>,
    text: Rc<TextDefinition>,
    min_width: Option<f32>,
}

impl NoteDefinition {
    /// Creates a new note definition with default values.
    ///
    /// This is equivalent to calling [`NoteDefinition::default()`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use orrery_core::draw::NoteDefinition;
    /// let definition = NoteDefinition::new();
    /// ```
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the background color for the note.
    ///
    /// When set to `Some(color)`, the note will be filled with the specified color.
    /// When set to `None`, the note will not have a background color.
    ///
    /// # Arguments
    ///
    /// * `color` - Optional background color. Use `None` for no background.
    ///
    /// # Examples
    ///
    /// ```
    /// # use orrery_core::draw::NoteDefinition;
    /// # use orrery_core::color::Color;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let mut definition = NoteDefinition::new();
    /// definition.set_background_color(Some(Color::new("lightyellow")?));
    /// # Ok(())
    /// # }
    /// ```
    pub fn set_background_color(&mut self, color: Option<Color>) {
        self.background_color = color;
    }

    /// Sets the minimum width for the note.
    ///
    /// When set, the note will expand to at least this width, useful for spanning notes
    /// across multiple participants in sequence diagrams.
    ///
    /// # Arguments
    ///
    /// * `width` - Optional minimum width in pixels. Use `None` for natural sizing.
    pub fn set_min_width(&mut self, width: Option<f32>) {
        self.min_width = width;
    }

    /// Returns the background color of the note.
    fn background_color(&self) -> Option<Color> {
        self.background_color
    }

    /// Gets the stroke definition
    pub fn stroke(&self) -> &Rc<StrokeDefinition> {
        &self.stroke
    }

    /// Gets the text definition
    pub fn text(&self) -> &Rc<TextDefinition> {
        &self.text
    }

    /// Set text definition using Rc.
    pub fn set_text(&mut self, text: Rc<TextDefinition>) {
        self.text = text;
    }

    /// Set stroke definition using Rc.
    pub fn set_stroke(&mut self, stroke: Rc<StrokeDefinition>) {
        self.stroke = stroke;
    }
}

impl Default for NoteDefinition {
    fn default() -> Self {
        Self {
            background_color: Some(Color::new("lightyellow").expect("Invalid color")),
            stroke: Rc::new(StrokeDefinition::default()),
            text: Rc::new(TextDefinition::default()),
            min_width: None,
        }
    }
}

/// A note drawable with a dog-eared corner.
///
/// `Note` represents a renderable note that combines a [`NoteDefinition`] with text content.
/// The note is rendered as a rectangle with a bent top-right corner, commonly used for
/// annotations in diagrams.
///
/// # Rendering
///
/// When rendered, the note creates an SVG group containing:
/// 1. The main note body (rectangle with cut corner)
/// 2. A small triangle showing the folded corner
/// 3. A diagonal fold line
/// 4. The text content
///
/// # Examples
///
/// ```
/// # use orrery_core::draw::{Note, NoteDefinition, Drawable};
/// # use orrery_core::geometry::Point;
/// # use std::rc::Rc;
/// #
/// let definition = NoteDefinition::new();
/// let note = Note::new(Rc::new(definition), "Important note".to_string());
///
/// // The note calculates its own size based on text
/// let size = note.size();
///
/// // Render at a specific position (center point)
/// let position = Point::new(150.0, 200.0);
/// let output = note.render_to_layers(position);
/// let svg_nodes = output.render();
/// ```
#[derive(Debug, Clone)]
pub struct Note {
    definition: Rc<NoteDefinition>,
    content: String,
}

impl Note {
    /// Creates a new note with the given definition and content.
    ///
    /// # Arguments
    ///
    /// * `definition` - The styling definition for the note (can be shared among multiple notes)
    /// * `content` - The text content to display inside the note
    ///
    /// # Examples
    ///
    /// ```
    /// # use orrery_core::draw::{Note, NoteDefinition};
    /// # use std::rc::Rc;
    /// #
    /// let definition = NoteDefinition::new();
    /// let note = Note::new(Rc::new(definition), "My note text".to_string());
    /// ```
    pub fn new(definition: Rc<NoteDefinition>, content: String) -> Self {
        Self {
            definition,
            content,
        }
    }

    /// Calculates the size of the text content without padding.
    fn text_size(&self) -> Size {
        if self.content.is_empty() {
            return Size::zero();
        }
        let text = Text::new(&self.definition.text, &self.content);
        text.size()
    }

    /// Calculates the total size of the note including padding.
    fn calculate_size(&self) -> Size {
        let text_size = self.text_size();
        let size = text_size.add_padding(Insets::new(10.0, 10.0 + CORNER_FOLD_SIZE, 10.0, 10.0));

        // Apply minimum width if specified
        if let Some(min_width) = self.definition.min_width {
            let width = size.width().max(min_width);
            Size::new(width, size.height())
        } else {
            size
        }
    }

    /// Creates the SVG path element for the main note body with dog-eared corner.
    fn create_dog_eared_path(&self, size: Size, position: Point) -> svg_element::Path {
        let bounds = position.to_bounds(size);
        let min_x = bounds.min_x();
        let min_y = bounds.min_y();
        let max_x = bounds.max_x();
        let max_y = bounds.max_y();

        // The dog-ear is at the top-right corner
        let fold_x = max_x - CORNER_FOLD_SIZE;
        let fold_y = min_y + CORNER_FOLD_SIZE;

        // Create path for the main body (rectangle with top-right corner cut)
        let path_data = format!(
            "M {} {} L {} {} L {} {} L {} {} L {} {} L {} {} Z",
            min_x,
            min_y, // Start at top-left
            fold_x,
            min_y, // Go to where fold starts (top edge)
            max_x,
            fold_y, // Diagonal to right edge at fold height
            max_x,
            max_y, // Down to bottom-right
            min_x,
            max_y, // Across to bottom-left
            min_x,
            min_y // Back up to top-left
        );

        let path = svg_element::Path::new().set("d", path_data);
        crate::apply_stroke!(path, self.definition.stroke())
    }

    /// Creates the SVG path element for the diagonal fold line.
    fn create_fold_line_path(&self, size: Size, position: Point) -> svg_element::Path {
        let bounds = position.to_bounds(size);
        let max_x = bounds.max_x();
        let min_y = bounds.min_y();

        // Fold line goes from the cut point on top edge to the cut point on right edge
        let fold_x = max_x - CORNER_FOLD_SIZE;
        let fold_y = min_y + CORNER_FOLD_SIZE;

        let path_data = format!(
            "M {} {} L {} {}",
            fold_x,
            min_y, // Start at top edge fold point
            max_x,
            fold_y // Go to right edge fold point
        );

        let path = svg_element::Path::new()
            .set("d", path_data)
            .set("fill", "none");
        crate::apply_stroke!(path, self.definition.stroke())
    }

    /// Creates the SVG path element for the small triangular fold-over.
    fn create_fold_triangle_path(&self, size: Size, position: Point) -> svg_element::Path {
        let bounds = position.to_bounds(size);
        let max_x = bounds.max_x();
        let min_y = bounds.min_y();

        let fold_x = max_x - CORNER_FOLD_SIZE;
        let fold_y = min_y + CORNER_FOLD_SIZE;

        // Small triangle to show the folded corner
        let path_data = format!(
            "M {} {} L {} {} L {} {} Z",
            fold_x,
            min_y, // Top edge fold point
            max_x,
            fold_y, // Right edge fold point
            fold_x,
            fold_y // Corner point of triangle
        );

        let path = svg_element::Path::new().set("d", path_data);
        crate::apply_stroke!(path, self.definition.stroke())
    }
}

impl Drawable for Note {
    fn render_to_layers(&self, position: Point) -> LayeredOutput {
        let mut output = LayeredOutput::new();
        let size = self.size();

        // Create the main note body with dog-eared corner
        let mut note_body = self.create_dog_eared_path(size, position);

        // Apply background color
        if let Some(bg_color) = self.definition.background_color() {
            note_body = note_body
                .set("fill", bg_color.to_string())
                .set("fill-opacity", bg_color.alpha());
        }

        output.add_to_layer(RenderLayer::Note, Box::new(note_body));

        // Add the small triangle for the folded corner (slightly darker)
        let mut fold_triangle = self.create_fold_triangle_path(size, position);

        // Make the fold slightly darker than the background
        if let Some(bg_color) = self.definition.background_color() {
            let darker = bg_color.with_alpha(bg_color.alpha() * 0.8);
            fold_triangle = fold_triangle
                .set("fill", darker.to_string())
                .set("fill-opacity", darker.alpha());
        } else {
            fold_triangle = fold_triangle
                .set("fill", "#e0e0e0")
                .set("fill-opacity", 0.8);
        }

        output.add_to_layer(RenderLayer::Note, Box::new(fold_triangle));

        // Add the fold line
        let fold_line = self.create_fold_line_path(size, position);

        output.add_to_layer(RenderLayer::Note, Box::new(fold_line));

        // Render the text content if present
        if !self.content.is_empty() {
            let text = Text::new(&self.definition.text, &self.content);
            let text_output = text.render_to_layers(position);
            output.merge(text_output);
        }

        output
    }

    fn size(&self) -> Size {
        self.calculate_size()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_note_definition_default() {
        let def = NoteDefinition::default();
        assert!(def.background_color().is_some());
        assert_eq!(def.stroke().width(), 1.0);
    }

    #[test]
    fn test_note_creation() {
        let def = NoteDefinition::new();
        let note = Note::new(Rc::new(def.clone()), "Test note".to_string());
        let size = note.size();
        // Verify note was created with non-zero size (indicates content was stored)
        assert!(size.width() > 0.0);
        assert!(size.height() > 0.0);
    }

    #[test]
    fn test_note_size_calculation() {
        let def = NoteDefinition::new();
        let note = Note::new(Rc::new(def.clone()), "Test".to_string());
        let size = note.size();
        assert!(size.width() > 0.0);
        assert!(size.height() > 0.0);
    }

    #[test]
    fn test_empty_note() {
        let def = NoteDefinition::new();
        let note = Note::new(Rc::new(def.clone()), String::new());
        let size = note.size();
        // Even empty notes should have some size due to padding
        assert!(size.width() > 0.0);
        assert!(size.height() > 0.0);
    }

    #[test]
    fn test_note_definition_customization() {
        let mut def = NoteDefinition::new();
        def.set_background_color(Some(Color::new("blue").expect("valid color")));
        let _note = Note::new(Rc::new(def.clone()), String::new());
        assert_eq!(
            def.background_color(),
            Some(Color::new("blue").expect("valid color"))
        );
    }
}