Skip to main content

rpic_core/
eval.rs

1//! Evaluator: walks the [`crate::ast`] and produces a placed-primitive
2//! [`Drawing`] using pic's positioning semantics.
3//!
4//! Model (Kernighan §3): a pen with a *current position* and *current
5//! direction* walks the plane dropping primitives. Closed objects (box/circle/
6//! ellipse/block) attach at the current point and advance by their extent;
7//! open objects (line/arrow/move/spline/arc) trace from the current point in
8//! the current direction. Labels, compass corners and ordinals
9//! (`last`/`nth`) resolve against previously placed objects.
10//!
11//! Approximations (documented; refined later): `arc` renders a default quarter
12//! turn.
13
14use std::collections::{HashMap, HashSet};
15use std::f64::consts::{FRAC_1_SQRT_2, PI};
16
17use crate::ast::*;
18use crate::geom::{Bbox, Point};
19use crate::ir::*;
20use crate::token::{self, Corner, Dir, EnvVar, Func1, Func2, LineType, Prim};
21
22/// An evaluation error.
23#[derive(Debug, Clone, PartialEq)]
24pub struct EvalError {
25    pub msg: String,
26}
27
28impl std::fmt::Display for EvalError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(f, "{}", self.msg)
31    }
32}
33
34type ER<T> = Result<T, EvalError>;
35
36const DP_TEXT_RATIO: f64 = 0.66;
37const TEXT_EM: f64 = 11.0 / 72.0;
38const TEXT_CHAR_W: f64 = 0.6 * TEXT_EM;
39const TEXT_LINE_H: f64 = 1.2 * TEXT_EM;
40const TEXT_XHEIGHT: f64 = DP_TEXT_RATIO * TEXT_EM;
41const DEFAULT_BRACE_DEPTH: f64 = 0.18;
42const DEFAULT_BRACE_POS: f64 = 0.5;
43const DEFAULT_HATCH_ANGLE: f64 = 45.0;
44const DEFAULT_HATCH_SEP: f64 = 0.08;
45const DEFAULT_HATCH_WIDTH: f64 = 0.8;
46
47fn err<T>(msg: impl Into<String>) -> ER<T> {
48    Err(EvalError { msg: msg.into() })
49}
50
51/// Evaluate a parsed picture into a [`Drawing`].
52pub fn eval(pic: &Picture) -> ER<Drawing> {
53    let mut st = State::new();
54    st.macros = pic.macros.clone();
55    st.base_dir = pic.base_dir.clone();
56    st.eval_stmts(&pic.stmts)?;
57    let want_w = match &pic.width {
58        Some(e) => Some(st.eval_expr(e)?),
59        None => None,
60    };
61    let want_h = match &pic.height {
62        Some(e) => Some(st.eval_expr(e)?),
63        None => None,
64    };
65    let (maxw, maxh) = (st.env.get(EnvVar::Maxpswid), st.env.get(EnvVar::Maxpsht));
66    let canvas_margin = st.canvas_margin()?;
67    let mut d = Drawing {
68        shapes: st.shapes,
69        shape_layers: st.shape_layers,
70        bbox: st.bbox,
71        prelude_thick: st.env.get(EnvVar::Linethick),
72        canvas_margin,
73        anims: st.anims,
74        diagnostics: st.diagnostics,
75    };
76    apply_ps_size(&mut d, want_w, want_h);
77    clamp_to_maxps(&mut d, maxw, maxh);
78    Ok(d)
79}
80
81/// Clamp the drawing to the `maxpswid`/`maxpsht` page bounds: if it exceeds
82/// either, scale the whole picture down uniformly to fit (never up), matching
83/// pic's PostScript page-fit behaviour.
84fn clamp_to_maxps(d: &mut Drawing, maxw: f64, maxh: f64) {
85    for _ in 0..4 {
86        if d.bbox.is_empty() {
87            return;
88        }
89        let (w, h) = (canvas_width(d), canvas_height(d));
90        let mut factor = 1.0_f64;
91        if maxw > 0.0 && w > maxw {
92            factor = factor.min(maxw / w);
93        }
94        if maxh > 0.0 && h > maxh {
95            factor = factor.min(maxh / h);
96        }
97        if factor >= 1.0 - 1e-9 {
98            return;
99        }
100        for sh in &mut d.shapes {
101            scale_shape(sh, factor);
102        }
103        d.prelude_thick *= factor;
104        d.canvas_margin.scale_by(factor);
105        d.bbox = drawing_painted_bbox(&d.shapes);
106    }
107}
108
109/// Apply `.PS <width> [<height>]` sizing: uniformly scale the whole drawing so
110/// it matches the requested width (or height if only height is given). Font size
111/// is left unchanged.
112fn apply_ps_size(d: &mut Drawing, want_w: Option<f64>, want_h: Option<f64>) {
113    if d.bbox.is_empty() {
114        return;
115    }
116    let factor = match (want_w, want_h) {
117        (Some(w), _) if w > 0.0 && d.bbox.width() > 0.0 => w / d.bbox.width(),
118        (None, Some(h)) if h > 0.0 && d.bbox.height() > 0.0 => h / d.bbox.height(),
119        _ => return,
120    };
121    if (factor - 1.0).abs() < 1e-9 {
122        return;
123    }
124    for sh in &mut d.shapes {
125        scale_shape(sh, factor);
126    }
127    d.prelude_thick *= factor;
128    d.canvas_margin.scale_by(factor);
129    d.bbox = drawing_painted_bbox(&d.shapes);
130}
131
132fn canvas_width(d: &Drawing) -> f64 {
133    d.bbox.width() + d.canvas_margin.horizontal()
134}
135
136fn canvas_height(d: &Drawing) -> f64 {
137    d.bbox.height() + d.canvas_margin.vertical()
138}
139
140/// The dimension variables that track `scale`.
141const SCALED_VARS: [EnvVar; 22] = [
142    EnvVar::Arcrad,
143    EnvVar::Arrowht,
144    EnvVar::Arrowwid,
145    EnvVar::Boxht,
146    EnvVar::Boxrad,
147    EnvVar::Boxwid,
148    EnvVar::Circlerad,
149    EnvVar::Dashwid,
150    EnvVar::Ellipseht,
151    EnvVar::Ellipsewid,
152    EnvVar::Lineht,
153    EnvVar::Linewid,
154    EnvVar::Moveht,
155    EnvVar::Movewid,
156    EnvVar::Textht,
157    EnvVar::Textwid,
158    EnvVar::Textoffset,
159    EnvVar::Margin,
160    EnvVar::Topmargin,
161    EnvVar::Rightmargin,
162    EnvVar::Bottommargin,
163    EnvVar::Leftmargin,
164];
165
166// ---- environment variables -------------------------------------------------
167
168#[derive(Clone)]
169struct EnvVars {
170    v: HashMap<u8, f64>,
171}
172
173fn ev_key(e: EnvVar) -> u8 {
174    e as u8
175}
176
177impl EnvVars {
178    fn new() -> Self {
179        use EnvVar::*;
180        let defaults = [
181            (Arcrad, 0.25),
182            (Arrowht, 0.1),
183            (Arrowwid, 0.05),
184            (Boxht, 0.5),
185            (Boxrad, 0.0),
186            (Boxwid, 0.75),
187            (Circlerad, 0.25),
188            (Dashwid, 0.05),
189            (Ellipseht, 0.5),
190            (Ellipsewid, 0.75),
191            (Lineht, 0.5),
192            (Linewid, 0.5),
193            (Moveht, 0.5),
194            (Movewid, 0.5),
195            (Textht, (11.0 / 72.0) * DP_TEXT_RATIO),
196            (Textoffset, 2.0 / 72.0),
197            (Textwid, 0.0),
198            (Arrowhead, 1.0),
199            (Fillval, 0.5),
200            (Linethick, 0.8),
201            (Maxpsht, 11.0),
202            (Maxpswid, 8.5),
203            (Scale, 1.0),
204            (Margin, 0.0),
205            (Topmargin, 0.0),
206            (Rightmargin, 0.0),
207            (Bottommargin, 0.0),
208            (Leftmargin, 0.0),
209        ];
210        let mut v = HashMap::new();
211        for (e, d) in defaults {
212            v.insert(ev_key(e), d);
213        }
214        EnvVars { v }
215    }
216    fn get(&self, e: EnvVar) -> f64 {
217        *self.v.get(&ev_key(e)).unwrap_or(&0.0)
218    }
219    fn set(&mut self, e: EnvVar, val: f64) {
220        self.v.insert(ev_key(e), val);
221    }
222}
223
224// ---- placed-object bookkeeping ---------------------------------------------
225
226#[derive(Clone, Copy, PartialEq, Eq)]
227enum PKind {
228    Box,
229    Circle,
230    Ellipse,
231    Line,
232    Move,
233    Spline,
234    Arc,
235    Brace,
236    Block,
237    Text,
238}
239
240#[derive(Clone)]
241struct Placed {
242    kind: PKind,
243    center: Point,
244    bbox: Bbox,
245    start: Point,
246    end: Point,
247    thick: f64,
248    points: Vec<Point>,
249    radius: f64,
250    box_rad: f64,
251    line_wid: f64,
252    line_ht: f64,
253    closed_path: bool,
254    layer: i32,
255    /// Index of the primary shape in `shapes` (None for point-only labels).
256    shape: Option<usize>,
257    /// For blocks: inner labels (sub-objects), translated into parent
258    /// coordinates, so `B.A` / `last [].Outer` resolve. Empty otherwise.
259    members: HashMap<String, Placed>,
260}
261
262impl Placed {
263    fn corner(&self, c: Corner) -> Point {
264        match self.kind {
265            PKind::Circle | PKind::Ellipse => self.ellipse_corner(c),
266            PKind::Line | PKind::Move | PKind::Spline => self.linear_corner(c),
267            PKind::Brace => self.brace_corner(c),
268            PKind::Arc => self.arc_corner(c),
269            PKind::Box => self.box_corner(c),
270            PKind::Block | PKind::Text => self.bbox_corner(c),
271        }
272    }
273
274    fn bbox_corner(&self, c: Corner) -> Point {
275        let (lo, hi) = (self.bbox.min, self.bbox.max);
276        let mid = self.center;
277        match c {
278            Corner::N => Point::new(mid.x, hi.y),
279            Corner::S => Point::new(mid.x, lo.y),
280            Corner::E => Point::new(hi.x, mid.y),
281            Corner::W => Point::new(lo.x, mid.y),
282            Corner::Ne => Point::new(hi.x, hi.y),
283            Corner::Se => Point::new(hi.x, lo.y),
284            Corner::Nw => Point::new(lo.x, hi.y),
285            Corner::Sw => Point::new(lo.x, lo.y),
286            Corner::Center => mid,
287            Corner::Start => self.start,
288            Corner::End => self.end,
289        }
290    }
291
292    fn ellipse_corner(&self, c: Corner) -> Point {
293        let (rx, ry) = (self.bbox.width() / 2.0, self.bbox.height() / 2.0);
294        let diag = |sx: f64, sy: f64| Point::new(sx * rx * FRAC_1_SQRT_2, sy * ry * FRAC_1_SQRT_2);
295        let off = match c {
296            Corner::N => Point::new(0.0, ry),
297            Corner::S => Point::new(0.0, -ry),
298            Corner::E => Point::new(rx, 0.0),
299            Corner::W => Point::new(-rx, 0.0),
300            Corner::Ne => diag(1.0, 1.0),
301            Corner::Se => diag(1.0, -1.0),
302            Corner::Nw => diag(-1.0, 1.0),
303            Corner::Sw => diag(-1.0, -1.0),
304            Corner::Center => Point::ZERO,
305            Corner::Start => return self.start,
306            Corner::End => return self.end,
307        };
308        self.center + off
309    }
310
311    fn linear_corner(&self, c: Corner) -> Point {
312        if self.points.is_empty() {
313            return self.bbox_corner(c);
314        }
315        if self.closed_path {
316            return match c {
317                Corner::Start => self.points[0],
318                Corner::End => *self.points.last().unwrap(),
319                _ => self.bbox_corner(c),
320            };
321        }
322        match c {
323            Corner::Center => (self.points[0] + *self.points.last().unwrap()) * 0.5,
324            Corner::Start => self.points[0],
325            Corner::End => *self.points.last().unwrap(),
326            _ => {
327                let mut best = self.points[0];
328                for p in self.points.iter().skip(1) {
329                    let better = match c {
330                        Corner::N => p.y > best.y,
331                        Corner::S => p.y < best.y,
332                        Corner::E => p.x > best.x,
333                        Corner::W => p.x < best.x,
334                        Corner::Ne => {
335                            (p.y > best.y && p.x >= best.x) || (p.y >= best.y && p.x > best.x)
336                        }
337                        Corner::Se => {
338                            (p.y < best.y && p.x >= best.x) || (p.y <= best.y && p.x > best.x)
339                        }
340                        Corner::Sw => {
341                            (p.y < best.y && p.x <= best.x) || (p.y <= best.y && p.x < best.x)
342                        }
343                        Corner::Nw => {
344                            (p.y > best.y && p.x <= best.x) || (p.y >= best.y && p.x < best.x)
345                        }
346                        Corner::Center | Corner::Start | Corner::End => false,
347                    };
348                    if better {
349                        best = *p;
350                    }
351                }
352                best
353            }
354        }
355    }
356
357    fn brace_corner(&self, c: Corner) -> Point {
358        match c {
359            Corner::Center => self.center,
360            Corner::Start => self.start,
361            Corner::End => self.end,
362            _ => {
363                let mut bb = Bbox::new();
364                for p in &self.points {
365                    bb.add(*p);
366                }
367                if bb.is_empty() {
368                    return self.bbox_corner(c);
369                }
370                bbox_point(&bb, c)
371            }
372        }
373    }
374
375    fn arc_corner(&self, c: Corner) -> Point {
376        let diag = self.radius * FRAC_1_SQRT_2;
377        let off = match c {
378            Corner::N => Point::new(0.0, self.radius),
379            Corner::S => Point::new(0.0, -self.radius),
380            Corner::E => Point::new(self.radius, 0.0),
381            Corner::W => Point::new(-self.radius, 0.0),
382            Corner::Ne => Point::new(diag, diag),
383            Corner::Se => Point::new(diag, -diag),
384            Corner::Nw => Point::new(-diag, diag),
385            Corner::Sw => Point::new(-diag, -diag),
386            Corner::Center => Point::ZERO,
387            Corner::Start => return self.start,
388            Corner::End => return self.end,
389        };
390        self.center + off
391    }
392
393    fn box_corner(&self, c: Corner) -> Point {
394        match c {
395            Corner::Ne | Corner::Se | Corner::Nw | Corner::Sw if self.box_rad > 0.0 => {
396                let inset = self
397                    .box_rad
398                    .min(self.bbox.width().abs().min(self.bbox.height().abs()) / 2.0)
399                    * (1.0 - FRAC_1_SQRT_2);
400                let x = self.bbox.width() / 2.0 - inset;
401                let y = self.bbox.height() / 2.0 - inset;
402                let off = match c {
403                    Corner::Ne => Point::new(x, y),
404                    Corner::Se => Point::new(x, -y),
405                    Corner::Nw => Point::new(-x, y),
406                    Corner::Sw => Point::new(-x, -y),
407                    _ => Point::ZERO,
408                };
409                self.center + off
410            }
411            _ => self.bbox_corner(c),
412        }
413    }
414
415    fn attr_width(&self) -> f64 {
416        match self.kind {
417            PKind::Brace => self.bbox.width(),
418            PKind::Line | PKind::Move | PKind::Spline => self.line_wid,
419            _ => self.bbox.width(),
420        }
421    }
422
423    fn attr_height(&self) -> f64 {
424        match self.kind {
425            PKind::Brace => self.bbox.height(),
426            PKind::Line | PKind::Move | PKind::Spline => self.line_ht,
427            _ => self.bbox.height(),
428        }
429    }
430
431    fn attr_radius(&self) -> f64 {
432        match self.kind {
433            PKind::Box => self.box_rad,
434            PKind::Circle => self.bbox.width() / 2.0,
435            PKind::Arc => self.radius,
436            _ => 0.0,
437        }
438    }
439
440    fn attr_diameter(&self) -> f64 {
441        match self.kind {
442            PKind::Circle => self.bbox.width(),
443            PKind::Arc => self.radius * 2.0,
444            _ => 0.0,
445        }
446    }
447
448    fn attr_length(&self) -> f64 {
449        match self.kind {
450            PKind::Line | PKind::Move | PKind::Spline | PKind::Brace => self.start.dist(self.end),
451            _ => 0.0,
452        }
453    }
454}
455
456fn bbox_point(bb: &Bbox, c: Corner) -> Point {
457    let lo = bb.min;
458    let hi = bb.max;
459    let mid = (lo + hi) * 0.5;
460    match c {
461        Corner::N => Point::new(mid.x, hi.y),
462        Corner::S => Point::new(mid.x, lo.y),
463        Corner::E => Point::new(hi.x, mid.y),
464        Corner::W => Point::new(lo.x, mid.y),
465        Corner::Ne => Point::new(hi.x, hi.y),
466        Corner::Se => Point::new(hi.x, lo.y),
467        Corner::Nw => Point::new(lo.x, hi.y),
468        Corner::Sw => Point::new(lo.x, lo.y),
469        Corner::Center => mid,
470        Corner::Start => lo,
471        Corner::End => hi,
472    }
473}
474
475// ---- evaluator state -------------------------------------------------------
476
477struct State {
478    pos: Point,
479    dir: Dir,
480    vars: HashMap<String, f64>,
481    inherited_vars: HashSet<String>,
482    export_vars: HashSet<String>,
483    env: EnvVars,
484    macros: Macros,
485    base_dir: Option<std::path::PathBuf>,
486    /// Labels visible from an enclosing scope (read-only, in absolute parent
487    /// coordinates). A block may reference outer labels but must not let them
488    /// affect its own `last`/nth/bbox, so they live here, not in `placed`.
489    outer_labels: HashMap<String, Placed>,
490    shapes: Vec<Shape>,
491    shape_layers: Vec<i32>,
492    placed: Vec<Placed>,
493    labels: HashMap<String, usize>,
494    /// Visible geometry and text only; this becomes the final drawing/viewBox.
495    bbox: Bbox,
496    /// All evaluated geometry, including invisible helpers, for block sizing
497    /// and anchor placement.
498    layout_bbox: Bbox,
499    // animation state
500    anims: Vec<Anim>,
501    diagnostics: Vec<String>,
502    anim_cursor: f64,
503    anim_end: HashMap<usize, f64>,
504    rng: GlibcRand,
505}
506
507const DEFAULT_ANIM_DUR: f64 = 0.6;
508
509fn dir_unit(d: Dir) -> Point {
510    match d {
511        Dir::Right => Point::new(1.0, 0.0),
512        Dir::Left => Point::new(-1.0, 0.0),
513        Dir::Up => Point::new(0.0, 1.0),
514        Dir::Down => Point::new(0.0, -1.0),
515    }
516}
517fn horizontal(d: Dir) -> bool {
518    matches!(d, Dir::Right | Dir::Left)
519}
520
521impl State {
522    fn new() -> Self {
523        let mut vars = HashMap::new();
524        install_dpic_compat_vars(&mut vars);
525        State {
526            pos: Point::ZERO,
527            dir: Dir::Right,
528            vars,
529            inherited_vars: HashSet::new(),
530            export_vars: HashSet::new(),
531            env: EnvVars::new(),
532            macros: HashMap::new(),
533            base_dir: None,
534            outer_labels: HashMap::new(),
535            shapes: Vec::new(),
536            shape_layers: Vec::new(),
537            placed: Vec::new(),
538            labels: HashMap::new(),
539            bbox: Bbox::new(),
540            layout_bbox: Bbox::new(),
541            anims: Vec::new(),
542            diagnostics: Vec::new(),
543            anim_cursor: 0.0,
544            anim_end: HashMap::new(),
545            rng: GlibcRand::new(1),
546        }
547    }
548
549    fn eval_stmts(&mut self, stmts: &[Stmt]) -> ER<()> {
550        for s in stmts {
551            self.eval_stmt(s)?;
552        }
553        Ok(())
554    }
555
556    /// Parse a deferred `if`/`for` body now, expanding macros along this path.
557    fn parse_body(&mut self, body: &Body) -> ER<Vec<Stmt>> {
558        crate::parser::parse_body_tokens(body, &mut self.macros, self.base_dir.as_deref())
559            .map_err(|e| EvalError { msg: e.to_string() })
560    }
561
562    fn eval_stmt(&mut self, s: &Stmt) -> ER<()> {
563        match s {
564            Stmt::Direction(d) => {
565                self.dir = *d;
566            }
567            Stmt::Assign(list) => {
568                for a in list {
569                    self.eval_assignment(a)?;
570                }
571            }
572            Stmt::Place { label, pos } => {
573                let p = self.eval_pos(pos)?;
574                let key = self.label_key(label)?;
575                let mut bb = Bbox::new();
576                bb.add(p);
577                let idx = self.placed.len();
578                self.placed.push(Placed {
579                    kind: PKind::Text,
580                    center: p,
581                    bbox: bb,
582                    start: p,
583                    end: p,
584                    thick: 0.0,
585                    points: Vec::new(),
586                    radius: 0.0,
587                    box_rad: 0.0,
588                    line_wid: 0.0,
589                    line_ht: 0.0,
590                    closed_path: false,
591                    layer: 0,
592                    shape: None,
593                    members: HashMap::new(),
594                });
595                self.labels.insert(key, idx);
596            }
597            Stmt::Group(stmts) => {
598                let (pos, dir) = (self.pos, self.dir);
599                self.eval_stmts(stmts)?;
600                self.pos = pos;
601                self.dir = dir;
602            }
603            Stmt::Object { label, object } => {
604                let idx = self.eval_object(object)?;
605                if let Some(l) = label {
606                    let key = self.label_key(l)?;
607                    self.labels.insert(key, idx);
608                }
609            }
610            Stmt::Animate(a) => self.eval_animate(a)?,
611            Stmt::If {
612                cond,
613                then_body,
614                else_body,
615            } => {
616                // only the taken branch is parsed (dead branches may be
617                // syntactically invalid, e.g. an empty default-argument body)
618                if self.eval_expr(cond)? != 0.0 {
619                    let stmts = self.parse_body(then_body)?;
620                    self.eval_stmts(&stmts)?;
621                } else if let Some(e) = else_body {
622                    let stmts = self.parse_body(e)?;
623                    self.eval_stmts(&stmts)?;
624                }
625            }
626            Stmt::For {
627                var,
628                subscript,
629                from,
630                to,
631                by,
632                mult,
633                body,
634            } => {
635                let body = self.parse_body(body)?;
636                let from = self.eval_expr(from)?;
637                let to = self.eval_expr(to)?;
638                let by = self.eval_expr(by)?;
639                let mut v = from;
640                let mut iters = 0u64;
641                const EPS: f64 = 1e-9;
642                loop {
643                    let cont = if *mult {
644                        if by >= 1.0 {
645                            v <= to + EPS
646                        } else {
647                            v >= to - EPS
648                        }
649                    } else if by >= 0.0 {
650                        v <= to + EPS
651                    } else {
652                        v >= to - EPS
653                    };
654                    if !cont {
655                        break;
656                    }
657                    let key = self.indexed_name(var, subscript.as_ref())?;
658                    self.vars.insert(key, v);
659                    self.eval_stmts(&body)?;
660                    let prev = v;
661                    v = if *mult { v * by } else { v + by };
662                    if (v - prev).abs() < f64::EPSILON {
663                        break; // no progress (by 0, or *1) — avoid infinite loop
664                    }
665                    iters += 1;
666                    if iters > 1_000_000 {
667                        return err("for loop exceeded 1,000,000 iterations");
668                    }
669                }
670            }
671            Stmt::Print(item) => match item {
672                PrintItem::Expr(e) => {
673                    let v = self.eval_expr(e)?;
674                    self.diagnostics.push(fmt_num(v));
675                }
676                PrintItem::Str(se) => {
677                    let s = self.eval_stringexpr(se)?;
678                    self.diagnostics.push(s);
679                }
680            },
681            Stmt::Exec { command, arg_frame } => {
682                let src = unescape_exec_source(&self.eval_stringexpr(command)?);
683                let stmts = crate::parser::parse_exec_source(
684                    &src,
685                    &self.macros,
686                    self.base_dir.as_deref(),
687                    arg_frame.as_deref(),
688                )
689                .map_err(|e| EvalError { msg: e.to_string() })?;
690                self.eval_stmts(&stmts)?;
691            }
692            Stmt::Reset(list) => {
693                if list.is_empty() {
694                    self.env = EnvVars::new();
695                } else {
696                    let d = EnvVars::new();
697                    for e in list {
698                        self.env.set(*e, d.get(*e));
699                    }
700                }
701            }
702        }
703        Ok(())
704    }
705
706    fn eval_animate(&mut self, a: &Animate) -> ER<()> {
707        let idx = self.place_index(&a.target)?;
708        let shape = self.placed[idx].shape.ok_or_else(|| EvalError {
709            msg: "cannot animate a point (no drawn shape)".into(),
710        })?;
711        let dur = match &a.duration {
712            Some(e) => self.eval_expr(e)?,
713            None => DEFAULT_ANIM_DUR,
714        };
715        let mut start = match &a.timing {
716            Timing::Sequential => self.anim_cursor,
717            Timing::At(e) => self.eval_expr(e)?,
718            Timing::After(p) => {
719                let i = self.place_index(p)?;
720                let sh = self.placed[i].shape.ok_or_else(|| EvalError {
721                    msg: "`after` target has no animation".into(),
722                })?;
723                *self.anim_end.get(&sh).unwrap_or(&0.0)
724            }
725        };
726        if let Some(d) = &a.delay {
727            start += self.eval_expr(d)?;
728        }
729        let end = start + dur;
730        self.anim_cursor = end;
731        self.anim_end.insert(shape, end);
732        self.anims.push(Anim {
733            shape,
734            effect: stringexpr_lit(&a.effect),
735            start,
736            duration: dur,
737        });
738        Ok(())
739    }
740
741    fn scale_value(&self) -> ER<f64> {
742        let scale = self.env.get(EnvVar::Scale);
743        if scale.abs() < 1e-12 {
744            return err("scale must be non-zero");
745        }
746        Ok(scale)
747    }
748
749    /// Convert a pic dimension from the current user units to internal inches.
750    fn to_internal_dim(&self, v: f64) -> ER<f64> {
751        Ok(v / self.scale_value()?)
752    }
753
754    fn env_dim(&self, e: EnvVar) -> ER<f64> {
755        self.to_internal_dim(self.env.get(e))
756    }
757
758    fn canvas_margin(&self) -> ER<CanvasMargin> {
759        let all = self.env_dim(EnvVar::Margin)?;
760        Ok(CanvasMargin {
761            top: all + self.env_dim(EnvVar::Topmargin)?,
762            right: all + self.env_dim(EnvVar::Rightmargin)?,
763            bottom: all + self.env_dim(EnvVar::Bottommargin)?,
764            left: all + self.env_dim(EnvVar::Leftmargin)?,
765        })
766    }
767
768    fn expr_dim(&mut self, e: &Expr) -> ER<f64> {
769        let v = self.eval_expr(e)?;
770        self.to_internal_dim(v)
771    }
772
773    /// Convert an internal geometric length back to pic's current user units.
774    fn to_user_dim(&self, v: f64) -> f64 {
775        v * self.env.get(EnvVar::Scale)
776    }
777
778    fn eval_assignment(&mut self, a: &Assignment) -> ER<()> {
779        let rhs = self.eval_expr(&a.value)?;
780        match &a.target {
781            AssignTarget::Var(name, subscript) => {
782                let key = self.indexed_name(name, subscript.as_ref())?;
783                let cur = match self.vars.get(&key).copied() {
784                    Some(v) => v,
785                    None if matches!(a.op, AssignOp::Set) => 0.0,
786                    None => return err(format!("variable not found `{key}`")),
787                };
788                let val = apply_op(a.op, cur, rhs)?;
789                if !matches!(a.op, AssignOp::Set) && self.inherited_vars.contains(&key) {
790                    self.export_vars.insert(key.clone());
791                }
792                self.vars.insert(key, val);
793            }
794            AssignTarget::Env(e) => {
795                let cur = self.env.get(*e);
796                let val = apply_op(a.op, cur, rhs)?;
797                if matches!(e, EnvVar::Scale) {
798                    if val.abs() < 1e-12 {
799                        return err("scale must be non-zero");
800                    }
801                    if cur.abs() >= 1e-12 {
802                        self.scale_existing_geometry(cur / val);
803                    }
804                    // Changing `scale` rescales all scaled dimension variables by
805                    // the ratio (dpic semantics). They remain in user units;
806                    // geometry converts them to internal inches by dividing by
807                    // the current scale at use sites.
808                    let ratio = if cur != 0.0 { val / cur } else { val };
809                    for sv in SCALED_VARS {
810                        let v = self.env.get(sv);
811                        self.env.set(sv, v * ratio);
812                    }
813                }
814                self.env.set(*e, val);
815            }
816        }
817        Ok(())
818    }
819
820    fn scale_existing_geometry(&mut self, factor: f64) {
821        if (factor - 1.0).abs() < 1e-12 {
822            return;
823        }
824        self.pos = self.pos * factor;
825        for sh in &mut self.shapes {
826            scale_shape(sh, factor);
827        }
828        for pl in &mut self.placed {
829            scale_placed(pl, factor);
830        }
831        scale_bbox_in_place(&mut self.bbox, factor);
832        scale_bbox_in_place(&mut self.layout_bbox, factor);
833    }
834
835    // ---- objects -----------------------------------------------------------
836
837    fn eval_object(&mut self, obj: &Object) -> ER<usize> {
838        match &obj.kind {
839            ObjectKind::Primitive(p) => match p {
840                Prim::Box | Prim::Circle | Prim::Ellipse => self.closed(*p, obj),
841                Prim::Line | Prim::Arrow | Prim::Move | Prim::Spline => self.open(*p, obj),
842                Prim::Arc => self.arc(obj),
843            },
844            ObjectKind::Text => self.text_obj(obj),
845            ObjectKind::Brace => self.brace(obj),
846            ObjectKind::Block(stmts) => self.block(stmts, obj),
847            ObjectKind::Empty => self.block(&[], obj),
848            ObjectKind::Continue => self.continue_obj(obj),
849        }
850    }
851
852    /// `continue`: append another segment to the most recent line/spline,
853    /// extending it from its current end in the current (or given) direction.
854    fn continue_obj(&mut self, obj: &Object) -> ER<usize> {
855        let idx = self
856            .shapes
857            .iter()
858            .rposition(|s| matches!(s, Shape::Path { .. } | Shape::Spline { .. }))
859            .ok_or_else(|| EvalError {
860                msg: "`continue` has no previous line to extend".into(),
861            })?;
862        let start = match &self.shapes[idx] {
863            Shape::Path { pts, .. } | Shape::Spline { pts, .. } => *pts.last().unwrap(),
864            _ => unreachable!(),
865        };
866        let deflen_h = self.env_dim(EnvVar::Linewid)?;
867        let deflen_v = self.env_dim(EnvVar::Lineht)?;
868
869        let mut pts = vec![start];
870        let mut pend = Point::ZERO;
871        let mut any = false;
872        let mut last_dir = self.dir;
873        for a in &obj.attrs {
874            match a {
875                Attr::Direction(d, opt) => {
876                    let dist = match opt {
877                        Some(e) => self.expr_dim(e)?,
878                        None => {
879                            if horizontal(*d) {
880                                deflen_h
881                            } else {
882                                deflen_v
883                            }
884                        }
885                    };
886                    pend = pend + dir_unit(*d) * dist;
887                    last_dir = *d;
888                    any = true;
889                }
890                Attr::Then => {
891                    let np = *pts.last().unwrap() + pend;
892                    pts.push(np);
893                    pend = Point::ZERO;
894                }
895                Attr::To(pos) => {
896                    if pend != Point::ZERO {
897                        let np = *pts.last().unwrap() + pend;
898                        pts.push(np);
899                        pend = Point::ZERO;
900                    }
901                    pts.push(self.eval_pos(pos)?);
902                    any = true;
903                }
904                Attr::By(pos) => {
905                    let dp = self.eval_pos(pos)?;
906                    pend = pend + (dp - Point::ZERO);
907                    any = true;
908                }
909                Attr::Dist(e) => {
910                    let dist = self.expr_dim(e)?;
911                    pend = pend + dir_unit(last_dir) * dist;
912                    any = true;
913                }
914                _ => {}
915            }
916        }
917        if pend != Point::ZERO {
918            let np = *pts.last().unwrap() + pend;
919            pts.push(np);
920        }
921        if pts.len() == 1 && !any {
922            let dist = if horizontal(self.dir) {
923                deflen_h
924            } else {
925                deflen_v
926            };
927            pts.push(start + dir_unit(self.dir) * dist);
928            last_dir = self.dir;
929        }
930
931        let new: Vec<Point> = pts[1..].to_vec();
932        let visible = match &mut self.shapes[idx] {
933            Shape::Path { pts: p, style, .. } | Shape::Spline { pts: p, style, .. } => {
934                p.extend_from_slice(&new);
935                (!style.invis, stroke_half_width(style))
936            }
937            _ => unreachable!(),
938        };
939        let end = *pts.last().unwrap();
940        let mut bb = Bbox::new();
941        for q in &new {
942            self.layout_bbox.add(*q);
943            bb.add(*q);
944        }
945        if visible.0 {
946            self.bbox.union(&painted_bbox(&bb, visible.1));
947        }
948        self.pos = end;
949        self.dir = last_dir;
950
951        let pidx = self.placed.iter().position(|pl| pl.shape == Some(idx));
952        if let Some(pi) = pidx {
953            self.placed[pi].end = end;
954            self.placed[pi].bbox.add(end);
955            self.placed[pi].center = (self.placed[pi].start + end) * 0.5;
956            self.placed[pi].points.extend_from_slice(&new);
957        }
958        Ok(pidx.unwrap_or(idx))
959    }
960
961    fn closed(&mut self, p: Prim, obj: &Object) -> ER<usize> {
962        let style = self.style_of(obj)?;
963        let (text, fit_text) = self.text_and_fit_text_of(obj)?;
964        let dir = self.dir_of(obj);
965        let scale = self.scale_of(obj)?;
966
967        // dimensions — `same` reuses the previous like-object's size as the
968        // default; explicit ht/wid/rad still override.
969        let prev = if obj.attrs.iter().any(|a| matches!(a, Attr::Same)) {
970            self.last_dims_of(p)
971        } else {
972            None
973        };
974        let (mut w, mut h, mut rad);
975        match p {
976            Prim::Circle => {
977                let def_r = prev
978                    .map(|(pw, _)| pw / 2.0)
979                    .unwrap_or(self.env_dim(EnvVar::Circlerad)?);
980                let r = self.dim(obj, DimKind::Rad)?.unwrap_or(def_r);
981                let r = self.dim(obj, DimKind::Diam)?.map(|d| d / 2.0).unwrap_or(r);
982                w = 2.0 * r;
983                h = 2.0 * r;
984                rad = r;
985            }
986            Prim::Ellipse => {
987                let (dw, dh) = prev.unwrap_or((
988                    self.env_dim(EnvVar::Ellipsewid)?,
989                    self.env_dim(EnvVar::Ellipseht)?,
990                ));
991                w = self.dim(obj, DimKind::Wid)?.unwrap_or(dw);
992                h = self.dim(obj, DimKind::Ht)?.unwrap_or(dh);
993                rad = 0.0;
994            }
995            _ => {
996                let (dw, dh) =
997                    prev.unwrap_or((self.env_dim(EnvVar::Boxwid)?, self.env_dim(EnvVar::Boxht)?));
998                w = self.dim(obj, DimKind::Wid)?.unwrap_or(dw);
999                h = self.dim(obj, DimKind::Ht)?.unwrap_or(dh);
1000                rad = self
1001                    .dim(obj, DimKind::Rad)?
1002                    .unwrap_or(self.env_dim(EnvVar::Boxrad)?);
1003            }
1004        }
1005        if let Some(fit_text) = fit_text {
1006            let (fit_w, fit_h) = fitted_text_size(&fit_text).ok_or_else(|| EvalError {
1007                msg: "`fit` requires visible text before the attribute".into(),
1008            })?;
1009            match p {
1010                Prim::Circle => {
1011                    if !self.has_dim(obj, DimKind::Rad) && !self.has_dim(obj, DimKind::Diam) {
1012                        let diam = fit_w.hypot(fit_h);
1013                        w = diam;
1014                        h = diam;
1015                        rad = diam / 2.0;
1016                    }
1017                }
1018                Prim::Ellipse => {
1019                    if !self.has_dim(obj, DimKind::Wid) {
1020                        w = fit_w;
1021                    }
1022                    if !self.has_dim(obj, DimKind::Ht) {
1023                        h = fit_h;
1024                    }
1025                }
1026                _ => {
1027                    if !self.has_dim(obj, DimKind::Wid) {
1028                        w = fit_w;
1029                    }
1030                    if !self.has_dim(obj, DimKind::Ht) {
1031                        h = fit_h;
1032                    }
1033                }
1034            }
1035        }
1036        w *= scale;
1037        h *= scale;
1038        rad *= scale;
1039
1040        let extent = if horizontal(dir) { w } else { h };
1041        let center = self.place_closed_center(p, obj, dir, extent, (w, h), rad)?;
1042
1043        let mut bb = Bbox::new();
1044        bb.add(center - Point::new(w / 2.0, h / 2.0));
1045        bb.add(center + Point::new(w / 2.0, h / 2.0));
1046        let layout_bb = if matches!(p, Prim::Box) {
1047            dpic_box_layout_bbox(center, w, h)
1048        } else {
1049            bb
1050        };
1051        self.layout_bbox.union(&layout_bb);
1052        if closed_shape_is_visible(&style) {
1053            self.bbox
1054                .union(&painted_bbox(&bb, stroke_half_width(&style)));
1055        }
1056        self.union_text(center, &text);
1057
1058        let shape = match p {
1059            Prim::Circle => Shape::Circle {
1060                c: center,
1061                r: rad,
1062                style,
1063                text,
1064            },
1065            Prim::Ellipse => Shape::Ellipse {
1066                c: center,
1067                w,
1068                h,
1069                style,
1070                text,
1071            },
1072            _ => Shape::Box {
1073                c: center,
1074                w,
1075                h,
1076                rad,
1077                style,
1078                text,
1079            },
1080        };
1081        let layer = self.layer_of(obj, 0)?;
1082        self.push_shape(shape, layer);
1083
1084        let half = dir_unit(dir) * (extent / 2.0);
1085        let start = center - half;
1086        let end = center + half;
1087        self.pos = end;
1088        self.dir = dir;
1089        let kind = match p {
1090            Prim::Circle => PKind::Circle,
1091            Prim::Ellipse => PKind::Ellipse,
1092            _ => PKind::Box,
1093        };
1094        let sh = self.shapes.len() - 1;
1095        let idx = self.record(kind, center, bb, start, end, 0.0, Some(sh));
1096        if matches!(kind, PKind::Box) {
1097            self.placed[idx].box_rad = rad;
1098        }
1099        Ok(idx)
1100    }
1101
1102    fn brace(&mut self, obj: &Object) -> ER<usize> {
1103        let style = self.style_of(obj)?;
1104        let text = self.text_of(obj)?;
1105        let start = self.find_from(obj)?.unwrap_or(self.pos);
1106        let has_to = obj.attrs.iter().any(|a| matches!(a, Attr::To(_)));
1107        let (end, last_dir) = self.brace_end(obj, start, has_to)?;
1108        let v = end - start;
1109        let len = v.len();
1110        if len <= 1e-12 {
1111            return err("brace endpoints must be distinct");
1112        }
1113
1114        let depth = self
1115            .dim(obj, DimKind::Wid)?
1116            .unwrap_or(DEFAULT_BRACE_DEPTH)
1117            .abs();
1118        let pos = self.brace_pos_of(obj)?;
1119        let label_offset = self.brace_label_offset_of(obj)?;
1120        let side = brace_side(v / len, self.brace_side_dir(obj, has_to));
1121        let cubics = brace_cubics(start, end, side * depth, pos);
1122        let cusp = brace_cusp(&cubics).unwrap_or(start + v * pos);
1123        let label_at =
1124            cusp + side * (self.env_dim(EnvVar::Textoffset)? + TEXT_LINE_H + label_offset);
1125        let mut bb = cubics_bbox(&cubics);
1126        self.layout_bbox.union(&bb);
1127        if !style.invis {
1128            self.bbox
1129                .union(&painted_bbox(&bb, stroke_half_width(&style)));
1130        } else if style.invis_bounds {
1131            self.bbox.union(&bb);
1132        }
1133        let text_bb = text_bbox(label_at, &text);
1134        self.bbox.union(&text_bb);
1135        bb.union(&text_bb);
1136
1137        let shape = Shape::Brace {
1138            a: start,
1139            b: end,
1140            cubics: cubics.clone(),
1141            label_at,
1142            style,
1143            text,
1144        };
1145        let layer = self.layer_of(obj, 0)?;
1146        self.push_shape(shape, layer);
1147
1148        self.pos = end;
1149        self.dir = last_dir;
1150        let sh = self.shapes.len() - 1;
1151        let idx = self.record(PKind::Brace, cusp, bb, start, end, 0.0, Some(sh));
1152        self.placed[idx].points = sample_cubics(&cubics, 6);
1153        Ok(idx)
1154    }
1155
1156    fn brace_end(&mut self, obj: &Object, start: Point, has_to: bool) -> ER<(Point, Dir)> {
1157        if has_to {
1158            let mut end = None;
1159            for a in &obj.attrs {
1160                if let Attr::To(pos) = a {
1161                    end = Some(self.eval_pos(pos)?);
1162                }
1163            }
1164            let end = end.unwrap();
1165            return Ok((end, nearest_dir(end - start)));
1166        }
1167
1168        let mut pend = Point::ZERO;
1169        let mut any = false;
1170        let mut last_dir = self.dir;
1171        for a in &obj.attrs {
1172            match a {
1173                Attr::Direction(d, opt) => {
1174                    let dist = match opt {
1175                        Some(e) => self.expr_dim(e)?,
1176                        None => {
1177                            if horizontal(*d) {
1178                                self.env_dim(EnvVar::Linewid)?
1179                            } else {
1180                                self.env_dim(EnvVar::Lineht)?
1181                            }
1182                        }
1183                    };
1184                    pend = pend + dir_unit(*d) * dist;
1185                    last_dir = *d;
1186                    any = true;
1187                }
1188                Attr::By(pos) => {
1189                    pend = pend + self.eval_pos(pos)?;
1190                    any = true;
1191                }
1192                Attr::Dist(e) => {
1193                    let dist = self.expr_dim(e)?;
1194                    pend = pend + dir_unit(last_dir) * dist;
1195                    any = true;
1196                }
1197                _ => {}
1198            }
1199        }
1200        if any {
1201            Ok((start + pend, last_dir))
1202        } else {
1203            let dist = if horizontal(self.dir) {
1204                self.env_dim(EnvVar::Linewid)?
1205            } else {
1206                self.env_dim(EnvVar::Lineht)?
1207            };
1208            Ok((start + dir_unit(self.dir) * dist, self.dir))
1209        }
1210    }
1211
1212    fn brace_side_dir(&self, obj: &Object, has_to: bool) -> Option<Dir> {
1213        if !has_to {
1214            return None;
1215        }
1216        obj.attrs.iter().rev().find_map(|a| match a {
1217            Attr::Direction(d, None) => Some(*d),
1218            _ => None,
1219        })
1220    }
1221
1222    fn brace_pos_of(&mut self, obj: &Object) -> ER<f64> {
1223        let mut pos = DEFAULT_BRACE_POS;
1224        for a in &obj.attrs {
1225            if let Attr::BracePos(e) = a {
1226                pos = self.eval_expr(e)?;
1227            }
1228        }
1229        if !pos.is_finite() || pos <= 0.0 || pos >= 1.0 {
1230            return err("bracepos must be between 0 and 1");
1231        }
1232        Ok(pos)
1233    }
1234
1235    fn brace_label_offset_of(&mut self, obj: &Object) -> ER<f64> {
1236        let mut offset = 0.0;
1237        for a in &obj.attrs {
1238            if let Attr::BraceLabelOffset(e) = a {
1239                offset = self.expr_dim(e)?;
1240            }
1241        }
1242        Ok(offset)
1243    }
1244
1245    fn open(&mut self, p: Prim, obj: &Object) -> ER<usize> {
1246        let mut style = self.style_of(obj)?;
1247        let text = self.text_of(obj)?;
1248        let line_wid = style.arrow_wid;
1249        let line_ht = style.arrow_ht;
1250        let is_move = matches!(p, Prim::Move);
1251        if is_move {
1252            style.invis = true;
1253            style.invis_bounds = true;
1254        }
1255        let (deflen_h, deflen_v) = if is_move {
1256            (
1257                self.env_dim(EnvVar::Movewid)?,
1258                self.env_dim(EnvVar::Moveht)?,
1259            )
1260        } else {
1261            (
1262                self.env_dim(EnvVar::Linewid)?,
1263                self.env_dim(EnvVar::Lineht)?,
1264            )
1265        };
1266        let same_vec = if obj.attrs.iter().any(|a| matches!(a, Attr::Same)) {
1267            self.last_open_vector(p)
1268        } else {
1269            None
1270        };
1271
1272        // starting point
1273        let start = self.find_from(obj)?.unwrap_or(self.pos);
1274        let mut pts = vec![start];
1275        let mut pend = Point::ZERO;
1276        let mut any = false;
1277        let mut last_dir = self.dir;
1278        let mut closed = false;
1279
1280        for a in &obj.attrs {
1281            match a {
1282                Attr::Direction(d, opt) => {
1283                    if closed {
1284                        return err("polygon is closed");
1285                    }
1286                    let dist = match opt {
1287                        Some(e) => self.expr_dim(e)?,
1288                        None => {
1289                            if horizontal(*d) {
1290                                deflen_h
1291                            } else {
1292                                deflen_v
1293                            }
1294                        }
1295                    };
1296                    pend = pend + dir_unit(*d) * dist;
1297                    last_dir = *d;
1298                    any = true;
1299                }
1300                Attr::Then => {
1301                    if closed {
1302                        return err("polygon is closed");
1303                    }
1304                    let np = *pts.last().unwrap() + pend;
1305                    pts.push(np);
1306                    pend = Point::ZERO;
1307                }
1308                Attr::To(pos) => {
1309                    if closed {
1310                        return err("polygon is closed");
1311                    }
1312                    if pend != Point::ZERO {
1313                        let np = *pts.last().unwrap() + pend;
1314                        pts.push(np);
1315                        pend = Point::ZERO;
1316                    }
1317                    pts.push(self.eval_pos(pos)?);
1318                    any = true;
1319                }
1320                Attr::By(pos) => {
1321                    if closed {
1322                        return err("polygon is closed");
1323                    }
1324                    let d = self.eval_pos(pos)?;
1325                    pend = pend + (d - Point::ZERO);
1326                    any = true;
1327                }
1328                Attr::Dist(e) => {
1329                    if closed {
1330                        return err("polygon is closed");
1331                    }
1332                    let dist = self.expr_dim(e)?;
1333                    pend = pend + dir_unit(last_dir) * dist;
1334                    any = true;
1335                }
1336                Attr::Close => {
1337                    if pend != Point::ZERO {
1338                        let np = *pts.last().unwrap() + pend;
1339                        pts.push(np);
1340                        pend = Point::ZERO;
1341                    }
1342                    if pts.len() < 3 {
1343                        return err("need at least 3 vertices in order to close the polygon");
1344                    }
1345                    if closed {
1346                        return err("polygon already closed");
1347                    }
1348                    let first = pts[0];
1349                    let last = *pts.last().unwrap();
1350                    if first.dist(last) > 1e-12 {
1351                        let closing = first - last;
1352                        pts.push(first);
1353                        last_dir = nearest_dir(closing);
1354                    }
1355                    closed = true;
1356                    any = true;
1357                }
1358                _ => {}
1359            }
1360        }
1361        if pend != Point::ZERO {
1362            let np = *pts.last().unwrap() + pend;
1363            pts.push(np);
1364        }
1365        if pts.len() == 1 && !any {
1366            if let Some(v) = same_vec.filter(|v| v.len() > 1e-12) {
1367                pts.push(start + v);
1368                last_dir = nearest_dir(v);
1369            } else {
1370                // bare line/arrow/move in the current direction
1371                let dist = if horizontal(self.dir) {
1372                    deflen_h
1373                } else {
1374                    deflen_v
1375                };
1376                pts.push(start + dir_unit(self.dir) * dist);
1377                last_dir = self.dir;
1378            }
1379        }
1380
1381        // `chop`: positive values trim each end; negative values extend it,
1382        // as in dpic examples such as `chop 0 chop -0.1`.
1383        if let Some((start_chop, end_chop)) = self.chop_of(obj)?
1384            && pts.len() >= 2
1385        {
1386            let n = pts.len();
1387            if start_chop != 0.0 {
1388                let d0 = pts[1] - pts[0];
1389                let l0 = d0.len();
1390                if start_chop < 0.0 || l0 > start_chop {
1391                    pts[0] = pts[0] + d0 / l0 * start_chop;
1392                }
1393            }
1394            if end_chop != 0.0 {
1395                let d1 = pts[n - 2] - pts[n - 1];
1396                let l1 = d1.len();
1397                if end_chop < 0.0 || l1 > end_chop {
1398                    pts[n - 1] = pts[n - 1] + d1 / l1 * end_chop;
1399                }
1400            }
1401        }
1402
1403        // arrowheads
1404        let arrows = self.arrows_of(obj, matches!(p, Prim::Arrow));
1405
1406        let mut bb = Bbox::new();
1407        for pt in &pts {
1408            bb.add(*pt);
1409        }
1410        self.layout_bbox.union(&bb);
1411        if !style.invis {
1412            self.bbox
1413                .union(&painted_bbox(&bb, stroke_half_width(&style)));
1414        } else if style.invis_bounds {
1415            self.bbox.union(&bb);
1416        }
1417
1418        let end = *pts.last().unwrap();
1419        let center = if closed {
1420            (bb.min + bb.max) * 0.5
1421        } else {
1422            (pts[0] + end) * 0.5
1423        };
1424        self.union_text(center, &text);
1425        let kind = match p {
1426            Prim::Spline => PKind::Spline,
1427            Prim::Move => PKind::Move,
1428            _ => PKind::Line,
1429        };
1430        let tension = if matches!(p, Prim::Spline) {
1431            let mut t = None;
1432            for a in &obj.attrs {
1433                if let Attr::SplineTension(e) = a {
1434                    t = Some(self.eval_expr(e)?);
1435                }
1436            }
1437            t
1438        } else {
1439            None
1440        };
1441        let shape = if matches!(p, Prim::Spline) {
1442            Shape::Spline {
1443                pts: pts.clone(),
1444                tension,
1445                arrows,
1446                style,
1447                text,
1448            }
1449        } else {
1450            Shape::Path {
1451                pts: pts.clone(),
1452                closed,
1453                arrows,
1454                style,
1455                text,
1456            }
1457        };
1458        let layer = self.layer_of(obj, 0)?;
1459        self.push_shape(shape, layer);
1460
1461        self.pos = end;
1462        self.dir = last_dir;
1463        let sh = self.shapes.len() - 1;
1464        let idx = self.record(kind, center, bb, pts[0], end, 0.0, Some(sh));
1465        self.placed[idx].points = pts;
1466        self.placed[idx].line_wid = line_wid;
1467        self.placed[idx].line_ht = line_ht;
1468        self.placed[idx].closed_path = closed;
1469        Ok(idx)
1470    }
1471
1472    fn arc(&mut self, obj: &Object) -> ER<usize> {
1473        let mut style = self.style_of(obj)?;
1474        if let Some(wid) = self.dim(obj, DimKind::Wid)? {
1475            style.arrow_wid = wid;
1476        }
1477        if let Some(ht) = self.dim(obj, DimKind::Ht)? {
1478            style.arrow_ht = ht;
1479        }
1480        let text = self.text_of(obj)?;
1481        let start = self.find_from(obj)?.unwrap_or(self.pos);
1482        let cw = self.arc_cw_of(obj);
1483        let arc_dir = self.dir_of(obj);
1484        let rad_attr = self.dim(obj, DimKind::Rad)?;
1485        let to = self.dest_of(obj)?;
1486        let explicit_center = if to.is_some() {
1487            self.arc_explicit_center(obj)?
1488        } else {
1489            None
1490        };
1491
1492        // (center, radius, start angle, end angle)
1493        let (center, r, a0, a1) = if let Some(end) = to {
1494            // arc from `start` to `to`, optional radius
1495            let chord = end - start;
1496            let clen = chord.len();
1497            if clen < 1e-9 {
1498                return err("degenerate arc: `from` and `to` coincide");
1499            }
1500            let r = rad_attr
1501                .unwrap_or(self.env_dim(EnvVar::Arcrad)?)
1502                .max(clen / 2.0);
1503            let dx = chord.x;
1504            let dy = chord.y;
1505            let ts = dx * dx + dy * dy;
1506            let mut t = ((4.0 * r * r - ts).max(0.0) / ts).sqrt();
1507            let arc_sign = if cw { -1.0 } else { 1.0 };
1508            // Dpic uses the prevailing direction to choose between the two
1509            // possible circle centers for a chord/radius pair.
1510            match arc_dir {
1511                Dir::Up => {
1512                    if arc_sign * ((-dx) - (t * dy)) < 0.0 {
1513                        t = -t;
1514                    }
1515                }
1516                Dir::Down => {
1517                    if arc_sign * ((-dx) - (t * dy)) > 0.0 {
1518                        t = -t;
1519                    }
1520                }
1521                Dir::Right => {
1522                    if arc_sign * (dy - (t * dx)) < 0.0 {
1523                        t = -t;
1524                    }
1525                }
1526                Dir::Left => {
1527                    if arc_sign * (dy - (t * dx)) > 0.0 {
1528                        t = -t;
1529                    }
1530                }
1531            }
1532            let center = start + Point::new(0.5 * (dx + t * dy), 0.5 * (dy - t * dx));
1533            let (center, r) = if let Some(center) = explicit_center {
1534                (center, end.dist(center))
1535            } else {
1536                (center, r)
1537            };
1538            let (a0, a1) = arc_angles(center, start, end, cw);
1539            (center, r, a0, a1)
1540        } else {
1541            // default: a quarter turn from the current heading
1542            let r = rad_attr.unwrap_or(self.env_dim(EnvVar::Arcrad)?);
1543            let din = dir_unit(self.dir);
1544            let normal = if cw {
1545                Point::new(din.y, -din.x)
1546            } else {
1547                Point::new(-din.y, din.x)
1548            };
1549            let center = start + normal * r;
1550            let a0 = (start - center).y.atan2((start - center).x);
1551            let sweep = if cw { -PI / 2.0 } else { PI / 2.0 };
1552            (center, r, a0, a0 + sweep)
1553        };
1554
1555        let at = |t: f64| center + Point::new(t.cos(), t.sin()) * r;
1556        let end = at(a1);
1557        let arrows = self.arrows_of(obj, false);
1558
1559        let mut bb = Bbox::new();
1560        bb.add(start);
1561        bb.add(end);
1562        for k in 0..=12 {
1563            bb.add(at(a0 + (a1 - a0) * (k as f64 / 12.0)));
1564        }
1565        self.layout_bbox.union(&bb);
1566        if !style.invis {
1567            self.bbox
1568                .union(&painted_bbox(&bb, stroke_half_width(&style)));
1569        }
1570        self.union_text(center, &text);
1571
1572        let layer = self.layer_of(obj, 0)?;
1573        self.push_shape(
1574            Shape::Arc {
1575                c: center,
1576                r,
1577                a0,
1578                a1,
1579                cw,
1580                arrows,
1581                style,
1582                text,
1583            },
1584            layer,
1585        );
1586
1587        // new heading is the tangent at the end point
1588        let tang = if a1 >= a0 {
1589            Point::new(-a1.sin(), a1.cos())
1590        } else {
1591            Point::new(a1.sin(), -a1.cos())
1592        };
1593        self.dir = nearest_dir(tang);
1594        self.pos = end;
1595        let sh = self.shapes.len() - 1;
1596        let idx = self.record(PKind::Arc, center, bb, start, end, 0.0, Some(sh));
1597        self.placed[idx].radius = r;
1598        Ok(idx)
1599    }
1600
1601    fn arc_cw_of(&self, obj: &Object) -> bool {
1602        obj.attrs
1603            .iter()
1604            .rev()
1605            .find_map(|a| match a {
1606                Attr::Cw => Some(true),
1607                Attr::Ccw => Some(false),
1608                _ => None,
1609            })
1610            .unwrap_or(false)
1611    }
1612
1613    fn arc_explicit_center(&mut self, obj: &Object) -> ER<Option<Point>> {
1614        for a in &obj.attrs {
1615            match a {
1616                Attr::At(pos) => return Ok(Some(self.eval_pos(pos)?)),
1617                Attr::With {
1618                    anchor: WithAnchor::Plain | WithAnchor::Corner(Corner::Center),
1619                    at,
1620                } => {
1621                    return Ok(Some(self.eval_pos(at)?));
1622                }
1623                _ => {}
1624            }
1625        }
1626        Ok(None)
1627    }
1628
1629    fn text_obj(&mut self, obj: &Object) -> ER<usize> {
1630        let text = self.text_of(obj)?;
1631        if obj.attrs.iter().any(|a| matches!(a, Attr::Opacity(_))) {
1632            return err("opacity applies only to filled regions");
1633        }
1634        let dir = self.dir_of(obj);
1635        let w = self
1636            .dim(obj, DimKind::Wid)?
1637            .unwrap_or(self.env_dim(EnvVar::Textwid)?);
1638        let h = match self.dim(obj, DimKind::Ht)? {
1639            Some(h) => h,
1640            None => self.env_dim(EnvVar::Textht)? * text.len().max(1) as f64,
1641        };
1642        let extent = if horizontal(dir) { w } else { h };
1643        let at = self.place_center(obj, dir, extent, w, h)?;
1644        let mut bb = Bbox::new();
1645        bb.add(at - Point::new(w / 2.0, h / 2.0));
1646        bb.add(at + Point::new(w / 2.0, h / 2.0));
1647        self.layout_bbox.union(&bb);
1648        let text_bb = text_object_bbox(at, &text, w, h);
1649        self.bbox.union(&text_bb);
1650        let layer = self.layer_of(obj, 0)?;
1651        self.push_shape(
1652            Shape::Text {
1653                at,
1654                text,
1655                bbox: text_bb,
1656                w,
1657                h,
1658                standalone: true,
1659            },
1660            layer,
1661        );
1662        let half = dir_unit(dir) * (extent / 2.0);
1663        let start = at - half;
1664        let end = at + half;
1665        self.pos = end;
1666        self.dir = dir;
1667        let sh = self.shapes.len() - 1;
1668        Ok(self.record(PKind::Text, at, bb, start, end, 0.0, Some(sh)))
1669    }
1670
1671    /// Union an estimated text extent into the drawing bbox, so wide labels and
1672    /// bare text objects aren't clipped by the SVG viewBox.
1673    fn union_text(&mut self, center: Point, lines: &[TextLine]) {
1674        let bb = text_bbox(center, lines);
1675        self.bbox.union(&bb);
1676    }
1677
1678    fn block(&mut self, stmts: &[Stmt], obj: &Object) -> ER<usize> {
1679        let block_text = self.text_of(obj)?;
1680        let block_fill_opacity = self.style_of(obj)?.fill_opacity;
1681        // Evaluate the block in a local scope at its own origin. Labels from
1682        // the containing scope are visible for references such as `$1.start`
1683        // inside macro-generated blocks, but are not captured as new members.
1684        let mut sub = State::new();
1685        sub.env = self.env.clone();
1686        sub.vars = self.vars.clone();
1687        sub.inherited_vars = self.vars.keys().cloned().collect();
1688        sub.export_vars.clear();
1689        sub.macros = self.macros.clone();
1690        sub.base_dir = self.base_dir.clone();
1691        sub.rng = self.rng.clone();
1692        // expose this scope's labels (read-only, absolute coords) to the block
1693        sub.outer_labels = self.outer_labels.clone();
1694        for (name, &i) in &self.labels {
1695            sub.outer_labels
1696                .insert(name.clone(), self.placed[i].clone());
1697        }
1698        sub.eval_stmts(stmts)?;
1699        // Variables, parameters and direction changes inside `[ ... ]` are
1700        // local to the block. Random draws still consume the shared sequence.
1701        self.rng = sub.rng.clone();
1702        self.diagnostics.append(&mut sub.diagnostics);
1703        for key in &sub.export_vars {
1704            if let Some(val) = sub.vars.get(key).copied() {
1705                self.vars.insert(key.clone(), val);
1706                if self.inherited_vars.contains(key) {
1707                    self.export_vars.insert(key.clone());
1708                }
1709            }
1710        }
1711
1712        let layout_sub_bb = if sub.layout_bbox.is_empty() {
1713            let mut b = Bbox::new();
1714            b.add(Point::ZERO);
1715            b
1716        } else {
1717            sub.layout_bbox
1718        };
1719        let local_center = (layout_sub_bb.min + layout_sub_bb.max) * 0.5;
1720        let w = layout_sub_bb.width();
1721        let h = layout_sub_bb.height();
1722
1723        let dir = self.dir_of(obj);
1724        let extent = if horizontal(dir) { w } else { h };
1725        let target = self.block_center(obj, dir, extent, w, h, local_center, &mut sub)?;
1726        let shift = target - local_center;
1727
1728        let first_shape = self.shapes.len();
1729        // capture inner labels (translated into parent space) before the block's
1730        // shapes are moved out, so `B.A` / `last [].Outer` can resolve.
1731        let mut members: HashMap<String, Placed> = HashMap::new();
1732        for (name, &i) in &sub.labels {
1733            let mut pl = sub.placed[i].clone();
1734            rebase_placed(&mut pl, shift, first_shape);
1735            members.insert(name.clone(), pl);
1736        }
1737        let layer_shift =
1738            self.layer_shift_for(obj, sub.shape_layers.iter().copied().max().unwrap_or(0))?;
1739        for (mut sh, layer) in sub.shapes.into_iter().zip(sub.shape_layers) {
1740            translate_shape(&mut sh, shift);
1741            if let Some(opacity) = block_fill_opacity {
1742                multiply_shape_fill_opacity(&mut sh, opacity);
1743            }
1744            self.push_shape(sh, layer + layer_shift);
1745        }
1746        let shape = if self.shapes.len() > first_shape {
1747            Some(first_shape)
1748        } else {
1749            None
1750        };
1751        let mut bb = Bbox::new();
1752        bb.add(layout_sub_bb.min + shift);
1753        bb.add(layout_sub_bb.max + shift);
1754        self.layout_bbox.union(&bb);
1755        if !sub.bbox.is_empty() {
1756            let mut visible_bb = Bbox::new();
1757            visible_bb.add(sub.bbox.min + shift);
1758            visible_bb.add(sub.bbox.max + shift);
1759            self.bbox.union(&visible_bb);
1760        }
1761        let block_text_bb = text_bbox(target, &block_text);
1762        self.bbox.union(&block_text_bb);
1763        if has_visible_text(&block_text) {
1764            let layer = self.layer_of(obj, 0)?;
1765            self.push_shape(
1766                Shape::Text {
1767                    at: target,
1768                    text: block_text,
1769                    bbox: block_text_bb,
1770                    w: 0.0,
1771                    h: 0.0,
1772                    standalone: false,
1773                },
1774                layer,
1775            );
1776        }
1777
1778        let half = dir_unit(dir) * (extent / 2.0);
1779        let start = target - half;
1780        let end = target + half;
1781        self.pos = end;
1782        self.dir = dir;
1783        let idx = self.record(PKind::Block, target, bb, start, end, 0.0, shape);
1784        self.placed[idx].members = members;
1785        Ok(idx)
1786    }
1787
1788    #[allow(clippy::too_many_arguments)]
1789    fn record(
1790        &mut self,
1791        kind: PKind,
1792        center: Point,
1793        bbox: Bbox,
1794        start: Point,
1795        end: Point,
1796        thick: f64,
1797        shape: Option<usize>,
1798    ) -> usize {
1799        let idx = self.placed.len();
1800        self.placed.push(Placed {
1801            kind,
1802            center,
1803            bbox,
1804            start,
1805            end,
1806            thick,
1807            points: Vec::new(),
1808            radius: 0.0,
1809            box_rad: 0.0,
1810            line_wid: 0.0,
1811            line_ht: 0.0,
1812            closed_path: false,
1813            layer: shape
1814                .and_then(|s| self.shape_layers.get(s).copied())
1815                .unwrap_or(0),
1816            shape,
1817            members: HashMap::new(),
1818        });
1819        idx
1820    }
1821
1822    fn push_shape(&mut self, shape: Shape, layer: i32) {
1823        self.shapes.push(shape);
1824        self.shape_layers.push(layer);
1825    }
1826
1827    fn layer_of(&mut self, obj: &Object, current: i32) -> ER<i32> {
1828        let mut layer = current;
1829        for a in &obj.attrs {
1830            if let Attr::Behind(place) = a {
1831                let target = self.resolve_obj(place)?;
1832                if layer >= target.layer {
1833                    layer = target.layer - 1;
1834                }
1835            }
1836        }
1837        Ok(layer)
1838    }
1839
1840    fn layer_shift_for(&mut self, obj: &Object, current_max: i32) -> ER<i32> {
1841        Ok(self.layer_of(obj, current_max)? - current_max)
1842    }
1843
1844    // ---- attribute helpers -------------------------------------------------
1845
1846    fn dim(&mut self, obj: &Object, kind: DimKind) -> ER<Option<f64>> {
1847        for a in &obj.attrs {
1848            if let Attr::Dim(k, e) = a
1849                && *k == kind
1850            {
1851                return Ok(Some(match kind {
1852                    DimKind::Thick | DimKind::Scaled => self.eval_expr(e)?,
1853                    DimKind::Ht | DimKind::Wid | DimKind::Rad | DimKind::Diam => {
1854                        self.expr_dim(e)?
1855                    }
1856                }));
1857            }
1858        }
1859        Ok(None)
1860    }
1861
1862    fn has_dim(&self, obj: &Object, kind: DimKind) -> bool {
1863        obj.attrs
1864            .iter()
1865            .any(|a| matches!(a, Attr::Dim(k, _) if *k == kind))
1866    }
1867
1868    fn scale_of(&mut self, obj: &Object) -> ER<f64> {
1869        Ok(self.dim(obj, DimKind::Scaled)?.unwrap_or(1.0))
1870    }
1871
1872    fn dir_of(&self, obj: &Object) -> Dir {
1873        obj.attrs
1874            .iter()
1875            .rev()
1876            .find_map(|a| match a {
1877                Attr::Direction(d, _) => Some(*d),
1878                _ => None,
1879            })
1880            .unwrap_or(self.dir)
1881    }
1882
1883    fn find_from(&mut self, obj: &Object) -> ER<Option<Point>> {
1884        for a in &obj.attrs {
1885            if let Attr::From(pos) = a {
1886                return Ok(Some(self.eval_pos(pos)?));
1887            }
1888        }
1889        Ok(None)
1890    }
1891
1892    fn at_of(&mut self, obj: &Object) -> ER<Option<Point>> {
1893        for a in &obj.attrs {
1894            if let Attr::At(pos) = a {
1895                return Ok(Some(self.eval_pos(pos)?));
1896            }
1897        }
1898        Ok(None)
1899    }
1900
1901    fn dest_of(&mut self, obj: &Object) -> ER<Option<Point>> {
1902        for a in &obj.attrs {
1903            if let Attr::To(pos) = a {
1904                return Ok(Some(self.eval_pos(pos)?));
1905            }
1906        }
1907        Ok(None)
1908    }
1909
1910    /// The start/end `chop` amounts. `chop r1 chop r2` trims each end
1911    /// independently; a single `chop` applies to both ends.
1912    fn chop_of(&mut self, obj: &Object) -> ER<Option<(f64, f64)>> {
1913        let mut vals = Vec::new();
1914        for a in &obj.attrs {
1915            if let Attr::Chop(opt) = a {
1916                let amt = match opt {
1917                    Some(e) => self.expr_dim(e)?,
1918                    None => self.env_dim(EnvVar::Circlerad)?,
1919                };
1920                vals.push(amt);
1921            }
1922        }
1923        Ok(match vals.as_slice() {
1924            [] => None,
1925            [one] => Some((*one, *one)),
1926            [start, end, ..] => Some((*start, *end)),
1927        })
1928    }
1929
1930    /// Width/height of the last placed object of the same closed kind (for `same`).
1931    fn last_dims_of(&self, p: Prim) -> Option<(f64, f64)> {
1932        let want = match p {
1933            Prim::Box => PKind::Box,
1934            Prim::Circle => PKind::Circle,
1935            Prim::Ellipse => PKind::Ellipse,
1936            _ => return None,
1937        };
1938        self.placed
1939            .iter()
1940            .rev()
1941            .find(|pl| pl.kind == want)
1942            .map(|pl| (pl.bbox.width(), pl.bbox.height()))
1943    }
1944
1945    /// End-to-end vector of the last placed open object of the same kind.
1946    fn last_open_vector(&self, p: Prim) -> Option<Point> {
1947        let want = match p {
1948            Prim::Move => PKind::Move,
1949            Prim::Spline => PKind::Spline,
1950            Prim::Line | Prim::Arrow => PKind::Line,
1951            _ => return None,
1952        };
1953        self.placed
1954            .iter()
1955            .rev()
1956            .find(|pl| pl.kind == want)
1957            .map(|pl| pl.end - pl.start)
1958    }
1959
1960    /// Compute the center of a closed object given direction and extents.
1961    fn place_center(&mut self, obj: &Object, dir: Dir, extent: f64, w: f64, h: f64) -> ER<Point> {
1962        self.place_with_corner_offset(obj, dir, extent, w, h, corner_offset)
1963    }
1964
1965    fn place_closed_center(
1966        &mut self,
1967        p: Prim,
1968        obj: &Object,
1969        dir: Dir,
1970        extent: f64,
1971        dims: (f64, f64),
1972        rad: f64,
1973    ) -> ER<Point> {
1974        let (w, h) = dims;
1975        self.place_with_corner_offset(obj, dir, extent, w, h, |c, w, h| {
1976            closed_corner_offset(p, c, w, h, rad)
1977        })
1978    }
1979
1980    fn place_with_corner_offset(
1981        &mut self,
1982        obj: &Object,
1983        dir: Dir,
1984        extent: f64,
1985        w: f64,
1986        h: f64,
1987        corner: impl Fn(Corner, f64, f64) -> Point,
1988    ) -> ER<Point> {
1989        if let Some(at) = self.at_of(obj)? {
1990            return Ok(at);
1991        }
1992        for a in &obj.attrs {
1993            if let Attr::With { anchor, at } = a {
1994                let ap = self.eval_pos(at)?;
1995                let off = match anchor {
1996                    WithAnchor::Corner(c) => corner(*c, w, h),
1997                    WithAnchor::Pair(x, y) => Point::new(self.expr_dim(x)?, self.expr_dim(y)?),
1998                    WithAnchor::Place(_) => {
1999                        return err("`with .label` anchors are only valid on blocks");
2000                    }
2001                    WithAnchor::Plain => Point::ZERO,
2002                };
2003                return Ok(ap - off);
2004            }
2005        }
2006        Ok(self.pos + dir_unit(dir) * (extent / 2.0))
2007    }
2008
2009    #[allow(clippy::too_many_arguments)]
2010    fn block_center(
2011        &mut self,
2012        obj: &Object,
2013        dir: Dir,
2014        extent: f64,
2015        w: f64,
2016        h: f64,
2017        local_center: Point,
2018        sub: &mut State,
2019    ) -> ER<Point> {
2020        if let Some(at) = self.at_of(obj)? {
2021            return Ok(at);
2022        }
2023        for a in &obj.attrs {
2024            if let Attr::With { anchor, at } = a {
2025                let ap = self.eval_pos(at)?;
2026                let off = match anchor {
2027                    WithAnchor::Corner(c) => corner_offset(*c, w, h),
2028                    WithAnchor::Pair(x, y) => {
2029                        Point::new(self.expr_dim(x)?, self.expr_dim(y)?) - local_center
2030                    }
2031                    WithAnchor::Place(place) => sub.place_point(place)? - local_center,
2032                    WithAnchor::Plain => Point::ZERO,
2033                };
2034                return Ok(ap - off);
2035            }
2036        }
2037        Ok(self.pos + dir_unit(dir) * (extent / 2.0))
2038    }
2039
2040    fn arrows_of(&self, obj: &Object, default_end: bool) -> Arrowheads {
2041        let mut found = None;
2042        for a in &obj.attrs {
2043            if let Attr::Arrowhead(h, _) = a {
2044                found = Some(match h {
2045                    token::Arrow::Left => Arrowheads::Start,
2046                    token::Arrow::Right => Arrowheads::End,
2047                    token::Arrow::Double => Arrowheads::Both,
2048                });
2049            }
2050        }
2051        found.unwrap_or(if default_end {
2052            Arrowheads::End
2053        } else {
2054            Arrowheads::None
2055        })
2056    }
2057
2058    fn style_of(&mut self, obj: &Object) -> ER<Style> {
2059        // arrowhead dimensions follow the current `arrowht`/`arrowwid` globals
2060        let mut s = Style {
2061            arrow_ht: self.env_dim(EnvVar::Arrowht)?,
2062            arrow_wid: self.env_dim(EnvVar::Arrowwid)?,
2063            // `arrowhead = 0` draws an open (two-stroke) head; anything else
2064            // (default 2) is a filled triangle.
2065            arrow_filled: self.env.get(EnvVar::Arrowhead).round() as i64 != 0,
2066            ..Default::default()
2067        };
2068        let lt = self.env.get(EnvVar::Linethick);
2069        if lt > 0.0 {
2070            s.thick = Some(lt);
2071        }
2072        for a in &obj.attrs {
2073            match a {
2074                Attr::LineStyle(lt, opt) => match lt {
2075                    LineType::Solid => s.dash = Dash::Solid,
2076                    LineType::Dashed => {
2077                        let w = match opt {
2078                            Some(e) => self.expr_dim(e)?,
2079                            None => self.env_dim(EnvVar::Dashwid)?,
2080                        };
2081                        s.dash = Dash::Dashed(w);
2082                    }
2083                    LineType::Dotted => {
2084                        s.dash = Dash::Dotted(match opt {
2085                            Some(e) => Some(self.expr_dim(e)?),
2086                            None => None,
2087                        });
2088                    }
2089                    LineType::Invis => s.invis = true,
2090                },
2091                Attr::Fill(opt) => {
2092                    let g = match opt {
2093                        Some(e) => self.eval_expr(e)?,
2094                        None => self.env.get(EnvVar::Fillval),
2095                    };
2096                    s.fill = Some(Fill::Gray(g));
2097                    s.fill_open = true;
2098                }
2099                Attr::Color(kind, se) => {
2100                    let name = self.eval_color_expr(se)?;
2101                    match kind {
2102                        token::Color::Outlined => s.stroke = Some(name),
2103                        token::Color::Colored => {
2104                            s.stroke = Some(name.clone());
2105                            s.fill = Some(Fill::Color(name));
2106                        }
2107                        token::Color::Shaded => {
2108                            s.fill = Some(Fill::Color(name));
2109                            s.fill_open = true;
2110                        }
2111                    }
2112                }
2113                Attr::Hatch(kind) => {
2114                    let h = ensure_hatch(&mut s);
2115                    h.cross = matches!(kind, HatchKind::Cross);
2116                    s.fill_open = true;
2117                }
2118                Attr::HatchAngle(e) => ensure_hatch(&mut s).angle = self.eval_expr(e)?,
2119                Attr::HatchSep(e) => {
2120                    let sep = self.expr_dim(e)?;
2121                    if sep <= 0.0 {
2122                        return err("hatchsep must be positive");
2123                    }
2124                    ensure_hatch(&mut s).sep = sep;
2125                    s.fill_open = true;
2126                }
2127                Attr::HatchWidth(e) => {
2128                    let width = self.eval_expr(e)?;
2129                    if width < 0.0 {
2130                        return err("hatchwidth must be non-negative");
2131                    }
2132                    ensure_hatch(&mut s).width = width;
2133                    s.fill_open = true;
2134                }
2135                Attr::HatchColor(se) => {
2136                    let name = self.eval_color_expr(se)?;
2137                    ensure_hatch(&mut s).color = name;
2138                    s.fill_open = true;
2139                }
2140                Attr::Opacity(e) => {
2141                    let opacity = self.eval_expr(e)?;
2142                    if !(0.0..=1.0).contains(&opacity) {
2143                        return err("opacity must be between 0 and 1");
2144                    }
2145                    s.fill_opacity = Some(opacity);
2146                }
2147                Attr::Dim(DimKind::Thick, e) => s.thick = Some(self.eval_expr(e)?),
2148                Attr::Arrowhead(_, Some(e)) => {
2149                    s.arrow_filled = self.eval_expr(e)?.round() as i64 != 0;
2150                }
2151                _ => {}
2152            }
2153        }
2154        Ok(s)
2155    }
2156
2157    fn eval_color_expr(&mut self, se: &StringExpr) -> ER<String> {
2158        if let StringExpr::Lit(name) = se
2159            && let Some(body) = self.macros.get(name).cloned()
2160        {
2161            if let Some(lit) = single_token_macro_string(&body) {
2162                return Ok(lit);
2163            }
2164            let parsed = crate::parser::parse_stringexpr_tokens(
2165                &body,
2166                &mut self.macros,
2167                self.base_dir.as_deref(),
2168            )
2169            .map_err(|e| EvalError { msg: e.to_string() })?;
2170            return self.eval_stringexpr(&parsed);
2171        }
2172        self.eval_stringexpr(se)
2173    }
2174
2175    fn text_of(&mut self, obj: &Object) -> ER<Vec<TextLine>> {
2176        Ok(self.text_and_fit_text_of(obj)?.0)
2177    }
2178
2179    fn text_and_fit_text_of(&mut self, obj: &Object) -> ER<(Vec<TextLine>, Option<Vec<TextLine>>)> {
2180        let mut lines: Vec<TextLine> = Vec::new();
2181        let mut fit_lines = None;
2182        let mut pending_halign = 0i8;
2183        let mut pending_valign = 0i8;
2184        for a in &obj.attrs {
2185            match a {
2186                Attr::TextPos(tp) => {
2187                    if let Some(line) = lines.last_mut() {
2188                        apply_text_pos(&mut line.halign, &mut line.valign, *tp);
2189                    } else {
2190                        apply_text_pos(&mut pending_halign, &mut pending_valign, *tp);
2191                    }
2192                }
2193                Attr::Text(se) => {
2194                    let s = self.eval_stringexpr(se)?;
2195                    lines.push(TextLine {
2196                        s,
2197                        halign: pending_halign,
2198                        valign: pending_valign,
2199                        text_offset: self.env_dim(EnvVar::Textoffset)?,
2200                    });
2201                    pending_halign = 0;
2202                    pending_valign = 0;
2203                }
2204                Attr::Fit if fit_lines.is_none() => {
2205                    fit_lines = Some(lines.clone());
2206                }
2207                _ => {}
2208            }
2209        }
2210        Ok((lines, fit_lines))
2211    }
2212
2213    // ---- positions & places ------------------------------------------------
2214
2215    fn eval_pos(&mut self, pos: &Position) -> ER<Point> {
2216        match pos {
2217            Position::Pair(x, y) => Ok(Point::new(self.expr_dim(x)?, self.expr_dim(y)?)),
2218            Position::Place(loc) => self.eval_loc(loc),
2219            Position::Between { frac, a, b, .. } => {
2220                let f = self.eval_expr(frac)?;
2221                let pa = self.eval_pos(a)?;
2222                let pb = self.eval_pos(b)?;
2223                Ok(pa.lerp(pb, f))
2224            }
2225            Position::Sum(sign, a, b) => {
2226                let pa = self.eval_pos(a)?;
2227                let pb = self.eval_pos(b)?;
2228                Ok(match sign {
2229                    Sign::Plus => pa + pb,
2230                    Sign::Minus => pa - pb,
2231                })
2232            }
2233            Position::Scale(p, s, div) => {
2234                let pp = self.eval_pos(p)?;
2235                let sv = self.eval_expr(s)?;
2236                if *div {
2237                    if sv == 0.0 {
2238                        return err("division by zero in position");
2239                    }
2240                    Ok(pp / sv)
2241                } else {
2242                    Ok(pp * sv)
2243                }
2244            }
2245        }
2246    }
2247
2248    fn eval_loc(&mut self, loc: &Location) -> ER<Point> {
2249        match loc {
2250            Location::Place(p) => self.place_point(p),
2251            Location::Paren(pos) => self.eval_pos(pos),
2252            Location::ParenPair(p1, p2) => {
2253                Ok(Point::new(self.eval_pos(p1)?.x, self.eval_pos(p2)?.y))
2254            }
2255        }
2256    }
2257
2258    fn place_point(&mut self, p: &Place) -> ER<Point> {
2259        match p {
2260            Place::Here => Ok(self.pos),
2261            Place::Name { name, subscript } => {
2262                let key = self.indexed_name(name, subscript.as_deref())?;
2263                Ok(self.resolve_label(&key)?.center)
2264            }
2265            Place::Nth { count, obj } => {
2266                let idx = self.nth_index(count, obj)?;
2267                Ok(self.placed[idx].center)
2268            }
2269            Place::Corner(inner, c) => {
2270                let pl = self.resolve_obj(inner)?;
2271                Ok(pl.corner(*c))
2272            }
2273            Place::CornerOf(c, inner) => {
2274                let pl = self.resolve_obj(inner)?;
2275                Ok(pl.corner(*c))
2276            }
2277            Place::Member(..) => Ok(self.resolve_obj(p)?.center),
2278        }
2279    }
2280
2281    /// Resolve an object-valued place to its [`Placed`] record, descending into
2282    /// block members for `B.A` / `last [].Outer`.
2283    fn resolve_obj(&mut self, p: &Place) -> ER<Placed> {
2284        match p {
2285            Place::Name { name, subscript } => {
2286                let key = self.indexed_name(name, subscript.as_deref())?;
2287                self.resolve_label(&key)
2288            }
2289            Place::Nth { count, obj } => {
2290                let idx = self.nth_index(count, obj)?;
2291                Ok(self.placed[idx].clone())
2292            }
2293            Place::Corner(inner, _) | Place::CornerOf(_, inner) => self.resolve_obj(inner),
2294            Place::Member(base, sub) => {
2295                let b = self.resolve_obj(base)?;
2296                let key = match sub.as_ref() {
2297                    Place::Name { name, subscript } => {
2298                        self.indexed_name(name, subscript.as_deref())?
2299                    }
2300                    _ => return err("a block sub-label must be a name"),
2301                };
2302                b.members.get(&key).cloned().ok_or_else(|| EvalError {
2303                    msg: format!("no sub-label `{key}` in that block"),
2304                })
2305            }
2306            Place::Here => err("`Here` is a point, not an object"),
2307        }
2308    }
2309
2310    fn place_index(&mut self, p: &Place) -> ER<usize> {
2311        match p {
2312            Place::Name { name, subscript } => {
2313                let key = self.indexed_name(name, subscript.as_deref())?;
2314                self.label_index(&key)
2315            }
2316            Place::Nth { count, obj } => self.nth_index(count, obj),
2317            Place::Corner(inner, _) | Place::CornerOf(_, inner) => self.place_index(inner),
2318            Place::Here => err("`Here` is a point, not an object"),
2319            Place::Member(_, _) => err("block sub-labels (B.A) are not supported yet"),
2320        }
2321    }
2322
2323    fn label_index(&self, name: &str) -> ER<usize> {
2324        self.labels.get(name).copied().ok_or_else(|| EvalError {
2325            msg: format!("unknown label `{name}`"),
2326        })
2327    }
2328
2329    /// Resolve a label to its [`Placed`], falling back to enclosing-scope labels
2330    /// (absolute coordinates) so a block can reference outer labels.
2331    fn resolve_label(&self, key: &str) -> ER<Placed> {
2332        if let Some(&idx) = self.labels.get(key) {
2333            Ok(self.placed[idx].clone())
2334        } else if let Some(pl) = self.outer_labels.get(key) {
2335            Ok(pl.clone())
2336        } else {
2337            err(format!("unknown label `{key}`"))
2338        }
2339    }
2340
2341    fn label_key(&mut self, label: &Label) -> ER<String> {
2342        self.indexed_name(&label.name, label.subscript.as_ref())
2343    }
2344
2345    fn indexed_name(&mut self, name: &str, subscript: Option<&Expr>) -> ER<String> {
2346        match subscript {
2347            Some(Expr::Index(items)) => {
2348                let mut parts = Vec::with_capacity(items.len());
2349                for e in items {
2350                    parts.push(fmt_num(self.eval_expr(e)?));
2351                }
2352                Ok(format!("{name}[{}]", parts.join(",")))
2353            }
2354            Some(e) => Ok(format!("{name}[{}]", fmt_num(self.eval_expr(e)?))),
2355            None => Ok(name.to_string()),
2356        }
2357    }
2358
2359    fn nth_index(&mut self, count: &Nth, obj: &PrimObj) -> ER<usize> {
2360        // Untyped `last` matches the most recent object of any kind; a typed
2361        // reference (`last box`) filters to that kind.
2362        let want = primobj_kind(obj);
2363        let matches: Vec<usize> = self
2364            .placed
2365            .iter()
2366            .enumerate()
2367            .filter(|(_, pl)| want.is_none_or(|w| pl.kind == w))
2368            .map(|(i, _)| i)
2369            .collect();
2370        if matches.is_empty() {
2371            return match want {
2372                Some(w) => err(format!("no {:?} object to reference", want_name(w))),
2373                None => err("no object to reference".to_string()),
2374            };
2375        }
2376        let idx = match count {
2377            Nth::Last => *matches.last().unwrap(),
2378            Nth::Count(e, from_last) => {
2379                let n = self.eval_expr(e)?.round() as i64;
2380                if n < 1 {
2381                    return err("ordinal must be >= 1");
2382                }
2383                let k = (n - 1) as usize;
2384                if *from_last {
2385                    if k >= matches.len() {
2386                        return err("ordinal out of range");
2387                    }
2388                    matches[matches.len() - 1 - k]
2389                } else {
2390                    if k >= matches.len() {
2391                        return err("ordinal out of range");
2392                    }
2393                    matches[k]
2394                }
2395            }
2396        };
2397        Ok(idx)
2398    }
2399
2400    // ---- string expressions ------------------------------------------------
2401
2402    fn eval_stringexpr(&mut self, se: &StringExpr) -> ER<String> {
2403        Ok(match se {
2404            StringExpr::Lit(s) => s.clone(),
2405            StringExpr::Concat(a, b) => {
2406                format!("{}{}", self.eval_stringexpr(a)?, self.eval_stringexpr(b)?)
2407            }
2408            StringExpr::Arg(n) => format!("${n}"), // should have been expanded
2409            StringExpr::Sprintf(fmt, args) => {
2410                let f = self.eval_stringexpr(fmt)?;
2411                let mut vals = Vec::with_capacity(args.len());
2412                for e in args {
2413                    vals.push(self.eval_printf_arg(e)?);
2414                }
2415                sprintf_fmt(&f, &vals)
2416            }
2417            StringExpr::SvgFont(_) => String::new(),
2418        })
2419    }
2420
2421    fn eval_printf_arg(&mut self, e: &Expr) -> ER<PrintfArg> {
2422        match e {
2423            Expr::Str(se) => Ok(PrintfArg::Str(self.eval_stringexpr(se)?)),
2424            _ => Ok(PrintfArg::Num(self.eval_expr(e)?)),
2425        }
2426    }
2427
2428    // ---- expressions -------------------------------------------------------
2429
2430    /// Evaluate an operand to its string form (for `==`/`!=`); a numeric operand
2431    /// is rendered textually so `"$1" == "2"` style comparisons work uniformly.
2432    fn expr_str(&mut self, e: &Expr) -> ER<String> {
2433        match e {
2434            Expr::Str(se) => self.eval_stringexpr(se),
2435            _ => Ok(fmt_num(self.eval_expr(e)?)),
2436        }
2437    }
2438
2439    fn eval_expr(&mut self, e: &Expr) -> ER<f64> {
2440        Ok(match e {
2441            Expr::Num(v) => *v,
2442            Expr::Str(_) => return err("a string is only valid as an `==`/`!=` operand"),
2443            Expr::Index(_) => return err("a comma subscript is only valid inside `name[...]`"),
2444            Expr::Var(name, subscript) => {
2445                let key = self.indexed_name(name, subscript.as_deref())?;
2446                self.vars.get(&key).copied().ok_or_else(|| EvalError {
2447                    msg: format!("variable not found `{key}`"),
2448                })?
2449            }
2450            Expr::Env(v) => self.env.get(*v),
2451            Expr::Unary(op, a) => {
2452                let x = self.eval_expr(a)?;
2453                match op {
2454                    UnOp::Neg => -x,
2455                    UnOp::Pos => x,
2456                    UnOp::Not => bool_f(x == 0.0),
2457                }
2458            }
2459            Expr::Bin(op, a, b) => {
2460                // string equality: pic compares string operands with `==`/`!=`
2461                if matches!(op, BinOp::Eq | BinOp::Ne)
2462                    && (matches!(a.as_ref(), Expr::Str(_)) || matches!(b.as_ref(), Expr::Str(_)))
2463                {
2464                    let sa = self.expr_str(a)?;
2465                    let sb = self.expr_str(b)?;
2466                    return Ok(bool_f(if matches!(op, BinOp::Eq) {
2467                        sa == sb
2468                    } else {
2469                        sa != sb
2470                    }));
2471                }
2472                let x = self.eval_expr(a)?;
2473                let y = self.eval_expr(b)?;
2474                match op {
2475                    BinOp::Add => x + y,
2476                    BinOp::Sub => x - y,
2477                    BinOp::Mul => x * y,
2478                    BinOp::Div => {
2479                        if y == 0.0 {
2480                            return err("division by zero");
2481                        }
2482                        x / y
2483                    }
2484                    BinOp::Mod => dpic_mod(x, y, "modulo by zero")?,
2485                    BinOp::Pow => dpic_pow(x, y)?,
2486                    BinOp::Eq => bool_f(x == y),
2487                    BinOp::Ne => bool_f(x != y),
2488                    BinOp::Lt => bool_f(x < y),
2489                    BinOp::Le => bool_f(x <= y),
2490                    BinOp::Gt => bool_f(x > y),
2491                    BinOp::Ge => bool_f(x >= y),
2492                    BinOp::And => bool_f(x != 0.0 && y != 0.0),
2493                    BinOp::Or => bool_f(x != 0.0 || y != 0.0),
2494                }
2495            }
2496            Expr::Func1(f, a) => {
2497                let x = self.eval_expr(a)?;
2498                match f {
2499                    Func1::Abs => x.abs(),
2500                    Func1::Acos => x.acos(),
2501                    Func1::Asin => x.asin(),
2502                    Func1::Cos => x.cos(),
2503                    Func1::Exp => 10f64.powf(x),
2504                    Func1::Expe => x.exp(),
2505                    Func1::Int => x.trunc(),
2506                    Func1::Log => x.log10(),
2507                    Func1::Loge => x.ln(),
2508                    Func1::Sign => {
2509                        if x >= 0.0 {
2510                            1.0
2511                        } else {
2512                            -1.0
2513                        }
2514                    }
2515                    Func1::Sin => x.sin(),
2516                    Func1::Sqrt => x.sqrt(),
2517                    Func1::Tan => x.tan(),
2518                    Func1::Floor => x.floor(),
2519                }
2520            }
2521            Expr::Func2(f, a, b) => {
2522                let x = self.eval_expr(a)?;
2523                let y = self.eval_expr(b)?;
2524                match f {
2525                    Func2::Atan2 => x.atan2(y),
2526                    Func2::Max => x.max(y),
2527                    Func2::Min => x.min(y),
2528                    Func2::Pmod => x.rem_euclid(y),
2529                }
2530            }
2531            Expr::Rand(seed) => {
2532                let seed = match seed {
2533                    Some(e) => Some(self.eval_expr(e)?),
2534                    None => None,
2535                };
2536                self.rand(seed)
2537            }
2538            Expr::Assign(name, subscript, v) => {
2539                let val = self.eval_expr(v)?;
2540                let key = self.indexed_name(name, subscript.as_deref())?;
2541                self.vars.insert(key, val);
2542                val
2543            }
2544            Expr::DotX(loc) => {
2545                let x = self.eval_loc(loc)?.x;
2546                self.to_user_dim(x)
2547            }
2548            Expr::DotY(loc) => {
2549                let y = self.eval_loc(loc)?.y;
2550                self.to_user_dim(y)
2551            }
2552            Expr::PlaceAttr(place, param) => {
2553                let pl = self.resolve_obj(place)?;
2554                match param {
2555                    token::Param::Width => self.to_user_dim(pl.attr_width()),
2556                    token::Param::Height => self.to_user_dim(pl.attr_height()),
2557                    token::Param::Radius => self.to_user_dim(pl.attr_radius()),
2558                    token::Param::Diameter => self.to_user_dim(pl.attr_diameter()),
2559                    token::Param::Length => self.to_user_dim(pl.attr_length()),
2560                    token::Param::Thickness => pl.thick,
2561                }
2562            }
2563        })
2564    }
2565
2566    fn rand(&mut self, seed: Option<f64>) -> f64 {
2567        if let Some(seed) = seed {
2568            self.rng.seed(seed.trunc() as i64);
2569        }
2570        self.rng.next_f64()
2571    }
2572}
2573
2574// ---- free helpers ----------------------------------------------------------
2575
2576fn apply_op(op: AssignOp, cur: f64, rhs: f64) -> ER<f64> {
2577    Ok(match op {
2578        AssignOp::Set | AssignOp::ColonSet => rhs,
2579        AssignOp::Add => cur + rhs,
2580        AssignOp::Sub => cur - rhs,
2581        AssignOp::Mul => cur * rhs,
2582        AssignOp::Div => {
2583            if rhs == 0.0 {
2584                return err("division by zero in assignment");
2585            }
2586            cur / rhs
2587        }
2588        AssignOp::Rem => dpic_mod(cur, rhs, "modulo by zero in assignment")?,
2589    })
2590}
2591
2592#[derive(Clone)]
2593struct GlibcRand {
2594    state: [u32; 31],
2595    f: usize,
2596    r: usize,
2597}
2598
2599impl GlibcRand {
2600    fn new(seed: i64) -> Self {
2601        let mut rng = GlibcRand {
2602            state: [0; 31],
2603            f: 3,
2604            r: 0,
2605        };
2606        rng.seed(seed);
2607        rng
2608    }
2609
2610    fn seed(&mut self, seed: i64) {
2611        let seed = if seed == 0 { 1 } else { seed };
2612        let mut x = seed.rem_euclid(2_147_483_647) as u32;
2613        if x == 0 {
2614            x = 1;
2615        }
2616        self.state[0] = x;
2617        for i in 1..31 {
2618            let prev = self.state[i - 1] as i64;
2619            let hi = prev / 127_773;
2620            let lo = prev % 127_773;
2621            let mut next = 16_807 * lo - 2_836 * hi;
2622            if next < 0 {
2623                next += 2_147_483_647;
2624            }
2625            self.state[i] = next as u32;
2626        }
2627        self.f = 3;
2628        self.r = 0;
2629        for _ in 0..310 {
2630            self.next_i31();
2631        }
2632    }
2633
2634    fn next_i31(&mut self) -> u32 {
2635        let x = self.state[self.f].wrapping_add(self.state[self.r]);
2636        self.state[self.f] = x;
2637        let out = (x >> 1) & 0x7fff_ffff;
2638        self.f = (self.f + 1) % 31;
2639        self.r = (self.r + 1) % 31;
2640        out
2641    }
2642
2643    fn next_f64(&mut self) -> f64 {
2644        self.next_i31() as f64 / 2_147_483_647.0
2645    }
2646}
2647
2648fn bool_f(b: bool) -> f64 {
2649    if b { 1.0 } else { 0.0 }
2650}
2651
2652fn dpic_round(x: f64) -> i64 {
2653    if x < 0.0 {
2654        -((-x + 0.5).floor() as i64)
2655    } else {
2656        (x + 0.5).floor() as i64
2657    }
2658}
2659
2660fn dpic_mod(x: f64, y: f64, zero_msg: &'static str) -> ER<f64> {
2661    let i = dpic_round(x);
2662    let j = dpic_round(y);
2663    if j == 0 {
2664        return err(zero_msg);
2665    }
2666    Ok((i - (i / j) * j) as f64)
2667}
2668
2669fn dpic_pow(x: f64, y: f64) -> ER<f64> {
2670    if x == 0.0 && y < 0.0 {
2671        return err("zero cannot be raised to a negative power");
2672    }
2673    let iy = dpic_round(y);
2674    if iy as f64 == y {
2675        return Ok(dpic_int_pow(x, iy));
2676    }
2677    if x < 0.0 {
2678        return err("negative base with non-integer exponent");
2679    }
2680    Ok(x.powf(y))
2681}
2682
2683fn dpic_int_pow(x: f64, y: i64) -> f64 {
2684    if y == 0 {
2685        return 1.0;
2686    }
2687    if x == 0.0 || y == 1 {
2688        return x;
2689    }
2690    if y < 0 {
2691        return dpic_int_pow(1.0 / x, -y);
2692    }
2693    if y == 2 {
2694        return x * x;
2695    }
2696    if y & 1 == 1 {
2697        x * dpic_int_pow(x, y - 1)
2698    } else {
2699        let half = dpic_int_pow(x, y >> 1);
2700        half * half
2701    }
2702}
2703
2704fn apply_text_pos(halign: &mut i8, valign: &mut i8, pos: token::TextPos) {
2705    match pos {
2706        token::TextPos::Ljust => *halign = -1,
2707        token::TextPos::Rjust => *halign = 1,
2708        token::TextPos::Center => {
2709            *halign = 0;
2710            *valign = 0;
2711        }
2712        token::TextPos::Above => *valign = 1,
2713        token::TextPos::Below => *valign = -1,
2714    }
2715}
2716
2717fn ensure_hatch(style: &mut Style) -> &mut Hatch {
2718    style.hatch.get_or_insert_with(|| Hatch {
2719        cross: false,
2720        angle: DEFAULT_HATCH_ANGLE,
2721        sep: DEFAULT_HATCH_SEP,
2722        width: DEFAULT_HATCH_WIDTH,
2723        color: "black".into(),
2724    })
2725}
2726
2727fn has_visible_text(lines: &[TextLine]) -> bool {
2728    lines.iter().any(|line| !line.s.is_empty())
2729}
2730
2731fn closed_shape_is_visible(style: &Style) -> bool {
2732    !style.invis || style.fill.is_some() || style.hatch.is_some()
2733}
2734
2735fn open_fill_is_visible(style: &Style) -> bool {
2736    style.fill_open && (style.fill.is_some() || style.hatch.is_some())
2737}
2738
2739fn stroke_half_width(style: &Style) -> f64 {
2740    if style.invis {
2741        0.0
2742    } else {
2743        style.thick.unwrap_or(0.8) / 144.0
2744    }
2745}
2746
2747fn painted_bbox(bb: &Bbox, pad: f64) -> Bbox {
2748    if bb.is_empty() || pad <= 0.0 {
2749        return *bb;
2750    }
2751    let mut out = Bbox::new();
2752    out.add(bb.min - Point::new(pad, pad));
2753    out.add(bb.max + Point::new(pad, pad));
2754    out
2755}
2756
2757fn dpic_box_layout_bbox(center: Point, w: f64, h: f64) -> Bbox {
2758    fn axis(center: f64, extent: f64) -> (f64, f64) {
2759        let lo = center - extent / 2.0;
2760        let hi = center + extent / 2.0;
2761        if lo <= hi { (lo, hi) } else { (0.0, 0.0) }
2762    }
2763
2764    let (x0, x1) = axis(center.x, w);
2765    let (y0, y1) = axis(center.y, h);
2766    let mut bb = Bbox::new();
2767    bb.add(Point::new(x0, y0));
2768    bb.add(Point::new(x1, y1));
2769    bb
2770}
2771
2772fn drawing_painted_bbox(shapes: &[Shape]) -> Bbox {
2773    let mut out = Bbox::new();
2774    for sh in shapes {
2775        out.union(&shape_painted_bbox(sh));
2776    }
2777    out
2778}
2779
2780fn shape_painted_bbox(sh: &Shape) -> Bbox {
2781    let mut out = Bbox::new();
2782    match sh {
2783        Shape::Box {
2784            c,
2785            w,
2786            h,
2787            style,
2788            text,
2789            ..
2790        } => {
2791            let mut bb = Bbox::new();
2792            bb.add(*c - Point::new(*w / 2.0, *h / 2.0));
2793            bb.add(*c + Point::new(*w / 2.0, *h / 2.0));
2794            if closed_shape_is_visible(style) {
2795                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2796            }
2797            out.union(&text_bbox(*c, text));
2798        }
2799        Shape::Circle { c, r, style, text } => {
2800            let mut bb = Bbox::new();
2801            bb.add(*c - Point::new(*r, *r));
2802            bb.add(*c + Point::new(*r, *r));
2803            if closed_shape_is_visible(style) {
2804                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2805            }
2806            out.union(&text_bbox(*c, text));
2807        }
2808        Shape::Ellipse {
2809            c,
2810            w,
2811            h,
2812            style,
2813            text,
2814        } => {
2815            let mut bb = Bbox::new();
2816            bb.add(*c - Point::new(*w / 2.0, *h / 2.0));
2817            bb.add(*c + Point::new(*w / 2.0, *h / 2.0));
2818            if closed_shape_is_visible(style) {
2819                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2820            }
2821            out.union(&text_bbox(*c, text));
2822        }
2823        Shape::Path {
2824            pts,
2825            closed,
2826            style,
2827            text,
2828            ..
2829        } => {
2830            let mut bb = Bbox::new();
2831            for p in pts {
2832                bb.add(*p);
2833            }
2834            if open_fill_is_visible(style) {
2835                out.union(&bb);
2836            }
2837            if !style.invis {
2838                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2839            } else if style.invis_bounds {
2840                out.union(&bb);
2841            }
2842            if !bb.is_empty() {
2843                let text_at = if *closed {
2844                    (bb.min + bb.max) * 0.5
2845                } else if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
2846                    (*first + *last) * 0.5
2847                } else {
2848                    Point::ZERO
2849                };
2850                out.union(&text_bbox(text_at, text));
2851            }
2852        }
2853        Shape::Spline {
2854            pts, style, text, ..
2855        } => {
2856            let mut bb = Bbox::new();
2857            for p in pts {
2858                bb.add(*p);
2859            }
2860            if open_fill_is_visible(style) {
2861                out.union(&bb);
2862            }
2863            if !style.invis {
2864                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2865            } else if style.invis_bounds {
2866                out.union(&bb);
2867            }
2868            if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
2869                out.union(&text_bbox((*first + *last) * 0.5, text));
2870            }
2871        }
2872        Shape::Arc {
2873            c,
2874            r,
2875            a0,
2876            a1,
2877            style,
2878            text,
2879            ..
2880        } => {
2881            let at = |t: f64| *c + Point::new(t.cos(), t.sin()) * *r;
2882            let mut bb = Bbox::new();
2883            for k in 0..=12 {
2884                bb.add(at(*a0 + (*a1 - *a0) * (k as f64 / 12.0)));
2885            }
2886            if open_fill_is_visible(style) {
2887                out.union(&bb);
2888            }
2889            if !style.invis {
2890                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2891            }
2892            out.union(&text_bbox(*c, text));
2893        }
2894        Shape::Brace {
2895            cubics,
2896            label_at,
2897            style,
2898            text,
2899            ..
2900        } => {
2901            let bb = cubics_bbox(cubics);
2902            if !style.invis {
2903                out.union(&painted_bbox(&bb, stroke_half_width(style)));
2904            } else if style.invis_bounds {
2905                out.union(&bb);
2906            }
2907            out.union(&text_bbox(*label_at, text));
2908        }
2909        Shape::Text { bbox, .. } => out.union(bbox),
2910    }
2911    out
2912}
2913
2914fn brace_side(unit: Point, side_dir: Option<Dir>) -> Point {
2915    let right = Point::new(unit.y, -unit.x);
2916    if let Some(d) = side_dir {
2917        let target = dir_unit(d);
2918        if right.x * target.x + right.y * target.y < 0.0 {
2919            right * -1.0
2920        } else {
2921            right
2922        }
2923    } else {
2924        right
2925    }
2926}
2927
2928fn brace_cubics(a: Point, b: Point, depth: Point, pos: f64) -> Vec<[Point; 4]> {
2929    let v = b - a;
2930    let len = v.len();
2931    let depth_len = depth.len();
2932    if len <= 1e-12 || depth_len <= 1e-12 {
2933        return vec![[a, a, b, b]];
2934    }
2935    let u = v / len;
2936    let side = depth / depth_len;
2937    let left_len = len * pos;
2938    let right_len = len - left_len;
2939    let depth_len = depth_len.min(left_len * 0.45).min(right_len * 0.45);
2940    let shoulder = depth_len * 0.45;
2941    let curl = depth_len - shoulder;
2942    let k = 0.552_284_749_830_793_6;
2943    let at = |x: f64, y: f64| a + u * x + side * y;
2944    let line = |p0: Point, p1: Point| [p0, p0, p1, p1];
2945
2946    let p0 = at(0.0, 0.0);
2947    let p1 = at(shoulder, shoulder);
2948    let p2_x = (left_len - curl).max(shoulder);
2949    let p2 = at(p2_x, shoulder);
2950    let cusp = at(left_len, depth_len);
2951    let p3_x = (left_len + curl).min(len - shoulder);
2952    let p3 = at(p3_x, shoulder);
2953    let p4_x = len - shoulder;
2954    let p4 = at(p4_x, shoulder);
2955    let p5 = at(len, 0.0);
2956
2957    vec![
2958        [
2959            p0,
2960            at(0.0, k * shoulder),
2961            at(shoulder - k * shoulder, shoulder),
2962            p1,
2963        ],
2964        line(p1, p2),
2965        [
2966            p2,
2967            at(p2_x + k * curl, shoulder),
2968            at(left_len, depth_len - k * curl),
2969            cusp,
2970        ],
2971        [
2972            cusp,
2973            at(left_len, depth_len - k * curl),
2974            at(p3_x - k * curl, shoulder),
2975            p3,
2976        ],
2977        line(p3, p4),
2978        [
2979            p4,
2980            at(p4_x + k * shoulder, shoulder),
2981            at(len, k * shoulder),
2982            p5,
2983        ],
2984    ]
2985}
2986
2987fn brace_cusp(cubics: &[[Point; 4]]) -> Option<Point> {
2988    cubics.get(2).map(|c| c[3])
2989}
2990
2991fn cubic_at(c: &[Point; 4], t: f64) -> Point {
2992    let mt = 1.0 - t;
2993    c[0] * (mt * mt * mt)
2994        + c[1] * (3.0 * mt * mt * t)
2995        + c[2] * (3.0 * mt * t * t)
2996        + c[3] * (t * t * t)
2997}
2998
2999fn sample_cubics(cubics: &[[Point; 4]], steps: usize) -> Vec<Point> {
3000    let steps = steps.max(1);
3001    let mut pts = Vec::new();
3002    for (i, c) in cubics.iter().enumerate() {
3003        if i == 0 {
3004            pts.push(c[0]);
3005        }
3006        for step in 1..=steps {
3007            pts.push(cubic_at(c, step as f64 / steps as f64));
3008        }
3009    }
3010    pts
3011}
3012
3013fn cubics_bbox(cubics: &[[Point; 4]]) -> Bbox {
3014    let mut bb = Bbox::new();
3015    for c in cubics {
3016        for p in c {
3017            bb.add(*p);
3018        }
3019    }
3020    for p in sample_cubics(cubics, 12) {
3021        bb.add(p);
3022    }
3023    bb
3024}
3025
3026fn text_bbox(center: Point, lines: &[TextLine]) -> Bbox {
3027    let mut bb = Bbox::new();
3028    if !has_visible_text(lines) {
3029        return bb;
3030    }
3031    let n = lines.len() as f64;
3032    for (i, line) in lines.iter().enumerate() {
3033        if line.s.is_empty() {
3034            continue;
3035        }
3036        let w = line.s.chars().count() as f64 * TEXT_CHAR_W;
3037        let base_y = center.y - (i as f64 - (n - 1.0) / 2.0) * TEXT_LINE_H;
3038        let y = base_y + line.valign as f64 * (TEXT_XHEIGHT / 2.0 + line.text_offset);
3039        let x = center.x
3040            + match line.halign {
3041                -1 => line.text_offset,
3042                1 => -line.text_offset,
3043                _ => 0.0,
3044            };
3045        let (min_x, max_x) = match line.halign {
3046            -1 => (x, x + w),
3047            1 => (x - w, x),
3048            _ => (x - w / 2.0, x + w / 2.0),
3049        };
3050        bb.add(Point::new(min_x, y - TEXT_LINE_H / 2.0));
3051        bb.add(Point::new(max_x, y + TEXT_LINE_H / 2.0));
3052    }
3053    bb
3054}
3055
3056fn fitted_text_size(lines: &[TextLine]) -> Option<(f64, f64)> {
3057    if !has_visible_text(lines) {
3058        return None;
3059    }
3060    let bb = text_bbox(Point::ZERO, lines);
3061    if bb.is_empty() {
3062        return None;
3063    }
3064    let half_w = bb.min.x.abs().max(bb.max.x.abs());
3065    let half_h = bb.min.y.abs().max(bb.max.y.abs());
3066    Some((
3067        2.0 * half_w + TEXT_CHAR_W,
3068        2.0 * half_h + TEXT_XHEIGHT / 2.0,
3069    ))
3070}
3071
3072fn text_object_bbox(center: Point, lines: &[TextLine], w: f64, h: f64) -> Bbox {
3073    let text_bb = text_bbox(center, lines);
3074    if text_bb.is_empty() {
3075        return text_bb;
3076    }
3077    let min_x = if w > 0.0 {
3078        center.x - w / 2.0
3079    } else {
3080        text_bb.min.x
3081    };
3082    let max_x = if w > 0.0 {
3083        center.x + w / 2.0
3084    } else {
3085        text_bb.max.x
3086    };
3087    let y_bb = if h > 0.0 {
3088        text_object_vertical_bbox(center, lines, h)
3089    } else {
3090        text_bb
3091    };
3092    let min_y = y_bb.min.y;
3093    let max_y = y_bb.max.y;
3094    let mut bb = Bbox::new();
3095    bb.add(Point::new(min_x, min_y));
3096    bb.add(Point::new(max_x, max_y));
3097    bb
3098}
3099
3100fn text_object_vertical_bbox(center: Point, lines: &[TextLine], h: f64) -> Bbox {
3101    let mut bb = Bbox::new();
3102    if !has_visible_text(lines) {
3103        return bb;
3104    }
3105    let n = lines.len() as f64;
3106    let v = n - 1.0 + DP_TEXT_RATIO;
3107    let lineskip = if v.abs() > 1e-12 { h / v } else { 11.0 / 72.0 };
3108    let xheight = lineskip * DP_TEXT_RATIO;
3109    let mut baseline_y = center.y + (v * lineskip / 2.0) - xheight;
3110    for line in lines {
3111        if line.s.is_empty() {
3112            baseline_y -= lineskip;
3113            continue;
3114        }
3115        let just_offset = xheight / 2.0 + line.text_offset;
3116        let y = baseline_y + (line.valign as f64) * just_offset;
3117        bb.add(Point::new(center.x, y));
3118        bb.add(Point::new(center.x, y + xheight));
3119        baseline_y -= lineskip;
3120    }
3121    bb
3122}
3123
3124/// Render a number the way pic's `%g`/string contexts do (no trailing zeros).
3125fn fmt_num(v: f64) -> String {
3126    format!("{v}")
3127}
3128
3129fn install_dpic_compat_vars(vars: &mut HashMap<String, f64>) {
3130    // dpic backend option constants are zero-based in the order used by dpic's
3131    // own `case(dpicopt, ...)` examples. rpic renders SVG.
3132    let opts = [
3133        ("optMFpic", 0.0),
3134        ("optMpost", 1.0),
3135        ("optPDF", 2.0),
3136        ("optPGF", 3.0),
3137        ("optPict2e", 4.0),
3138        ("optPS", 5.0),
3139        ("optPSfrag", 6.0),
3140        ("optPSTricks", 7.0),
3141        ("optSVG", 8.0),
3142        ("optTeX", 9.0),
3143        ("opttTeX", 10.0),
3144        ("optxfig", 11.0),
3145    ];
3146    for (name, val) in opts {
3147        vars.insert(name.to_string(), val);
3148    }
3149    vars.insert("dpicopt".to_string(), 8.0);
3150}
3151
3152fn corner_offset(c: Corner, w: f64, h: f64) -> Point {
3153    let (hw, hh) = (w / 2.0, h / 2.0);
3154    match c {
3155        Corner::N => Point::new(0.0, hh),
3156        Corner::S => Point::new(0.0, -hh),
3157        Corner::E => Point::new(hw, 0.0),
3158        Corner::W => Point::new(-hw, 0.0),
3159        Corner::Ne => Point::new(hw, hh),
3160        Corner::Se => Point::new(hw, -hh),
3161        Corner::Nw => Point::new(-hw, hh),
3162        Corner::Sw => Point::new(-hw, -hh),
3163        Corner::Center | Corner::Start | Corner::End => Point::ZERO,
3164    }
3165}
3166
3167fn closed_corner_offset(p: Prim, c: Corner, w: f64, h: f64, rad: f64) -> Point {
3168    match p {
3169        Prim::Circle | Prim::Ellipse => ellipse_corner_offset(c, w, h),
3170        Prim::Box => box_corner_offset(c, w, h, rad),
3171        _ => corner_offset(c, w, h),
3172    }
3173}
3174
3175fn arc_angles(center: Point, start: Point, end: Point, cw: bool) -> (f64, f64) {
3176    let a0 = (start - center).y.atan2((start - center).x);
3177    let mut a1 = (end - center).y.atan2((end - center).x);
3178    if cw {
3179        while a1 > a0 {
3180            a1 -= 2.0 * PI;
3181        }
3182    } else {
3183        while a1 < a0 {
3184            a1 += 2.0 * PI;
3185        }
3186    }
3187    (a0, a1)
3188}
3189
3190fn ellipse_corner_offset(c: Corner, w: f64, h: f64) -> Point {
3191    let (rx, ry) = (w / 2.0, h / 2.0);
3192    let diag = |sx: f64, sy: f64| Point::new(sx * rx * FRAC_1_SQRT_2, sy * ry * FRAC_1_SQRT_2);
3193    match c {
3194        Corner::N => Point::new(0.0, ry),
3195        Corner::S => Point::new(0.0, -ry),
3196        Corner::E => Point::new(rx, 0.0),
3197        Corner::W => Point::new(-rx, 0.0),
3198        Corner::Ne => diag(1.0, 1.0),
3199        Corner::Se => diag(1.0, -1.0),
3200        Corner::Nw => diag(-1.0, 1.0),
3201        Corner::Sw => diag(-1.0, -1.0),
3202        Corner::Center | Corner::Start | Corner::End => Point::ZERO,
3203    }
3204}
3205
3206fn box_corner_offset(c: Corner, w: f64, h: f64, rad: f64) -> Point {
3207    if rad > 0.0 && matches!(c, Corner::Ne | Corner::Se | Corner::Nw | Corner::Sw) {
3208        let inset = rad.min(w.abs().min(h.abs()) / 2.0) * (1.0 - FRAC_1_SQRT_2);
3209        let x = w / 2.0 - inset;
3210        let y = h / 2.0 - inset;
3211        return match c {
3212            Corner::Ne => Point::new(x, y),
3213            Corner::Se => Point::new(x, -y),
3214            Corner::Nw => Point::new(-x, y),
3215            Corner::Sw => Point::new(-x, -y),
3216            _ => Point::ZERO,
3217        };
3218    }
3219    corner_offset(c, w, h)
3220}
3221
3222/// The placed-object kind a `PrimObj` selects, or `None` for untyped `Any`
3223/// (matches every kind).
3224fn primobj_kind(o: &PrimObj) -> Option<PKind> {
3225    Some(match o {
3226        PrimObj::Prim(p) => match p {
3227            Prim::Box => PKind::Box,
3228            Prim::Circle => PKind::Circle,
3229            Prim::Ellipse => PKind::Ellipse,
3230            Prim::Line | Prim::Arrow => PKind::Line,
3231            Prim::Move => PKind::Move,
3232            Prim::Spline => PKind::Spline,
3233            Prim::Arc => PKind::Arc,
3234        },
3235        PrimObj::Brace => PKind::Brace,
3236        PrimObj::Block | PrimObj::EmptyBrack => PKind::Block,
3237        PrimObj::Str(_) => PKind::Text,
3238        PrimObj::Any => return None,
3239    })
3240}
3241
3242fn want_name(k: PKind) -> &'static str {
3243    match k {
3244        PKind::Box => "box",
3245        PKind::Circle => "circle",
3246        PKind::Ellipse => "ellipse",
3247        PKind::Line => "line",
3248        PKind::Move => "move",
3249        PKind::Spline => "spline",
3250        PKind::Arc => "arc",
3251        PKind::Brace => "brace",
3252        PKind::Block => "block",
3253        PKind::Text => "text",
3254    }
3255}
3256
3257fn nearest_dir(v: Point) -> Dir {
3258    if v.x.abs() >= v.y.abs() {
3259        if v.x >= 0.0 { Dir::Right } else { Dir::Left }
3260    } else if v.y >= 0.0 {
3261        Dir::Up
3262    } else {
3263        Dir::Down
3264    }
3265}
3266
3267enum PrintfArg {
3268    Num(f64),
3269    Str(String),
3270}
3271
3272impl PrintfArg {
3273    fn num(&self) -> f64 {
3274        match self {
3275            PrintfArg::Num(v) => *v,
3276            PrintfArg::Str(s) => s.parse::<f64>().unwrap_or(0.0),
3277        }
3278    }
3279
3280    fn string(&self) -> String {
3281        match self {
3282            PrintfArg::Num(v) => fmt_num(*v),
3283            PrintfArg::Str(s) => s.clone(),
3284        }
3285    }
3286}
3287
3288/// Minimal printf-style formatter supporting `%d %i %f %e %g %s %%` with
3289/// optional `.precision`. Width/flags are accepted but ignored.
3290fn sprintf_fmt(fmt: &str, vals: &[PrintfArg]) -> String {
3291    let mut out = String::new();
3292    let mut chars = fmt.chars().peekable();
3293    let mut ai = 0usize;
3294    while let Some(c) = chars.next() {
3295        if c != '%' {
3296            out.push(c);
3297            continue;
3298        }
3299        let mut spec = String::new();
3300        while let Some(&n) = chars.peek() {
3301            if "+-0123456789. #".contains(n) {
3302                spec.push(n);
3303                chars.next();
3304            } else {
3305                break;
3306            }
3307        }
3308        let conv = match chars.next() {
3309            Some(c) => c,
3310            None => break,
3311        };
3312        if conv == '%' {
3313            out.push('%');
3314            continue;
3315        }
3316        let arg = vals.get(ai);
3317        ai += 1;
3318        let prec = spec.split('.').nth(1).and_then(|p| {
3319            p.chars()
3320                .take_while(|c| c.is_ascii_digit())
3321                .collect::<String>()
3322                .parse::<usize>()
3323                .ok()
3324        });
3325        match conv {
3326            'd' | 'i' => out.push_str(&format!(
3327                "{}",
3328                arg.map(PrintfArg::num).unwrap_or(0.0).round() as i64
3329            )),
3330            'f' | 'F' => out.push_str(&format!(
3331                "{:.*}",
3332                prec.unwrap_or(6),
3333                arg.map(PrintfArg::num).unwrap_or(0.0)
3334            )),
3335            'e' | 'E' => out.push_str(&format!(
3336                "{:.*e}",
3337                prec.unwrap_or(6),
3338                arg.map(PrintfArg::num).unwrap_or(0.0)
3339            )),
3340            'g' | 'G' => out.push_str(&format!("{}", arg.map(PrintfArg::num).unwrap_or(0.0))),
3341            's' => out.push_str(&arg.map(PrintfArg::string).unwrap_or_default()),
3342            other => {
3343                out.push('%');
3344                out.push(other);
3345            }
3346        }
3347    }
3348    out
3349}
3350
3351fn stringexpr_lit(se: &StringExpr) -> String {
3352    match se {
3353        StringExpr::Lit(s) => s.clone(),
3354        StringExpr::Concat(a, b) => format!("{}{}", stringexpr_lit(a), stringexpr_lit(b)),
3355        StringExpr::Arg(n) => format!("${n}"),
3356        StringExpr::Sprintf(fmt, _) => stringexpr_lit(fmt),
3357        StringExpr::SvgFont(_) => String::new(),
3358    }
3359}
3360
3361fn single_token_macro_string(toks: &[crate::lexer::Spanned]) -> Option<String> {
3362    let mut toks = toks
3363        .iter()
3364        .filter(|s| !matches!(s.tok, token::Token::Newline | token::Token::Eof));
3365    let tok = &toks.next()?.tok;
3366    if toks.next().is_some() {
3367        return None;
3368    }
3369    match tok {
3370        token::Token::Str(s) | token::Token::Name(s) | token::Token::Label(s) => Some(s.clone()),
3371        _ => None,
3372    }
3373}
3374
3375fn unescape_exec_source(src: &str) -> String {
3376    let mut out = String::with_capacity(src.len());
3377    let mut chars = src.chars();
3378    while let Some(c) = chars.next() {
3379        if c == '\\'
3380            && let Some('"') = chars.clone().next()
3381        {
3382            chars.next();
3383            out.push('"');
3384        } else {
3385            out.push(c);
3386        }
3387    }
3388    out
3389}
3390
3391/// Shift a [`Placed`] (and its block members) by `d` and re-index its shape
3392/// references by `shape_off`, mapping a block's local records into the parent.
3393fn rebase_placed(pl: &mut Placed, d: Point, shape_off: usize) {
3394    pl.center = pl.center + d;
3395    pl.start = pl.start + d;
3396    pl.end = pl.end + d;
3397    for p in &mut pl.points {
3398        *p = *p + d;
3399    }
3400    let mut bb = Bbox::new();
3401    bb.add(pl.bbox.min + d);
3402    bb.add(pl.bbox.max + d);
3403    pl.bbox = bb;
3404    if let Some(s) = pl.shape {
3405        pl.shape = Some(s + shape_off);
3406    }
3407    for m in pl.members.values_mut() {
3408        rebase_placed(m, d, shape_off);
3409    }
3410}
3411
3412fn scale_placed(pl: &mut Placed, f: f64) {
3413    pl.center = pl.center * f;
3414    pl.start = pl.start * f;
3415    pl.end = pl.end * f;
3416    for p in &mut pl.points {
3417        *p = *p * f;
3418    }
3419    pl.radius *= f;
3420    pl.box_rad *= f;
3421    pl.line_wid *= f;
3422    pl.line_ht *= f;
3423    scale_bbox_in_place(&mut pl.bbox, f);
3424    for m in pl.members.values_mut() {
3425        scale_placed(m, f);
3426    }
3427}
3428
3429fn translate_shape(sh: &mut Shape, d: Point) {
3430    let mv = |p: &mut Point| *p = *p + d;
3431    match sh {
3432        Shape::Box { c, .. } | Shape::Circle { c, .. } | Shape::Ellipse { c, .. } => mv(c),
3433        Shape::Path { pts, .. } | Shape::Spline { pts, .. } => {
3434            for p in pts {
3435                mv(p);
3436            }
3437        }
3438        Shape::Arc { c, .. } => mv(c),
3439        Shape::Brace {
3440            a,
3441            b,
3442            cubics,
3443            label_at,
3444            ..
3445        } => {
3446            mv(a);
3447            mv(b);
3448            mv(label_at);
3449            for cubic in cubics {
3450                for p in cubic {
3451                    mv(p);
3452                }
3453            }
3454        }
3455        Shape::Text { at, bbox, .. } => {
3456            mv(at);
3457            bbox.min = bbox.min + d;
3458            bbox.max = bbox.max + d;
3459        }
3460    }
3461}
3462
3463fn scale_bbox_in_place(bb: &mut Bbox, f: f64) {
3464    if bb.is_empty() {
3465        return;
3466    }
3467    let mut b = Bbox::new();
3468    b.add(bb.min * f);
3469    b.add(bb.max * f);
3470    *bb = b;
3471}
3472
3473fn scale_style(style: &mut Style, f: f64) {
3474    let f = f.abs();
3475    style.arrow_ht *= f;
3476    style.arrow_wid *= f;
3477    if let Some(hatch) = &mut style.hatch {
3478        hatch.sep *= f;
3479    }
3480    match &mut style.dash {
3481        Dash::Dashed(w) => *w *= f,
3482        Dash::Dotted(Some(w)) => *w *= f,
3483        Dash::Solid | Dash::Dotted(None) => {}
3484    }
3485}
3486
3487fn multiply_shape_fill_opacity(sh: &mut Shape, opacity: f64) {
3488    match sh {
3489        Shape::Box { style, .. }
3490        | Shape::Circle { style, .. }
3491        | Shape::Ellipse { style, .. }
3492        | Shape::Path { style, .. }
3493        | Shape::Spline { style, .. }
3494        | Shape::Arc { style, .. }
3495        | Shape::Brace { style, .. } => {
3496            style.fill_opacity = Some(style.fill_opacity.unwrap_or(1.0) * opacity);
3497        }
3498        Shape::Text { .. } => {}
3499    }
3500}
3501
3502/// Uniformly scale a shape's geometry about the origin (font size unchanged).
3503fn scale_shape(sh: &mut Shape, f: f64) {
3504    match sh {
3505        Shape::Box {
3506            c,
3507            w,
3508            h,
3509            rad,
3510            style,
3511            ..
3512        } => {
3513            *c = *c * f;
3514            *w *= f;
3515            *h *= f;
3516            *rad *= f;
3517            scale_style(style, f);
3518        }
3519        Shape::Circle { c, r, style, .. } => {
3520            *c = *c * f;
3521            *r *= f;
3522            scale_style(style, f);
3523        }
3524        Shape::Ellipse { c, w, h, style, .. } => {
3525            *c = *c * f;
3526            *w *= f;
3527            *h *= f;
3528            scale_style(style, f);
3529        }
3530        Shape::Path { pts, style, .. } | Shape::Spline { pts, style, .. } => {
3531            for p in pts {
3532                *p = *p * f;
3533            }
3534            scale_style(style, f);
3535        }
3536        Shape::Arc { c, r, style, .. } => {
3537            *c = *c * f;
3538            *r *= f;
3539            scale_style(style, f);
3540        }
3541        Shape::Brace {
3542            a,
3543            b,
3544            cubics,
3545            label_at,
3546            style,
3547            ..
3548        } => {
3549            *a = *a * f;
3550            *b = *b * f;
3551            *label_at = *label_at * f;
3552            for cubic in cubics {
3553                for p in cubic {
3554                    *p = *p * f;
3555                }
3556            }
3557            scale_style(style, f);
3558        }
3559        Shape::Text { at, bbox, w, h, .. } => {
3560            *at = *at * f;
3561            scale_bbox_in_place(bbox, f);
3562            *w *= f;
3563            *h *= f;
3564        }
3565    }
3566}
3567
3568#[cfg(test)]
3569mod tests {
3570    use super::*;
3571    use crate::parser::{parse, parse_in_dir};
3572
3573    fn draw(src: &str) -> Drawing {
3574        eval(&parse(src).unwrap()).unwrap()
3575    }
3576
3577    fn scalar(src: &str) -> ER<f64> {
3578        let prog = parse(&format!("x = {src}")).unwrap();
3579        let mut st = State::new();
3580        st.eval_stmts(&prog.stmts)?;
3581        Ok(st.vars["x"])
3582    }
3583
3584    fn assert_box_size(shape: &Shape, want_w: f64, want_h: f64) {
3585        let Shape::Box { w, h, .. } = shape else {
3586            panic!()
3587        };
3588        assert!((*w - want_w).abs() < 1e-9, "w = {w}, want {want_w}");
3589        assert!((*h - want_h).abs() < 1e-9, "h = {h}, want {want_h}");
3590    }
3591
3592    const DEFAULT_STROKE_IN: f64 = 0.8 / 72.0;
3593
3594    #[test]
3595    fn pipeline_chains_left_to_right() {
3596        let d = draw(".PS\nellipse \"document\"\narrow\nbox \"PIC\"\narrow\nbox \"TROFF\"\n.PE");
3597        // 5 shapes
3598        assert_eq!(d.shapes.len(), 5);
3599        // first ellipse centered at (0.375, 0): ellipsewid/2
3600        if let Shape::Ellipse { c, .. } = &d.shapes[0] {
3601            assert!((c.x - 0.375).abs() < 1e-9, "ellipse x = {}", c.x);
3602            assert!(c.y.abs() < 1e-9);
3603        } else {
3604            panic!("expected ellipse");
3605        }
3606        // bbox grows to the right
3607        assert!(d.bbox.width() > 2.0);
3608        assert!((d.bbox.height() - (0.5 + DEFAULT_STROKE_IN)).abs() < 1e-9);
3609    }
3610
3611    #[test]
3612    fn box_at_absolute() {
3613        let d = draw("box ht 0.3 wid 0.5 at 1,2");
3614        let Shape::Box { c, w, h, .. } = &d.shapes[0] else {
3615            panic!()
3616        };
3617        assert_eq!(*c, Point::new(1.0, 2.0));
3618        assert!((*w - 0.5).abs() < 1e-9 && (*h - 0.3).abs() < 1e-9);
3619    }
3620
3621    #[test]
3622    fn diamond_is_closed() {
3623        let d = draw("line up right then down right then down left then up left");
3624        let Shape::Path { pts, .. } = &d.shapes[0] else {
3625            panic!()
3626        };
3627        assert_eq!(pts.len(), 5);
3628        // returns to start
3629        assert!(pts[0].dist(*pts.last().unwrap()) < 1e-9);
3630    }
3631
3632    #[test]
3633    fn close_line_marks_polygon_and_uses_bbox_center_anchor() {
3634        let d = draw(
3635            "L: line right 1 then up 1 close\nbox wid .1 ht .1 at L.c\nbox wid .1 ht .1 at L.end",
3636        );
3637        let Shape::Path { pts, closed, .. } = &d.shapes[0] else {
3638            panic!()
3639        };
3640        assert!(*closed);
3641        assert_eq!(pts.len(), 4);
3642        assert_eq!(pts[0], Point::ZERO);
3643        assert_eq!(pts[1], Point::new(1.0, 0.0));
3644        assert_eq!(pts[2], Point::new(1.0, 1.0));
3645        assert_eq!(pts[3], Point::ZERO);
3646
3647        let Shape::Box { c, .. } = &d.shapes[1] else {
3648            panic!()
3649        };
3650        assert_eq!(*c, Point::new(0.5, 0.5));
3651
3652        let Shape::Box { c, .. } = &d.shapes[2] else {
3653            panic!()
3654        };
3655        assert_eq!(*c, Point::ZERO);
3656    }
3657
3658    #[test]
3659    fn close_line_requires_three_vertices_and_ends_path() {
3660        let err = eval(&parse("line right close").unwrap()).unwrap_err();
3661        assert!(err.msg.contains("need at least 3 vertices"), "{err}");
3662
3663        let err = eval(&parse("line right then up close then left").unwrap()).unwrap_err();
3664        assert!(err.msg.contains("polygon is closed"), "{err}");
3665    }
3666
3667    #[test]
3668    fn corners_and_labels() {
3669        let d = draw("A: box wid 1 ht 1 at 0,0\nbox wid 0.5 ht 0.5 with .sw at A.ne");
3670        // second box sw corner at A.ne (0.5,0.5) => its center at (0.75,0.75)
3671        let Shape::Box { c, .. } = &d.shapes[1] else {
3672            panic!()
3673        };
3674        assert!(
3675            (c.x - 0.75).abs() < 1e-9 && (c.y - 0.75).abs() < 1e-9,
3676            "c = {c:?}"
3677        );
3678    }
3679
3680    #[test]
3681    fn circle_corner_anchors_are_on_the_circumference() {
3682        let d = draw("C: circle rad 1 at 0,0\nline from C.sw to C.ne");
3683        let Shape::Path { pts, .. } = &d.shapes[1] else {
3684            panic!()
3685        };
3686        let a = 1.0 / 2.0_f64.sqrt();
3687        assert_eq!(pts.len(), 2);
3688        assert!((pts[0].x + a).abs() < 1e-9 && (pts[0].y + a).abs() < 1e-9);
3689        assert!((pts[1].x - a).abs() < 1e-9 && (pts[1].y - a).abs() < 1e-9);
3690    }
3691
3692    #[test]
3693    fn type_specific_corner_anchors_match_dpic() {
3694        let d = draw("L: line from (0,0) to (1,0) then to (1,1)\ncircle rad .01 at L.nw");
3695        let Shape::Circle { c, .. } = &d.shapes[1] else {
3696            panic!()
3697        };
3698        assert!(c.dist(Point::new(0.0, 0.0)) < 1e-9, "line nw = {c:?}");
3699
3700        let d = draw("A: arc cw rad 0.5 from (0,0) to (0.5,0.5)\ncircle rad .01 at A.s");
3701        let Shape::Circle { c, .. } = &d.shapes[1] else {
3702            panic!()
3703        };
3704        assert!(c.dist(Point::new(0.5, -0.5)) < 1e-9, "arc s = {c:?}");
3705
3706        let d = draw("B: box wid 1 ht 1 rad 0.3 at (0,0)\ncircle rad .01 at B.ne");
3707        let Shape::Circle { c, .. } = &d.shapes[1] else {
3708            panic!()
3709        };
3710        let inset = 0.3 * (1.0 - FRAC_1_SQRT_2);
3711        let expected = Point::new(0.5 - inset, 0.5 - inset);
3712        assert!(c.dist(expected) < 1e-9, "rounded box ne = {c:?}");
3713    }
3714
3715    #[test]
3716    fn with_corner_uses_ellipse_geometry_for_placed_object() {
3717        let d = draw("ellipse; ellipse with .nw at last ellipse.se");
3718        let Shape::Ellipse { c: first, .. } = &d.shapes[0] else {
3719            panic!()
3720        };
3721        let Shape::Ellipse { c: second, .. } = &d.shapes[1] else {
3722            panic!()
3723        };
3724        let expected = *first + Point::new(0.75 * FRAC_1_SQRT_2, -0.5 * FRAC_1_SQRT_2);
3725        assert!(
3726            second.dist(expected) < 1e-9,
3727            "second center = {second:?}, expected = {expected:?}"
3728        );
3729    }
3730
3731    #[test]
3732    fn for_loop_repeats() {
3733        let d = draw("for i = 1 to 3 do { box }");
3734        assert_eq!(d.shapes.len(), 3);
3735    }
3736
3737    #[test]
3738    fn for_loop_can_assign_subscripted_counter() {
3739        let d = draw("i = 1\nfor A[i] = 1 to 3 do { i += 1 }\nbox wid A[1] + A[2] + A[3] ht 0.3");
3740        let Shape::Box { w, .. } = &d.shapes[0] else {
3741            panic!()
3742        };
3743        assert!((*w - 6.0).abs() < 1e-9, "w = {w}");
3744    }
3745
3746    #[test]
3747    fn if_else_branches() {
3748        let d1 = draw("x = 1\nif x > 0 then { box } else { circle }");
3749        assert!(matches!(d1.shapes[0], Shape::Box { .. }));
3750        let d2 = draw("x = 0\nif x > 0 then { box } else { circle }");
3751        assert!(matches!(d2.shapes[0], Shape::Circle { .. }));
3752    }
3753
3754    #[test]
3755    fn define_macro_with_args() {
3756        let d = draw("define elem { box wid $1 }\nelem(0.5)\nelem(1.25)");
3757        assert_eq!(d.shapes.len(), 2);
3758        let Shape::Box { w: w0, .. } = &d.shapes[0] else {
3759            panic!()
3760        };
3761        let Shape::Box { w: w1, .. } = &d.shapes[1] else {
3762            panic!()
3763        };
3764        assert!((*w0 - 0.5).abs() < 1e-9 && (*w1 - 1.25).abs() < 1e-9);
3765    }
3766
3767    #[test]
3768    fn define_accepts_arbitrary_delimiter() {
3769        let d = draw("define elem / box /\nelem");
3770        assert_eq!(d.shapes.len(), 1);
3771        assert!(matches!(d.shapes[0], Shape::Box { .. }));
3772    }
3773
3774    #[test]
3775    fn labelled_call_to_multiline_body_macro() {
3776        // A multi-line `{ … }` body picks up newlines after `{` / before `}`.
3777        // They must not leak into a labelled call (`Q: m()` -> `Q: ⏎ <obj>`),
3778        // which would be a parse error. The block's terminals must still resolve.
3779        let d = draw(
3780            "define elem {\n  [\n    box wid 0.4 ht 0.2\n    L: last box.w\n    R: last box.e\n  ]\n}\nQ: elem() with .L at (1,1)\n\"x\" at Q.R",
3781        );
3782        // the block drew its box, and Q.R (a block sub-label) resolved for the text
3783        assert!(d.shapes.iter().any(|s| matches!(s, Shape::Box { .. })));
3784        let Shape::Text { at, .. } = d.shapes.last().unwrap() else {
3785            panic!()
3786        };
3787        // Q placed with .L (west) at (1,1); .R (east) is one box-width to the right
3788        assert!(
3789            (at.x - 1.4).abs() < 1e-6 && (at.y - 1.0).abs() < 1e-6,
3790            "at = {at:?}"
3791        );
3792    }
3793
3794    #[test]
3795    fn copied_forward_macro_expands_in_deferred_multiline_call() {
3796        let dir = std::env::temp_dir().join(format!("rpic_forward_macro_{}", std::process::id()));
3797        std::fs::create_dir_all(&dir).unwrap();
3798        std::fs::write(
3799            dir.join("lib.pic"),
3800            "define outer { if gate then { inner(0.2,\n  0.3) } }\ndefine inner { box wid $1 ht $2 }\n",
3801        )
3802        .unwrap();
3803
3804        let pic = parse_in_dir(
3805            "if 1 then { copy \"lib.pic\" }\ngate = 1\nouter()",
3806            Some(dir.as_path()),
3807        )
3808        .unwrap();
3809        let d = eval(&pic).unwrap();
3810        let _ = std::fs::remove_dir_all(&dir);
3811
3812        let Shape::Box { w, h, .. } = &d.shapes[0] else {
3813            panic!()
3814        };
3815        assert!((*w - 0.2).abs() < 1e-9 && (*h - 0.3).abs() < 1e-9);
3816    }
3817
3818    #[test]
3819    fn deferred_body_uses_macro_frame_from_own_expansion() {
3820        let d = draw(
3821            "define draw_one { [\n  scalev = 2\n  define project { $1/scalev }\n  if use_it then { box wid project(4) ht 0.1 }\n] }\ndefine draw_two { define project { $1/future_scale } }\nuse_it = 1\ndraw_one()\ndraw_two()",
3822        );
3823        let Shape::Box { w, .. } = &d.shapes[0] else {
3824            panic!()
3825        };
3826        assert!((*w - 2.0).abs() < 1e-9, "w = {w}");
3827    }
3828
3829    #[test]
3830    fn subscripted_variables_store_by_index() {
3831        let d = draw("P[1] = 0.4\nP[2] = 0.9\nP[2] += 0.1\nbox wid P[2] ht P[1]");
3832        let Shape::Box { w, h, .. } = &d.shapes[0] else {
3833            panic!()
3834        };
3835        assert!((*w - 1.0).abs() < 1e-9 && (*h - 0.4).abs() < 1e-9);
3836    }
3837
3838    #[test]
3839    fn multidimensional_variables_store_by_index_tuple() {
3840        let d = draw("M[1,2] = 0.7\nj = 2\nM[1,j] += 0.2\nbox wid M[1,2] ht 0.3");
3841        let Shape::Box { w, .. } = &d.shapes[0] else {
3842            panic!()
3843        };
3844        assert!((*w - 0.9).abs() < 1e-9, "w = {w}");
3845    }
3846
3847    #[test]
3848    fn subscripted_label_places_resolve_by_index() {
3849        let d =
3850            draw("for i = 1 to 2 do { A[i]: circle rad 0.01 at (i,0) }\nline from A[1] to A[2]");
3851        let Shape::Path { pts, .. } = &d.shapes[2] else {
3852            panic!()
3853        };
3854        assert!(
3855            pts[0].dist(Point::new(1.0, 0.0)) < 1e-9,
3856            "start {:?}",
3857            pts[0]
3858        );
3859        assert!(pts[1].dist(Point::new(2.0, 0.0)) < 1e-9, "end {:?}", pts[1]);
3860    }
3861
3862    #[test]
3863    fn for_loop_places_in_a_row() {
3864        // boxes step right by default; bbox should be ~3*boxwid wide
3865        let d = draw("for i = 1 to 3 do { box; move }");
3866        assert!(d.bbox.width() > 2.0);
3867    }
3868
3869    #[test]
3870    fn animate_timing() {
3871        let d = draw(
3872            "A: box\narrow\nbox\nanimate A with \"fade\" for 0.5\nanimate last arrow with \"draw\"\nanimate 2nd box with \"pop\" after A",
3873        );
3874        assert_eq!(d.anims.len(), 3);
3875        // A: fade, start 0, dur 0.5, targets shape 0
3876        assert_eq!(d.anims[0].effect, "fade");
3877        assert_eq!(d.anims[0].shape, 0);
3878        assert!((d.anims[0].start).abs() < 1e-9 && (d.anims[0].duration - 0.5).abs() < 1e-9);
3879        // arrow: draw, sequential after A -> start 0.5, default dur 0.6, shape 1
3880        assert_eq!(d.anims[1].shape, 1);
3881        assert!((d.anims[1].start - 0.5).abs() < 1e-9);
3882        // 2nd box: pop, after A (ends at 0.5) -> start 0.5, shape 2
3883        assert_eq!(d.anims[2].shape, 2);
3884        assert!((d.anims[2].start - 0.5).abs() < 1e-9);
3885    }
3886
3887    #[test]
3888    fn behind_sets_render_layer_without_changing_shape_indices() {
3889        let d = draw("A: box\nB: box behind A\nanimate B with \"fade\"");
3890        assert_eq!(d.shapes.len(), 2);
3891        assert_eq!(d.shape_layers, vec![0, -1]);
3892        assert_eq!(d.anims.len(), 1);
3893        assert_eq!(d.anims[0].shape, 1);
3894    }
3895
3896    #[test]
3897    fn behind_keeps_last_and_ordinals_semantic() {
3898        let d =
3899            draw("A: box at (0,0)\nB: box behind A at (2,0)\nline from last box.c to 1st box.c");
3900        let Shape::Path { pts, .. } = &d.shapes[2] else {
3901            panic!()
3902        };
3903        assert_eq!(pts.len(), 2);
3904        assert!(
3905            pts[0].dist(Point::new(2.0, 0.0)) < 1e-9,
3906            "start = {:?}",
3907            pts[0]
3908        );
3909        assert!(
3910            pts[1].dist(Point::new(0.0, 0.0)) < 1e-9,
3911            "end = {:?}",
3912            pts[1]
3913        );
3914    }
3915
3916    #[test]
3917    fn continue_extends_previous_line() {
3918        // issue #7: `continue` appends a segment to the last line (no new shape)
3919        let d = draw("line right 1\ncontinue down 0.5");
3920        assert_eq!(d.shapes.len(), 1, "should extend, not add a shape");
3921        let Shape::Path { pts, .. } = &d.shapes[0] else {
3922            panic!()
3923        };
3924        assert_eq!(pts.len(), 3);
3925        assert!(pts[2].dist(Point::new(1.0, -0.5)) < 1e-9, "{:?}", pts[2]);
3926        // bare continue extends in the current direction by linewid
3927        let d2 = draw("line right 1\ncontinue");
3928        let Shape::Path { pts, .. } = &d2.shapes[0] else {
3929            panic!()
3930        };
3931        assert!((pts.last().unwrap().x - 1.5).abs() < 1e-9);
3932    }
3933
3934    #[test]
3935    fn arc_from_to_endpoints_and_radius() {
3936        // issue #6: `arc from A to B` passes through both endpoints
3937        let d = draw("A:(0,0)\nB:(1,1)\narc from A to B");
3938        let Shape::Arc { c, r, a0, a1, .. } = &d.shapes[0] else {
3939            panic!()
3940        };
3941        assert!((*r - 2.0_f64.sqrt() / 2.0).abs() < 1e-9, "r = {r}");
3942        let s = *c + Point::new(a0.cos(), a0.sin()) * *r;
3943        let e = *c + Point::new(a1.cos(), a1.sin()) * *r;
3944        assert!(s.dist(Point::new(0.0, 0.0)) < 1e-9, "start {s:?}");
3945        assert!(e.dist(Point::new(1.0, 1.0)) < 1e-9, "end {e:?}");
3946
3947        let d_short = draw("arc from (0,0) to (0.3,0)");
3948        let Shape::Arc { r, .. } = &d_short.shapes[0] else {
3949            panic!()
3950        };
3951        assert!((*r - 0.25).abs() < 1e-9, "r = {r}");
3952
3953        let d_custom = draw("arcrad = 0.5\narc from (0,0) to (0.3,0)");
3954        let Shape::Arc { r, .. } = &d_custom.shapes[0] else {
3955            panic!()
3956        };
3957        assert!((*r - 0.5).abs() < 1e-9, "r = {r}");
3958
3959        // explicit radius is honored
3960        let d2 = draw("A:(0,0)\nB:(1,0)\narc from A to B rad 2");
3961        let Shape::Arc { r, .. } = &d2.shapes[0] else {
3962            panic!()
3963        };
3964        assert!((*r - 2.0).abs() < 1e-9, "r = {r}");
3965    }
3966
3967    #[test]
3968    fn arc_direction_disambiguates_from_to_radius() {
3969        let d = draw(
3970            "arc left from (0.5,0) to (0,0.5) rad 0.5\n\
3971             arc right from (0.5,0) to (0,0.5) rad 0.5 dashed",
3972        );
3973        let Shape::Arc {
3974            c: left,
3975            a0: left_a0,
3976            a1: left_a1,
3977            ..
3978        } = &d.shapes[0]
3979        else {
3980            panic!()
3981        };
3982        let Shape::Arc {
3983            c: right,
3984            a0: right_a0,
3985            a1: right_a1,
3986            ..
3987        } = &d.shapes[1]
3988        else {
3989            panic!()
3990        };
3991
3992        assert!(left.dist(Point::new(0.0, 0.0)) < 1e-9, "left = {left:?}");
3993        assert!(right.dist(Point::new(0.5, 0.5)) < 1e-9, "right = {right:?}");
3994        assert!((left_a1 - left_a0).abs() < PI);
3995        assert!((right_a1 - right_a0).abs() > PI);
3996    }
3997
3998    #[test]
3999    fn arc_with_center_at_disambiguates_large_clockwise_sweep() {
4000        let d = draw("arc cw rad 1 from (0,-1) to (1,0) with .c at (0,0)");
4001        let Shape::Arc { c, r, a0, a1, .. } = &d.shapes[0] else {
4002            panic!()
4003        };
4004        let start = *c + Point::new(a0.cos(), a0.sin()) * *r;
4005        let end = *c + Point::new(a1.cos(), a1.sin()) * *r;
4006
4007        assert!(c.dist(Point::ZERO) < 1e-9, "center = {c:?}");
4008        assert!((*r - 1.0).abs() < 1e-9, "r = {r}");
4009        assert!(
4010            start.dist(Point::new(0.0, -1.0)) < 1e-9,
4011            "start = {start:?}"
4012        );
4013        assert!(end.dist(Point::new(1.0, 0.0)) < 1e-9, "end = {end:?}");
4014        assert!(*a1 - *a0 < -PI, "sweep = {}", *a1 - *a0);
4015    }
4016
4017    #[test]
4018    fn arc_width_height_attrs_size_arrowheads() {
4019        let d = draw("arc <-> wid .5 ht .75");
4020        let Shape::Arc { style, .. } = &d.shapes[0] else {
4021            panic!()
4022        };
4023
4024        assert!(
4025            (style.arrow_wid - 0.5).abs() < 1e-9,
4026            "wid = {}",
4027            style.arrow_wid
4028        );
4029        assert!(
4030            (style.arrow_ht - 0.75).abs() < 1e-9,
4031            "ht = {}",
4032            style.arrow_ht
4033        );
4034    }
4035
4036    #[test]
4037    fn scale_converts_user_units_to_inches() {
4038        // `scale = 2` means two user units per inch: defaults stay the same
4039        // physical size, while explicit dimensions and coordinates are halved.
4040        let d = draw("scale = 2\nbox");
4041        let Shape::Box { w, h, .. } = &d.shapes[0] else {
4042            panic!()
4043        };
4044        assert!(
4045            (*w - 0.75).abs() < 1e-9 && (*h - 0.5).abs() < 1e-9,
4046            "{w} x {h}"
4047        );
4048
4049        let d = draw("scale = 2\nbox wid 2 ht 1");
4050        let Shape::Box { w, h, .. } = &d.shapes[0] else {
4051            panic!()
4052        };
4053        assert!((*w - 1.0).abs() < 1e-9 && (*h - 0.5).abs() < 1e-9);
4054
4055        let d = draw("scale = 2\nline from (0,0) to (2,0)");
4056        let Shape::Path { pts, .. } = &d.shapes[0] else {
4057            panic!()
4058        };
4059        assert!((pts.last().unwrap().x - 1.0).abs() < 1e-9, "{pts:?}");
4060
4061        let d = draw("scale = 2\nA: (2,0)\nbox wid A.x ht .2");
4062        let Shape::Box { w, .. } = &d.shapes[0] else {
4063            panic!()
4064        };
4065        assert!((*w - 1.0).abs() < 1e-9, "w = {w}");
4066
4067        let d = draw("scale = 2\nbox wid 2 ht 1\nscale = 1\nbox wid 1 ht .5");
4068        let Shape::Box { w, .. } = &d.shapes[0] else {
4069            panic!()
4070        };
4071        assert!((*w - 2.0).abs() < 1e-9, "w = {w}");
4072        let Shape::Box { c, .. } = &d.shapes[1] else {
4073            panic!()
4074        };
4075        assert!((c.x - 2.5).abs() < 1e-9, "center = {c:?}");
4076    }
4077
4078    #[test]
4079    fn same_reuses_previous_dims() {
4080        // issue #4: `box same` reuses the previous box's dimensions
4081        let d = draw("box wid 1 ht 0.4 at 0,0\nbox same at 2,0");
4082        let Shape::Box { w, h, .. } = &d.shapes[1] else {
4083            panic!()
4084        };
4085        assert!(
4086            (*w - 1.0).abs() < 1e-9 && (*h - 0.4).abs() < 1e-9,
4087            "{w} x {h}"
4088        );
4089    }
4090
4091    #[test]
4092    fn same_reuses_previous_open_vector() {
4093        let d = draw("line up 1\nright\nline same");
4094        let Shape::Path { pts, .. } = &d.shapes[1] else {
4095            panic!()
4096        };
4097        assert!(pts[0].dist(Point::new(0.0, 1.0)) < 1e-9, "{pts:?}");
4098        assert!(pts[1].dist(Point::new(0.0, 2.0)) < 1e-9, "{pts:?}");
4099    }
4100
4101    #[test]
4102    fn spline_expr_is_tension_not_distance() {
4103        // issue #63: `spline <expr>` is a dpic tension parameter, not a bare
4104        // distance. The control polygon (and thus start/end) must be unchanged,
4105        // and the tension recorded.
4106        let d = draw("spline 0.5 from 0,0 to 1,1 to 2,0");
4107        let Shape::Spline { pts, tension, .. } = &d.shapes[0] else {
4108            panic!("expected a spline")
4109        };
4110        assert_eq!(*tension, Some(0.5));
4111        assert_eq!(pts.len(), 3, "tension must not add a segment: {pts:?}");
4112        assert!(pts[0].dist(Point::new(0.0, 0.0)) < 1e-9, "{pts:?}");
4113        assert!(pts[2].dist(Point::new(2.0, 0.0)) < 1e-9, "{pts:?}");
4114    }
4115
4116    #[test]
4117    fn spline_variable_tension_does_not_drift() {
4118        // The doc/spline.pic idiom: `for x … { spline x from 0,0 … }`. Each
4119        // tensioned spline must keep the same start/end as the untensioned one
4120        // (only the curvature changes), instead of `x` shifting the geometry.
4121        let plain = draw("spline from 0,0 up 1.5 then right 2 then down 1.5");
4122        let Shape::Spline {
4123            pts: p0,
4124            tension: t0,
4125            ..
4126        } = &plain.shapes[0]
4127        else {
4128            panic!()
4129        };
4130        assert_eq!(*t0, None);
4131
4132        let tensioned = draw("x = 0.6\nspline x from 0,0 up 1.5 then right 2 then down 1.5");
4133        let Shape::Spline {
4134            pts: p1,
4135            tension: t1,
4136            ..
4137        } = &tensioned.shapes[0]
4138        else {
4139            panic!()
4140        };
4141        assert_eq!(*t1, Some(0.6));
4142        assert_eq!(p0.len(), p1.len());
4143        assert!(p0.first().unwrap().dist(*p1.first().unwrap()) < 1e-9);
4144        assert!(p0.last().unwrap().dist(*p1.last().unwrap()) < 1e-9);
4145    }
4146
4147    #[test]
4148    fn chop_trims_line_endpoints() {
4149        // issue #4: `chop` trims circlerad (0.25) off each end
4150        let d = draw("circle at 0,0\ncircle at 2,0\nline from 1st circle to 2nd circle chop");
4151        let Shape::Path { pts, .. } = &d.shapes[2] else {
4152            panic!()
4153        };
4154        assert!((pts[0].x - 0.25).abs() < 1e-9, "start {:?}", pts[0]);
4155        assert!(
4156            (pts.last().unwrap().x - 1.75).abs() < 1e-9,
4157            "end {:?}",
4158            pts.last()
4159        );
4160
4161        let d = draw("line from (0,0) to (2,0) chop 0 chop .5");
4162        let Shape::Path { pts, .. } = &d.shapes[0] else {
4163            panic!()
4164        };
4165        assert!((pts[0].x - 0.0).abs() < 1e-9, "{pts:?}");
4166        assert!((pts[1].x - 1.5).abs() < 1e-9, "{pts:?}");
4167
4168        let d = draw("line from (0,0) to (2,0) chop .5 chop 0");
4169        let Shape::Path { pts, .. } = &d.shapes[0] else {
4170            panic!()
4171        };
4172        assert!((pts[0].x - 0.5).abs() < 1e-9, "{pts:?}");
4173        assert!((pts[1].x - 2.0).abs() < 1e-9, "{pts:?}");
4174
4175        let d = draw("line from (0,0) to (2,0) chop -.25 chop -.5");
4176        let Shape::Path { pts, .. } = &d.shapes[0] else {
4177            panic!()
4178        };
4179        assert!((pts[0].x + 0.25).abs() < 1e-9, "{pts:?}");
4180        assert!((pts[1].x - 2.5).abs() < 1e-9, "{pts:?}");
4181    }
4182
4183    #[test]
4184    fn unknown_variables_are_errors() {
4185        assert!(eval(&parse("box wid typo ht 0.2").unwrap()).is_err());
4186        assert!(eval(&parse("typo += 1").unwrap()).is_err());
4187    }
4188
4189    #[test]
4190    fn rand_advances_and_seed_repeats() {
4191        let mut st = State::new();
4192        let a = st.eval_expr(&Expr::Rand(None)).unwrap();
4193        let b = st.eval_expr(&Expr::Rand(None)).unwrap();
4194        assert!((0.0..1.0).contains(&a));
4195        assert!((0.0..1.0).contains(&b));
4196        assert_ne!(a, b);
4197
4198        let seeded_a = st
4199            .eval_expr(&Expr::Rand(Some(Box::new(Expr::Num(1.0)))))
4200            .unwrap();
4201        let seeded_b = st
4202            .eval_expr(&Expr::Rand(Some(Box::new(Expr::Num(1.0)))))
4203            .unwrap();
4204        assert_eq!(seeded_a, seeded_b);
4205        assert!((seeded_a - 0.840_187_717).abs() < 1e-9, "{seeded_a}");
4206        let next = st.eval_expr(&Expr::Rand(None)).unwrap();
4207        assert!((next - 0.394_382_927).abs() < 1e-9, "{next}");
4208    }
4209
4210    #[test]
4211    fn arithmetic_matches_dpic_edge_cases() {
4212        assert_eq!(scalar("5.5 % 2").unwrap(), 0.0);
4213        assert_eq!(scalar("2.5 % 2").unwrap(), 1.0);
4214        assert_eq!(scalar("-2.5 % 2").unwrap(), -1.0);
4215        assert!(scalar("5 % 0.4").is_err());
4216
4217        let mut st = State::new();
4218        st.eval_stmts(&parse("x = 5.5; x %= 2").unwrap().stmts)
4219            .unwrap();
4220        assert_eq!(st.vars["x"], 0.0);
4221
4222        assert_eq!(scalar("sign(0)").unwrap(), 1.0);
4223        assert_eq!(scalar("sign(-0.1)").unwrap(), -1.0);
4224
4225        assert_eq!(scalar("(-2)^3").unwrap(), -8.0);
4226        assert_eq!(scalar("(-2)^2").unwrap(), 4.0);
4227        assert_eq!(scalar("0^0").unwrap(), 1.0);
4228        assert!(scalar("(-2)^0.5").is_err());
4229        assert!(scalar("0^-1").is_err());
4230    }
4231
4232    #[test]
4233    fn standalone_text_occupies_invisible_box() {
4234        let d = draw("textwid = 1; textht = .2\n\"x\"\nbox wid .2 ht .2");
4235        let Shape::Text { at, .. } = &d.shapes[0] else {
4236            panic!()
4237        };
4238        assert!((at.x - 0.5).abs() < 1e-9, "text at {at:?}");
4239        let Shape::Box { c, .. } = &d.shapes[1] else {
4240            panic!()
4241        };
4242        assert!((c.x - 1.1).abs() < 1e-9, "box center {c:?}");
4243    }
4244
4245    #[test]
4246    fn text_position_modifies_the_preceding_string_only() {
4247        let d = draw("\"LLLL\" ljust");
4248        let Shape::Text { text, .. } = &d.shapes[0] else {
4249            panic!()
4250        };
4251        assert_eq!(text[0].halign, -1);
4252
4253        let d = draw("\"RRRR\" rjust");
4254        let Shape::Text { text, .. } = &d.shapes[0] else {
4255            panic!()
4256        };
4257        assert_eq!(text[0].halign, 1);
4258
4259        let d = draw("box wid 1 ht .6 \"AAAA\" above \"BBBB\" below");
4260        let Shape::Box { text, .. } = &d.shapes[0] else {
4261            panic!()
4262        };
4263        assert_eq!(text[0].valign, 1);
4264        assert_eq!(text[1].valign, -1);
4265
4266        let d = draw("box \"AAAA\" above \"BBBB\"");
4267        let Shape::Box { text, .. } = &d.shapes[0] else {
4268            panic!()
4269        };
4270        assert_eq!(text[0].valign, 1);
4271        assert_eq!(text[1].valign, 0);
4272    }
4273
4274    #[test]
4275    fn fit_sizes_closed_objects_to_preceding_text() {
4276        let d = draw(
4277            "box \"wide label\" fit\n\
4278             ellipse \"one\" \"two\" \"three\" fit\n\
4279             circle \"wide label\" fit",
4280        );
4281
4282        let Shape::Box { w, h, text, .. } = &d.shapes[0] else {
4283            panic!()
4284        };
4285        let (want_w, want_h) = fitted_text_size(text).unwrap();
4286        assert!((*w - want_w).abs() < 1e-9, "box w = {w}, want {want_w}");
4287        assert!((*h - want_h).abs() < 1e-9, "box h = {h}, want {want_h}");
4288
4289        let Shape::Ellipse { w, h, text, .. } = &d.shapes[1] else {
4290            panic!()
4291        };
4292        let (want_w, want_h) = fitted_text_size(text).unwrap();
4293        assert!((*w - want_w).abs() < 1e-9, "ellipse w = {w}, want {want_w}");
4294        assert!((*h - want_h).abs() < 1e-9, "ellipse h = {h}, want {want_h}");
4295
4296        let Shape::Circle { r, text, .. } = &d.shapes[2] else {
4297            panic!()
4298        };
4299        let (want_w, want_h) = fitted_text_size(text).unwrap();
4300        let want_r = want_w.hypot(want_h) / 2.0;
4301        assert!((*r - want_r).abs() < 1e-9, "circle r = {r}, want {want_r}");
4302    }
4303
4304    #[test]
4305    fn fit_respects_explicit_dimensions_and_text_order() {
4306        let d = draw("box wid 1 ht .2 \"very long label\" fit");
4307        assert_box_size(&d.shapes[0], 1.0, 0.2);
4308
4309        let before = draw("box \"short\" fit");
4310        let after = draw("box \"short\" fit \"this later text does not affect fit\"");
4311        let Shape::Box {
4312            w: before_w,
4313            h: before_h,
4314            ..
4315        } = &before.shapes[0]
4316        else {
4317            panic!()
4318        };
4319        let Shape::Box {
4320            w: after_w,
4321            h: after_h,
4322            ..
4323        } = &after.shapes[0]
4324        else {
4325            panic!()
4326        };
4327        assert!((*before_w - *after_w).abs() < 1e-9);
4328        assert!((*before_h - *after_h).abs() < 1e-9);
4329    }
4330
4331    #[test]
4332    fn fit_without_preceding_visible_text_errors() {
4333        let err = eval(&parse("box fit").unwrap()).unwrap_err();
4334        assert!(err.msg.contains("visible text"), "{}", err.msg);
4335
4336        let err = eval(&parse("box \"\" fit").unwrap()).unwrap_err();
4337        assert!(err.msg.contains("visible text"), "{}", err.msg);
4338    }
4339
4340    #[test]
4341    fn brace_draws_curly_annotation_between_points() {
4342        let d = draw("brace from (0,0) to (2,0) down \"n\" wid .25 bracepos .25");
4343        let Shape::Brace {
4344            a,
4345            b,
4346            cubics,
4347            label_at,
4348            text,
4349            ..
4350        } = &d.shapes[0]
4351        else {
4352            panic!()
4353        };
4354        assert!(a.dist(Point::new(0.0, 0.0)) < 1e-9, "a = {a:?}");
4355        assert!(b.dist(Point::new(2.0, 0.0)) < 1e-9, "b = {b:?}");
4356        assert_eq!(text[0].s, "n");
4357        assert!(label_at.y < -0.25, "label_at = {label_at:?}");
4358        assert!(
4359            (cubics[2][3].x - 0.5).abs() < 1e-9,
4360            "cusp = {:?}",
4361            cubics[2][3]
4362        );
4363        assert!(cubics[2][3].y < -0.2, "cusp = {:?}", cubics[2][3]);
4364        assert!(d.bbox.min.y < -0.25, "bbox = {:?}", d.bbox);
4365    }
4366
4367    #[test]
4368    fn brace_side_words_choose_absolute_side() {
4369        let up = draw("brace from (0,0) to (2,0) up wid .2");
4370        let down = draw("brace from (0,0) to (2,0) down wid .2");
4371        let Shape::Brace {
4372            label_at: up_label, ..
4373        } = &up.shapes[0]
4374        else {
4375            panic!()
4376        };
4377        let Shape::Brace {
4378            label_at: down_label,
4379            ..
4380        } = &down.shapes[0]
4381        else {
4382            panic!()
4383        };
4384        assert!(up_label.y > 0.0, "up label = {up_label:?}");
4385        assert!(down_label.y < 0.0, "down label = {down_label:?}");
4386    }
4387
4388    #[test]
4389    fn brace_labeloffset_moves_label_outward_from_cusp() {
4390        let base = draw("brace from (0,0) to (2,0) up \"n\" wid .25");
4391        let far = draw("brace from (0,0) to (2,0) up \"n\" wid .25 labeloffset .2");
4392        let Shape::Brace {
4393            label_at: base_label,
4394            ..
4395        } = &base.shapes[0]
4396        else {
4397            panic!()
4398        };
4399        let Shape::Brace {
4400            label_at: far_label,
4401            ..
4402        } = &far.shapes[0]
4403        else {
4404            panic!()
4405        };
4406        assert!(
4407            (far_label.y - base_label.y - 0.2).abs() < 1e-9,
4408            "base = {base_label:?}, far = {far_label:?}"
4409        );
4410    }
4411
4412    #[test]
4413    fn brace_compass_anchors_use_curve_bbox() {
4414        let d = draw(
4415            "B: brace from (0,0) to (2,0) up wid .25 bracepos .25\n\
4416             circle rad .01 at B.nw\n\
4417             circle rad .01 at B.ne\n\
4418             circle rad .01 at B.n\n\
4419             circle rad .01 at B.c",
4420        );
4421        let circles: Vec<Point> = d
4422            .shapes
4423            .iter()
4424            .skip(1)
4425            .map(|shape| {
4426                let Shape::Circle { c, .. } = shape else {
4427                    panic!()
4428                };
4429                *c
4430            })
4431            .collect();
4432
4433        assert!(circles[0].x.abs() < 1e-9, "nw = {:?}", circles[0]);
4434        assert!((circles[1].x - 2.0).abs() < 1e-9, "ne = {:?}", circles[1]);
4435        assert!(
4436            (circles[0].y - circles[1].y).abs() < 1e-9,
4437            "nw = {:?}, ne = {:?}",
4438            circles[0],
4439            circles[1]
4440        );
4441        assert!(
4442            (circles[2].x - 1.0).abs() < 1e-9 && circles[2].y > 0.2,
4443            "n = {:?}",
4444            circles[2]
4445        );
4446        assert!(
4447            (circles[3].x - 0.5).abs() < 1e-9 && circles[3].y > 0.2,
4448            "c = {:?}",
4449            circles[3]
4450        );
4451    }
4452
4453    #[test]
4454    fn brace_has_open_object_anchors_and_length() {
4455        let d = draw(
4456            "B: brace from (0,0) to (2,0) down wid .25\n\
4457             box wid (B.len) ht .1 at B.c\n\
4458             line from B.start to B.end",
4459        );
4460        assert_box_size(&d.shapes[1], 2.0, 0.1);
4461        let Shape::Path { pts, .. } = &d.shapes[2] else {
4462            panic!()
4463        };
4464        assert!(pts[0].dist(Point::new(0.0, 0.0)) < 1e-9);
4465        assert!(pts[1].dist(Point::new(2.0, 0.0)) < 1e-9);
4466    }
4467
4468    #[test]
4469    fn bracepos_must_be_inside_segment() {
4470        let err = eval(&parse("brace from (0,0) to (1,0) bracepos 1").unwrap()).unwrap_err();
4471        assert!(err.msg.contains("bracepos"), "{}", err.msg);
4472    }
4473
4474    #[test]
4475    fn style_globals_and_dash_lengths_apply() {
4476        let d = draw("linethick = 3\nline dashed .2\nline dotted .05");
4477        let Shape::Path { style, .. } = &d.shapes[0] else {
4478            panic!()
4479        };
4480        assert_eq!(style.thick, Some(3.0));
4481        assert_eq!(style.dash, Dash::Dashed(0.2));
4482        let Shape::Path { style, .. } = &d.shapes[1] else {
4483            panic!()
4484        };
4485        assert_eq!(style.dash, Dash::Dotted(Some(0.05)));
4486    }
4487
4488    #[test]
4489    fn hatch_style_records_pattern_attributes() {
4490        let d = draw("box crosshatch hatchangle 30 hatchsep .05 hatchwidth 1.5 hatchcolor red");
4491        let Shape::Box { style, .. } = &d.shapes[0] else {
4492            panic!()
4493        };
4494        let hatch = style.hatch.as_ref().expect("expected hatch style");
4495        assert!(hatch.cross);
4496        assert!((hatch.angle - 30.0).abs() < 1e-9);
4497        assert!((hatch.sep - 0.05).abs() < 1e-9);
4498        assert!((hatch.width - 1.5).abs() < 1e-9);
4499        assert_eq!(hatch.color, "red");
4500        assert!(style.fill_open);
4501    }
4502
4503    #[test]
4504    fn opacity_style_records_fill_opacity() {
4505        let d = draw("box fill .8 opacity .4");
4506        let Shape::Box { style, .. } = &d.shapes[0] else {
4507            panic!()
4508        };
4509        assert_eq!(style.fill_opacity, Some(0.4));
4510    }
4511
4512    #[test]
4513    fn block_opacity_multiplies_child_fill_opacity() {
4514        let d = draw("[ box opacity .5; circle ] opacity .5");
4515        let Shape::Box { style, .. } = &d.shapes[0] else {
4516            panic!()
4517        };
4518        assert_eq!(style.fill_opacity, Some(0.25));
4519        let Shape::Circle { style, .. } = &d.shapes[1] else {
4520            panic!()
4521        };
4522        assert_eq!(style.fill_opacity, Some(0.5));
4523    }
4524
4525    #[test]
4526    fn opacity_must_be_between_zero_and_one() {
4527        let err = eval(&parse("box opacity 1.1").unwrap()).unwrap_err();
4528        assert!(err.msg.contains("opacity"), "{}", err.msg);
4529        let err = eval(&parse("box opacity -0.1").unwrap()).unwrap_err();
4530        assert!(err.msg.contains("opacity"), "{}", err.msg);
4531    }
4532
4533    #[test]
4534    fn standalone_text_rejects_opacity() {
4535        let err = eval(&parse("\"note\" opacity .5").unwrap()).unwrap_err();
4536        assert!(err.msg.contains("filled regions"), "{}", err.msg);
4537    }
4538
4539    #[test]
4540    fn color_attribute_expands_runtime_macro_string() {
4541        let d = draw(
4542            "r = 0; g = 0; b = 0.6\n\
4543             if dpicopt == optSVG then {\n\
4544               define customcolor { sprintf(\"rgb(%g,%g,%g)\", int(r*255), int(g*255), int(b*255)) }\n\
4545             }\n\
4546             arc color customcolor",
4547        );
4548        let Shape::Arc { style, .. } = &d.shapes[0] else {
4549            panic!()
4550        };
4551        assert_eq!(style.stroke.as_deref(), Some("rgb(0,0,153)"));
4552        assert_eq!(style.fill, Some(Fill::Color("rgb(0,0,153)".into())));
4553    }
4554
4555    #[test]
4556    fn color_attribute_accepts_dpictools_rgbstring_macro_call() {
4557        let d = draw(
4558            "if dpicopt == optSVG then {\n\
4559               define rgbstring { sprintf(\"rgb(%g,%g,%g)\", int(($1)*255+0.5), int(($2)*255+0.5), int(($3)*255+0.5)) }\n\
4560             }\n\
4561             circle shaded rgbstring(1,0.84,0) outlined \"black\"",
4562        );
4563        let Shape::Circle { style, .. } = &d.shapes[0] else {
4564            panic!()
4565        };
4566        assert_eq!(style.fill, Some(Fill::Color("rgb(255,214,0)".into())));
4567        assert_eq!(style.stroke.as_deref(), Some("black"));
4568    }
4569
4570    #[test]
4571    fn ps_width_scales_drawing() {
4572        // issue #4, dpic oracle: `.PS 6` scales the painted picture, so the
4573        // box geometry is slightly under 6in once default stroke is reserved.
4574        let d = draw(".PS 6\nbox\n.PE");
4575        let Shape::Box { w, .. } = &d.shapes[0] else {
4576            panic!()
4577        };
4578        assert!((*w - 5.912_408_759).abs() < 1e-9, "w = {w}");
4579        assert!(
4580            (d.bbox.width() - 5.923_519_870).abs() < 1e-9,
4581            "w = {}",
4582            d.bbox.width()
4583        );
4584    }
4585
4586    #[test]
4587    fn text_extent_in_bbox() {
4588        // issue #5: a bare label must yield a non-degenerate bbox (no clipping)
4589        let d = draw("\"a long label here\"");
4590        assert!(d.bbox.width() > 0.5, "w = {}", d.bbox.width());
4591        assert!(d.bbox.height() > 0.1, "h = {}", d.bbox.height());
4592        // text wider than its box widens the bbox beyond the box
4593        let d2 = draw("box wid 0.2 ht 0.2 \"a very wide label\"");
4594        assert!(d2.bbox.width() > 0.3, "w = {}", d2.bbox.width());
4595    }
4596
4597    #[test]
4598    fn text_object_width_bounds_rendered_bbox() {
4599        // dpic oracle: a standalone text object's `wid` controls its bbox;
4600        // the literal text width is not used when an explicit width is given.
4601        let d = draw("\"abcdefghij\" wid 0.1");
4602        assert!(
4603            (d.bbox.width() - 0.1).abs() < 1e-9,
4604            "w = {}",
4605            d.bbox.width()
4606        );
4607
4608        let d = draw(".PS 1\n\"abcdefghij\" wid 0.1\n.PE");
4609        assert!(
4610            (d.bbox.width() - 1.0).abs() < 1e-9,
4611            "w = {}",
4612            d.bbox.width()
4613        );
4614    }
4615
4616    #[test]
4617    fn text_position_and_offset_expand_bbox_in_the_rendered_direction() {
4618        let d = draw("textoffset = 0.1\n\"abc\" ljust at (0,0)");
4619        assert!(d.bbox.min.x >= 0.1 - 1e-9, "{:?}", d.bbox);
4620
4621        let d = draw("textoffset = 0.1\n\"abc\" rjust at (0,0)");
4622        assert!(d.bbox.max.x <= -0.1 + 1e-9, "{:?}", d.bbox);
4623
4624        let d = draw("textoffset = 0.1\n\"abc\" above at (0,0)");
4625        assert!(d.bbox.min.y > 0.0, "{:?}", d.bbox);
4626
4627        let d = draw("textoffset = 0.1\n\"abc\" below at (0,0)");
4628        assert!(d.bbox.max.y < 0.0, "{:?}", d.bbox);
4629    }
4630
4631    #[test]
4632    fn invisible_geometry_does_not_expand_drawing_bbox() {
4633        let d = draw("box invis wid 1000 ht 1000 at (0,0)\nbox wid 1 ht 1 at (0,0)");
4634        assert!(
4635            (d.bbox.width() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4636            "w = {}",
4637            d.bbox.width()
4638        );
4639        assert!(
4640            (d.bbox.height() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4641            "h = {}",
4642            d.bbox.height()
4643        );
4644
4645        let d2 = draw("line invis from (0,0) to (1000,1000)\nbox wid 1 ht 1 at (0,0)");
4646        assert!(
4647            (d2.bbox.width() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4648            "w = {}",
4649            d2.bbox.width()
4650        );
4651        assert!(
4652            (d2.bbox.height() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4653            "h = {}",
4654            d2.bbox.height()
4655        );
4656
4657        let d3 = draw("I: box invis wid 1000 ht 1000 at (0,0)\nbox wid 1 ht 1 with .sw at I.ne");
4658        let Shape::Box { c, .. } = &d3.shapes[1] else {
4659            panic!()
4660        };
4661        assert!(c.dist(Point::new(500.5, 500.5)) < 1e-9, "c = {c:?}");
4662        assert!(
4663            (d3.bbox.width() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4664            "w = {}",
4665            d3.bbox.width()
4666        );
4667        assert!(
4668            (d3.bbox.height() - (1.0 + DEFAULT_STROKE_IN)).abs() < 1e-9,
4669            "h = {}",
4670            d3.bbox.height()
4671        );
4672    }
4673
4674    #[test]
4675    fn move_expands_drawing_bbox_like_dpic() {
4676        let d = draw("line from (0,0) to (1,0)\nmove left 0.4 from (0,0)");
4677        assert!(
4678            (d.bbox.width() - (1.4 + DEFAULT_STROKE_IN / 2.0)).abs() < 1e-9,
4679            "w = {}",
4680            d.bbox.width()
4681        );
4682    }
4683
4684    #[test]
4685    fn division_by_zero_errors() {
4686        // a zero divisor must error rather than silently produce NaN coordinates
4687        assert!(eval(&parse("box wid 1/0").unwrap()).is_err());
4688        assert!(eval(&parse("A:(0,0)\nB:(0,0)\nx = (B.x-A.x)/(B.y-A.y)").unwrap()).is_err());
4689    }
4690
4691    #[test]
4692    fn place_dot_in_coordinate_pair() {
4693        // (A.x, A.y - 1) — place scalar accessors inside a coordinate pair (issue #3)
4694        let d = draw("A: box wid 1 ht 1 at 2,3\nbox wid 0.2 ht 0.2 at (A.x, A.y - 1)");
4695        let Shape::Box { c, .. } = &d.shapes[1] else {
4696            panic!()
4697        };
4698        assert!(
4699            (c.x - 2.0).abs() < 1e-9 && (c.y - 2.0).abs() < 1e-9,
4700            "c = {c:?}"
4701        );
4702    }
4703
4704    #[test]
4705    fn bare_coordinate_pair_places_label() {
4706        let d = draw("P: 1,2\nbox wid 0.2 ht 0.2 at P");
4707        let Shape::Box { c, .. } = &d.shapes[0] else {
4708            panic!()
4709        };
4710        assert!(c.dist(Point::new(1.0, 2.0)) < 1e-9, "c = {c:?}");
4711    }
4712
4713    #[test]
4714    fn block_sub_labels_resolve() {
4715        // `B.A` and `B.A.corner` reach a labelled object inside a block
4716        let d = draw("B: [ A: box wid 1 ht 1 at 0,0 ]\nbox wid 0.2 ht 0.2 with .sw at B.A.ne");
4717        let Shape::Box { c, .. } = &d.shapes.last().unwrap() else {
4718            panic!()
4719        };
4720        // the block is placed with its center at (0.5,0); inner A.ne is then
4721        // (1.0,0.5), and the small box centers 0.1 beyond that corner.
4722        assert!(
4723            (c.x - 1.1).abs() < 1e-9 && (c.y - 0.6).abs() < 1e-9,
4724            "c = {c:?}"
4725        );
4726    }
4727
4728    #[test]
4729    fn block_can_anchor_on_own_member() {
4730        // The block bbox center is not A.c, so this catches the two-pass anchor
4731        // resolution rather than accidentally aligning the block center.
4732        let d = draw("P:(2,3)\n[ A: box wid 1 ht 1 at 0,0; circle rad 0.1 at 2,0 ] with .A.c at P");
4733        let Shape::Box { c, .. } = &d.shapes[0] else {
4734            panic!()
4735        };
4736        assert!(c.dist(Point::new(2.0, 3.0)) < 1e-9, "A center = {c:?}");
4737        let Shape::Circle { c, .. } = &d.shapes[1] else {
4738            panic!()
4739        };
4740        assert!(c.dist(Point::new(4.0, 3.0)) < 1e-9, "circle center = {c:?}");
4741    }
4742
4743    #[test]
4744    fn block_pair_anchor_uses_local_coordinates() {
4745        // dpic oracle: for `[ ... ] with (x,y) at P`, `(x,y)` is a point in
4746        // the block's local coordinate system, not an offset from its center.
4747        let d = draw("[ box wid 2 ht 1 at (1,0) ] with (0,0) at (10,20)");
4748        let Shape::Box { c, .. } = &d.shapes[0] else {
4749            panic!()
4750        };
4751        assert!(c.dist(Point::new(11.0, 20.0)) < 1e-9, "c = {c:?}");
4752    }
4753
4754    #[test]
4755    fn block_layout_matches_dpic_for_negative_box_width() {
4756        let d = draw("move 1\n[ box wid -0.5 ht 0.5 ]; box wid 0.75 ht 0.75");
4757        let boxes: Vec<Point> = d
4758            .shapes
4759            .iter()
4760            .filter_map(|shape| match shape {
4761                Shape::Box { c, .. } => Some(*c),
4762                _ => None,
4763            })
4764            .collect();
4765        assert_eq!(boxes.len(), 2);
4766        assert!(
4767            boxes[0].dist(Point::new(0.75, 0.0)) < 1e-9,
4768            "negative box center = {:?}",
4769            boxes[0]
4770        );
4771        assert!(
4772            boxes[1].dist(Point::new(1.375, 0.0)) < 1e-9,
4773            "following box center = {:?}",
4774            boxes[1]
4775        );
4776    }
4777
4778    #[test]
4779    fn block_object_renders_attached_text() {
4780        let d = draw("[ box ] \"block label\"");
4781        assert!(d.shapes.iter().any(|s| {
4782            matches!(
4783                s,
4784                Shape::Text { text, .. } if text.iter().any(|line| line.s == "block label")
4785            )
4786        }));
4787    }
4788
4789    #[test]
4790    fn block_anchors_ignore_attached_text_extents() {
4791        // dpic oracle: text contributes to the painted bbox, but not to block
4792        // anchors such as `last [].s`; those come from the geometric objects.
4793        let d = draw(
4794            r#"B: [ right; box "{\bf veryveryverywide}"; move; box ]
4795box wid 0.1 ht 0.1 at B.s"#,
4796        );
4797        let Shape::Box { c, .. } = d.shapes.last().unwrap() else {
4798            panic!()
4799        };
4800        assert!(c.dist(Point::new(1.0, -0.25)) < 1e-9, "c = {c:?}");
4801    }
4802
4803    #[test]
4804    fn nested_macro_block_can_reference_parent_label() {
4805        let d = draw(
4806            "define marker { [ P: circle rad 0.01 at $1.start ] with .P at $1.start }\n[ A: arrow from (0,0) to (1,0); marker(A) ]",
4807        );
4808        let Shape::Path { pts, .. } = &d.shapes[0] else {
4809            panic!()
4810        };
4811        let Shape::Circle { c, .. } = &d.shapes[1] else {
4812            panic!()
4813        };
4814        assert!(
4815            c.dist(pts[0]) < 1e-9,
4816            "circle = {c:?}, arrow start = {:?}",
4817            pts[0]
4818        );
4819    }
4820
4821    #[test]
4822    fn position_vector_arithmetic() {
4823        // (w,h)/2 and p + q with correct precedence
4824        let d = draw("box wid 0.2 ht 0.2 at (2,4)/2 + (1,0)");
4825        let Shape::Box { c, .. } = &d.shapes[0] else {
4826            panic!()
4827        };
4828        assert!(
4829            (c.x - 2.0).abs() < 1e-9 && (c.y - 2.0).abs() < 1e-9,
4830            "c = {c:?}"
4831        );
4832    }
4833
4834    #[test]
4835    fn interpolation_angle_brackets() {
4836        let d = draw("A:(0,0)\nB:(2,0)\nbox wid 0.1 ht 0.1 at 0.5 <A,B>");
4837        let Shape::Box { c, .. } = &d.shapes.last().unwrap() else {
4838            panic!()
4839        };
4840        assert!((c.x - 1.0).abs() < 1e-9, "c = {c:?}");
4841    }
4842
4843    #[test]
4844    fn string_equality_in_if() {
4845        // the `"$1"==""` default-argument idiom (here without a macro)
4846        let d1 = draw("if \"a\" == \"\" then { box } else { circle }");
4847        assert!(matches!(d1.shapes[0], Shape::Circle { .. }));
4848        let d2 = draw("if \"\" == \"\" then { box } else { circle }");
4849        assert!(matches!(d2.shapes[0], Shape::Box { .. }));
4850    }
4851
4852    #[test]
4853    fn dpicopt_defaults_to_svg_backend() {
4854        let d = draw("if dpicopt == optSVG then { box } else { circle }");
4855        assert!(matches!(d.shapes[0], Shape::Box { .. }));
4856    }
4857
4858    #[test]
4859    fn svg_font_stub_and_string_sprintf_are_harmless() {
4860        let d = draw("box sprintf(\"x%s\", svg_font(\"Times\", 12))");
4861        let Shape::Box { text, .. } = &d.shapes[0] else {
4862            panic!()
4863        };
4864        assert_eq!(text[0].s, "x");
4865    }
4866
4867    #[test]
4868    fn inch_suffix_and_bare_distance() {
4869        // `.5i` is half an inch; `move 1` / `move -0.1` advance the pen
4870        let d = draw("box wid .5i ht .5i");
4871        let Shape::Box { w, .. } = &d.shapes[0] else {
4872            panic!()
4873        };
4874        assert!((*w - 0.5).abs() < 1e-9, "w = {w}");
4875        let d2 = draw("right\nmove 1\nbox at Here");
4876        let Shape::Box { c, .. } = &d2.shapes.last().unwrap() else {
4877            panic!()
4878        };
4879        assert!(c.x > 0.9, "moved to {c:?}");
4880    }
4881
4882    #[test]
4883    fn embedded_assignment_returns_value() {
4884        let d = draw("if (s = 3) > 1 then { box wid s ht 0.1 }");
4885        let Shape::Box { w, .. } = &d.shapes[0] else {
4886            panic!()
4887        };
4888        assert!((*w - 3.0).abs() < 1e-9, "w = {w}");
4889    }
4890
4891    #[test]
4892    fn copy_includes_a_file() {
4893        // `copy "file"` splices another pic file relative to the base directory
4894        let dir = std::env::temp_dir().join(format!("rpic_copy_{}", std::process::id()));
4895        std::fs::create_dir_all(&dir).unwrap();
4896        std::fs::write(dir.join("inc.pic"), "box wid 0.5 ht 0.5\n").unwrap();
4897        let pic = parse_in_dir("copy \"inc.pic\"\ncircle", Some(dir.as_path())).unwrap();
4898        let d = eval(&pic).unwrap();
4899        assert!(d.shapes.iter().any(|s| matches!(s, Shape::Box { .. })));
4900        assert!(d.shapes.iter().any(|s| matches!(s, Shape::Circle { .. })));
4901        let _ = std::fs::remove_dir_all(&dir);
4902    }
4903
4904    #[test]
4905    fn paren_position_coordinate() {
4906        // `.x`/`.y` on a parenthesised position expression
4907        let d = draw("A:(0,0)\nB:(3,1)\nbox wid (B-A).x ht (B-A).y at 0,-2");
4908        let Shape::Box { w, h, .. } = &d.shapes[0] else {
4909            panic!()
4910        };
4911        assert!(
4912            (*w - 3.0).abs() < 1e-9 && (*h - 1.0).abs() < 1e-9,
4913            "{w} x {h}"
4914        );
4915    }
4916
4917    #[test]
4918    fn arrowhead_size_follows_globals() {
4919        // arrowht/arrowwid control the rendered arrowhead, not a hardcoded size
4920        let d = draw("arrowht = 0.3; arrowwid = 0.2\narrow right 1");
4921        let Shape::Path { style, .. } = &d.shapes[0] else {
4922            panic!()
4923        };
4924        assert!(
4925            (style.arrow_ht - 0.3).abs() < 1e-9,
4926            "ht = {}",
4927            style.arrow_ht
4928        );
4929        assert!(
4930            (style.arrow_wid - 0.2).abs() < 1e-9,
4931            "wid = {}",
4932            style.arrow_wid
4933        );
4934    }
4935
4936    #[test]
4937    fn scaling_existing_geometry_scales_arrowhead_metadata() {
4938        // manual/man35 draws at a temporary scale, then restores `scale`.
4939        // The points are scaled at restore time, and the arrowhead dimensions
4940        // attached to the already-emitted path must scale with them.
4941        let factor = 6.6 / 8.2;
4942        let d = draw("scale = 6.6/8.2\nline <-\nscale = 1");
4943        let Shape::Path { style, .. } = &d.shapes[0] else {
4944            panic!()
4945        };
4946        assert!(
4947            (style.arrow_ht - 0.1 * factor).abs() < 1e-9,
4948            "ht = {}",
4949            style.arrow_ht
4950        );
4951        assert!(
4952            (style.arrow_wid - 0.05 * factor).abs() < 1e-9,
4953            "wid = {}",
4954            style.arrow_wid
4955        );
4956    }
4957
4958    #[test]
4959    fn dpic_default_env_values_are_readable() {
4960        assert!((scalar("textoffset").unwrap() - 2.0 / 72.0).abs() < 1e-9);
4961        assert!((scalar("textht").unwrap() - (11.0 / 72.0) * 0.66).abs() < 1e-9);
4962        assert!((scalar("arrowhead").unwrap() - 1.0).abs() < 1e-9);
4963        assert!((scalar("linethick").unwrap() - 0.8).abs() < 1e-9);
4964        assert_eq!(scalar("margin").unwrap(), 0.0);
4965        assert_eq!(scalar("topmargin").unwrap(), 0.0);
4966        assert_eq!(scalar("rightmargin").unwrap(), 0.0);
4967        assert_eq!(scalar("bottommargin").unwrap(), 0.0);
4968        assert_eq!(scalar("leftmargin").unwrap(), 0.0);
4969    }
4970
4971    #[test]
4972    fn canvas_margin_vars_are_scaled_dimensions() {
4973        let d = draw("margin = 1; topmargin = 0.5; rightmargin = 0.25; line right");
4974        assert_eq!(
4975            d.canvas_margin,
4976            CanvasMargin {
4977                top: 1.5,
4978                right: 1.25,
4979                bottom: 1.0,
4980                left: 1.0,
4981            }
4982        );
4983
4984        let d = draw("scale = 2; margin = 1; topmargin = 1; line right");
4985        assert_eq!(
4986            d.canvas_margin,
4987            CanvasMargin {
4988                top: 1.0,
4989                right: 0.5,
4990                bottom: 0.5,
4991                left: 0.5,
4992            }
4993        );
4994
4995        let d = draw("margin = 1; scale = 2; line right");
4996        assert_eq!(
4997            d.canvas_margin,
4998            CanvasMargin {
4999                top: 1.0,
5000                right: 1.0,
5001                bottom: 1.0,
5002                left: 1.0,
5003            }
5004        );
5005    }
5006
5007    #[test]
5008    fn print_statements_collect_diagnostics() {
5009        let d = draw("print 5.5\nprint 5.5%2\nprint \"hello\"\nprint sprintf(\"x=%g\", 1.25)");
5010        assert_eq!(d.diagnostics, ["5.5", "0", "hello", "x=1.25"]);
5011
5012        let d = draw("[ print \"inside\"; box ]\nprint 7");
5013        assert_eq!(d.diagnostics, ["inside", "7"]);
5014    }
5015
5016    #[test]
5017    fn command_and_sh_are_silent_noops() {
5018        // Policy (#129): `command` raw backend text is never injected and `sh`
5019        // is never executed. Both are tolerated so dpic sources keep
5020        // compiling, and they emit no diagnostic lines and no shapes.
5021        let d = draw("box wid 1 ht 1\nsh \"echo hi\"\ncommand \"</g>\"\nbox wid 1 ht 1");
5022        assert!(d.diagnostics.is_empty(), "{:?}", d.diagnostics);
5023        assert_eq!(d.shapes.len(), 2);
5024
5025        // Geometry flows across the skipped directives unchanged: the second
5026        // box lands exactly where it would without them.
5027        let plain = draw("box wid 1 ht 1\nbox wid 1 ht 1");
5028        assert_eq!(d.bbox, plain.bbox);
5029    }
5030
5031    #[test]
5032    fn open_object_width_height_attrs_are_arrowhead_dimensions() {
5033        let d = draw("arrowwid = 0.2; arrowht = 0.3\nA: line right 2\nbox wid (A.wid) ht (A.ht)");
5034        assert_box_size(&d.shapes[1], 0.2, 0.3);
5035
5036        let d = draw("arrowwid = 0.12; arrowht = 0.34\nA: move right 2\nbox wid (A.wid) ht (A.ht)");
5037        assert_box_size(&d.shapes[1], 0.12, 0.34);
5038
5039        let d = draw(
5040            "arrowwid = 0.23; arrowht = 0.31\nA: spline from (0,0) to (2,1)\nbox wid (A.wid) ht (A.ht)",
5041        );
5042        assert_box_size(&d.shapes[1], 0.23, 0.31);
5043    }
5044
5045    #[test]
5046    fn radius_and_diameter_attrs_are_type_specific() {
5047        let d = draw("B: box rad 0.1 wid 1 ht 1\nbox wid (B.rad) ht 0.3");
5048        assert_box_size(&d.shapes[1], 0.1, 0.3);
5049
5050        let d = draw("C: arc rad 0.7 from (0,0) to (0,1.4)\nbox wid (C.rad) ht (C.diam)");
5051        assert_box_size(&d.shapes[1], 0.7, 1.4);
5052    }
5053
5054    #[test]
5055    fn invalid_type_scalar_attrs_match_dpic_zero() {
5056        let prog = parse(
5057            "E: ellipse wid 2 ht 1\nB: box wid 1 ht 1\nA: arc rad .5\n\
5058             e_rad = E.rad\ne_diam = E.diam\nb_diam = B.diam\na_len = A.len",
5059        )
5060        .unwrap();
5061        let mut st = State::new();
5062        st.eval_stmts(&prog.stmts).unwrap();
5063        assert_eq!(st.vars["e_rad"], 0.0);
5064        assert_eq!(st.vars["e_diam"], 0.0);
5065        assert_eq!(st.vars["b_diam"], 0.0);
5066        assert_eq!(st.vars["a_len"], 0.0);
5067    }
5068
5069    #[test]
5070    fn arrowhead_type_open_vs_filled() {
5071        // default is a filled head; `arrowhead = 0` is an open (two-stroke) head
5072        let d = draw("arrow right 1");
5073        let Shape::Path { style, .. } = &d.shapes[0] else {
5074            panic!()
5075        };
5076        assert!(style.arrow_filled, "default should be filled");
5077        let d2 = draw("arrowhead = 0\narrow right 1");
5078        let Shape::Path { style, .. } = &d2.shapes[0] else {
5079            panic!()
5080        };
5081        assert!(!style.arrow_filled, "arrowhead=0 should be open");
5082
5083        let d3 = draw("arrowhead = 0\nline <- 1 up");
5084        let Shape::Path { style, .. } = &d3.shapes[0] else {
5085            panic!()
5086        };
5087        assert!(style.arrow_filled, "`<- 1` should override the global");
5088
5089        let d4 = draw("line <- 0 up");
5090        let Shape::Path { style, .. } = &d4.shapes[0] else {
5091            panic!()
5092        };
5093        assert!(!style.arrow_filled, "`<- 0` should override the global");
5094    }
5095
5096    #[test]
5097    fn maxps_clamps_oversized_drawing() {
5098        // larger than the default 8.5x11in page → scaled down to fit
5099        let d = draw("box wid 20 ht 30");
5100        assert!(
5101            d.bbox.width() <= 8.5 + 1e-6 && d.bbox.height() <= 11.0 + 1e-6,
5102            "{}x{}",
5103            d.bbox.width(),
5104            d.bbox.height()
5105        );
5106        // raising the limits disables the clamp
5107        let d2 = draw("maxpsht = 200; maxpswid = 50\nbox wid 20 ht 30");
5108        assert!(
5109            (d2.bbox.height() - (30.0 + DEFAULT_STROKE_IN)).abs() < 1e-6,
5110            "h = {}",
5111            d2.bbox.height()
5112        );
5113        // a small drawing is untouched
5114        let d3 = draw("box wid 2 ht 1");
5115        assert!((d3.bbox.width() - (2.0 + DEFAULT_STROKE_IN)).abs() < 1e-6);
5116
5117        let d4 = draw("maxpswid = 2; maxpsht = 100\nmargin = 1\nbox wid 1 ht 0.5");
5118        assert!(
5119            d4.bbox.width() + d4.canvas_margin.horizontal() <= 2.0 + 1e-6,
5120            "canvas width = {}",
5121            d4.bbox.width() + d4.canvas_margin.horizontal()
5122        );
5123        assert!(
5124            d4.canvas_margin.left < 1.0 && d4.canvas_margin.right < 1.0,
5125            "{:?}",
5126            d4.canvas_margin
5127        );
5128    }
5129
5130    #[test]
5131    fn block_variable_assignments_are_local() {
5132        let d = draw("x = 1\n[ x = 5 ]\nbox wid x ht 0.3");
5133        let Shape::Box { w, .. } = d.shapes.last().unwrap() else {
5134            panic!()
5135        };
5136        assert!((*w - 1.0).abs() < 1e-9, "w = {w}");
5137
5138        assert!(eval(&parse("[ x = 5 ]\nbox wid x ht 0.3").unwrap()).is_err());
5139    }
5140
5141    #[test]
5142    fn block_env_assignments_are_local() {
5143        let d = draw("[ boxwid = 2; box ]\nbox");
5144        let Shape::Box { w, .. } = &d.shapes[0] else {
5145            panic!()
5146        };
5147        assert!((*w - 2.0).abs() < 1e-9, "inner w = {w}");
5148        let Shape::Box { w, .. } = &d.shapes[1] else {
5149            panic!()
5150        };
5151        assert!((*w - 0.75).abs() < 1e-9, "outer w = {w}");
5152    }
5153
5154    #[test]
5155    fn block_mutating_var_assignments_update_inherited_vars() {
5156        let d = draw("x = 1\n[ x := 5 ]\nbox wid x ht 0.3");
5157        let Shape::Box { w, .. } = d.shapes.last().unwrap() else {
5158            panic!()
5159        };
5160        assert!((*w - 5.0).abs() < 1e-9, "w = {w}");
5161
5162        let d = draw("x = 1\n[ x += 2 ]\nbox wid x ht 0.3");
5163        let Shape::Box { w, .. } = d.shapes.last().unwrap() else {
5164            panic!()
5165        };
5166        assert!((*w - 3.0).abs() < 1e-9, "w = {w}");
5167
5168        assert!(eval(&parse("[ x = 1; x += 2 ]\nbox wid x ht 0.3").unwrap()).is_err());
5169
5170        let d = draw("boxwid = 0.75\n[ boxwid := 2; box ]\nbox");
5171        let Shape::Box { w, .. } = &d.shapes[1] else {
5172            panic!()
5173        };
5174        assert!((*w - 0.75).abs() < 1e-9, "outer w = {w}");
5175    }
5176
5177    #[test]
5178    fn figuras_examples_compile() {
5179        // a few of André Leite's circuit_macros figures (examples/figuras/),
5180        // adapted with the compatibility shim — they must keep compiling/drawing.
5181        for src in [
5182            include_str!("../../../examples/figuras/fig01.pic"),
5183            include_str!("../../../examples/figuras/fig36.pic"),
5184            include_str!("../../../examples/figuras/fig40.pic"),
5185        ] {
5186            let d = eval(&parse(src).unwrap()).unwrap();
5187            assert!(!d.shapes.is_empty());
5188        }
5189    }
5190
5191    #[test]
5192    fn figuras_element_examples_compile() {
5193        // André Leite's circuit_macros figures that use the *element API*
5194        // (resistor(dir len), bi_tr, opamp, …). These render with the circuit
5195        // library (-c) plus the compatibility shim, which reuses the native
5196        // element geometry. The shim is `copy`-d in by each file; here we splice
5197        // it in directly and prepend the circuit library.
5198        let shim = include_str!("../../../examples/figuras/circuit_macros.pic");
5199        for body in [
5200            include_str!("../../../examples/figuras/fig21.pic"),
5201            include_str!("../../../examples/figuras/fig23.pic"),
5202            include_str!("../../../examples/figuras/fig26.pic"),
5203            include_str!("../../../examples/figuras/fig27.pic"),
5204            include_str!("../../../examples/figuras/fig28.pic"),
5205            include_str!("../../../examples/figuras/fig30.pic"),
5206            include_str!("../../../examples/figuras/fig33.pic"),
5207            include_str!("../../../examples/figuras/fig45.pic"),
5208            include_str!("../../../examples/figuras/fig46.pic"),
5209            include_str!("../../../examples/figuras/fig09.pic"),
5210            include_str!("../../../examples/figuras/fig11.pic"),
5211        ] {
5212            let body = body.replace("copy \"circuit_macros.pic\"", shim);
5213            let src = format!("{}\n{}", crate::CIRCUITS, body);
5214            let d = eval(&parse(&src).unwrap()).unwrap();
5215            assert!(!d.shapes.is_empty());
5216        }
5217    }
5218
5219    #[test]
5220    fn lib3d_examples_compile() {
5221        // The lib3D shim (3D -> 2D axonometric projection) and its demos must
5222        // keep compiling and drawing. The demos `copy` the shim; splice it in.
5223        let shim = include_str!("../../../examples/lib3d/lib3d.pic");
5224        for body in [
5225            include_str!("../../../examples/lib3d/frame.pic"),
5226            include_str!("../../../examples/lib3d/views.pic"),
5227        ] {
5228            let src = body.replace("copy \"lib3d.pic\"", shim);
5229            let d = eval(&parse(&src).unwrap()).unwrap();
5230            assert!(!d.shapes.is_empty());
5231        }
5232    }
5233
5234    #[test]
5235    fn brace_ncount_as_place() {
5236        // `{expr}th last box` — a brace-counted ordinal used as a place
5237        let d = draw(
5238            "box at 0,0\nbox at 2,0\nbox at 4,0\narrow from {2}th last box.e to {1}th last box.w",
5239        );
5240        let Shape::Path { pts, .. } = d.shapes.last().unwrap() else {
5241            panic!()
5242        };
5243        assert!(pts[0].x > 2.0 && pts.last().unwrap().x < 4.0, "{pts:?}");
5244    }
5245
5246    #[test]
5247    fn dpic_unit_suffix() {
5248        // `72bp__` == 72 * scale/72 == 1 inch
5249        let d = draw("box wid 72bp__ ht 0.3");
5250        let Shape::Box { w, .. } = &d.shapes[0] else {
5251            panic!()
5252        };
5253        assert!((*w - 1.0).abs() < 1e-9, "w = {w}");
5254    }
5255
5256    #[test]
5257    fn block_sees_outer_labels() {
5258        // a label defined before a block is visible (read-only) inside it
5259        let d = draw("A: (0,0)\n[ line from A to (2,0) ]");
5260        assert!(
5261            d.shapes.iter().any(|s| matches!(s, Shape::Path { .. })),
5262            "block should draw a line referencing the outer label A"
5263        );
5264        // outer labels must not pollute the block's `last`/nth: a box drawn
5265        // before the block isn't the block's `last box`.
5266        assert!(eval(&parse("box\n[ circle; \"x\" at last box ]").unwrap()).is_err());
5267    }
5268
5269    #[test]
5270    fn arg_count_macro() {
5271        // `$+` is the number of arguments passed to the current macro
5272        let d = draw("define cnt { $+ }\nx = cnt(a, b, c)\nbox wid x ht 0.3");
5273        let Shape::Box { w, .. } = &d.shapes[0] else {
5274            panic!()
5275        };
5276        assert!((*w - 3.0).abs() < 1e-9, "w = {w}");
5277    }
5278
5279    #[test]
5280    fn exec_evaluates_generated_pic_in_macro_arg_scope() {
5281        let d = draw(
5282            "define array { for i_array=2 to $+ do { exec sprintf(\"$1[%g] = $%g\", i_array-1, i_array) } }\narray(a, 0, 1, 3)\nbox wid a[2] ht a[3]",
5283        );
5284        let Shape::Box { w, h, .. } = &d.shapes[0] else {
5285            panic!()
5286        };
5287        assert!(
5288            (*w - 1.0).abs() < 1e-9 && (*h - 3.0).abs() < 1e-9,
5289            "{w} x {h}"
5290        );
5291    }
5292
5293    #[test]
5294    fn exec_unescapes_generated_quoted_text() {
5295        let d = draw("exec sprintf(\"\\\"x\\\" at Here\")");
5296        let Shape::Text { text, .. } = &d.shapes[0] else {
5297            panic!()
5298        };
5299        assert_eq!(text[0].s, "x");
5300    }
5301
5302    #[test]
5303    fn macro_token_pasting_concatenates_adjacent_args() {
5304        let d = draw("define mark { $1$2: (1,0) }\nmark(A,B)\nbox wid 0.2 ht 0.2 at AB");
5305        let Shape::Box { c, .. } = &d.shapes[0] else {
5306            panic!()
5307        };
5308        assert!(c.dist(Point::new(1.0, 0.0)) < 1e-9, "c = {c:?}");
5309    }
5310
5311    #[test]
5312    fn macro_string_substitution_preserves_dot_prefixed_arguments() {
5313        let d = draw("define label { \"$1\"; \"$2\" }\nlabel(.ne,above)");
5314        let labels: Vec<&str> = d
5315            .shapes
5316            .iter()
5317            .filter_map(|shape| match shape {
5318                Shape::Text { text, .. } => Some(text[0].s.as_str()),
5319                _ => None,
5320            })
5321            .collect();
5322        assert_eq!(labels, [".ne", "above"]);
5323    }
5324
5325    #[test]
5326    fn recursive_macro_terminates() {
5327        // a self-calling macro bounded by `if`: textual pre-expansion would
5328        // diverge, but lazy (eval-time) expansion of the taken branch stops it.
5329        let d = draw("define rec { if $1 <= 0 then { circle } else { box; rec($1-1) } }\nrec(3)");
5330        let boxes = d
5331            .shapes
5332            .iter()
5333            .filter(|s| matches!(s, Shape::Box { .. }))
5334            .count();
5335        let circles = d
5336            .shapes
5337            .iter()
5338            .filter(|s| matches!(s, Shape::Circle { .. }))
5339            .count();
5340        assert_eq!((boxes, circles), (3, 1), "shapes = {:?}", d.shapes.len());
5341    }
5342
5343    #[test]
5344    fn default_argument_idiom() {
5345        // empty argument: the dead `else { w = $1 }` becomes `w =`, which must
5346        // not be parsed because the then-branch is taken.
5347        let d = draw(
5348            "define b { if \"$1\"==\"\" then { w = 1 } else { w = $1 }\n box wid w ht 0.2 }\nb()",
5349        );
5350        let Shape::Box { w, .. } = &d.shapes[0] else {
5351            panic!()
5352        };
5353        assert!((*w - 1.0).abs() < 1e-9, "w = {w}");
5354        // and with an argument supplied, the else-branch value is used
5355        let d2 = draw(
5356            "define b { if \"$1\"==\"\" then { w = 1 } else { w = $1 }\n box wid w ht 0.2 }\nb(2.5)",
5357        );
5358        let Shape::Box { w, .. } = &d2.shapes[0] else {
5359            panic!()
5360        };
5361        assert!((*w - 2.5).abs() < 1e-9, "w = {w}");
5362    }
5363
5364    #[test]
5365    fn last_ordinal() {
5366        let d = draw("box at 0,0\nbox at 2,0\narrow from 1st box.e to 2nd box.w");
5367        let Shape::Path { pts, .. } = &d.shapes[2] else {
5368            panic!()
5369        };
5370        // from first box east edge to second box west edge
5371        assert!(pts[0].x > 0.0 && pts.last().unwrap().x < 2.0);
5372    }
5373
5374    #[test]
5375    fn untyped_last_references_any_kind() {
5376        // `last.c` after a circle resolves to that circle (no `last circle`).
5377        let d = draw("circle rad 0.5 at (3,1)\n\"x\" at last.c");
5378        let Shape::Text { at, .. } = d.shapes.last().unwrap() else {
5379            panic!()
5380        };
5381        assert!((at.x - 3.0).abs() < 1e-9 && (at.y - 1.0).abs() < 1e-9);
5382    }
5383
5384    #[test]
5385    fn untyped_last_corner_after_box() {
5386        // `last.n` (north of the most recent object, whatever its kind).
5387        let d = draw("box wid 2 ht 1 at (0,0)\n\"y\" at last.n");
5388        let Shape::Text { at, .. } = d.shapes.last().unwrap() else {
5389            panic!()
5390        };
5391        assert!((at.x - 0.0).abs() < 1e-9 && (at.y - 0.5).abs() < 1e-9);
5392    }
5393
5394    #[test]
5395    fn untyped_nth_last_spans_kinds() {
5396        // `2nd last` counts across kinds: box, then circle -> 2nd last is the box.
5397        let d = draw("box at (0,0)\ncircle at (2,0)\n\"z\" at 2nd last.c");
5398        let Shape::Text { at, .. } = d.shapes.last().unwrap() else {
5399            panic!()
5400        };
5401        assert!((at.x - 0.0).abs() < 1e-9, "x = {}", at.x);
5402    }
5403
5404    #[test]
5405    fn typed_last_still_filters_by_kind() {
5406        // an explicit type keyword keeps filtering: `last box` skips the circle.
5407        let d = draw("box at (0,0)\ncircle at (2,0)\n\"w\" at last box.c");
5408        let Shape::Text { at, .. } = d.shapes.last().unwrap() else {
5409            panic!()
5410        };
5411        assert!((at.x - 0.0).abs() < 1e-9, "x = {}", at.x);
5412    }
5413
5414    #[test]
5415    fn untyped_last_with_no_object_errors() {
5416        assert!(eval(&parse("\"q\" at last.c").unwrap()).is_err());
5417    }
5418}