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