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