1use std::collections::BTreeMap;
9
10use dotloom_document::{
11 Entity, EntityId, PropValue,
12 builtin::{get_geometry_param, is_builtin, types},
13};
14use dotloom_geometry::{
15 Aabb, Anchor, AnchorKind, Arc, Circle, HAlign, Point, Polygon, Polyline, Segment, Shape, Text, TransformPolicy,
16 VAlign, Vector,
17 dimension::{self, DimensionStyle, LinearKind},
18 units::LengthUnit,
19};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 DocView,
24 lang::{Axis, Compiled, Leaf},
25 registry::{CompiledPrim, CompiledType, PrimStyle, Registry},
26};
27
28pub const MAX_REF_DEPTH: usize = 16;
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(tag = "reason", rename_all = "camelCase")]
34pub enum ReadOnly {
35 MissingPlugin {
37 type_id: String,
39 },
40 DisabledPlugin {
42 type_id: String,
44 },
45 NewerVersion {
47 type_id: String,
49 found: u32,
51 supported: u32,
53 },
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum Drawable {
59 Shape(Shape, PrimStyle),
61 Text(Text, PrimStyle),
63 Arrow {
65 tip: Point,
67 direction: Vector,
69 size: f64,
71 },
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct Evaluated {
77 pub anchors: Vec<Anchor>,
79 pub drawables: Vec<Drawable>,
81 pub bbox: Aabb,
83 pub read_only: Option<ReadOnly>,
85 pub error: Option<String>,
87 pub measured: Option<f64>,
89}
90
91impl Evaluated {
92 fn empty() -> Self {
93 Self {
94 anchors: Vec::new(),
95 drawables: Vec::new(),
96 bbox: Aabb::EMPTY,
97 read_only: None,
98 error: None,
99 measured: None,
100 }
101 }
102
103 #[must_use]
105 pub fn anchor(&self, name: &str) -> Option<Point> {
106 self.anchors.iter().find(|a| a.name == name).map(|a| a.point)
107 }
108
109 pub fn shapes(&self) -> impl Iterator<Item = Shape> + '_ {
111 self.drawables.iter().filter_map(|d| match d {
112 Drawable::Shape(s, _) => Some(s.clone()),
113 Drawable::Text(t, _) => Some(Shape::Text(t.clone())),
114 Drawable::Arrow { .. } => None,
115 })
116 }
117}
118
119fn bbox_of(ds: &[Drawable]) -> Aabb {
120 ds.iter().fold(Aabb::EMPTY, |b, d| match d {
121 Drawable::Shape(s, _) => b.union(s.bbox()),
122 Drawable::Text(t, _) => b.union(Shape::Text(t.clone()).bbox()),
123 Drawable::Arrow { tip, direction, size } => b.include(*tip).include(*tip - *direction * *size),
124 })
125}
126
127#[derive(Debug, Clone, Copy)]
129pub struct Ctx<'a> {
130 pub view: &'a dyn DocView,
132 pub registry: &'a Registry,
134}
135
136impl core::fmt::Debug for dyn DocView + '_ {
137 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138 f.write_str("DocView")
139 }
140}
141
142pub fn plugin_for<'r>(registry: &'r Registry, e: &Entity) -> Result<&'r CompiledType, ReadOnly> {
144 let t = e.type_id.to_string();
145 let Some(entry) = registry.get(&e.type_id) else {
146 return Err(ReadOnly::MissingPlugin { type_id: t });
147 };
148 if !entry.enabled {
149 return Err(ReadOnly::DisabledPlugin { type_id: t });
150 }
151 if e.type_version > entry.compiled.def.version {
152 return Err(ReadOnly::NewerVersion {
153 type_id: t,
154 found: e.type_version,
155 supported: entry.compiled.def.version,
156 });
157 }
158 Ok(&entry.compiled)
159}
160
161#[must_use]
163pub fn evaluate(ctx: Ctx<'_>, id: EntityId) -> Evaluated {
164 evaluate_depth(ctx, id, 0)
165}
166
167fn evaluate_depth(ctx: Ctx<'_>, id: EntityId, depth: usize) -> Evaluated {
168 let Some(e) = ctx.view.entity(id) else {
169 let mut ev = Evaluated::empty();
170 ev.error = Some(format!("{id} does not exist"));
171 return ev;
172 };
173 if depth > MAX_REF_DEPTH {
174 let mut ev = Evaluated::empty();
175 ev.error = Some("reference chain too deep or cyclic".into());
176 return ev;
177 }
178 if e.type_id.as_str() == types::DIMENSION {
179 return evaluate_dimension(ctx, e, depth);
180 }
181 if is_builtin(&e.type_id) {
182 return evaluate_builtin(e);
183 }
184 match plugin_for(ctx.registry, e) {
185 Ok(def) => evaluate_plugin(ctx, e, def, depth),
186 Err(ro) => {
187 let mut ev = Evaluated::empty();
188 ev.drawables =
189 e.fallback.iter().flatten().map(|s| Drawable::Shape(s.clone(), PrimStyle::default())).collect();
190 ev.bbox = bbox_of(&ev.drawables);
191 ev.read_only = Some(ro);
192 ev
193 }
194 }
195}
196
197fn evaluate_builtin(e: &Entity) -> Evaluated {
198 let mut ev = Evaluated::empty();
199 let Some(g) = &e.geometry else {
200 ev.error = Some("missing geometry".into());
201 return ev;
202 };
203 let t = e.transform;
204 match g.transform(t, TransformPolicy::Convert) {
205 Ok(Shape::Text(tx)) => ev.drawables.push(Drawable::Text(tx, PrimStyle::default())),
206 Ok(s) => ev.drawables.push(Drawable::Shape(s, PrimStyle::default())),
207 Err(err) => ev.error = Some(err.to_string()),
208 }
209 ev.anchors = g.anchors().into_iter().map(|a| Anchor { point: t.apply(a.point), ..a }).collect();
210 ev.bbox = bbox_of(&ev.drawables);
211 ev
212}
213
214fn world_anchor(ctx: Ctx<'_>, id: EntityId, name: &str, depth: usize) -> Option<Point> {
216 evaluate_depth(ctx, id, depth + 1).anchor(name)
217}
218
219fn num_prop(e: &Entity, def: Option<&CompiledType>, name: &str) -> Option<f64> {
220 match e.props.get(name) {
221 Some(PropValue::Number(v)) => Some(*v),
222 Some(_) => None,
223 None => def.and_then(|d| match d.def.props.get(name) {
224 Some(crate::registry::PropDef::Number { dim, default: Some(l), .. }) => l.value(dim.dim()).ok(),
225 _ => None,
226 }),
227 }
228}
229
230fn point_prop(e: &Entity, def: Option<&CompiledType>, name: &str) -> Option<Point> {
231 match e.props.get(name) {
232 Some(PropValue::Point(p)) => Some(*p),
233 Some(_) => None,
234 None => def.and_then(|d| match d.def.props.get(name) {
235 Some(crate::registry::PropDef::Point { default: Some([x, y]), .. }) => Some(Point::new(*x, *y)),
236 _ => None,
237 }),
238 }
239}
240
241#[must_use]
243pub fn param_value(ctx: Ctx<'_>, e: &Entity, name: &str) -> Option<f64> {
244 if is_builtin(&e.type_id) {
245 return e.geometry.as_ref().and_then(|g| get_geometry_param(g, name));
246 }
247 let def = ctx.registry.get(&e.type_id).map(|t| t.compiled.as_ref());
248 if let Some((base, axis)) = name.rsplit_once('.') {
249 let p = point_prop(e, def, base)?;
250 return match axis {
251 "x" => Some(p.x),
252 "y" => Some(p.y),
253 _ => None,
254 };
255 }
256 num_prop(e, def, name)
257}
258
259pub(crate) fn leaf_value(ctx: Ctx<'_>, e: &Entity, def: &CompiledType, leaf: &Leaf, depth: usize) -> Option<f64> {
261 match leaf {
262 Leaf::Prop(n) => num_prop(e, Some(def), n),
263 Leaf::PropPoint(n, a) => point_prop(e, Some(def), n).map(|p| if *a == Axis::X { p.x } else { p.y }),
264 Leaf::RefAnchor(prop, anchor, axis) => {
265 let target = match e.props.get(prop) {
266 Some(PropValue::Ref(r)) => r.entity,
267 _ => return None,
268 };
269 let w = world_anchor(ctx, target, anchor, depth)?;
270 let local = e.transform.inverse().ok()?.apply(w);
271 Some(if *axis == Axis::X { local.x } else { local.y })
272 }
273 Leaf::RefParam(prop, name) => {
274 let target = match e.props.get(prop) {
275 Some(PropValue::Ref(r)) => r.entity,
276 _ => return None,
277 };
278 let t = ctx.view.entity(target)?;
279 param_value(ctx, t, name)
280 }
281 Leaf::AxisScale => ctx.view.settings().time_axis.map(|a| a.mm_per_second),
282 Leaf::AxisOrigin => ctx.view.settings().time_axis.map(|a| a.origin_s),
283 }
284}
285
286fn eval_point(ctx: Ctx<'_>, e: &Entity, def: &CompiledType, c: &Compiled, depth: usize) -> Option<Point> {
287 let v = c.eval(&mut |l| leaf_value(ctx, e, def, l, depth))?;
288 Some(Point::new(*v.first()?, *v.get(1)?))
289}
290
291fn eval_scalar(ctx: Ctx<'_>, e: &Entity, def: &CompiledType, c: &Compiled, depth: usize) -> Option<f64> {
292 c.eval(&mut |l| leaf_value(ctx, e, def, l, depth))?.first().copied()
293}
294
295#[must_use]
297pub fn fill_template(template: &str, e: &Entity) -> String {
298 let mut out = String::with_capacity(template.len());
299 let mut rest = template;
300 while let Some(i) = rest.find('{') {
301 out.push_str(&rest[..i]);
302 let after = &rest[i + 1..];
303 match after.find('}') {
304 Some(j) => {
305 let key = &after[..j];
306 match e.props.get(key) {
307 Some(PropValue::Text(t)) => out.push_str(t),
308 Some(PropValue::Number(n)) => out.push_str(&format_number(*n, 1)),
309 Some(PropValue::Bool(b)) => out.push_str(if *b { "yes" } else { "no" }),
310 _ => {}
311 }
312 rest = &after[j + 1..];
313 }
314 None => {
315 out.push_str(&rest[i..]);
316 rest = "";
317 }
318 }
319 }
320 out.push_str(rest);
321 out
322}
323
324#[must_use]
326pub fn format_number(v: f64, decimals: usize) -> String {
327 let s = format!("{v:.decimals$}");
328 if s.contains('.') {
329 let t = s.trim_end_matches('0').trim_end_matches('.');
330 if t == "-0" { "0".into() } else { t.to_owned() }
331 } else {
332 s
333 }
334}
335
336fn evaluate_plugin(ctx: Ctx<'_>, e: &Entity, def: &CompiledType, depth: usize) -> Evaluated {
337 let mut ev = Evaluated::empty();
338 let t = e.transform;
339 for (name, kind, c) in &def.anchors {
340 match eval_point(ctx, e, def, c, depth) {
341 Some(p) => ev.anchors.push(Anchor { name: name.clone().into(), kind: *kind, point: t.apply(p) }),
342 None => ev.error = Some(format!("anchor `{name}` cannot be evaluated")),
343 }
344 }
345 for prim in &def.primitives {
346 let local: Option<Drawable> = match prim {
347 CompiledPrim::Line(a, b, st) => (|| {
348 Some(Drawable::Shape(
349 Shape::Line(Segment::new(eval_point(ctx, e, def, a, depth)?, eval_point(ctx, e, def, b, depth)?)),
350 st.clone(),
351 ))
352 })(),
353 CompiledPrim::Polyline(pts, closed, st) => {
354 pts.iter().map(|c| eval_point(ctx, e, def, c, depth)).collect::<Option<Vec<_>>>().map(|p| {
355 Drawable::Shape(
356 Shape::Polyline(Polyline { points: p, bulges: Vec::new(), closed: *closed }),
357 st.clone(),
358 )
359 })
360 }
361 CompiledPrim::Polygon(pts, st) => pts
362 .iter()
363 .map(|c| eval_point(ctx, e, def, c, depth))
364 .collect::<Option<Vec<_>>>()
365 .map(|p| Drawable::Shape(Shape::Polygon(Polygon { outer: p, holes: Vec::new() }), st.clone())),
366 CompiledPrim::Circle(c, r, st) => (|| {
367 let center = eval_point(ctx, e, def, c, depth)?;
368 let radius = eval_scalar(ctx, e, def, r, depth)?;
369 Circle::new(center, radius).ok().map(|c| Drawable::Shape(Shape::Circle(c), st.clone()))
370 })(),
371 CompiledPrim::Arc(c, r, s0, sw, st) => (|| {
372 let a = Arc::new(
373 eval_point(ctx, e, def, c, depth)?,
374 eval_scalar(ctx, e, def, r, depth)?,
375 eval_scalar(ctx, e, def, s0, depth)?,
376 eval_scalar(ctx, e, def, sw, depth)?,
377 )
378 .ok()?;
379 Some(Drawable::Shape(Shape::Arc(a), st.clone()))
380 })(),
381 CompiledPrim::Text { position, content, height, rotation, halign, valign, style } => (|| {
382 let pos = eval_point(ctx, e, def, position, depth)?;
383 let h = eval_scalar(ctx, e, def, height, depth)?;
384 let rot = match rotation {
385 Some(r) => eval_scalar(ctx, e, def, r, depth)?,
386 None => 0.0,
387 };
388 (h > 0.0).then(|| {
389 Drawable::Text(
390 Text {
391 position: pos,
392 content: fill_template(content, e),
393 height: h,
394 rotation: rot,
395 halign: *halign,
396 valign: *valign,
397 },
398 style.clone(),
399 )
400 })
401 })(),
402 };
403 let Some(d) = local else {
404 ev.error = Some("a primitive cannot be evaluated".into());
405 continue;
406 };
407 let world = match d {
408 Drawable::Shape(s, st) => match s.transform(t, TransformPolicy::Convert) {
409 Ok(w) => Some(Drawable::Shape(w, st)),
410 Err(_) => None,
411 },
412 Drawable::Text(tx, st) => match Shape::Text(tx).transform(t, TransformPolicy::Strict) {
413 Ok(Shape::Text(w)) => Some(Drawable::Text(w, st)),
414 _ => None,
415 },
416 other => Some(other),
417 };
418 if let Some(w) = world {
419 ev.drawables.push(w);
420 }
421 }
422 ev.bbox = bbox_of(&ev.drawables);
423 if ev.bbox.is_empty() {
424 ev.bbox = Aabb::from_points(ev.anchors.iter().map(|a| a.point));
425 }
426 ev
427}
428
429fn anchor_prop(ctx: Ctx<'_>, e: &Entity, key: &str, depth: usize) -> Option<Point> {
430 match e.props.get(key) {
431 Some(PropValue::Anchor(a)) => world_anchor(ctx, a.entity, &a.anchor, depth),
432 Some(PropValue::Point(p)) => Some(e.transform.apply(*p)),
433 _ => None,
434 }
435}
436
437fn text_prop<'e>(e: &'e Entity, key: &str) -> Option<&'e str> {
438 match e.props.get(key) {
439 Some(PropValue::Text(t)) => Some(t),
440 _ => None,
441 }
442}
443
444#[must_use]
446pub fn format_measure(value: f64, angular: bool, unit: LengthUnit, decimals: usize) -> String {
447 if angular {
448 format!("{}°", format_number(value.to_degrees(), decimals))
449 } else {
450 format!("{} {}", format_number(unit.from_mm(value), decimals), unit.symbol())
451 }
452}
453
454fn evaluate_dimension(ctx: Ctx<'_>, e: &Entity, depth: usize) -> Evaluated {
461 let mut ev = Evaluated::empty();
462 let num = |k: &str| e.props.get(k).and_then(PropValue::as_number);
463 let text_h = num("textHeight").filter(|v| *v > 0.0).unwrap_or(100.0);
464 let style = DimensionStyle {
465 extension_gap: text_h * 0.3,
466 extension_overshoot: text_h * 0.5,
467 arrow_size: text_h * 0.8,
468 text_height: text_h,
469 };
470 let decimals = num("precision").map_or(0, |p| p.clamp(0.0, 6.0) as usize);
471 let unit = ctx.view.settings().display_unit;
472 let kind = text_prop(e, "kind").unwrap_or("linear");
473 let geom = match kind {
474 "linear" => (|| {
475 let a = anchor_prop(ctx, e, "a", depth)?;
476 let b = anchor_prop(ctx, e, "b", depth)?;
477 let through = match e.props.get("through") {
478 Some(PropValue::Point(p)) => e.transform.apply(*p),
479 _ => a.midpoint(b),
480 };
481 let orient = match text_prop(e, "orientation") {
482 Some("horizontal") => LinearKind::Horizontal,
483 Some("vertical") => LinearKind::Vertical,
484 _ => LinearKind::Aligned,
485 };
486 dimension::linear(a, b, through, orient, style).ok()
487 })(),
488 "radial" => (|| {
489 let target = match e.props.get("circle") {
490 Some(PropValue::Ref(r)) => r.entity,
491 _ => return None,
492 };
493 let te = evaluate_depth(ctx, target, depth + 1);
494 let (center, radius) = te.drawables.iter().find_map(|d| match d {
495 Drawable::Shape(Shape::Circle(c), _) => Some((c.center, c.radius)),
496 Drawable::Shape(Shape::Arc(a), _) => Some((a.center, a.radius)),
497 _ => None,
498 })?;
499 dimension::radial(center, radius, num("angle").unwrap_or(core::f64::consts::FRAC_PI_4), style).ok()
500 })(),
501 "angular" => (|| {
502 let line_pts = |k: &str| -> Option<(Point, Point)> {
503 let id = match e.props.get(k) {
504 Some(PropValue::Ref(r)) => r.entity,
505 _ => return None,
506 };
507 let te = evaluate_depth(ctx, id, depth + 1);
508 Some((te.anchor("start")?, te.anchor("end")?))
509 };
510 let (a0, a1) = line_pts("line1")?;
511 let (b0, b1) = line_pts("line2")?;
512 let hit = dotloom_geometry::intersect::intersect(
513 &dotloom_geometry::Curve::Line(Segment::new(a0 - (a1 - a0) * 1e6, a1 + (a1 - a0) * 1e6)),
514 &dotloom_geometry::Curve::Line(Segment::new(b0 - (b1 - b0) * 1e6, b1 + (b1 - b0) * 1e6)),
515 dotloom_geometry::ModelTolerance::DEFAULT,
516 );
517 let vertex = hit.points.first()?.point;
518 let far = |p: Point, q: Point| if vertex.distance(p) > vertex.distance(q) { p } else { q };
519 dimension::angular(vertex, far(a0, a1), far(b0, b1), num("radius").unwrap_or(text_h * 10.0), style).ok()
520 })(),
521 _ => None,
522 };
523 let Some(g) = geom else {
524 ev.error = Some("dimension references cannot be resolved".into());
525 return ev;
526 };
527 let style = PrimStyle::default();
528 for l in &g.lines {
529 ev.drawables.push(Drawable::Shape(Shape::Line(*l), style.clone()));
530 }
531 if let Some(a) = g.arc {
532 ev.drawables.push(Drawable::Shape(Shape::Arc(a), style.clone()));
533 }
534 for a in &g.arrows {
535 ev.drawables.push(Drawable::Arrow { tip: a.tip, direction: a.direction, size: text_h * 0.8 });
536 }
537 let label = format_measure(g.value, kind == "angular", unit, decimals);
538 let prefix = text_prop(e, "prefix").unwrap_or(if kind == "radial" { "R " } else { "" });
539 ev.drawables.push(Drawable::Text(
540 Text {
541 position: g.text_position,
542 content: format!("{prefix}{label}"),
543 height: text_h,
544 rotation: g.text_rotation,
545 halign: HAlign::Center,
546 valign: VAlign::Middle,
547 },
548 style,
549 ));
550 ev.anchors.push(Anchor { name: "text".into(), kind: AnchorKind::Insert, point: g.text_position });
551 ev.measured = Some(g.value);
552 ev.bbox = bbox_of(&ev.drawables);
553 ev
554}
555
556#[derive(Debug, Clone, Default)]
558pub struct EvalCache {
559 map: BTreeMap<EntityId, Evaluated>,
560}
561
562impl EvalCache {
563 pub fn get(&mut self, ctx: Ctx<'_>, id: EntityId) -> &Evaluated {
565 self.map.entry(id).or_insert_with(|| {
566 let mut ev = evaluate(ctx, id);
570 ev.anchors.shrink_to_fit();
571 ev.drawables.shrink_to_fit();
572 ev
573 })
574 }
575
576 #[must_use]
578 pub fn peek(&self, id: EntityId) -> Option<&Evaluated> {
579 self.map.get(&id)
580 }
581
582 pub fn invalidate(&mut self, ids: impl IntoIterator<Item = EntityId>) {
584 for id in ids {
585 self.map.remove(&id);
586 }
587 }
588
589 pub fn clear(&mut self) {
591 self.map.clear();
592 }
593}