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