Skip to main content

dotloom_document/
model.rs

1//! Entities, layers, groups, metadata and settings.
2
3use std::collections::BTreeMap;
4
5use dotloom_geometry::{
6    Affine, Shape,
7    units::{LengthUnit, TimeAxis},
8};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::{Color, EntityId, GroupId, LayerId, PropValue, Style, TypeId};
13
14fn one() -> u32 {
15    1
16}
17
18#[allow(clippy::trivially_copy_pass_by_ref)]
19fn is_one(v: &u32) -> bool {
20    *v == 1
21}
22
23#[allow(clippy::trivially_copy_pass_by_ref)]
24fn is_false(v: &bool) -> bool {
25    !*v
26}
27
28#[allow(clippy::trivially_copy_pass_by_ref)]
29fn is_identity(a: &Affine) -> bool {
30    a.is_identity()
31}
32
33fn yes() -> bool {
34    true
35}
36
37#[allow(clippy::trivially_copy_pass_by_ref)]
38fn is_true(v: &bool) -> bool {
39    *v
40}
41
42/// A document entity.
43///
44/// Built-in types (`dotloom.*`) carry canonical `geometry`. Plugin types carry typed
45/// `props`; their drawable geometry is *derived* by the type definition and is not
46/// stored, except for the optional `fallback` representation that lets viewers
47/// without the plugin display (not edit) the entity.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Entity {
51    /// Stable identifier.
52    pub id: EntityId,
53    /// Namespaced type.
54    #[serde(rename = "type")]
55    pub type_id: TypeId,
56    /// Version of the type's schema the properties follow.
57    #[serde(default = "one", skip_serializing_if = "is_one")]
58    pub type_version: u32,
59    /// Layer membership.
60    pub layer: LayerId,
61    /// Local-to-world transform.
62    #[serde(default, skip_serializing_if = "is_identity")]
63    pub transform: Affine,
64    /// Canonical geometry (built-in types) in local coordinates.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub geometry: Option<Shape>,
67    /// Typed properties (canonical units).
68    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
69    pub props: BTreeMap<String, PropValue>,
70    /// Opaque, namespaced plugin payloads preserved verbatim.
71    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
72    pub data: BTreeMap<String, Value>,
73    /// Optional user-visible name.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub name: Option<String>,
76    /// Style overrides.
77    #[serde(default, skip_serializing_if = "Style::is_default")]
78    pub style: Style,
79    /// Locked against interactive editing (not a solver lock).
80    #[serde(default, skip_serializing_if = "is_false")]
81    pub locked: bool,
82    /// Hidden from rendering and picking.
83    #[serde(default, skip_serializing_if = "is_false")]
84    pub hidden: bool,
85    /// Standard representation for viewers that lack the plugin (derived snapshot,
86    /// world coordinates). Never used for editing or measurement.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub fallback: Option<Vec<Shape>>,
89    /// Unknown fields from newer producers, preserved on save.
90    #[serde(flatten)]
91    pub extra: BTreeMap<String, Value>,
92}
93
94impl Entity {
95    /// New entity with default metadata.
96    #[must_use]
97    pub fn new(id: EntityId, type_id: TypeId, layer: LayerId) -> Self {
98        Self {
99            id,
100            type_id,
101            type_version: 1,
102            layer,
103            transform: Affine::IDENTITY,
104            geometry: None,
105            props: BTreeMap::new(),
106            data: BTreeMap::new(),
107            name: None,
108            style: Style::default(),
109            locked: false,
110            hidden: false,
111            fallback: None,
112            extra: BTreeMap::new(),
113        }
114    }
115
116    /// Builder: geometry.
117    #[must_use]
118    pub fn with_geometry(mut self, s: Shape) -> Self {
119        self.geometry = Some(s);
120        self
121    }
122
123    /// Builder: property.
124    #[must_use]
125    pub fn with_prop(mut self, k: impl Into<String>, v: PropValue) -> Self {
126        self.props.insert(k.into(), v);
127        self
128    }
129
130    /// Entities referenced by properties.
131    pub fn referenced_entities(&self) -> impl Iterator<Item = EntityId> + '_ {
132        self.props.values().filter_map(PropValue::referenced_entity)
133    }
134}
135
136/// A layer.
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct Layer {
140    /// Identifier.
141    pub id: LayerId,
142    /// Display name.
143    pub name: String,
144    /// Visible.
145    #[serde(default = "yes", skip_serializing_if = "is_true")]
146    pub visible: bool,
147    /// Locked against editing.
148    #[serde(default, skip_serializing_if = "is_false")]
149    pub locked: bool,
150    /// Default stroke color for entities on this layer.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub color: Option<Color>,
153    /// Unknown fields preserved.
154    #[serde(flatten)]
155    pub extra: BTreeMap<String, Value>,
156}
157
158impl Layer {
159    /// New visible, unlocked layer.
160    #[must_use]
161    pub fn new(id: LayerId, name: impl Into<String>) -> Self {
162        Self { id, name: name.into(), visible: true, locked: false, color: None, extra: BTreeMap::new() }
163    }
164}
165
166/// A group of entities and nested groups. Each entity and group has at most one
167/// parent group; nesting is acyclic.
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct Group {
171    /// Identifier.
172    pub id: GroupId,
173    /// Display name.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub name: Option<String>,
176    /// Member entities.
177    #[serde(default)]
178    pub members: Vec<EntityId>,
179    /// Nested groups.
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub children: Vec<GroupId>,
182    /// Unknown fields preserved.
183    #[serde(flatten)]
184    pub extra: BTreeMap<String, Value>,
185}
186
187/// Document metadata.
188#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct Meta {
191    /// Title.
192    #[serde(default, skip_serializing_if = "String::is_empty")]
193    pub title: String,
194    /// Unknown fields preserved.
195    #[serde(flatten)]
196    pub extra: BTreeMap<String, Value>,
197}
198
199/// Grid settings (presentation defaults stored with the document).
200#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct GridSettings {
203    /// Grid spacing in mm.
204    pub spacing: f64,
205    /// Major line every N minor lines.
206    pub major_every: u32,
207}
208
209impl Default for GridSettings {
210    fn default() -> Self {
211        Self { spacing: 10.0, major_every: 10 }
212    }
213}
214
215/// Document settings.
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct Settings {
219    /// Unit shown to users and used by importers by default (storage is always mm).
220    #[serde(default)]
221    pub display_unit: LengthUnit,
222    /// Grid defaults.
223    #[serde(default)]
224    pub grid: GridSettings,
225    /// Optional time axis for timeline documents.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub time_axis: Option<TimeAxis>,
228    /// Unknown fields preserved.
229    #[serde(flatten)]
230    pub extra: BTreeMap<String, Value>,
231}
232
233impl Default for Settings {
234    fn default() -> Self {
235        Self {
236            display_unit: LengthUnit::Millimetre,
237            grid: GridSettings::default(),
238            time_axis: None,
239            extra: BTreeMap::new(),
240        }
241    }
242}