1use std::collections::{HashMap, HashSet};
15use std::f64::consts::{FRAC_1_SQRT_2, PI};
16
17use crate::ast::*;
18use crate::diagnostic::{Diagnostic, Span};
19use crate::geom::{Bbox, Point};
20use crate::ir::*;
21use crate::token::{self, Corner, Dir, EnvVar, Func1, Func2, LineType, Prim};
22
23#[derive(Debug, Clone, PartialEq)]
28pub struct EvalError {
29 pub msg: String,
30 pub info: Option<Box<Diagnostic>>,
31}
32
33impl std::fmt::Display for EvalError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "{}", self.msg)
36 }
37}
38
39type ER<T> = Result<T, EvalError>;
40
41use crate::ir::{DP_TEXT_RATIO, TEXT_EM_IN as TEXT_EM};
45const FONT_PT_MATH: f64 = crate::ir::FONT_PT_CLASSIC;
47const TEXT_CHAR_W: f64 = crate::ir::TEXT_CHAR_W_RATIO * TEXT_EM;
48const TEXT_LINE_H: f64 = crate::ir::TEXT_LINE_H_RATIO * TEXT_EM;
49const TEXT_XHEIGHT: f64 = DP_TEXT_RATIO * TEXT_EM;
50const DEFAULT_BRACE_DEPTH: f64 = 0.18;
51const DEFAULT_BRACE_POS: f64 = 0.5;
52const DEFAULT_HATCH_ANGLE: f64 = 45.0;
53const DEFAULT_HATCH_SEP: f64 = 0.08;
54const DEFAULT_HATCH_WIDTH: f64 = 0.8;
55
56fn err<T>(msg: impl Into<String>) -> ER<T> {
57 Err(EvalError {
58 msg: msg.into(),
59 info: None,
60 })
61}
62
63fn err_diag<T>(d: Diagnostic) -> ER<T> {
65 Err(diag_error(d))
66}
67
68fn diag_error(d: Diagnostic) -> EvalError {
69 EvalError {
70 msg: d.message.clone(),
71 info: Some(Box::new(d)),
72 }
73}
74
75fn parse_eval_error(e: crate::parser::ParseError) -> EvalError {
79 EvalError {
80 msg: e.to_string(),
81 info: Some(Box::new(e.diagnostic())),
82 }
83}
84
85fn unknown_label_error(name: &str, span: Option<&Span>) -> EvalError {
86 let mut d = Diagnostic::new("unknown_label", format!("unknown label `{name}`")).found(name);
87 if let Some(s) = span {
88 d = d.at(s.clone());
89 }
90 diag_error(d)
91}
92
93fn ordinal_diagnostic(n: i64, available: usize, span: Option<&Span>) -> Diagnostic {
94 let mut d = Diagnostic::new(
95 "ordinal_out_of_range",
96 format!("ordinal {n} out of range (available {available})"),
97 )
98 .found(n.to_string())
99 .expected(format!("1..{available}"));
100 if let Some(s) = span {
101 d = d.at(s.clone());
102 }
103 d
104}
105
106fn finite(v: f64, context: &str) -> ER<f64> {
107 if v.is_finite() {
108 Ok(v)
109 } else {
110 err(format!("{context} produced non-finite numeric value"))
111 }
112}
113
114pub fn eval(pic: &Picture) -> ER<Drawing> {
116 eval_with_limits(pic, EvalLimits::default())
117}
118
119pub fn eval_with_limits(pic: &Picture, limits: EvalLimits) -> ER<Drawing> {
121 let limits = limits.validate()?;
122 let mut st = State::with_limits(limits);
123 st.macros = pic.macros.clone();
124 st.includes = pic.includes.clone();
125 st.eval_stmts(&pic.stmts)?;
126 let want_w = match &pic.width {
127 Some(e) => Some(st.eval_expr(e)?),
128 None => None,
129 };
130 let want_h = match &pic.height {
131 Some(e) => Some(st.eval_expr(e)?),
132 None => None,
133 };
134 let (maxw, maxh) = (st.env.get(EnvVar::Maxpswid), st.env.get(EnvVar::Maxpsht));
135 let canvas_margin = st.canvas_margin()?;
136 let mut d = Drawing {
137 shapes: st.shapes,
138 shape_layers: st.shape_layers,
139 shape_classes: st.shape_classes,
140 shape_links: st.shape_links,
141 shape_spans: st.shape_spans,
142 bbox: st.bbox,
143 prelude_thick: st.env.get(EnvVar::Linethick),
144 canvas_margin,
145 canvas: st.canvas,
146 anims: st.anims,
147 interactions: st.interactions,
148 anim_scroll: st.anim_scroll,
149 diagnostics: st.diagnostics,
150 warnings: st.warnings,
151 };
152 apply_ps_size(&mut d, want_w, want_h);
153 clamp_to_maxps(&mut d, maxw, maxh);
154 Ok(d)
155}
156
157fn clamp_to_maxps(d: &mut Drawing, maxw: f64, maxh: f64) {
161 for _ in 0..4 {
162 if d.bbox.is_empty() {
163 return;
164 }
165 let (w, h) = (canvas_width(d), canvas_height(d));
166 let mut factor = 1.0_f64;
167 if maxw > 0.0 && w > maxw {
168 factor = factor.min(maxw / w);
169 }
170 if maxh > 0.0 && h > maxh {
171 factor = factor.min(maxh / h);
172 }
173 if factor >= 1.0 - 1e-9 {
174 return;
175 }
176 for sh in &mut d.shapes {
177 scale_shape(sh, factor);
178 }
179 scale_canvas(d, factor);
180 d.prelude_thick *= factor;
181 d.canvas_margin.scale_by(factor);
182 d.bbox = drawing_painted_bbox(&d.shapes);
183 }
184}
185
186fn scale_canvas(d: &mut Drawing, factor: f64) {
187 if let Some(c) = d.canvas {
188 let mut bb = Bbox::new();
189 bb.add(Point::new(c.min.x * factor, c.min.y * factor));
190 bb.add(Point::new(c.max.x * factor, c.max.y * factor));
191 d.canvas = Some(bb);
192 }
193}
194
195fn apply_ps_size(d: &mut Drawing, want_w: Option<f64>, want_h: Option<f64>) {
199 if d.bbox.is_empty() {
200 return;
201 }
202 let factor = match (want_w, want_h) {
203 (Some(w), _) if w > 0.0 && d.bbox.width() > 0.0 => w / d.bbox.width(),
204 (None, Some(h)) if h > 0.0 && d.bbox.height() > 0.0 => h / d.bbox.height(),
205 _ => return,
206 };
207 if (factor - 1.0).abs() < 1e-9 {
208 return;
209 }
210 for sh in &mut d.shapes {
211 scale_shape(sh, factor);
212 }
213 scale_canvas(d, factor);
214 d.prelude_thick *= factor;
215 d.canvas_margin.scale_by(factor);
216 d.bbox = drawing_painted_bbox(&d.shapes);
217}
218
219fn canvas_width(d: &Drawing) -> f64 {
220 let raw = d.canvas.map_or_else(|| d.bbox.width(), |c| c.width());
221 raw + d.canvas_margin.horizontal()
222}
223
224fn canvas_height(d: &Drawing) -> f64 {
225 let raw = d.canvas.map_or_else(|| d.bbox.height(), |c| c.height());
226 raw + d.canvas_margin.vertical()
227}
228
229const SCALED_VARS: [EnvVar; 23] = [
231 EnvVar::Arcrad,
232 EnvVar::Arrowht,
233 EnvVar::Arrowwid,
234 EnvVar::Boxht,
235 EnvVar::Boxrad,
236 EnvVar::Boxwid,
237 EnvVar::Circlerad,
238 EnvVar::Dashwid,
239 EnvVar::Ellipseht,
240 EnvVar::Ellipsewid,
241 EnvVar::Lineht,
242 EnvVar::Linewid,
243 EnvVar::Moveht,
244 EnvVar::Movewid,
245 EnvVar::Textht,
246 EnvVar::Textwid,
247 EnvVar::Textoffset,
248 EnvVar::Margin,
249 EnvVar::Topmargin,
250 EnvVar::Rightmargin,
251 EnvVar::Bottommargin,
252 EnvVar::Leftmargin,
253 EnvVar::Dotrad,
254];
255
256#[derive(Debug, Clone, Copy, PartialEq)]
259pub struct EvalLimits {
260 pub max_animation_seconds: f64,
261 pub max_animation_repeat: i64,
262 pub max_loop_iterations: u64,
263 pub max_shapes: usize,
264}
265
266pub const DEFAULT_MAX_ANIMATION_SECONDS: f64 = 1_000_000.0;
267pub const DEFAULT_MAX_ANIMATION_REPEAT: i64 = 1_000_000;
268pub const DEFAULT_MAX_LOOP_ITERATIONS: u64 = 1_000_000;
269pub const DEFAULT_MAX_SHAPES: usize = 1_000_000;
270
271impl Default for EvalLimits {
272 fn default() -> Self {
273 Self {
274 max_animation_seconds: DEFAULT_MAX_ANIMATION_SECONDS,
275 max_animation_repeat: DEFAULT_MAX_ANIMATION_REPEAT,
276 max_loop_iterations: DEFAULT_MAX_LOOP_ITERATIONS,
277 max_shapes: DEFAULT_MAX_SHAPES,
278 }
279 }
280}
281
282impl EvalLimits {
283 fn validate(self) -> ER<Self> {
284 if !self.max_animation_seconds.is_finite() {
285 return err("max_animation_seconds option must be finite");
286 }
287 if self.max_animation_seconds < 0.0 {
288 return err("max_animation_seconds option must be non-negative");
289 }
290 if self.max_animation_repeat < 0 {
291 return err("max_animation_repeat option must be non-negative");
292 }
293 Ok(self)
294 }
295}
296
297#[derive(Clone)]
298struct EnvVars {
299 v: HashMap<u8, f64>,
300}
301
302fn ev_key(e: EnvVar) -> u8 {
303 e as u8
304}
305
306impl EnvVars {
307 fn new(limits: EvalLimits) -> Self {
308 use EnvVar::*;
309 let defaults = [
310 (Arcrad, 0.25),
311 (Arrowht, 0.1),
312 (Arrowwid, 0.05),
313 (Boxht, 0.5),
314 (Boxrad, 0.0),
315 (Boxwid, 0.75),
316 (Circlerad, 0.25),
317 (Dashwid, 0.05),
318 (Ellipseht, 0.5),
319 (Ellipsewid, 0.75),
320 (Lineht, 0.5),
321 (Linewid, 0.5),
322 (Moveht, 0.5),
323 (Movewid, 0.5),
324 (Textht, (11.0 / 72.0) * DP_TEXT_RATIO),
325 (Textoffset, 2.0 / 72.0),
326 (Textwid, 0.0),
327 (Arrowhead, 1.0),
328 (Fillval, 0.5),
329 (Linethick, 0.8),
330 (Maxpsht, 11.0),
331 (Maxpswid, 8.5),
332 (Scale, 1.0),
333 (Margin, 0.0),
334 (Topmargin, 0.0),
335 (Rightmargin, 0.0),
336 (Bottommargin, 0.0),
337 (Leftmargin, 0.0),
338 (Texlabels, 0.0),
339 (Dotrad, 0.035),
340 (Maxanimrepeat, limits.max_animation_repeat as f64),
341 (Maxanimseconds, limits.max_animation_seconds),
342 ];
343 let mut v = HashMap::new();
344 for (e, d) in defaults {
345 v.insert(ev_key(e), d);
346 }
347 EnvVars { v }
348 }
349 fn get(&self, e: EnvVar) -> f64 {
350 *self.v.get(&ev_key(e)).unwrap_or(&0.0)
351 }
352 fn set(&mut self, e: EnvVar, val: f64) {
353 self.v.insert(ev_key(e), val);
354 }
355}
356
357#[derive(Clone, Copy, PartialEq, Eq)]
360enum PKind {
361 Box,
362 Circle,
363 Ellipse,
364 Line,
365 Move,
366 Spline,
367 Arc,
368 Brace,
369 Block,
370 Text,
371}
372
373#[derive(Clone)]
374struct Placed {
375 kind: PKind,
376 center: Point,
377 bbox: Bbox,
378 start: Point,
379 end: Point,
380 thick: f64,
381 points: Vec<Point>,
382 radius: f64,
383 box_rad: f64,
384 line_wid: f64,
385 line_ht: f64,
386 closed_path: bool,
387 layer: i32,
388 shape: Option<usize>,
390 members: HashMap<String, Placed>,
393 block_shapes: Option<(usize, usize)>,
397}
398
399impl Placed {
400 fn corner(&self, c: Corner) -> Point {
401 match self.kind {
402 PKind::Circle | PKind::Ellipse => self.ellipse_corner(c),
403 PKind::Line | PKind::Move | PKind::Spline => self.linear_corner(c),
404 PKind::Brace => self.brace_corner(c),
405 PKind::Arc => self.arc_corner(c),
406 PKind::Box => self.box_corner(c),
407 PKind::Block | PKind::Text => self.bbox_corner(c),
408 }
409 }
410
411 fn bbox_corner(&self, c: Corner) -> Point {
412 let (lo, hi) = (self.bbox.min, self.bbox.max);
413 let mid = self.center;
414 match c {
415 Corner::N => Point::new(mid.x, hi.y),
416 Corner::S => Point::new(mid.x, lo.y),
417 Corner::E => Point::new(hi.x, mid.y),
418 Corner::W => Point::new(lo.x, mid.y),
419 Corner::Ne => Point::new(hi.x, hi.y),
420 Corner::Se => Point::new(hi.x, lo.y),
421 Corner::Nw => Point::new(lo.x, hi.y),
422 Corner::Sw => Point::new(lo.x, lo.y),
423 Corner::Center => mid,
424 Corner::Start => self.start,
425 Corner::End => self.end,
426 }
427 }
428
429 fn ellipse_corner(&self, c: Corner) -> Point {
430 let (rx, ry) = (self.bbox.width() / 2.0, self.bbox.height() / 2.0);
431 let diag = |sx: f64, sy: f64| Point::new(sx * rx * FRAC_1_SQRT_2, sy * ry * FRAC_1_SQRT_2);
432 let off = match c {
433 Corner::N => Point::new(0.0, ry),
434 Corner::S => Point::new(0.0, -ry),
435 Corner::E => Point::new(rx, 0.0),
436 Corner::W => Point::new(-rx, 0.0),
437 Corner::Ne => diag(1.0, 1.0),
438 Corner::Se => diag(1.0, -1.0),
439 Corner::Nw => diag(-1.0, 1.0),
440 Corner::Sw => diag(-1.0, -1.0),
441 Corner::Center => Point::ZERO,
442 Corner::Start => return self.start,
443 Corner::End => return self.end,
444 };
445 self.center + off
446 }
447
448 fn linear_corner(&self, c: Corner) -> Point {
449 if self.points.is_empty() {
450 return self.bbox_corner(c);
451 }
452 if self.closed_path {
453 return match c {
454 Corner::Start => self.points[0],
455 Corner::End => *self.points.last().unwrap(),
456 _ => self.bbox_corner(c),
457 };
458 }
459 match c {
460 Corner::Center => (self.points[0] + *self.points.last().unwrap()) * 0.5,
461 Corner::Start => self.points[0],
462 Corner::End => *self.points.last().unwrap(),
463 _ => {
464 let mut best = self.points[0];
465 for p in self.points.iter().skip(1) {
466 let better = match c {
467 Corner::N => p.y > best.y,
468 Corner::S => p.y < best.y,
469 Corner::E => p.x > best.x,
470 Corner::W => p.x < best.x,
471 Corner::Ne => {
472 (p.y > best.y && p.x >= best.x) || (p.y >= best.y && p.x > best.x)
473 }
474 Corner::Se => {
475 (p.y < best.y && p.x >= best.x) || (p.y <= best.y && p.x > best.x)
476 }
477 Corner::Sw => {
478 (p.y < best.y && p.x <= best.x) || (p.y <= best.y && p.x < best.x)
479 }
480 Corner::Nw => {
481 (p.y > best.y && p.x <= best.x) || (p.y >= best.y && p.x < best.x)
482 }
483 Corner::Center | Corner::Start | Corner::End => false,
484 };
485 if better {
486 best = *p;
487 }
488 }
489 best
490 }
491 }
492 }
493
494 fn brace_corner(&self, c: Corner) -> Point {
495 match c {
496 Corner::Center => self.center,
497 Corner::Start => self.start,
498 Corner::End => self.end,
499 _ => {
500 let mut bb = Bbox::new();
501 for p in &self.points {
502 bb.add(*p);
503 }
504 if bb.is_empty() {
505 return self.bbox_corner(c);
506 }
507 bbox_point(&bb, c)
508 }
509 }
510 }
511
512 fn arc_corner(&self, c: Corner) -> Point {
513 let diag = self.radius * FRAC_1_SQRT_2;
514 let off = match c {
515 Corner::N => Point::new(0.0, self.radius),
516 Corner::S => Point::new(0.0, -self.radius),
517 Corner::E => Point::new(self.radius, 0.0),
518 Corner::W => Point::new(-self.radius, 0.0),
519 Corner::Ne => Point::new(diag, diag),
520 Corner::Se => Point::new(diag, -diag),
521 Corner::Nw => Point::new(-diag, diag),
522 Corner::Sw => Point::new(-diag, -diag),
523 Corner::Center => Point::ZERO,
524 Corner::Start => return self.start,
525 Corner::End => return self.end,
526 };
527 self.center + off
528 }
529
530 fn box_corner(&self, c: Corner) -> Point {
531 match c {
532 Corner::Ne | Corner::Se | Corner::Nw | Corner::Sw if self.box_rad > 0.0 => {
533 let inset = self
534 .box_rad
535 .min(self.bbox.width().abs().min(self.bbox.height().abs()) / 2.0)
536 * (1.0 - FRAC_1_SQRT_2);
537 let x = self.bbox.width() / 2.0 - inset;
538 let y = self.bbox.height() / 2.0 - inset;
539 let off = match c {
540 Corner::Ne => Point::new(x, y),
541 Corner::Se => Point::new(x, -y),
542 Corner::Nw => Point::new(-x, y),
543 Corner::Sw => Point::new(-x, -y),
544 _ => Point::ZERO,
545 };
546 self.center + off
547 }
548 _ => self.bbox_corner(c),
549 }
550 }
551
552 fn attr_width(&self) -> f64 {
553 match self.kind {
554 PKind::Brace => self.bbox.width(),
555 PKind::Line | PKind::Move | PKind::Spline => self.line_wid,
556 _ => self.bbox.width(),
557 }
558 }
559
560 fn attr_height(&self) -> f64 {
561 match self.kind {
562 PKind::Brace => self.bbox.height(),
563 PKind::Line | PKind::Move | PKind::Spline => self.line_ht,
564 _ => self.bbox.height(),
565 }
566 }
567
568 fn attr_radius(&self) -> f64 {
569 match self.kind {
570 PKind::Box => self.box_rad,
571 PKind::Circle => self.bbox.width() / 2.0,
572 PKind::Arc => self.radius,
573 _ => 0.0,
574 }
575 }
576
577 fn attr_diameter(&self) -> f64 {
578 match self.kind {
579 PKind::Circle => self.bbox.width(),
580 PKind::Arc => self.radius * 2.0,
581 _ => 0.0,
582 }
583 }
584
585 fn attr_length(&self) -> f64 {
586 match self.kind {
587 PKind::Line | PKind::Move | PKind::Spline | PKind::Brace => self.start.dist(self.end),
588 _ => 0.0,
589 }
590 }
591}
592
593fn bbox_point(bb: &Bbox, c: Corner) -> Point {
594 let lo = bb.min;
595 let hi = bb.max;
596 let mid = (lo + hi) * 0.5;
597 match c {
598 Corner::N => Point::new(mid.x, hi.y),
599 Corner::S => Point::new(mid.x, lo.y),
600 Corner::E => Point::new(hi.x, mid.y),
601 Corner::W => Point::new(lo.x, mid.y),
602 Corner::Ne => Point::new(hi.x, hi.y),
603 Corner::Se => Point::new(hi.x, lo.y),
604 Corner::Nw => Point::new(lo.x, hi.y),
605 Corner::Sw => Point::new(lo.x, lo.y),
606 Corner::Center => mid,
607 Corner::Start => lo,
608 Corner::End => hi,
609 }
610}
611
612struct State {
615 pos: Point,
616 dir: Dir,
617 vars: HashMap<String, f64>,
618 inherited_vars: HashSet<String>,
619 export_vars: HashSet<String>,
620 env: EnvVars,
621 macros: Macros,
622 includes: IncludeCtx,
623 outer_labels: HashMap<String, Placed>,
627 shapes: Vec<Shape>,
628 shape_layers: Vec<i32>,
629 shape_classes: Vec<Option<String>>,
630 shape_links: Vec<Option<String>>,
631 shape_spans: Vec<Option<Span>>,
632 current_span: Option<Span>,
635 canvas: Option<Bbox>,
637 placed: Vec<Placed>,
638 labels: HashMap<String, usize>,
639 bbox: Bbox,
641 layout_bbox: Bbox,
644 anims: Vec<Anim>,
646 interactions: Vec<Interaction>,
647 anim_scroll: bool,
648 diagnostics: Vec<String>,
649 warnings: Vec<Diagnostic>,
650 anim_cursor: f64,
651 anim_end: HashMap<usize, f64>,
652 limits: EvalLimits,
653 rng: GlibcRand,
654}
655
656const DEFAULT_ANIM_DUR: f64 = 0.6;
657
658fn checked_anim_time(name: &str, value: f64, max: f64) -> ER<f64> {
659 if !value.is_finite() {
660 return err(format!("animation {name} must be finite"));
661 }
662 if value < 0.0 {
663 return err(format!("animation {name} must be non-negative"));
664 }
665 if value > max {
666 return err(format!("animation {name} must be at most {max} seconds"));
667 }
668 Ok(value)
669}
670
671fn checked_anim_seconds_limit(value: f64, host_max: f64) -> ER<f64> {
672 if !value.is_finite() {
673 return err("maxanimseconds must be finite");
674 }
675 if value < 0.0 {
676 return err("maxanimseconds must be non-negative");
677 }
678 if value > host_max {
679 return err(format!("maxanimseconds must be at most {host_max}"));
680 }
681 Ok(value)
682}
683
684fn checked_anim_repeat(value: f64, max: i64) -> ER<i64> {
685 if !value.is_finite() {
686 return err("animation repeat must be finite");
687 }
688 if value < 0.0 && (value + 1.0).abs() > f64::EPSILON {
689 return err("animation repeat must be -1 or non-negative");
690 }
691
692 let repeat = value.round();
693 if repeat > max as f64 {
694 return err(format!("animation repeat must be at most {max}"));
695 }
696 Ok(repeat as i64)
697}
698
699fn checked_anim_repeat_limit(value: f64, host_max: i64) -> ER<i64> {
700 if !value.is_finite() {
701 return err("maxanimrepeat must be finite");
702 }
703 if value < 0.0 {
704 return err("maxanimrepeat must be non-negative");
705 }
706
707 let max = value.round();
708 if max > host_max as f64 {
709 return err(format!("maxanimrepeat must be at most {host_max}"));
710 }
711 Ok(max as i64)
712}
713
714fn additive_loop_iterations(from: f64, to: f64, by: f64) -> Option<u64> {
715 const EPS: f64 = 1e-9;
716
717 if !from.is_finite() || !to.is_finite() || !by.is_finite() {
718 return None;
719 }
720
721 let cont = if by >= 0.0 {
722 from <= to + EPS
723 } else {
724 from >= to - EPS
725 };
726 if !cont {
727 return Some(0);
728 }
729 if by.abs() < f64::EPSILON {
730 return Some(1);
731 }
732
733 let span = if by > 0.0 {
734 to + EPS - from
735 } else {
736 from - (to - EPS)
737 };
738 let step = by.abs();
739 let iterations = (span / step).floor() + 1.0;
740 if !iterations.is_finite() || iterations >= u64::MAX as f64 {
741 return Some(u64::MAX);
742 }
743
744 Some(iterations.max(0.0) as u64)
745}
746
747fn dir_unit(d: Dir) -> Point {
748 match d {
749 Dir::Right => Point::new(1.0, 0.0),
750 Dir::Left => Point::new(-1.0, 0.0),
751 Dir::Up => Point::new(0.0, 1.0),
752 Dir::Down => Point::new(0.0, -1.0),
753 }
754}
755fn horizontal(d: Dir) -> bool {
756 matches!(d, Dir::Right | Dir::Left)
757}
758
759impl State {
760 #[cfg(test)]
761 fn new() -> Self {
762 Self::with_limits(EvalLimits::default())
763 }
764
765 fn with_limits(limits: EvalLimits) -> Self {
766 let mut vars = HashMap::new();
767 install_dpic_compat_vars(&mut vars);
768 State {
769 pos: Point::ZERO,
770 dir: Dir::Right,
771 vars,
772 inherited_vars: HashSet::new(),
773 export_vars: HashSet::new(),
774 env: EnvVars::new(limits),
775 macros: HashMap::new(),
776 includes: IncludeCtx::default(),
777 outer_labels: HashMap::new(),
778 shapes: Vec::new(),
779 shape_layers: Vec::new(),
780 shape_classes: Vec::new(),
781 shape_links: Vec::new(),
782 shape_spans: Vec::new(),
783 current_span: None,
784 canvas: None,
785 placed: Vec::new(),
786 labels: HashMap::new(),
787 bbox: Bbox::new(),
788 layout_bbox: Bbox::new(),
789 anims: Vec::new(),
790 interactions: Vec::new(),
791 anim_scroll: false,
792 diagnostics: Vec::new(),
793 warnings: Vec::new(),
794 anim_cursor: 0.0,
795 anim_end: HashMap::new(),
796 limits,
797 rng: GlibcRand::new(1),
798 }
799 }
800
801 fn eval_stmts(&mut self, stmts: &[Stmt]) -> ER<()> {
802 for s in stmts {
803 self.eval_stmt(s)?;
804 }
805 Ok(())
806 }
807
808 fn checked_env_value(&self, e: EnvVar, value: f64) -> ER<f64> {
809 match e {
810 EnvVar::Maxanimrepeat => {
811 Ok(checked_anim_repeat_limit(value, self.limits.max_animation_repeat)? as f64)
812 }
813 EnvVar::Maxanimseconds => {
814 checked_anim_seconds_limit(value, self.limits.max_animation_seconds)
815 }
816 _ => Ok(value),
817 }
818 }
819
820 fn max_anim_seconds(&self) -> ER<f64> {
821 checked_anim_seconds_limit(
822 self.env.get(EnvVar::Maxanimseconds),
823 self.limits.max_animation_seconds,
824 )
825 }
826
827 fn max_anim_repeat(&self) -> ER<i64> {
828 checked_anim_repeat_limit(
829 self.env.get(EnvVar::Maxanimrepeat),
830 self.limits.max_animation_repeat,
831 )
832 }
833
834 fn parse_body(&mut self, body: &Body) -> ER<Vec<Stmt>> {
836 crate::parser::parse_body_tokens(body, &mut self.macros, &self.includes)
837 .map_err(parse_eval_error)
838 }
839
840 fn eval_stmt(&mut self, s: &Stmt) -> ER<()> {
841 match s {
842 Stmt::Direction(d) => {
843 self.dir = *d;
844 }
845 Stmt::Assign(list) => {
846 for a in list {
847 self.eval_assignment(a)?;
848 }
849 }
850 Stmt::Place { label, pos } => {
851 let p = self.eval_pos(pos)?;
852 let key = self.label_key(label)?;
853 let mut bb = Bbox::new();
854 bb.add(p);
855 let idx = self.placed.len();
856 self.placed.push(Placed {
857 kind: PKind::Text,
858 center: p,
859 bbox: bb,
860 start: p,
861 end: p,
862 thick: 0.0,
863 points: Vec::new(),
864 radius: 0.0,
865 box_rad: 0.0,
866 line_wid: 0.0,
867 line_ht: 0.0,
868 closed_path: false,
869 layer: 0,
870 shape: None,
871 members: HashMap::new(),
872 block_shapes: None,
873 });
874 self.labels.insert(key, idx);
875 }
876 Stmt::Group(stmts) => {
877 let (pos, dir) = (self.pos, self.dir);
878 self.eval_stmts(stmts)?;
879 self.pos = pos;
880 self.dir = dir;
881 }
882 Stmt::Object { label, object } => {
883 let idx = self.eval_object(object)?;
884 if let Some(l) = label {
885 let key = self.label_key(l)?;
886 self.labels.insert(key, idx);
887 }
888 }
889 Stmt::Animate(a) => self.eval_animate(a)?,
890 Stmt::AnimateScroll => self.anim_scroll = true,
891 Stmt::Draggable(d) => self.eval_draggable(d)?,
892 Stmt::Class { target, class } => {
893 let idx = self.place_index(target)?;
894 let name = self.eval_stringexpr(class)?;
895 self.append_class_at(idx, &name)?;
896 }
897 Stmt::Link { target, url } => {
898 let idx = self.place_index(target)?;
899 let url = self.eval_stringexpr(url)?;
900 self.set_link_at(idx, &url)?;
901 }
902 Stmt::Canvas { from, to } => {
903 let a = self.eval_pos(from)?;
904 let b = self.eval_pos(to)?;
905 let mut bb = Bbox::new();
906 bb.add(a);
907 bb.add(b);
908 if bb.width() <= 0.0 || bb.height() <= 0.0 {
909 return err("canvas must have positive width and height");
910 }
911 self.canvas = Some(bb);
912 }
913 Stmt::If {
914 cond,
915 then_body,
916 else_body,
917 } => {
918 if self.eval_expr(cond)? != 0.0 {
921 let stmts = self.parse_body(then_body)?;
922 self.eval_stmts(&stmts)?;
923 } else if let Some(e) = else_body {
924 let stmts = self.parse_body(e)?;
925 self.eval_stmts(&stmts)?;
926 }
927 }
928 Stmt::For {
929 var,
930 subscript,
931 from,
932 to,
933 by,
934 mult,
935 body,
936 } => {
937 let from = self.eval_expr(from)?;
938 let to = self.eval_expr(to)?;
939 let by = self.eval_expr(by)?;
940 let mut v = from;
941 let mut iters = 0u64;
942 if !*mult
943 && let Some(total) = additive_loop_iterations(from, to, by)
944 && total > self.limits.max_loop_iterations
945 {
946 return err(format!(
947 "for loop exceeded {} iterations",
948 self.limits.max_loop_iterations
949 ));
950 }
951 let mut parsed: Option<Vec<Stmt>> = None;
955 const EPS: f64 = 1e-9;
956 loop {
957 let cont = if *mult {
958 if by >= 1.0 {
959 v <= to + EPS
960 } else {
961 v >= to - EPS
962 }
963 } else if by >= 0.0 {
964 v <= to + EPS
965 } else {
966 v >= to - EPS
967 };
968 if !cont {
969 break;
970 }
971 if iters >= self.limits.max_loop_iterations {
972 return err(format!(
973 "for loop exceeded {} iterations",
974 self.limits.max_loop_iterations
975 ));
976 }
977 iters += 1;
978 let key = self.indexed_name(var, subscript.as_ref())?;
979 self.vars.insert(key, v);
980 if parsed.is_none() {
981 parsed = Some(self.parse_body(body)?);
982 }
983 self.eval_stmts(parsed.as_deref().unwrap())?;
984 let prev = v;
985 v = if *mult { v * by } else { v + by };
986 if (v - prev).abs() < f64::EPSILON {
987 break; }
989 }
990 }
991 Stmt::Print(item) => match item {
992 PrintItem::Expr(e) => {
993 let v = self.eval_expr(e)?;
994 self.diagnostics.push(fmt_num(v));
995 }
996 PrintItem::Str(se) => {
997 let s = self.eval_stringexpr(se)?;
998 self.diagnostics.push(s);
999 }
1000 },
1001 Stmt::Exec { command, arg_frame } => {
1002 let src = unescape_exec_source(&self.eval_stringexpr(command)?);
1003 let stmts = crate::parser::parse_exec_source(
1004 &src,
1005 &mut self.macros,
1006 &self.includes,
1007 arg_frame.as_ref().map(|a| a.as_slice()),
1008 )
1009 .map_err(parse_eval_error)?;
1010 self.eval_stmts(&stmts)?;
1011 }
1012 Stmt::Reset(list) => {
1013 if list.is_empty() {
1014 self.env = EnvVars::new(self.limits);
1015 } else {
1016 let d = EnvVars::new(self.limits);
1017 for e in list {
1018 self.env.set(*e, d.get(*e));
1019 }
1020 }
1021 }
1022 }
1023 Ok(())
1024 }
1025
1026 fn eval_animate(&mut self, a: &Animate) -> ER<()> {
1027 let idx = self.place_index(&a.target)?;
1028 let shape = self.placed[idx].shape.ok_or_else(|| EvalError {
1029 msg: "cannot animate a point (no drawn shape)".into(),
1030 info: None,
1031 })?;
1032 let max_seconds = self.max_anim_seconds()?;
1033 let dur = match &a.duration {
1034 Some(e) => checked_anim_time("duration", self.eval_expr(e)?, max_seconds)?,
1035 None => checked_anim_time("duration", DEFAULT_ANIM_DUR, max_seconds)?,
1036 };
1037 let mut start = match &a.timing {
1038 Timing::Sequential => self.anim_cursor,
1039 Timing::At(e) => self.eval_expr(e)?,
1040 Timing::After(p) => {
1041 let i = self.place_index(p)?;
1042 let sh = self.placed[i].shape.ok_or_else(|| EvalError {
1043 msg: "`after` target has no animation".into(),
1044 info: None,
1045 })?;
1046 *self.anim_end.get(&sh).unwrap_or(&0.0)
1047 }
1048 };
1049 start = checked_anim_time("start time", start, max_seconds)?;
1050 if let Some(d) = &a.delay {
1051 let delay = checked_anim_time("delay", self.eval_expr(d)?, max_seconds)?;
1052 start = checked_anim_time("start time", start + delay, max_seconds)?;
1053 }
1054 let end = checked_anim_time("end time", start + dur, max_seconds)?;
1058 let max_repeat = self.max_anim_repeat()?;
1059 let repeat = match &a.repeat {
1060 Some(e) => checked_anim_repeat(self.eval_expr(e)?, max_repeat)?,
1061 None => 0,
1062 };
1063 self.anim_cursor = end;
1064 self.anim_end.insert(shape, end);
1065 if a.yoyo && repeat == 0 {
1066 let mut warning = Diagnostic::new(
1067 "yoyo_without_repeat",
1068 "`yoyo` has no effect without `repeat` (there is nothing to reverse)",
1069 );
1070 if let Some(span) = &a.effect_span {
1071 warning = warning.at(span.clone());
1072 }
1073 self.warnings.push(warning);
1074 }
1075 let ease = a.ease.as_ref().map(stringexpr_lit);
1076 let effect = stringexpr_lit(&a.effect);
1077 let is_move = effect == "move";
1078 let is_highlight = effect == "highlight";
1079 let is_slide = effect == "slide";
1080 let is_morph = effect == "morph";
1081 let is_draw = effect == "draw";
1082 let from = a.slide_from.map(|d| {
1083 match d {
1084 Dir::Up => "up",
1085 Dir::Down => "down",
1086 Dir::Left => "left",
1087 Dir::Right => "right",
1088 }
1089 .to_string()
1090 });
1091 let path = match &a.along {
1093 Some(p) => {
1094 let i = self.place_index(p)?;
1095 let sh = self.placed[i].shape.ok_or_else(|| EvalError {
1096 msg: "`along` target has no drawn path".into(),
1097 info: None,
1098 })?;
1099 Some(sh)
1100 }
1101 None => None,
1102 };
1103 let morph = match &a.morph_into {
1105 Some(p) => {
1106 let i = self.place_index(p)?;
1107 let sh = self.placed[i].shape.ok_or_else(|| EvalError {
1108 msg: "`into` target has no drawn shape".into(),
1109 info: None,
1110 })?;
1111 Some(sh)
1112 }
1113 None => None,
1114 };
1115 let color = match &a.color {
1119 Some(se) => Some(self.eval_color_expr(se, None)?),
1120 None => None,
1121 };
1122 if is_move && path.is_none() {
1123 return Err(EvalError {
1124 msg: "`move` needs a path: `animate <obj> with \"move\" along <path>`".into(),
1125 info: None,
1126 });
1127 }
1128 if path.is_some() && !is_move {
1129 let mut warning = Diagnostic::new(
1130 "along_without_move",
1131 "`along` only applies to the `move` effect and is ignored here",
1132 );
1133 if let Some(span) = &a.effect_span {
1134 warning = warning.at(span.clone());
1135 }
1136 self.warnings.push(warning);
1137 }
1138 if color.is_some() && !is_highlight {
1139 let mut warning = Diagnostic::new(
1140 "to_without_highlight",
1141 "`to <colour>` only applies to the `highlight` effect and is ignored here",
1142 );
1143 if let Some(span) = &a.effect_span {
1144 warning = warning.at(span.clone());
1145 }
1146 self.warnings.push(warning);
1147 }
1148 if is_slide && from.is_none() {
1149 return Err(EvalError {
1150 msg: "`slide` needs a direction: `animate <obj> with \"slide\" from <dir>`".into(),
1151 info: None,
1152 });
1153 }
1154 if from.is_some() && !is_slide {
1155 let mut warning = Diagnostic::new(
1156 "from_without_slide",
1157 "`from <dir>` only applies to the `slide` effect and is ignored here",
1158 );
1159 if let Some(span) = &a.effect_span {
1160 warning = warning.at(span.clone());
1161 }
1162 self.warnings.push(warning);
1163 }
1164 if is_morph && morph.is_none() {
1165 return Err(EvalError {
1166 msg: "`morph` needs a target: `animate <obj> with \"morph\" into <shape>`".into(),
1167 info: None,
1168 });
1169 }
1170 if morph.is_some() && !is_morph {
1171 let mut warning = Diagnostic::new(
1172 "into_without_morph",
1173 "`into <shape>` only applies to the `morph` effect and is ignored here",
1174 );
1175 if let Some(span) = &a.effect_span {
1176 warning = warning.at(span.clone());
1177 }
1178 self.warnings.push(warning);
1179 }
1180 let is_type = effect == "type";
1181 let is_scramble = effect == "scramble";
1182 if a.type_unit.is_some() && !is_type {
1183 let mut warning = Diagnostic::new(
1184 "by_without_type",
1185 "`by word`/`by char` only applies to the `type` effect and is ignored here",
1186 );
1187 if let Some(span) = &a.effect_span {
1188 warning = warning.at(span.clone());
1189 }
1190 self.warnings.push(warning);
1191 }
1192 if a.scramble_chars.is_some() && !is_scramble {
1193 let mut warning = Diagnostic::new(
1194 "by_without_scramble",
1195 "`by \"<chars>\"` only applies to the `scramble` effect and is ignored here",
1196 );
1197 if let Some(span) = &a.effect_span {
1198 warning = warning.at(span.clone());
1199 }
1200 self.warnings.push(warning);
1201 }
1202 let is_wiggle = effect == "wiggle";
1203 if a.wiggles.is_some() && !is_wiggle {
1204 let mut warning = Diagnostic::new(
1205 "wiggles_without_wiggle",
1206 "`wiggles <n>` only applies to the `wiggle` effect and is ignored here",
1207 );
1208 if let Some(span) = &a.effect_span {
1209 warning = warning.at(span.clone());
1210 }
1211 self.warnings.push(warning);
1212 }
1213 let (draw_from, draw_to) = if is_draw {
1220 let f = match &a.draw_from {
1221 Some(e) => Some(self.eval_expr(e)?.clamp(0.0, 1.0)),
1222 None => None,
1223 };
1224 let t = match &a.draw_to {
1225 Some(e) => Some(self.eval_expr(e)?.clamp(0.0, 1.0)),
1226 None => None,
1227 };
1228 (f, t)
1229 } else {
1230 (None, None)
1231 };
1232 if !matches!(
1233 effect.as_str(),
1234 "draw"
1235 | "fade"
1236 | "pop"
1237 | "move"
1238 | "highlight"
1239 | "slide"
1240 | "morph"
1241 | "type"
1242 | "scramble"
1243 | "wiggle"
1244 ) {
1245 let mut warning = Diagnostic::new(
1246 "unknown_animation_effect",
1247 format!(
1248 "unknown animation effect `{effect}`; supported effects are `draw`, `fade`, `pop`, `move`, `highlight`, `slide`, `morph`, `type`, `scramble`, and `wiggle`"
1249 ),
1250 )
1251 .found(effect.clone())
1252 .expected("draw, fade, pop, move, highlight, slide, morph, type, scramble, or wiggle");
1253 if let Some(span) = &a.effect_span {
1254 warning = warning.at(span.clone());
1255 }
1256 self.warnings.push(warning);
1257 }
1258 let path = if is_move { path } else { None };
1259 let color = if is_highlight { color } else { None };
1260 let from = if is_slide { from } else { None };
1261 let morph = if is_morph { morph } else { None };
1262 let type_word = is_type && matches!(a.type_unit, Some(TypeUnit::Word));
1263 let scramble_chars = if is_scramble {
1264 a.scramble_chars.as_ref().map(stringexpr_lit)
1265 } else {
1266 None
1267 };
1268 let wiggles = match (is_wiggle, &a.wiggles) {
1269 (true, Some(e)) => Some(self.eval_expr(e)?.round() as i64),
1270 _ => None,
1271 };
1272 let make = |shape: usize, start: f64| Anim {
1273 shape,
1274 effect: effect.clone(),
1275 start,
1276 duration: dur,
1277 repeat,
1278 yoyo: a.yoyo,
1279 ease: ease.clone(),
1280 path,
1281 color: color.clone(),
1282 out: a.out,
1283 from: from.clone(),
1284 morph,
1285 type_word,
1286 scramble_chars: scramble_chars.clone(),
1287 wiggles,
1288 draw_from,
1289 draw_to,
1290 };
1291 if let Some(se) = &a.stagger {
1295 let step = checked_anim_time("stagger", self.eval_expr(se)?, max_seconds)?;
1296 let children: Vec<usize> = self.placed[idx]
1297 .block_shapes
1298 .map(|(lo, hi)| (lo..hi).filter(|&i| self.shapes[i].is_visible()).collect())
1299 .unwrap_or_default();
1300 if !children.is_empty() {
1301 let mut last_start = start;
1302 for (k, &child) in children.iter().enumerate() {
1303 let child_start =
1304 checked_anim_time("start time", start + k as f64 * step, max_seconds)?;
1305 last_start = child_start;
1306 self.anims.push(make(child, child_start));
1307 }
1308 let last_end = checked_anim_time("end time", last_start + dur, max_seconds)?;
1309 self.anim_cursor = last_end;
1310 self.anim_end.insert(shape, last_end);
1315 return Ok(());
1316 }
1317 let mut warning = Diagnostic::new(
1318 "stagger_without_block",
1319 "`stagger` only applies to a block target with drawn children; animating the single object instead",
1320 );
1321 if let Some(span) = &a.effect_span {
1322 warning = warning.at(span.clone());
1323 }
1324 self.warnings.push(warning);
1325 }
1326 self.anims.push(make(shape, start));
1327 Ok(())
1328 }
1329
1330 fn eval_draggable(&mut self, d: &Draggable) -> ER<()> {
1333 let idx = self.place_index(&d.target)?;
1334 let shape = self.placed[idx].shape.ok_or_else(|| EvalError {
1335 msg: "cannot make a point draggable (no drawn shape)".into(),
1336 info: None,
1337 })?;
1338 let bounds = match &d.bounds {
1339 Some(p) => {
1340 let i = self.place_index(p)?;
1341 let sh = self.placed[i].shape.ok_or_else(|| EvalError {
1342 msg: "`bounds` target has no drawn shape".into(),
1343 info: None,
1344 })?;
1345 Some(sh)
1346 }
1347 None => None,
1348 };
1349 let axis = d.axis.map(|a| match a {
1350 DragAxis::X => "x",
1351 DragAxis::Y => "y",
1352 });
1353 self.interactions.push(Interaction {
1354 shape,
1355 inertia: d.inertia,
1356 bounds,
1357 axis,
1358 });
1359 Ok(())
1360 }
1361
1362 fn append_class_at(&mut self, placed_idx: usize, name: &str) -> ER<()> {
1365 let name = name.trim();
1366 validate_class(name)?;
1367 let pl = &self.placed[placed_idx];
1368 if matches!(pl.kind, PKind::Block) {
1369 return err("`class` on a block is not supported yet; class its inner objects");
1370 }
1371 let Some(sh) = pl.shape else {
1372 return err("cannot attach a class to a point (no drawn shape)");
1373 };
1374 match &mut self.shape_classes[sh] {
1375 Some(existing) => {
1376 existing.push(' ');
1377 existing.push_str(name);
1378 }
1379 slot @ None => *slot = Some(name.to_string()),
1380 }
1381 Ok(())
1382 }
1383
1384 fn set_link_at(&mut self, placed_idx: usize, url: &str) -> ER<()> {
1387 let url = url.trim();
1388 validate_link(url)?;
1389 let pl = &self.placed[placed_idx];
1390 if matches!(pl.kind, PKind::Block) {
1391 return err("`link` on a block is not supported yet; link its inner objects");
1392 }
1393 let Some(sh) = pl.shape else {
1394 return err("cannot attach a link to a point (no drawn shape)");
1395 };
1396 self.shape_links[sh] = Some(url.to_string());
1397 Ok(())
1398 }
1399
1400 fn math_span_for(&mut self, s: &str) -> Option<crate::math::MathSpan> {
1406 if self.env.get(EnvVar::Texlabels) == 0.0 {
1407 return None;
1408 }
1409 let t = s.trim();
1410 let inner = t.strip_prefix('$')?.strip_suffix('$')?;
1411 if inner.is_empty() || inner.contains('$') {
1412 return None;
1413 }
1414 let Some(render) = crate::math::math_renderer() else {
1415 self.diagnostics.push(format!(
1416 "texlabels: no math renderer in this build; `{t}` kept literal"
1417 ));
1418 return None;
1419 };
1420 match render(inner, FONT_PT_MATH) {
1421 Ok(span) => Some(span),
1422 Err(e) => {
1423 self.diagnostics.push(format!(
1424 "texlabels: `{t}` is not valid TeX math ({e}); label kept literal"
1425 ));
1426 None
1427 }
1428 }
1429 }
1430
1431 fn scale_value(&self) -> ER<f64> {
1432 let scale = self.env.get(EnvVar::Scale);
1433 if scale.abs() < 1e-12 {
1434 return err("scale must be non-zero");
1435 }
1436 Ok(scale)
1437 }
1438
1439 fn to_internal_dim(&self, v: f64) -> ER<f64> {
1441 Ok(v / self.scale_value()?)
1442 }
1443
1444 fn env_dim(&self, e: EnvVar) -> ER<f64> {
1445 self.to_internal_dim(self.env.get(e))
1446 }
1447
1448 fn canvas_margin(&self) -> ER<CanvasMargin> {
1449 let all = self.env_dim(EnvVar::Margin)?;
1450 Ok(CanvasMargin {
1451 top: all + self.env_dim(EnvVar::Topmargin)?,
1452 right: all + self.env_dim(EnvVar::Rightmargin)?,
1453 bottom: all + self.env_dim(EnvVar::Bottommargin)?,
1454 left: all + self.env_dim(EnvVar::Leftmargin)?,
1455 })
1456 }
1457
1458 fn expr_dim(&mut self, e: &Expr) -> ER<f64> {
1459 let v = self.eval_expr(e)?;
1460 self.to_internal_dim(v)
1461 }
1462
1463 fn to_user_dim(&self, v: f64) -> f64 {
1465 v * self.env.get(EnvVar::Scale)
1466 }
1467
1468 fn eval_assignment(&mut self, a: &Assignment) -> ER<()> {
1469 let rhs = self.eval_expr(&a.value)?;
1470 match &a.target {
1471 AssignTarget::Var(name, subscript) => {
1472 let key = self.indexed_name(name, subscript.as_ref())?;
1473 let cur = match self.vars.get(&key).copied() {
1474 Some(v) => v,
1475 None if matches!(a.op, AssignOp::Set) => 0.0,
1476 None => return err(format!("variable not found `{key}`")),
1477 };
1478 let val = apply_op(a.op, cur, rhs)?;
1479 if !matches!(a.op, AssignOp::Set) && self.inherited_vars.contains(&key) {
1480 self.export_vars.insert(key.clone());
1481 }
1482 self.vars.insert(key, val);
1483 }
1484 AssignTarget::Env(e) => {
1485 let cur = self.env.get(*e);
1486 let val = self.checked_env_value(*e, apply_op(a.op, cur, rhs)?)?;
1487 if matches!(e, EnvVar::Scale) {
1488 if val.abs() < 1e-12 {
1489 return err("scale must be non-zero");
1490 }
1491 if cur.abs() >= 1e-12 {
1492 self.scale_existing_geometry(cur / val);
1493 }
1494 let ratio = if cur != 0.0 { val / cur } else { val };
1499 for sv in SCALED_VARS {
1500 let v = self.env.get(sv);
1501 self.env.set(sv, v * ratio);
1502 }
1503 }
1504 self.env.set(*e, val);
1505 }
1506 }
1507 Ok(())
1508 }
1509
1510 fn scale_existing_geometry(&mut self, factor: f64) {
1511 if (factor - 1.0).abs() < 1e-12 {
1512 return;
1513 }
1514 self.pos = self.pos * factor;
1515 for sh in &mut self.shapes {
1516 scale_shape(sh, factor);
1517 }
1518 for pl in &mut self.placed {
1519 scale_placed(pl, factor);
1520 }
1521 scale_bbox_in_place(&mut self.bbox, factor);
1522 scale_bbox_in_place(&mut self.layout_bbox, factor);
1523 }
1524
1525 fn eval_object(&mut self, obj: &Object) -> ER<usize> {
1528 if !object_uses_bare_distance(&obj.kind) {
1529 self.warn_ignored_dist_attrs(obj);
1530 }
1531 let saved_span = std::mem::replace(&mut self.current_span, obj.span.clone());
1534 let result = self.eval_object_inner(obj);
1535 self.current_span = saved_span;
1536 let idx = result?;
1537 for a in &obj.attrs {
1538 if let Attr::Class(se) = a {
1539 let name = self.eval_stringexpr(se)?;
1540 self.append_class_at(idx, &name)?;
1541 }
1542 if let Attr::Link(se) = a {
1543 let url = self.eval_stringexpr(se)?;
1544 self.set_link_at(idx, &url)?;
1545 }
1546 }
1547 Ok(idx)
1548 }
1549
1550 fn warn_ignored_dist_attrs(&mut self, obj: &Object) {
1551 for attr in &obj.attrs {
1552 let Attr::Dist(expr, span) = attr else {
1553 continue;
1554 };
1555 let found = expr_bare_name(expr).unwrap_or("bare distance");
1556 let mut warning = Diagnostic::new(
1557 "ignored_attribute",
1558 format!("ignored `{found}` because this object does not accept a bare distance"),
1559 )
1560 .found(found)
1561 .expected("an attribute");
1562 if let Some(hint) = suggest_attribute(found) {
1563 warning = warning.hint(format!("did you mean `{hint}`?"));
1564 }
1565 if let Some(span) = span {
1566 warning = warning.at(span.clone());
1567 }
1568 self.warnings.push(warning);
1569 }
1570 }
1571
1572 fn eval_object_inner(&mut self, obj: &Object) -> ER<usize> {
1573 match &obj.kind {
1574 ObjectKind::Primitive(p) => match p {
1575 Prim::Box | Prim::Circle | Prim::Ellipse => self.closed(*p, obj),
1576 Prim::Line | Prim::Arrow | Prim::Move | Prim::Spline => self.open(*p, obj),
1577 Prim::Arc => self.arc(obj),
1578 },
1579 ObjectKind::Text => self.text_obj(obj),
1580 ObjectKind::Brace => self.brace(obj),
1581 ObjectKind::Dot => self.closed_dot(obj),
1582 ObjectKind::Block(stmts) => self.block(stmts, obj),
1583 ObjectKind::Empty => self.block(&[], obj),
1584 ObjectKind::Continue => self.continue_obj(obj),
1585 }
1586 }
1587
1588 fn dim(&mut self, obj: &Object, kind: DimKind) -> ER<Option<f64>> {
1591 for a in &obj.attrs {
1592 if let Attr::Dim(k, e) = a
1593 && *k == kind
1594 {
1595 return Ok(Some(match kind {
1596 DimKind::Thick | DimKind::Scaled => self.eval_expr(e)?,
1597 DimKind::Ht | DimKind::Wid | DimKind::Rad | DimKind::Diam => {
1598 self.expr_dim(e)?
1599 }
1600 }));
1601 }
1602 }
1603 Ok(None)
1604 }
1605
1606 fn has_dim(&self, obj: &Object, kind: DimKind) -> bool {
1607 obj.attrs
1608 .iter()
1609 .any(|a| matches!(a, Attr::Dim(k, _) if *k == kind))
1610 }
1611
1612 fn scale_of(&mut self, obj: &Object) -> ER<f64> {
1613 Ok(self.dim(obj, DimKind::Scaled)?.unwrap_or(1.0))
1614 }
1615
1616 fn dir_of(&self, obj: &Object) -> Dir {
1617 obj.attrs
1618 .iter()
1619 .rev()
1620 .find_map(|a| match a {
1621 Attr::Direction(d, _) => Some(*d),
1622 _ => None,
1623 })
1624 .unwrap_or(self.dir)
1625 }
1626
1627 fn find_from(&mut self, obj: &Object) -> ER<Option<Point>> {
1628 for a in &obj.attrs {
1629 if let Attr::From(pos) = a {
1630 return Ok(Some(self.eval_pos(pos)?));
1631 }
1632 }
1633 Ok(None)
1634 }
1635
1636 fn at_of(&mut self, obj: &Object) -> ER<Option<Point>> {
1637 for a in &obj.attrs {
1638 if let Attr::At(pos) = a {
1639 return Ok(Some(self.eval_pos(pos)?));
1640 }
1641 }
1642 Ok(None)
1643 }
1644
1645 fn dest_of(&mut self, obj: &Object) -> ER<Option<Point>> {
1646 for a in &obj.attrs {
1647 if let Attr::To(pos) = a {
1648 return Ok(Some(self.eval_pos(pos)?));
1649 }
1650 }
1651 Ok(None)
1652 }
1653
1654 fn chop_of(&mut self, obj: &Object) -> ER<Option<(f64, f64)>> {
1657 let mut vals = Vec::new();
1658 for a in &obj.attrs {
1659 if let Attr::Chop(opt) = a {
1660 let amt = match opt {
1661 Some(e) => self.expr_dim(e)?,
1662 None => self.env_dim(EnvVar::Circlerad)?,
1663 };
1664 vals.push(amt);
1665 }
1666 }
1667 Ok(match vals.as_slice() {
1668 [] => None,
1669 [one] => Some((*one, *one)),
1670 [start, end, ..] => Some((*start, *end)),
1671 })
1672 }
1673
1674 fn last_dims_of(&self, p: Prim) -> Option<(f64, f64)> {
1676 let want = match p {
1677 Prim::Box => PKind::Box,
1678 Prim::Circle => PKind::Circle,
1679 Prim::Ellipse => PKind::Ellipse,
1680 _ => return None,
1681 };
1682 self.placed
1683 .iter()
1684 .rev()
1685 .find(|pl| pl.kind == want)
1686 .map(|pl| (pl.bbox.width(), pl.bbox.height()))
1687 }
1688
1689 fn last_open_vector(&self, p: Prim) -> Option<Point> {
1691 let want = match p {
1692 Prim::Move => PKind::Move,
1693 Prim::Spline => PKind::Spline,
1694 Prim::Line | Prim::Arrow => PKind::Line,
1695 _ => return None,
1696 };
1697 self.placed
1698 .iter()
1699 .rev()
1700 .find(|pl| pl.kind == want)
1701 .map(|pl| pl.end - pl.start)
1702 }
1703
1704 fn place_center(&mut self, obj: &Object, dir: Dir, extent: f64, w: f64, h: f64) -> ER<Point> {
1706 self.place_with_corner_offset(obj, dir, extent, w, h, corner_offset)
1707 }
1708
1709 fn place_closed_center(
1710 &mut self,
1711 p: Prim,
1712 obj: &Object,
1713 dir: Dir,
1714 extent: f64,
1715 dims: (f64, f64),
1716 rad: f64,
1717 ) -> ER<Point> {
1718 let (w, h) = dims;
1719 self.place_with_corner_offset(obj, dir, extent, w, h, |c, w, h| {
1720 closed_corner_offset(p, c, w, h, rad)
1721 })
1722 }
1723
1724 fn place_with_corner_offset(
1725 &mut self,
1726 obj: &Object,
1727 dir: Dir,
1728 extent: f64,
1729 w: f64,
1730 h: f64,
1731 corner: impl Fn(Corner, f64, f64) -> Point,
1732 ) -> ER<Point> {
1733 if let Some(at) = self.at_of(obj)? {
1734 return Ok(at);
1735 }
1736 for a in &obj.attrs {
1737 if let Attr::With { anchor, at } = a {
1738 let ap = self.eval_pos(at)?;
1739 let off = match anchor {
1740 WithAnchor::Corner(c) => corner(dir_start_end_corner(*c, dir), w, h),
1741 WithAnchor::Pair(x, y) => Point::new(self.expr_dim(x)?, self.expr_dim(y)?),
1742 WithAnchor::Place(_) => {
1743 return err("`with .label` anchors are only valid on blocks");
1744 }
1745 WithAnchor::Plain => Point::ZERO,
1746 };
1747 return Ok(ap - off);
1748 }
1749 }
1750 Ok(self.pos + dir_unit(dir) * (extent / 2.0))
1751 }
1752
1753 #[allow(clippy::too_many_arguments)]
1754 fn block_center(
1755 &mut self,
1756 obj: &Object,
1757 dir: Dir,
1758 extent: f64,
1759 w: f64,
1760 h: f64,
1761 local_center: Point,
1762 sub: &mut State,
1763 ) -> ER<Point> {
1764 if let Some(at) = self.at_of(obj)? {
1765 return Ok(at);
1766 }
1767 for a in &obj.attrs {
1768 if let Attr::With { anchor, at } = a {
1769 let ap = self.eval_pos(at)?;
1770 let off = match anchor {
1771 WithAnchor::Corner(c) => corner_offset(dir_start_end_corner(*c, dir), w, h),
1772 WithAnchor::Pair(x, y) => {
1773 Point::new(self.expr_dim(x)?, self.expr_dim(y)?) - local_center
1774 }
1775 WithAnchor::Place(place) => sub.place_point(place)? - local_center,
1776 WithAnchor::Plain => Point::ZERO,
1777 };
1778 return Ok(ap - off);
1779 }
1780 }
1781 Ok(self.pos + dir_unit(dir) * (extent / 2.0))
1782 }
1783
1784 fn arrows_of(&self, obj: &Object, default_end: bool) -> Arrowheads {
1785 let mut found = None;
1786 for a in &obj.attrs {
1787 if let Attr::Arrowhead(h, _) = a {
1788 found = Some(match h {
1789 token::Arrow::Left => Arrowheads::Start,
1790 token::Arrow::Right => Arrowheads::End,
1791 token::Arrow::Double => Arrowheads::Both,
1792 });
1793 }
1794 }
1795 found.unwrap_or(if default_end {
1796 Arrowheads::End
1797 } else {
1798 Arrowheads::None
1799 })
1800 }
1801
1802 fn style_of(&mut self, obj: &Object) -> ER<Style> {
1803 let mut s = Style {
1805 arrow_ht: self.env_dim(EnvVar::Arrowht)?,
1806 arrow_wid: self.env_dim(EnvVar::Arrowwid)?,
1807 arrow_filled: self.env.get(EnvVar::Arrowhead).round() as i64 != 0,
1810 ..Default::default()
1811 };
1812 let lt = self.env.get(EnvVar::Linethick);
1813 if lt > 0.0 {
1814 s.thick = Some(lt);
1815 }
1816 for a in &obj.attrs {
1817 match a {
1818 Attr::LineStyle(lt, opt) => match lt {
1819 LineType::Solid => s.dash = Dash::Solid,
1820 LineType::Dashed => {
1821 let w = match opt {
1822 Some(e) => self.expr_dim(e)?,
1823 None => self.env_dim(EnvVar::Dashwid)?,
1824 };
1825 let w = if w.is_finite() && w > 0.0 {
1828 w
1829 } else {
1830 self.env_dim(EnvVar::Dashwid)?
1831 };
1832 s.dash = Dash::Dashed(w);
1833 }
1834 LineType::Dotted => {
1835 let pitch = match opt {
1838 Some(e) => {
1839 let w = self.expr_dim(e)?;
1840 (w.is_finite() && w > 0.0).then_some(w)
1841 }
1842 None => None,
1843 };
1844 s.dash = Dash::Dotted(pitch);
1845 }
1846 LineType::Invis => s.invis = true,
1847 },
1848 Attr::Fill(opt) => {
1849 let g = match opt {
1850 Some(e) => self.eval_expr(e)?,
1851 None => self.env.get(EnvVar::Fillval),
1852 };
1853 s.fill = Some(Fill::Gray(g));
1854 s.fill_open = true;
1855 }
1856 Attr::Color(kind, se, span) => {
1857 let name = self.eval_color_expr(se, span.as_ref())?;
1858 match kind {
1859 token::Color::Outlined => s.stroke = Some(name),
1860 token::Color::Colored => {
1861 s.stroke = Some(name.clone());
1862 s.fill = Some(Fill::Color(name));
1863 }
1864 token::Color::Shaded => {
1865 s.fill = Some(Fill::Color(name));
1866 s.fill_open = true;
1867 }
1868 }
1869 }
1870 Attr::Hatch(kind) => {
1871 let h = ensure_hatch(&mut s);
1872 h.cross = matches!(kind, HatchKind::Cross);
1873 s.fill_open = true;
1874 }
1875 Attr::HatchAngle(e) => ensure_hatch(&mut s).angle = self.eval_expr(e)?,
1876 Attr::HatchSep(e) => {
1877 let sep = self.expr_dim(e)?;
1878 if sep <= 0.0 {
1879 return err("hatchsep must be positive");
1880 }
1881 ensure_hatch(&mut s).sep = sep;
1882 s.fill_open = true;
1883 }
1884 Attr::HatchWidth(e) => {
1885 let width = self.eval_expr(e)?;
1886 if width < 0.0 {
1887 return err("hatchwidth must be non-negative");
1888 }
1889 ensure_hatch(&mut s).width = width;
1890 s.fill_open = true;
1891 }
1892 Attr::HatchColor(se, span) => {
1893 let name = self.eval_color_expr(se, span.as_ref())?;
1894 ensure_hatch(&mut s).color = name;
1895 s.fill_open = true;
1896 }
1897 Attr::Gradient(a, a_span, b, b_span) => {
1898 let from = self.eval_color_expr(a, a_span.as_ref())?;
1899 let to = self.eval_color_expr(b, b_span.as_ref())?;
1900 let g = ensure_gradient(&mut s);
1901 g.from = from;
1902 g.to = to;
1903 s.fill_open = true;
1904 }
1905 Attr::GradientAngle(e) => {
1906 ensure_gradient(&mut s).angle = self.eval_expr(e)?;
1907 s.fill_open = true;
1908 }
1909 Attr::Opacity(e) => {
1910 let opacity = self.eval_expr(e)?;
1911 if !(0.0..=1.0).contains(&opacity) {
1912 return err("opacity must be between 0 and 1");
1913 }
1914 s.fill_opacity = Some(opacity);
1915 }
1916 Attr::Dim(DimKind::Thick, e) => s.thick = Some(self.eval_expr(e)?),
1917 Attr::Thin => s.thick = Some(self.env.get(EnvVar::Linethick) * 2.0 / 3.0),
1919 Attr::Arrowhead(_, Some(e)) => {
1920 s.arrow_filled = self.eval_expr(e)?.round() as i64 != 0;
1921 }
1922 _ => {}
1923 }
1924 }
1925 Ok(s)
1926 }
1927
1928 fn eval_color_expr(&mut self, se: &StringExpr, span: Option<&Span>) -> ER<String> {
1929 if let StringExpr::Lit(name) = se {
1930 if let Some(&v) = self.vars.get(name) {
1936 return num_to_color(v);
1937 }
1938 if let Some(body) = self.macros.get(name).cloned() {
1939 let s = if let Some(lit) = single_token_macro_string(&body) {
1940 lit
1941 } else {
1942 let parsed = crate::parser::parse_stringexpr_tokens(
1943 &body,
1944 &mut self.macros,
1945 &self.includes,
1946 )
1947 .map_err(parse_eval_error)?;
1948 self.eval_stringexpr(&parsed)?
1949 };
1950 return self.checked_color(normalize_color_string(s), span);
1951 }
1952 }
1953 let color = normalize_color_string(self.eval_stringexpr(se)?);
1954 self.checked_color(color, span)
1955 }
1956
1957 fn checked_color(&mut self, color: String, span: Option<&Span>) -> ER<String> {
1961 if let Some(reason) = unsafe_svg_colour_reason(&color) {
1962 return err(reason);
1963 }
1964 if !crate::color::is_valid_color(&color) {
1965 let mut warning = Diagnostic::new(
1966 "invalid_color",
1967 format!("`{color}` is not a known colour name or hex/rgb() value"),
1968 )
1969 .found(color.clone())
1970 .expected("a CSS/xcolor colour name, #hex, or rgb(...)");
1971 if let Some(hint) = crate::color::suggest(&color) {
1972 warning = warning.hint(format!("did you mean `{hint}`?"));
1973 }
1974 if let Some(span) = span {
1975 warning = warning.at(span.clone());
1976 }
1977 self.warnings.push(warning);
1978 }
1979 Ok(color)
1980 }
1981
1982 fn text_of(&mut self, obj: &Object) -> ER<Vec<TextLine>> {
1983 Ok(self.text_and_fit_text_of(obj)?.0)
1984 }
1985
1986 fn text_and_fit_text_of(&mut self, obj: &Object) -> ER<(Vec<TextLine>, Option<Vec<TextLine>>)> {
1987 let mut lines: Vec<TextLine> = Vec::new();
1988 let mut fit_lines = None;
1989 let mut pending_halign = 0i8;
1990 let mut pending_valign = 0i8;
1991 let mut pending_style = PendingStyle::default();
1994 for a in &obj.attrs {
1995 match a {
1996 Attr::TextPos(tp) => {
1997 if let Some(line) = lines.last_mut() {
1998 apply_text_pos(&mut line.halign, &mut line.valign, *tp);
1999 } else {
2000 apply_text_pos(&mut pending_halign, &mut pending_valign, *tp);
2001 }
2002 }
2003 Attr::Bold => match lines.last_mut() {
2004 Some(line) => line.bold = true,
2005 None => pending_style.bold = true,
2006 },
2007 Attr::Italic => match lines.last_mut() {
2008 Some(line) => line.italic = true,
2009 None => pending_style.italic = true,
2010 },
2011 Attr::Mono => match lines.last_mut() {
2012 Some(line) => line.family = Some("monospace".into()),
2013 None => pending_style.family = Some("monospace".into()),
2014 },
2015 Attr::Font(se) => {
2016 let family = self.eval_stringexpr(se)?;
2017 match lines.last_mut() {
2018 Some(line) => line.family = Some(family),
2019 None => pending_style.family = Some(family),
2020 }
2021 }
2022 Attr::FontSize(e) => {
2023 let pt = self.eval_expr(e)?;
2024 if !pt.is_finite() || pt <= 0.0 {
2025 return err("fontsize must be a positive number of points");
2026 }
2027 match lines.last_mut() {
2028 Some(line) => line.size_pt = Some(pt),
2029 None => pending_style.size_pt = Some(pt),
2030 }
2031 }
2032 Attr::Rotated(e) => {
2033 let deg = self.eval_expr(e)?;
2034 if !deg.is_finite() {
2035 return err("rotated angle must be finite");
2036 }
2037 match lines.last_mut() {
2038 Some(line) => line.rotate = Some(deg),
2039 None => pending_style.rotate = Some(deg),
2040 }
2041 }
2042 Attr::Aligned => match lines.last_mut() {
2043 Some(line) => line.aligned = true,
2044 None => pending_style.aligned = true,
2045 },
2046 Attr::Sized(big) => {
2047 let pt = FONT_PT_CLASSIC * if *big { 1.5 } else { 0.7 };
2049 match lines.last_mut() {
2050 Some(line) => line.size_pt = Some(pt),
2051 None => pending_style.size_pt = Some(pt),
2052 }
2053 }
2054 Attr::Text(se) => {
2055 let s = self.eval_stringexpr(se)?;
2056 let math = self.math_span_for(&s);
2057 lines.push(TextLine {
2058 s,
2059 math,
2060 halign: pending_halign,
2061 valign: pending_valign,
2062 text_offset: self.env_dim(EnvVar::Textoffset)?,
2063 bold: pending_style.bold,
2064 italic: pending_style.italic,
2065 family: pending_style.family.take(),
2066 size_pt: pending_style.size_pt,
2067 rotate: pending_style.rotate,
2068 aligned: pending_style.aligned,
2069 });
2070 pending_halign = 0;
2071 pending_valign = 0;
2072 pending_style = PendingStyle::default();
2073 }
2074 Attr::Fit if fit_lines.is_none() => {
2075 fit_lines = Some(lines.clone());
2076 }
2077 _ => {}
2078 }
2079 }
2080 Ok((lines, fit_lines))
2081 }
2082}
2083
2084mod build;
2085mod helpers;
2086mod resolve;
2087use helpers::*;
2088
2089#[cfg(test)]
2090mod tests;