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