1use dotloom_geometry::{PathEl, Point, Shape, ShapeKind};
4
5use crate::{DocError, TypeId};
6
7pub mod types {
9 pub const POINT: &str = "dotloom.point";
11 pub const LINE: &str = "dotloom.line";
13 pub const POLYLINE: &str = "dotloom.polyline";
15 pub const RECT: &str = "dotloom.rect";
17 pub const CIRCLE: &str = "dotloom.circle";
19 pub const ARC: &str = "dotloom.arc";
21 pub const PATH: &str = "dotloom.path";
23 pub const POLYGON: &str = "dotloom.polygon";
25 pub const TEXT: &str = "dotloom.text";
27 pub const DIMENSION: &str = "dotloom.dimension";
29}
30
31#[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#[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#[must_use]
67pub fn is_builtin(t: &TypeId) -> bool {
68 t.namespace() == "dotloom"
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ParamDim {
74 Length,
76 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#[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#[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
172pub 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}