Skip to main content

dotloom_document/
builtin.rs

1//! Built-in entity types and their geometry parameters.
2
3use dotloom_geometry::{PathEl, Point, Shape, ShapeKind};
4
5use crate::{DocError, TypeId};
6
7/// Built-in geometry type IDs.
8pub mod types {
9    /// Point marker.
10    pub const POINT: &str = "dotloom.point";
11    /// Line segment.
12    pub const LINE: &str = "dotloom.line";
13    /// Polyline.
14    pub const POLYLINE: &str = "dotloom.polyline";
15    /// Rectangle.
16    pub const RECT: &str = "dotloom.rect";
17    /// Circle.
18    pub const CIRCLE: &str = "dotloom.circle";
19    /// Arc.
20    pub const ARC: &str = "dotloom.arc";
21    /// Bézier path.
22    pub const PATH: &str = "dotloom.path";
23    /// Polygon.
24    pub const POLYGON: &str = "dotloom.polygon";
25    /// Text.
26    pub const TEXT: &str = "dotloom.text";
27    /// Associative dimension (props-based, see `crate::dimension`).
28    pub const DIMENSION: &str = "dotloom.dimension";
29}
30
31/// Shape kind required by a built-in geometry type.
32#[must_use]
33pub fn builtin_shape_kind(t: &TypeId) -> Option<ShapeKind> {
34    Some(match t.as_str() {
35        types::POINT => ShapeKind::Point,
36        types::LINE => ShapeKind::Line,
37        types::POLYLINE => ShapeKind::Polyline,
38        types::RECT => ShapeKind::Rect,
39        types::CIRCLE => ShapeKind::Circle,
40        types::ARC => ShapeKind::Arc,
41        types::PATH => ShapeKind::Path,
42        types::POLYGON => ShapeKind::Polygon,
43        types::TEXT => ShapeKind::Text,
44        _ => return None,
45    })
46}
47
48/// Built-in type ID for a shape kind.
49#[must_use]
50pub fn builtin_type_for(kind: ShapeKind) -> TypeId {
51    let s = match kind {
52        ShapeKind::Point => types::POINT,
53        ShapeKind::Line => types::LINE,
54        ShapeKind::Polyline => types::POLYLINE,
55        ShapeKind::Rect => types::RECT,
56        ShapeKind::Circle => types::CIRCLE,
57        ShapeKind::Arc => types::ARC,
58        ShapeKind::Path => types::PATH,
59        ShapeKind::Polygon => types::POLYGON,
60        ShapeKind::Text => types::TEXT,
61    };
62    TypeId(s.to_owned())
63}
64
65/// Whether the type ID belongs to the `dotloom` namespace.
66#[must_use]
67pub fn is_builtin(t: &TypeId) -> bool {
68    t.namespace() == "dotloom"
69}
70
71/// Physical meaning of a geometry parameter.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ParamDim {
74    /// Length (mm).
75    Length,
76    /// Angle (rad).
77    Angle,
78}
79
80fn pt_params(prefix: &str, p: Point, out: &mut Vec<(String, f64, ParamDim)>) {
81    out.push((format!("{prefix}.x"), p.x, ParamDim::Length));
82    out.push((format!("{prefix}.y"), p.y, ParamDim::Length));
83}
84
85/// Named numeric parameters of a shape (solver variables), in stable order.
86#[must_use]
87pub fn geometry_params(s: &Shape) -> Vec<(String, f64, ParamDim)> {
88    let mut out = Vec::new();
89    match s {
90        Shape::Point(p) => {
91            out.push(("x".into(), p.at.x, ParamDim::Length));
92            out.push(("y".into(), p.at.y, ParamDim::Length));
93        }
94        Shape::Line(l) => {
95            pt_params("a", l.a, &mut out);
96            pt_params("b", l.b, &mut out);
97        }
98        Shape::Polyline(p) => {
99            for (i, v) in p.points.iter().enumerate() {
100                pt_params(&format!("v{i}"), *v, &mut out);
101            }
102        }
103        Shape::Polygon(p) => {
104            for (i, v) in p.outer.iter().enumerate() {
105                pt_params(&format!("v{i}"), *v, &mut out);
106            }
107        }
108        Shape::Rect(r) => {
109            out.push(("x".into(), r.origin.x, ParamDim::Length));
110            out.push(("y".into(), r.origin.y, ParamDim::Length));
111            out.push(("width".into(), r.width, ParamDim::Length));
112            out.push(("height".into(), r.height, ParamDim::Length));
113        }
114        Shape::Circle(c) => {
115            out.push(("cx".into(), c.center.x, ParamDim::Length));
116            out.push(("cy".into(), c.center.y, ParamDim::Length));
117            out.push(("r".into(), c.radius, ParamDim::Length));
118        }
119        Shape::Arc(a) => {
120            out.push(("cx".into(), a.center.x, ParamDim::Length));
121            out.push(("cy".into(), a.center.y, ParamDim::Length));
122            out.push(("r".into(), a.radius, ParamDim::Length));
123            out.push(("start".into(), a.start, ParamDim::Angle));
124            out.push(("sweep".into(), a.sweep, ParamDim::Angle));
125        }
126        Shape::Path(p) => {
127            let mut i = 0;
128            for el in &p.elements {
129                let pts: Vec<Point> = match *el {
130                    PathEl::MoveTo(a) | PathEl::LineTo(a) => vec![a],
131                    PathEl::QuadTo(a, b) => vec![a, b],
132                    PathEl::CubicTo(a, b, c) => vec![a, b, c],
133                    PathEl::Close => vec![],
134                };
135                for q in pts {
136                    pt_params(&format!("p{i}"), q, &mut out);
137                    i += 1;
138                }
139            }
140        }
141        Shape::Text(t) => {
142            out.push(("x".into(), t.position.x, ParamDim::Length));
143            out.push(("y".into(), t.position.y, ParamDim::Length));
144            out.push(("height".into(), t.height, ParamDim::Length));
145            out.push(("rotation".into(), t.rotation, ParamDim::Angle));
146        }
147    }
148    out
149}
150
151/// Read one geometry parameter.
152#[must_use]
153pub fn get_geometry_param(s: &Shape, name: &str) -> Option<f64> {
154    geometry_params(s).into_iter().find(|(n, _, _)| n == name).map(|(_, v, _)| v)
155}
156
157fn set_point(p: &mut Point, axis: &str, v: f64) -> bool {
158    match axis {
159        "x" => p.x = v,
160        "y" => p.y = v,
161        _ => return false,
162    }
163    true
164}
165
166fn indexed(name: &str, prefix: char) -> Option<(usize, &str)> {
167    let rest = name.strip_prefix(prefix)?;
168    let (idx, axis) = rest.split_once('.')?;
169    Some((idx.parse().ok()?, axis))
170}
171
172/// Write one geometry parameter. Does not validate the resulting shape.
173pub fn set_geometry_param(s: &mut Shape, name: &str, v: f64) -> Result<(), DocError> {
174    if !v.is_finite() {
175        return Err(DocError::InvalidValue(format!("parameter `{name}` must be finite")));
176    }
177    let ok = match s {
178        Shape::Point(p) => set_point(&mut p.at, name, v),
179        Shape::Line(l) => match name.split_once('.') {
180            Some(("a", axis)) => set_point(&mut l.a, axis, v),
181            Some(("b", axis)) => set_point(&mut l.b, axis, v),
182            _ => false,
183        },
184        Shape::Polyline(p) => {
185            indexed(name, 'v').is_some_and(|(i, axis)| p.points.get_mut(i).is_some_and(|q| set_point(q, axis, v)))
186        }
187        Shape::Polygon(p) => {
188            indexed(name, 'v').is_some_and(|(i, axis)| p.outer.get_mut(i).is_some_and(|q| set_point(q, axis, v)))
189        }
190        Shape::Rect(r) => match name {
191            "x" => set_point(&mut r.origin, "x", v),
192            "y" => set_point(&mut r.origin, "y", v),
193            "width" => {
194                r.width = v;
195                true
196            }
197            "height" => {
198                r.height = v;
199                true
200            }
201            _ => false,
202        },
203        Shape::Circle(c) => match name {
204            "cx" => set_point(&mut c.center, "x", v),
205            "cy" => set_point(&mut c.center, "y", v),
206            "r" => {
207                c.radius = v;
208                true
209            }
210            _ => false,
211        },
212        Shape::Arc(a) => match name {
213            "cx" => set_point(&mut a.center, "x", v),
214            "cy" => set_point(&mut a.center, "y", v),
215            "r" => {
216                a.radius = v;
217                true
218            }
219            "start" => {
220                a.start = v;
221                true
222            }
223            "sweep" => {
224                a.sweep = v;
225                true
226            }
227            _ => false,
228        },
229        Shape::Path(p) => match indexed(name, 'p') {
230            Some((target, axis)) => {
231                let mut i = 0;
232                let mut done = false;
233                for el in &mut p.elements {
234                    let pts: Vec<&mut Point> = match el {
235                        PathEl::MoveTo(a) | PathEl::LineTo(a) => vec![a],
236                        PathEl::QuadTo(a, b) => vec![a, b],
237                        PathEl::CubicTo(a, b, c) => vec![a, b, c],
238                        PathEl::Close => vec![],
239                    };
240                    for q in pts {
241                        if i == target {
242                            done = set_point(q, axis, v);
243                        }
244                        i += 1;
245                    }
246                }
247                done
248            }
249            None => false,
250        },
251        Shape::Text(t) => match name {
252            "x" => set_point(&mut t.position, "x", v),
253            "y" => set_point(&mut t.position, "y", v),
254            "height" => {
255                t.height = v;
256                true
257            }
258            "rotation" => {
259                t.rotation = v;
260                true
261            }
262            _ => false,
263        },
264    };
265    if ok { Ok(()) } else { Err(DocError::InvalidValue(format!("unknown geometry parameter `{name}`"))) }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use dotloom_geometry::{Arc, Polyline, Segment};
272
273    #[test]
274    fn params_roundtrip_for_every_shape() {
275        let shapes = vec![
276            Shape::Line(Segment::new(Point::new(1.0, 2.0), Point::new(3.0, 4.0))),
277            Shape::Polyline(Polyline::open(vec![Point::ORIGIN, Point::new(1.0, 1.0), Point::new(2.0, 0.0)])),
278            Shape::Arc(Arc::new(Point::ORIGIN, 5.0, 0.1, 1.0).unwrap()),
279            Shape::Path(dotloom_geometry::Path {
280                elements: vec![
281                    PathEl::MoveTo(Point::ORIGIN),
282                    PathEl::CubicTo(Point::new(1.0, 1.0), Point::new(2.0, 1.0), Point::new(3.0, 0.0)),
283                ],
284            }),
285        ];
286        for mut s in shapes {
287            for (name, value, _) in geometry_params(&s) {
288                set_geometry_param(&mut s, &name, value + 0.5).unwrap();
289                assert_eq!(get_geometry_param(&s, &name), Some(value + 0.5), "{name}");
290            }
291            assert!(set_geometry_param(&mut s, "nope", 1.0).is_err());
292            assert!(set_geometry_param(&mut s, "v0.x", f64::NAN).is_err());
293        }
294    }
295}