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; `rand()` is deterministic (0.5); unknown variables read as 0.
13
14use std::collections::HashMap;
15use std::f64::consts::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
36fn err<T>(msg: impl Into<String>) -> ER<T> {
37    Err(EvalError { msg: msg.into() })
38}
39
40/// Evaluate a parsed picture into a [`Drawing`].
41pub fn eval(pic: &Picture) -> ER<Drawing> {
42    let mut st = State::new();
43    st.eval_stmts(&pic.stmts)?;
44    Ok(Drawing {
45        shapes: st.shapes,
46        bbox: st.bbox,
47        anims: st.anims,
48    })
49}
50
51// ---- environment variables -------------------------------------------------
52
53#[derive(Clone)]
54struct EnvVars {
55    v: HashMap<u8, f64>,
56}
57
58fn ev_key(e: EnvVar) -> u8 {
59    e as u8
60}
61
62impl EnvVars {
63    fn new() -> Self {
64        use EnvVar::*;
65        let defaults = [
66            (Arcrad, 0.25),
67            (Arrowht, 0.1),
68            (Arrowwid, 0.05),
69            (Boxht, 0.5),
70            (Boxrad, 0.0),
71            (Boxwid, 0.75),
72            (Circlerad, 0.25),
73            (Dashwid, 0.05),
74            (Ellipseht, 0.5),
75            (Ellipsewid, 0.75),
76            (Lineht, 0.5),
77            (Linewid, 0.5),
78            (Moveht, 0.5),
79            (Movewid, 0.5),
80            (Textht, 0.0),
81            (Textoffset, 0.05),
82            (Textwid, 0.0),
83            (Arrowhead, 2.0),
84            (Fillval, 0.5),
85            (Linethick, -1.0),
86            (Maxpsht, 11.0),
87            (Maxpswid, 8.5),
88            (Scale, 1.0),
89        ];
90        let mut v = HashMap::new();
91        for (e, d) in defaults {
92            v.insert(ev_key(e), d);
93        }
94        EnvVars { v }
95    }
96    fn get(&self, e: EnvVar) -> f64 {
97        *self.v.get(&ev_key(e)).unwrap_or(&0.0)
98    }
99    fn set(&mut self, e: EnvVar, val: f64) {
100        self.v.insert(ev_key(e), val);
101    }
102}
103
104// ---- placed-object bookkeeping ---------------------------------------------
105
106#[derive(Clone, Copy, PartialEq, Eq)]
107enum PKind {
108    Box,
109    Circle,
110    Ellipse,
111    Line,
112    Move,
113    Spline,
114    Arc,
115    Block,
116    Text,
117}
118
119#[derive(Clone)]
120struct Placed {
121    kind: PKind,
122    center: Point,
123    bbox: Bbox,
124    start: Point,
125    end: Point,
126    thick: f64,
127    /// Index of the primary shape in `shapes` (None for point-only labels).
128    shape: Option<usize>,
129}
130
131impl Placed {
132    fn corner(&self, c: Corner) -> Point {
133        let (lo, hi) = (self.bbox.min, self.bbox.max);
134        let mid = self.center;
135        match c {
136            Corner::N => Point::new(mid.x, hi.y),
137            Corner::S => Point::new(mid.x, lo.y),
138            Corner::E => Point::new(hi.x, mid.y),
139            Corner::W => Point::new(lo.x, mid.y),
140            Corner::Ne => Point::new(hi.x, hi.y),
141            Corner::Se => Point::new(hi.x, lo.y),
142            Corner::Nw => Point::new(lo.x, hi.y),
143            Corner::Sw => Point::new(lo.x, lo.y),
144            Corner::Center => mid,
145            Corner::Start => self.start,
146            Corner::End => self.end,
147        }
148    }
149}
150
151// ---- evaluator state -------------------------------------------------------
152
153struct State {
154    pos: Point,
155    dir: Dir,
156    vars: HashMap<String, f64>,
157    env: EnvVars,
158    shapes: Vec<Shape>,
159    placed: Vec<Placed>,
160    labels: HashMap<String, usize>,
161    bbox: Bbox,
162    // animation state
163    anims: Vec<Anim>,
164    anim_cursor: f64,
165    anim_end: HashMap<usize, f64>,
166}
167
168const DEFAULT_ANIM_DUR: f64 = 0.6;
169
170fn dir_unit(d: Dir) -> Point {
171    match d {
172        Dir::Right => Point::new(1.0, 0.0),
173        Dir::Left => Point::new(-1.0, 0.0),
174        Dir::Up => Point::new(0.0, 1.0),
175        Dir::Down => Point::new(0.0, -1.0),
176    }
177}
178fn horizontal(d: Dir) -> bool {
179    matches!(d, Dir::Right | Dir::Left)
180}
181
182impl State {
183    fn new() -> Self {
184        State {
185            pos: Point::ZERO,
186            dir: Dir::Right,
187            vars: HashMap::new(),
188            env: EnvVars::new(),
189            shapes: Vec::new(),
190            placed: Vec::new(),
191            labels: HashMap::new(),
192            bbox: Bbox::new(),
193            anims: Vec::new(),
194            anim_cursor: 0.0,
195            anim_end: HashMap::new(),
196        }
197    }
198
199    fn eval_stmts(&mut self, stmts: &[Stmt]) -> ER<()> {
200        for s in stmts {
201            self.eval_stmt(s)?;
202        }
203        Ok(())
204    }
205
206    fn eval_stmt(&mut self, s: &Stmt) -> ER<()> {
207        match s {
208            Stmt::Direction(d) => {
209                self.dir = *d;
210            }
211            Stmt::Assign(list) => {
212                for a in list {
213                    self.eval_assignment(a)?;
214                }
215            }
216            Stmt::Place { label, pos } => {
217                let p = self.eval_pos(pos)?;
218                let mut bb = Bbox::new();
219                bb.add(p);
220                let idx = self.placed.len();
221                self.placed.push(Placed {
222                    kind: PKind::Text,
223                    center: p,
224                    bbox: bb,
225                    start: p,
226                    end: p,
227                    thick: 0.0,
228                    shape: None,
229                });
230                self.labels.insert(label.name.clone(), idx);
231            }
232            Stmt::Group(stmts) => {
233                let (pos, dir) = (self.pos, self.dir);
234                self.eval_stmts(stmts)?;
235                self.pos = pos;
236                self.dir = dir;
237            }
238            Stmt::Object { label, object } => {
239                let idx = self.eval_object(object)?;
240                if let Some(l) = label {
241                    self.labels.insert(l.name.clone(), idx);
242                }
243            }
244            Stmt::Animate(a) => self.eval_animate(a)?,
245            Stmt::If {
246                cond,
247                then_body,
248                else_body,
249            } => {
250                if self.eval_expr(cond)? != 0.0 {
251                    self.eval_stmts(then_body)?;
252                } else if let Some(e) = else_body {
253                    self.eval_stmts(e)?;
254                }
255            }
256            Stmt::For {
257                var,
258                from,
259                to,
260                by,
261                mult,
262                body,
263            } => {
264                let from = self.eval_expr(from)?;
265                let to = self.eval_expr(to)?;
266                let by = self.eval_expr(by)?;
267                let mut v = from;
268                let mut iters = 0u64;
269                const EPS: f64 = 1e-9;
270                loop {
271                    let cont = if *mult {
272                        if by >= 1.0 {
273                            v <= to + EPS
274                        } else {
275                            v >= to - EPS
276                        }
277                    } else if by >= 0.0 {
278                        v <= to + EPS
279                    } else {
280                        v >= to - EPS
281                    };
282                    if !cont {
283                        break;
284                    }
285                    self.vars.insert(var.clone(), v);
286                    self.eval_stmts(body)?;
287                    let prev = v;
288                    v = if *mult { v * by } else { v + by };
289                    if (v - prev).abs() < f64::EPSILON {
290                        break; // no progress (by 0, or *1) — avoid infinite loop
291                    }
292                    iters += 1;
293                    if iters > 1_000_000 {
294                        return err("for loop exceeded 1,000,000 iterations");
295                    }
296                }
297            }
298            Stmt::Print(item) => match item {
299                PrintItem::Expr(e) => {
300                    self.eval_expr(e)?;
301                }
302                PrintItem::Str(_) => {}
303            },
304            Stmt::Reset(list) => {
305                if list.is_empty() {
306                    self.env = EnvVars::new();
307                } else {
308                    let d = EnvVars::new();
309                    for e in list {
310                        self.env.set(*e, d.get(*e));
311                    }
312                }
313            }
314        }
315        Ok(())
316    }
317
318    fn eval_animate(&mut self, a: &Animate) -> ER<()> {
319        let idx = self.place_index(&a.target)?;
320        let shape = self.placed[idx].shape.ok_or_else(|| EvalError {
321            msg: "cannot animate a point (no drawn shape)".into(),
322        })?;
323        let dur = match &a.duration {
324            Some(e) => self.eval_expr(e)?,
325            None => DEFAULT_ANIM_DUR,
326        };
327        let mut start = match &a.timing {
328            Timing::Sequential => self.anim_cursor,
329            Timing::At(e) => self.eval_expr(e)?,
330            Timing::After(p) => {
331                let i = self.place_index(p)?;
332                let sh = self.placed[i].shape.ok_or_else(|| EvalError {
333                    msg: "`after` target has no animation".into(),
334                })?;
335                *self.anim_end.get(&sh).unwrap_or(&0.0)
336            }
337        };
338        if let Some(d) = &a.delay {
339            start += self.eval_expr(d)?;
340        }
341        let end = start + dur;
342        self.anim_cursor = end;
343        self.anim_end.insert(shape, end);
344        self.anims.push(Anim {
345            shape,
346            effect: stringexpr_lit(&a.effect),
347            start,
348            duration: dur,
349        });
350        Ok(())
351    }
352
353    fn eval_assignment(&mut self, a: &Assignment) -> ER<()> {
354        let rhs = self.eval_expr(&a.value)?;
355        match &a.target {
356            AssignTarget::Var(name, _sub) => {
357                let cur = *self.vars.get(name).unwrap_or(&0.0);
358                let val = apply_op(a.op, cur, rhs);
359                self.vars.insert(name.clone(), val);
360            }
361            AssignTarget::Env(e) => {
362                let cur = self.env.get(*e);
363                self.env.set(*e, apply_op(a.op, cur, rhs));
364            }
365        }
366        Ok(())
367    }
368
369    // ---- objects -----------------------------------------------------------
370
371    fn eval_object(&mut self, obj: &Object) -> ER<usize> {
372        match &obj.kind {
373            ObjectKind::Primitive(p) => match p {
374                Prim::Box | Prim::Circle | Prim::Ellipse => self.closed(*p, obj),
375                Prim::Line | Prim::Arrow | Prim::Move | Prim::Spline => self.open(*p, obj),
376                Prim::Arc => self.arc(obj),
377            },
378            ObjectKind::Text => self.text_obj(obj),
379            ObjectKind::Block(stmts) => self.block(stmts, obj),
380            ObjectKind::Empty => self.block(&[], obj),
381        }
382    }
383
384    fn closed(&mut self, p: Prim, obj: &Object) -> ER<usize> {
385        let style = self.style_of(obj)?;
386        let text = self.text_of(obj)?;
387        let dir = self.dir_of(obj);
388        let scale = self.scale_of(obj)?;
389
390        // dimensions
391        let (mut w, mut h, mut rad);
392        match p {
393            Prim::Circle => {
394                let r = self
395                    .dim(obj, DimKind::Rad)?
396                    .unwrap_or(self.env.get(EnvVar::Circlerad));
397                let r = self.dim(obj, DimKind::Diam)?.map(|d| d / 2.0).unwrap_or(r);
398                w = 2.0 * r;
399                h = 2.0 * r;
400                rad = r;
401            }
402            Prim::Ellipse => {
403                w = self
404                    .dim(obj, DimKind::Wid)?
405                    .unwrap_or(self.env.get(EnvVar::Ellipsewid));
406                h = self
407                    .dim(obj, DimKind::Ht)?
408                    .unwrap_or(self.env.get(EnvVar::Ellipseht));
409                rad = 0.0;
410            }
411            _ => {
412                // box
413                w = self
414                    .dim(obj, DimKind::Wid)?
415                    .unwrap_or(self.env.get(EnvVar::Boxwid));
416                h = self
417                    .dim(obj, DimKind::Ht)?
418                    .unwrap_or(self.env.get(EnvVar::Boxht));
419                rad = self
420                    .dim(obj, DimKind::Rad)?
421                    .unwrap_or(self.env.get(EnvVar::Boxrad));
422            }
423        }
424        w *= scale;
425        h *= scale;
426        rad *= scale;
427
428        let extent = if horizontal(dir) { w } else { h };
429        let center = self.place_center(obj, dir, extent, w, h)?;
430
431        let mut bb = Bbox::new();
432        bb.add(center - Point::new(w / 2.0, h / 2.0));
433        bb.add(center + Point::new(w / 2.0, h / 2.0));
434
435        let shape = match p {
436            Prim::Circle => Shape::Circle {
437                c: center,
438                r: rad,
439                style,
440                text,
441            },
442            Prim::Ellipse => Shape::Ellipse {
443                c: center,
444                w,
445                h,
446                style,
447                text,
448            },
449            _ => Shape::Box {
450                c: center,
451                w,
452                h,
453                rad,
454                style,
455                text,
456            },
457        };
458        self.shapes.push(shape);
459        self.bbox.union(&bb);
460
461        let half = dir_unit(dir) * (extent / 2.0);
462        let start = center - half;
463        let end = center + half;
464        self.pos = end;
465        self.dir = dir;
466        let kind = match p {
467            Prim::Circle => PKind::Circle,
468            Prim::Ellipse => PKind::Ellipse,
469            _ => PKind::Box,
470        };
471        let sh = self.shapes.len() - 1;
472        Ok(self.record(kind, center, bb, start, end, 0.0, Some(sh)))
473    }
474
475    fn open(&mut self, p: Prim, obj: &Object) -> ER<usize> {
476        let style = self.style_of(obj)?;
477        let text = self.text_of(obj)?;
478        let is_move = matches!(p, Prim::Move);
479        let (deflen_h, deflen_v) = if is_move {
480            (self.env.get(EnvVar::Movewid), self.env.get(EnvVar::Moveht))
481        } else {
482            (self.env.get(EnvVar::Linewid), self.env.get(EnvVar::Lineht))
483        };
484
485        // starting point
486        let start = self.from_of(obj)?.unwrap_or(self.pos);
487        let mut pts = vec![start];
488        let mut pend = Point::ZERO;
489        let mut any = false;
490        let mut last_dir = self.dir;
491
492        for a in &obj.attrs {
493            match a {
494                Attr::Direction(d, opt) => {
495                    let dist = match opt {
496                        Some(e) => self.eval_expr(e)?,
497                        None => {
498                            if horizontal(*d) {
499                                deflen_h
500                            } else {
501                                deflen_v
502                            }
503                        }
504                    };
505                    pend = pend + dir_unit(*d) * dist;
506                    last_dir = *d;
507                    any = true;
508                }
509                Attr::Then => {
510                    let np = *pts.last().unwrap() + pend;
511                    pts.push(np);
512                    pend = Point::ZERO;
513                }
514                Attr::To(pos) => {
515                    if pend != Point::ZERO {
516                        let np = *pts.last().unwrap() + pend;
517                        pts.push(np);
518                        pend = Point::ZERO;
519                    }
520                    pts.push(self.eval_pos(pos)?);
521                    any = true;
522                }
523                Attr::By(pos) => {
524                    let d = self.eval_pos(pos)?;
525                    pend = pend + (d - Point::ZERO);
526                    any = true;
527                }
528                _ => {}
529            }
530        }
531        if pend != Point::ZERO {
532            let np = *pts.last().unwrap() + pend;
533            pts.push(np);
534        }
535        if pts.len() == 1 && !any {
536            // bare line/arrow/move in the current direction
537            let dist = if horizontal(self.dir) {
538                deflen_h
539            } else {
540                deflen_v
541            };
542            pts.push(start + dir_unit(self.dir) * dist);
543            last_dir = self.dir;
544        }
545
546        // arrowheads
547        let arrows = self.arrows_of(obj, matches!(p, Prim::Arrow));
548
549        let mut bb = Bbox::new();
550        for pt in &pts {
551            bb.add(*pt);
552        }
553        self.bbox.union(&bb);
554
555        let end = *pts.last().unwrap();
556        let center = (pts[0] + end) * 0.5;
557        let kind = match p {
558            Prim::Spline => PKind::Spline,
559            Prim::Move => PKind::Move,
560            _ => PKind::Line,
561        };
562        let mut style = style;
563        if is_move {
564            style.invis = true;
565        }
566        let shape = if matches!(p, Prim::Spline) {
567            Shape::Spline {
568                pts: pts.clone(),
569                arrows,
570                style,
571                text,
572            }
573        } else {
574            Shape::Path {
575                pts: pts.clone(),
576                arrows,
577                style,
578                text,
579            }
580        };
581        self.shapes.push(shape);
582
583        self.pos = end;
584        self.dir = last_dir;
585        let sh = self.shapes.len() - 1;
586        Ok(self.record(kind, center, bb, pts[0], end, 0.0, Some(sh)))
587    }
588
589    fn arc(&mut self, obj: &Object) -> ER<usize> {
590        let style = self.style_of(obj)?;
591        let text = self.text_of(obj)?;
592        let start = self.from_of(obj)?.unwrap_or(self.pos);
593        let r = self
594            .dim(obj, DimKind::Rad)?
595            .unwrap_or(self.env.get(EnvVar::Arcrad));
596        let cw = obj.attrs.iter().any(|a| matches!(a, Attr::Cw));
597        let din = dir_unit(self.dir);
598        // center sits a radius to the left (ccw) or right (cw) of the heading
599        let normal = if cw {
600            Point::new(din.y, -din.x)
601        } else {
602            Point::new(-din.y, din.x)
603        };
604        let center = start + normal * r;
605        let a0 = (start - center).y.atan2((start - center).x);
606        let sweep = if cw { -PI / 2.0 } else { PI / 2.0 };
607        let a1 = a0 + sweep;
608        let end = center + Point::new(a1.cos(), a1.sin()) * r;
609
610        let arrows = self.arrows_of(obj, false);
611
612        let mut bb = Bbox::new();
613        bb.add(start);
614        bb.add(end);
615        for k in 0..=8 {
616            let t = a0 + sweep * (k as f64 / 8.0);
617            bb.add(center + Point::new(t.cos(), t.sin()) * r);
618        }
619        self.bbox.union(&bb);
620
621        self.shapes.push(Shape::Arc {
622            c: center,
623            r,
624            a0,
625            a1,
626            cw,
627            arrows,
628            style,
629            text,
630        });
631
632        // new heading is rotated by the sweep
633        let new = din.rotate(sweep);
634        self.dir = nearest_dir(new);
635        self.pos = end;
636        let sh = self.shapes.len() - 1;
637        Ok(self.record(PKind::Arc, center, bb, start, end, 0.0, Some(sh)))
638    }
639
640    fn text_obj(&mut self, obj: &Object) -> ER<usize> {
641        let text = self.text_of(obj)?;
642        let at = self.at_of(obj)?.unwrap_or(self.pos);
643        let mut bb = Bbox::new();
644        bb.add(at);
645        self.bbox.union(&bb);
646        self.shapes.push(Shape::Text { at, text });
647        let sh = self.shapes.len() - 1;
648        Ok(self.record(PKind::Text, at, bb, at, at, 0.0, Some(sh)))
649    }
650
651    fn block(&mut self, stmts: &[Stmt], obj: &Object) -> ER<usize> {
652        // evaluate the block in a fresh local scope at its own origin
653        let mut sub = State::new();
654        sub.env = self.env.clone();
655        sub.vars = self.vars.clone();
656        sub.eval_stmts(stmts)?;
657
658        let sub_bb = if sub.bbox.is_empty() {
659            let mut b = Bbox::new();
660            b.add(Point::ZERO);
661            b
662        } else {
663            sub.bbox
664        };
665        let local_center = (sub_bb.min + sub_bb.max) * 0.5;
666        let w = sub_bb.width();
667        let h = sub_bb.height();
668
669        let dir = self.dir_of(obj);
670        let extent = if horizontal(dir) { w } else { h };
671        let target = self.place_center(obj, dir, extent, w, h)?;
672        let shift = target - local_center;
673
674        let first_shape = self.shapes.len();
675        for mut sh in sub.shapes.into_iter() {
676            translate_shape(&mut sh, shift);
677            self.shapes.push(sh);
678        }
679        let shape = if self.shapes.len() > first_shape {
680            Some(first_shape)
681        } else {
682            None
683        };
684        let mut bb = Bbox::new();
685        bb.add(sub_bb.min + shift);
686        bb.add(sub_bb.max + shift);
687        self.bbox.union(&bb);
688
689        let half = dir_unit(dir) * (extent / 2.0);
690        let start = target - half;
691        let end = target + half;
692        self.pos = end;
693        self.dir = dir;
694        Ok(self.record(PKind::Block, target, bb, start, end, 0.0, shape))
695    }
696
697    #[allow(clippy::too_many_arguments)]
698    fn record(
699        &mut self,
700        kind: PKind,
701        center: Point,
702        bbox: Bbox,
703        start: Point,
704        end: Point,
705        thick: f64,
706        shape: Option<usize>,
707    ) -> usize {
708        let idx = self.placed.len();
709        self.placed.push(Placed {
710            kind,
711            center,
712            bbox,
713            start,
714            end,
715            thick,
716            shape,
717        });
718        idx
719    }
720
721    // ---- attribute helpers -------------------------------------------------
722
723    fn dim(&mut self, obj: &Object, kind: DimKind) -> ER<Option<f64>> {
724        for a in &obj.attrs {
725            if let Attr::Dim(k, e) = a {
726                if *k == kind {
727                    return Ok(Some(self.eval_expr(e)?));
728                }
729            }
730        }
731        Ok(None)
732    }
733
734    fn scale_of(&mut self, obj: &Object) -> ER<f64> {
735        Ok(self.dim(obj, DimKind::Scaled)?.unwrap_or(1.0))
736    }
737
738    fn dir_of(&self, obj: &Object) -> Dir {
739        obj.attrs
740            .iter()
741            .rev()
742            .find_map(|a| match a {
743                Attr::Direction(d, _) => Some(*d),
744                _ => None,
745            })
746            .unwrap_or(self.dir)
747    }
748
749    fn from_of(&mut self, obj: &Object) -> ER<Option<Point>> {
750        for a in &obj.attrs {
751            if let Attr::From(pos) = a {
752                return Ok(Some(self.eval_pos(pos)?));
753            }
754        }
755        Ok(None)
756    }
757
758    fn at_of(&mut self, obj: &Object) -> ER<Option<Point>> {
759        for a in &obj.attrs {
760            if let Attr::At(pos) = a {
761                return Ok(Some(self.eval_pos(pos)?));
762            }
763        }
764        Ok(None)
765    }
766
767    /// Compute the center of a closed object given direction and extents.
768    fn place_center(&mut self, obj: &Object, dir: Dir, extent: f64, w: f64, h: f64) -> ER<Point> {
769        if let Some(at) = self.at_of(obj)? {
770            return Ok(at);
771        }
772        for a in &obj.attrs {
773            if let Attr::With { anchor, at } = a {
774                let ap = self.eval_pos(at)?;
775                let off = match anchor {
776                    WithAnchor::Corner(c) => corner_offset(*c, w, h),
777                    WithAnchor::Pair(x, y) => Point::new(self.eval_expr(x)?, self.eval_expr(y)?),
778                    WithAnchor::Plain => Point::ZERO,
779                };
780                return Ok(ap - off);
781            }
782        }
783        Ok(self.pos + dir_unit(dir) * (extent / 2.0))
784    }
785
786    fn arrows_of(&self, obj: &Object, default_end: bool) -> Arrowheads {
787        let mut found = None;
788        for a in &obj.attrs {
789            if let Attr::Arrowhead(h, _) = a {
790                found = Some(match h {
791                    token::Arrow::Left => Arrowheads::Start,
792                    token::Arrow::Right => Arrowheads::End,
793                    token::Arrow::Double => Arrowheads::Both,
794                });
795            }
796        }
797        found.unwrap_or(if default_end {
798            Arrowheads::End
799        } else {
800            Arrowheads::None
801        })
802    }
803
804    fn style_of(&mut self, obj: &Object) -> ER<Style> {
805        let mut s = Style::default();
806        for a in &obj.attrs {
807            match a {
808                Attr::LineStyle(lt, _) => match lt {
809                    LineType::Solid => s.dash = Dash::Solid,
810                    LineType::Dashed => s.dash = Dash::Dashed,
811                    LineType::Dotted => s.dash = Dash::Dotted,
812                    LineType::Invis => s.invis = true,
813                },
814                Attr::Fill(opt) => {
815                    let g = match opt {
816                        Some(e) => self.eval_expr(e)?,
817                        None => self.env.get(EnvVar::Fillval),
818                    };
819                    s.fill = Some(Fill::Gray(g));
820                }
821                Attr::Color(kind, se) => {
822                    let name = stringexpr_lit(se);
823                    match kind {
824                        token::Color::Outlined => s.stroke = Some(name),
825                        token::Color::Colored => {
826                            s.stroke = Some(name.clone());
827                            s.fill = Some(Fill::Color(name));
828                        }
829                        token::Color::Shaded => s.fill = Some(Fill::Color(name)),
830                    }
831                }
832                Attr::Dim(DimKind::Thick, e) => s.thick = Some(self.eval_expr(e)?),
833                _ => {}
834            }
835        }
836        Ok(s)
837    }
838
839    fn text_of(&mut self, obj: &Object) -> ER<Vec<TextLine>> {
840        let mut lines = Vec::new();
841        let mut halign = 0i8;
842        let mut valign = 0i8;
843        for a in &obj.attrs {
844            match a {
845                Attr::TextPos(tp) => match tp {
846                    token::TextPos::Ljust => halign = -1,
847                    token::TextPos::Rjust => halign = 1,
848                    token::TextPos::Center => {
849                        halign = 0;
850                        valign = 0;
851                    }
852                    token::TextPos::Above => valign = 1,
853                    token::TextPos::Below => valign = -1,
854                },
855                Attr::Text(se) => {
856                    let s = self.eval_stringexpr(se)?;
857                    lines.push(TextLine { s, halign, valign });
858                }
859                _ => {}
860            }
861        }
862        Ok(lines)
863    }
864
865    // ---- positions & places ------------------------------------------------
866
867    fn eval_pos(&mut self, pos: &Position) -> ER<Point> {
868        match pos {
869            Position::Pair(x, y) => Ok(Point::new(self.eval_expr(x)?, self.eval_expr(y)?)),
870            Position::Place(loc, shifts) => {
871                let mut p = self.eval_loc(loc)?;
872                for s in shifts {
873                    let d = self.eval_loc(&s.loc)?;
874                    p = match s.sign {
875                        Sign::Plus => p + d,
876                        Sign::Minus => p - d,
877                    };
878                }
879                Ok(p)
880            }
881            Position::Between { frac, a, b, .. } => {
882                let f = self.eval_expr(frac)?;
883                let pa = self.eval_pos(a)?;
884                let pb = self.eval_pos(b)?;
885                Ok(pa.lerp(pb, f))
886            }
887        }
888    }
889
890    fn eval_loc(&mut self, loc: &Location) -> ER<Point> {
891        match loc {
892            Location::Place(p) => self.place_point(p),
893            Location::Paren(pos) => self.eval_pos(pos),
894            Location::ParenPair(p1, p2) => {
895                Ok(Point::new(self.eval_pos(p1)?.x, self.eval_pos(p2)?.y))
896            }
897        }
898    }
899
900    fn place_point(&mut self, p: &Place) -> ER<Point> {
901        match p {
902            Place::Here => Ok(self.pos),
903            Place::Name { name, .. } => {
904                let idx = self.label_index(name)?;
905                Ok(self.placed[idx].center)
906            }
907            Place::Nth { count, obj } => {
908                let idx = self.nth_index(count, obj)?;
909                Ok(self.placed[idx].center)
910            }
911            Place::Corner(inner, c) => {
912                let idx = self.place_index(inner)?;
913                Ok(self.placed[idx].corner(*c))
914            }
915            Place::CornerOf(c, inner) => {
916                let idx = self.place_index(inner)?;
917                Ok(self.placed[idx].corner(*c))
918            }
919            Place::Member(_, _) => err("block sub-labels (B.A) are not supported yet"),
920        }
921    }
922
923    fn place_index(&mut self, p: &Place) -> ER<usize> {
924        match p {
925            Place::Name { name, .. } => self.label_index(name),
926            Place::Nth { count, obj } => self.nth_index(count, obj),
927            Place::Corner(inner, _) | Place::CornerOf(_, inner) => self.place_index(inner),
928            Place::Here => err("`Here` is a point, not an object"),
929            Place::Member(_, _) => err("block sub-labels (B.A) are not supported yet"),
930        }
931    }
932
933    fn label_index(&self, name: &str) -> ER<usize> {
934        self.labels.get(name).copied().ok_or_else(|| EvalError {
935            msg: format!("unknown label `{name}`"),
936        })
937    }
938
939    fn nth_index(&mut self, count: &Nth, obj: &PrimObj) -> ER<usize> {
940        let want = primobj_kind(obj);
941        let matches: Vec<usize> = self
942            .placed
943            .iter()
944            .enumerate()
945            .filter(|(_, pl)| pl.kind == want)
946            .map(|(i, _)| i)
947            .collect();
948        if matches.is_empty() {
949            return err(format!("no {:?} object to reference", want_name(want)));
950        }
951        let idx = match count {
952            Nth::Last => *matches.last().unwrap(),
953            Nth::Count(e, from_last) => {
954                let n = self.eval_expr(e)?.round() as i64;
955                if n < 1 {
956                    return err("ordinal must be >= 1");
957                }
958                let k = (n - 1) as usize;
959                if *from_last {
960                    if k >= matches.len() {
961                        return err("ordinal out of range");
962                    }
963                    matches[matches.len() - 1 - k]
964                } else {
965                    if k >= matches.len() {
966                        return err("ordinal out of range");
967                    }
968                    matches[k]
969                }
970            }
971        };
972        Ok(idx)
973    }
974
975    // ---- string expressions ------------------------------------------------
976
977    fn eval_stringexpr(&mut self, se: &StringExpr) -> ER<String> {
978        Ok(match se {
979            StringExpr::Lit(s) => s.clone(),
980            StringExpr::Concat(a, b) => {
981                format!("{}{}", self.eval_stringexpr(a)?, self.eval_stringexpr(b)?)
982            }
983            StringExpr::Arg(n) => format!("${n}"), // should have been expanded
984            StringExpr::Sprintf(fmt, args) => {
985                let f = self.eval_stringexpr(fmt)?;
986                let mut vals = Vec::with_capacity(args.len());
987                for e in args {
988                    vals.push(self.eval_expr(e)?);
989                }
990                sprintf_fmt(&f, &vals)
991            }
992        })
993    }
994
995    // ---- expressions -------------------------------------------------------
996
997    fn eval_expr(&mut self, e: &Expr) -> ER<f64> {
998        Ok(match e {
999            Expr::Num(v) => *v,
1000            Expr::Var(name) => *self.vars.get(name).unwrap_or(&0.0),
1001            Expr::Env(v) => self.env.get(*v),
1002            Expr::Unary(op, a) => {
1003                let x = self.eval_expr(a)?;
1004                match op {
1005                    UnOp::Neg => -x,
1006                    UnOp::Pos => x,
1007                    UnOp::Not => bool_f(x == 0.0),
1008                }
1009            }
1010            Expr::Bin(op, a, b) => {
1011                let x = self.eval_expr(a)?;
1012                let y = self.eval_expr(b)?;
1013                match op {
1014                    BinOp::Add => x + y,
1015                    BinOp::Sub => x - y,
1016                    BinOp::Mul => x * y,
1017                    BinOp::Div => x / y,
1018                    BinOp::Mod => x % y,
1019                    BinOp::Pow => x.powf(y),
1020                    BinOp::Eq => bool_f(x == y),
1021                    BinOp::Ne => bool_f(x != y),
1022                    BinOp::Lt => bool_f(x < y),
1023                    BinOp::Le => bool_f(x <= y),
1024                    BinOp::Gt => bool_f(x > y),
1025                    BinOp::Ge => bool_f(x >= y),
1026                    BinOp::And => bool_f(x != 0.0 && y != 0.0),
1027                    BinOp::Or => bool_f(x != 0.0 || y != 0.0),
1028                }
1029            }
1030            Expr::Func1(f, a) => {
1031                let x = self.eval_expr(a)?;
1032                match f {
1033                    Func1::Abs => x.abs(),
1034                    Func1::Acos => x.acos(),
1035                    Func1::Asin => x.asin(),
1036                    Func1::Cos => x.cos(),
1037                    Func1::Exp => 10f64.powf(x),
1038                    Func1::Expe => x.exp(),
1039                    Func1::Int => x.trunc(),
1040                    Func1::Log => x.log10(),
1041                    Func1::Loge => x.ln(),
1042                    Func1::Sign => {
1043                        if x > 0.0 {
1044                            1.0
1045                        } else if x < 0.0 {
1046                            -1.0
1047                        } else {
1048                            0.0
1049                        }
1050                    }
1051                    Func1::Sin => x.sin(),
1052                    Func1::Sqrt => x.sqrt(),
1053                    Func1::Tan => x.tan(),
1054                    Func1::Floor => x.floor(),
1055                }
1056            }
1057            Expr::Func2(f, a, b) => {
1058                let x = self.eval_expr(a)?;
1059                let y = self.eval_expr(b)?;
1060                match f {
1061                    Func2::Atan2 => x.atan2(y),
1062                    Func2::Max => x.max(y),
1063                    Func2::Min => x.min(y),
1064                    Func2::Pmod => x.rem_euclid(y),
1065                }
1066            }
1067            Expr::Rand(_) => 0.5, // deterministic placeholder
1068            Expr::DotX(loc) => self.eval_loc(loc)?.x,
1069            Expr::DotY(loc) => self.eval_loc(loc)?.y,
1070            Expr::PlaceAttr(place, param) => {
1071                let idx = self.place_index(place)?;
1072                let pl = &self.placed[idx];
1073                match param {
1074                    token::Param::Width => pl.bbox.width(),
1075                    token::Param::Height => pl.bbox.height(),
1076                    token::Param::Radius => pl.bbox.width() / 2.0,
1077                    token::Param::Diameter => pl.bbox.width(),
1078                    token::Param::Length => pl.start.dist(pl.end),
1079                    token::Param::Thickness => pl.thick,
1080                }
1081            }
1082        })
1083    }
1084}
1085
1086// ---- free helpers ----------------------------------------------------------
1087
1088fn apply_op(op: AssignOp, cur: f64, rhs: f64) -> f64 {
1089    match op {
1090        AssignOp::Set => rhs,
1091        AssignOp::Add => cur + rhs,
1092        AssignOp::Sub => cur - rhs,
1093        AssignOp::Mul => cur * rhs,
1094        AssignOp::Div => cur / rhs,
1095        AssignOp::Rem => cur % rhs,
1096    }
1097}
1098
1099fn bool_f(b: bool) -> f64 {
1100    if b { 1.0 } else { 0.0 }
1101}
1102
1103fn corner_offset(c: Corner, w: f64, h: f64) -> Point {
1104    let (hw, hh) = (w / 2.0, h / 2.0);
1105    match c {
1106        Corner::N => Point::new(0.0, hh),
1107        Corner::S => Point::new(0.0, -hh),
1108        Corner::E => Point::new(hw, 0.0),
1109        Corner::W => Point::new(-hw, 0.0),
1110        Corner::Ne => Point::new(hw, hh),
1111        Corner::Se => Point::new(hw, -hh),
1112        Corner::Nw => Point::new(-hw, hh),
1113        Corner::Sw => Point::new(-hw, -hh),
1114        Corner::Center | Corner::Start | Corner::End => Point::ZERO,
1115    }
1116}
1117
1118fn primobj_kind(o: &PrimObj) -> PKind {
1119    match o {
1120        PrimObj::Prim(p) => match p {
1121            Prim::Box => PKind::Box,
1122            Prim::Circle => PKind::Circle,
1123            Prim::Ellipse => PKind::Ellipse,
1124            Prim::Line | Prim::Arrow => PKind::Line,
1125            Prim::Move => PKind::Move,
1126            Prim::Spline => PKind::Spline,
1127            Prim::Arc => PKind::Arc,
1128        },
1129        PrimObj::Block | PrimObj::EmptyBrack => PKind::Block,
1130        PrimObj::Str(_) => PKind::Text,
1131    }
1132}
1133
1134fn want_name(k: PKind) -> &'static str {
1135    match k {
1136        PKind::Box => "box",
1137        PKind::Circle => "circle",
1138        PKind::Ellipse => "ellipse",
1139        PKind::Line => "line",
1140        PKind::Move => "move",
1141        PKind::Spline => "spline",
1142        PKind::Arc => "arc",
1143        PKind::Block => "block",
1144        PKind::Text => "text",
1145    }
1146}
1147
1148fn nearest_dir(v: Point) -> Dir {
1149    if v.x.abs() >= v.y.abs() {
1150        if v.x >= 0.0 { Dir::Right } else { Dir::Left }
1151    } else if v.y >= 0.0 {
1152        Dir::Up
1153    } else {
1154        Dir::Down
1155    }
1156}
1157
1158/// Minimal printf-style formatter supporting `%d %i %f %e %g %%` with optional
1159/// `.precision`. Width/flags are accepted but ignored; arguments are numeric.
1160fn sprintf_fmt(fmt: &str, vals: &[f64]) -> String {
1161    let mut out = String::new();
1162    let mut chars = fmt.chars().peekable();
1163    let mut ai = 0usize;
1164    while let Some(c) = chars.next() {
1165        if c != '%' {
1166            out.push(c);
1167            continue;
1168        }
1169        let mut spec = String::new();
1170        while let Some(&n) = chars.peek() {
1171            if "+-0123456789. #".contains(n) {
1172                spec.push(n);
1173                chars.next();
1174            } else {
1175                break;
1176            }
1177        }
1178        let conv = match chars.next() {
1179            Some(c) => c,
1180            None => break,
1181        };
1182        if conv == '%' {
1183            out.push('%');
1184            continue;
1185        }
1186        let v = vals.get(ai).copied().unwrap_or(0.0);
1187        ai += 1;
1188        let prec = spec.split('.').nth(1).and_then(|p| {
1189            p.chars()
1190                .take_while(|c| c.is_ascii_digit())
1191                .collect::<String>()
1192                .parse::<usize>()
1193                .ok()
1194        });
1195        match conv {
1196            'd' | 'i' => out.push_str(&format!("{}", v.round() as i64)),
1197            'f' | 'F' => out.push_str(&format!("{:.*}", prec.unwrap_or(6), v)),
1198            'e' | 'E' => out.push_str(&format!("{:.*e}", prec.unwrap_or(6), v)),
1199            'g' | 'G' => out.push_str(&format!("{v}")),
1200            's' => out.push_str(&format!("{v}")),
1201            other => {
1202                out.push('%');
1203                out.push(other);
1204            }
1205        }
1206    }
1207    out
1208}
1209
1210fn stringexpr_lit(se: &StringExpr) -> String {
1211    match se {
1212        StringExpr::Lit(s) => s.clone(),
1213        StringExpr::Concat(a, b) => format!("{}{}", stringexpr_lit(a), stringexpr_lit(b)),
1214        StringExpr::Arg(n) => format!("${n}"),
1215        StringExpr::Sprintf(fmt, _) => stringexpr_lit(fmt),
1216    }
1217}
1218
1219fn translate_shape(sh: &mut Shape, d: Point) {
1220    let mv = |p: &mut Point| *p = *p + d;
1221    match sh {
1222        Shape::Box { c, .. } | Shape::Circle { c, .. } | Shape::Ellipse { c, .. } => mv(c),
1223        Shape::Path { pts, .. } | Shape::Spline { pts, .. } => {
1224            for p in pts {
1225                mv(p);
1226            }
1227        }
1228        Shape::Arc { c, .. } => mv(c),
1229        Shape::Text { at, .. } => mv(at),
1230    }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236    use crate::parser::parse;
1237
1238    fn draw(src: &str) -> Drawing {
1239        eval(&parse(src).unwrap()).unwrap()
1240    }
1241
1242    #[test]
1243    fn pipeline_chains_left_to_right() {
1244        let d = draw(".PS\nellipse \"document\"\narrow\nbox \"PIC\"\narrow\nbox \"TROFF\"\n.PE");
1245        // 5 shapes
1246        assert_eq!(d.shapes.len(), 5);
1247        // first ellipse centered at (0.375, 0): ellipsewid/2
1248        if let Shape::Ellipse { c, .. } = &d.shapes[0] {
1249            assert!((c.x - 0.375).abs() < 1e-9, "ellipse x = {}", c.x);
1250            assert!(c.y.abs() < 1e-9);
1251        } else {
1252            panic!("expected ellipse");
1253        }
1254        // bbox grows to the right
1255        assert!(d.bbox.width() > 2.0);
1256        assert!((d.bbox.height() - 0.5).abs() < 1e-9);
1257    }
1258
1259    #[test]
1260    fn box_at_absolute() {
1261        let d = draw("box ht 0.3 wid 0.5 at 1,2");
1262        let Shape::Box { c, w, h, .. } = &d.shapes[0] else {
1263            panic!()
1264        };
1265        assert_eq!(*c, Point::new(1.0, 2.0));
1266        assert!((*w - 0.5).abs() < 1e-9 && (*h - 0.3).abs() < 1e-9);
1267    }
1268
1269    #[test]
1270    fn diamond_is_closed() {
1271        let d = draw("line up right then down right then down left then up left");
1272        let Shape::Path { pts, .. } = &d.shapes[0] else {
1273            panic!()
1274        };
1275        assert_eq!(pts.len(), 5);
1276        // returns to start
1277        assert!(pts[0].dist(*pts.last().unwrap()) < 1e-9);
1278    }
1279
1280    #[test]
1281    fn corners_and_labels() {
1282        let d = draw("A: box wid 1 ht 1 at 0,0\nbox wid 0.5 ht 0.5 with .sw at A.ne");
1283        // second box sw corner at A.ne (0.5,0.5) => its center at (0.75,0.75)
1284        let Shape::Box { c, .. } = &d.shapes[1] else {
1285            panic!()
1286        };
1287        assert!(
1288            (c.x - 0.75).abs() < 1e-9 && (c.y - 0.75).abs() < 1e-9,
1289            "c = {c:?}"
1290        );
1291    }
1292
1293    #[test]
1294    fn for_loop_repeats() {
1295        let d = draw("for i = 1 to 3 do { box }");
1296        assert_eq!(d.shapes.len(), 3);
1297    }
1298
1299    #[test]
1300    fn if_else_branches() {
1301        let d1 = draw("x = 1\nif x > 0 then { box } else { circle }");
1302        assert!(matches!(d1.shapes[0], Shape::Box { .. }));
1303        let d2 = draw("x = 0\nif x > 0 then { box } else { circle }");
1304        assert!(matches!(d2.shapes[0], Shape::Circle { .. }));
1305    }
1306
1307    #[test]
1308    fn define_macro_with_args() {
1309        let d = draw("define elem { box wid $1 }\nelem(0.5)\nelem(1.25)");
1310        assert_eq!(d.shapes.len(), 2);
1311        let Shape::Box { w: w0, .. } = &d.shapes[0] else {
1312            panic!()
1313        };
1314        let Shape::Box { w: w1, .. } = &d.shapes[1] else {
1315            panic!()
1316        };
1317        assert!((*w0 - 0.5).abs() < 1e-9 && (*w1 - 1.25).abs() < 1e-9);
1318    }
1319
1320    #[test]
1321    fn for_loop_places_in_a_row() {
1322        // boxes step right by default; bbox should be ~3*boxwid wide
1323        let d = draw("for i = 1 to 3 do { box; move }");
1324        assert!(d.bbox.width() > 2.0);
1325    }
1326
1327    #[test]
1328    fn animate_timing() {
1329        let d = draw(
1330            "A: box\narrow\nbox\nanimate A with \"fade\" for 0.5\nanimate last arrow with \"draw\"\nanimate 2nd box with \"pop\" after A",
1331        );
1332        assert_eq!(d.anims.len(), 3);
1333        // A: fade, start 0, dur 0.5, targets shape 0
1334        assert_eq!(d.anims[0].effect, "fade");
1335        assert_eq!(d.anims[0].shape, 0);
1336        assert!((d.anims[0].start).abs() < 1e-9 && (d.anims[0].duration - 0.5).abs() < 1e-9);
1337        // arrow: draw, sequential after A -> start 0.5, default dur 0.6, shape 1
1338        assert_eq!(d.anims[1].shape, 1);
1339        assert!((d.anims[1].start - 0.5).abs() < 1e-9);
1340        // 2nd box: pop, after A (ends at 0.5) -> start 0.5, shape 2
1341        assert_eq!(d.anims[2].shape, 2);
1342        assert!((d.anims[2].start - 0.5).abs() < 1e-9);
1343    }
1344
1345    #[test]
1346    fn last_ordinal() {
1347        let d = draw("box at 0,0\nbox at 2,0\narrow from 1st box.e to 2nd box.w");
1348        let Shape::Path { pts, .. } = &d.shapes[2] else {
1349            panic!()
1350        };
1351        // from first box east edge to second box west edge
1352        assert!(pts[0].x > 0.0 && pts.last().unwrap().x < 2.0);
1353    }
1354}