rpic_core/ir.rs
1//! Intermediate representation: the placed-primitive tree produced by the
2//! evaluator and consumed by the render backends. All coordinates are absolute,
3//! in pic units (inches), y pointing up.
4
5use crate::diagnostic::Diagnostic;
6use crate::geom::{Bbox, Point};
7
8/// A fully evaluated drawing.
9#[derive(Debug, Clone, PartialEq)]
10pub struct Drawing {
11 pub shapes: Vec<Shape>,
12 /// Render layer per shape. Lower layers are emitted first; equal layers keep
13 /// source order and shape ids stable.
14 pub shape_layers: Vec<i32>,
15 /// rpic extension: CSS class hook per shape, emitted on the shape's
16 /// `<g id="sN">` group. Inert unless the host document styles it; `None`
17 /// keeps the group byte-identical to classic output.
18 pub shape_classes: Vec<Option<String>>,
19 /// Source span of the statement that produced each shape (`None` for
20 /// shapes without one). Drives the per-object geometry export; never
21 /// affects rendering.
22 pub shape_spans: Vec<Option<crate::diagnostic::Span>>,
23 pub bbox: Bbox,
24 /// Global `linethick` in points after picture-wide sizing, used only for
25 /// dpic-style backend prelude padding. Per-shape strokes keep their own
26 /// unscaled point thickness.
27 pub prelude_thick: f64,
28 /// Extra canvas whitespace in inches. This is an rpic extension inspired by
29 /// Pikchr: it affects native backend framing only, not pic geometry.
30 pub canvas_margin: CanvasMargin,
31 /// rpic extension: a fixed page rectangle (`canvas from … to …`) in model
32 /// space. When set, the SVG viewBox is derived from it instead of the
33 /// content bounds; content outside is clipped by the viewBox.
34 pub canvas: Option<Bbox>,
35 pub anims: Vec<Anim>,
36 /// Lines emitted by pic `print` statements, without trailing newlines.
37 pub diagnostics: Vec<String>,
38 /// Non-fatal compiler warnings for accepted but likely unintended input.
39 pub warnings: Vec<Diagnostic>,
40}
41
42/// Extra whitespace around the rendered canvas, in internal inches.
43#[derive(Debug, Clone, Copy, PartialEq, Default)]
44pub struct CanvasMargin {
45 pub top: f64,
46 pub right: f64,
47 pub bottom: f64,
48 pub left: f64,
49}
50
51impl CanvasMargin {
52 pub fn horizontal(self) -> f64 {
53 self.left + self.right
54 }
55
56 pub fn vertical(self) -> f64 {
57 self.top + self.bottom
58 }
59
60 pub fn scale_by(&mut self, factor: f64) {
61 self.top *= factor;
62 self.right *= factor;
63 self.bottom *= factor;
64 self.left *= factor;
65 }
66}
67
68/// A resolved animation entry. `shape` indexes into [`Drawing::shapes`]; the
69/// SVG backend gives that shape the id `s{shape}` so the player can target it.
70#[derive(Debug, Clone, PartialEq)]
71pub struct Anim {
72 pub shape: usize,
73 pub effect: String,
74 /// Absolute start time in seconds.
75 pub start: f64,
76 pub duration: f64,
77}
78
79/// Line dash style.
80#[derive(Debug, Clone, Copy, PartialEq, Default)]
81pub enum Dash {
82 #[default]
83 Solid,
84 /// Dash/gap base length in inches.
85 Dashed(f64),
86 /// Explicit dot spacing in inches; `None` means dpic's stroke-relative default.
87 Dotted(Option<f64>),
88}
89
90/// Fill specification.
91#[derive(Debug, Clone, PartialEq)]
92pub enum Fill {
93 /// Gray level, pic convention: 0 = black, 1 = white.
94 Gray(f64),
95 /// A named/explicit color.
96 Color(String),
97}
98
99/// Hatch fill pattern.
100#[derive(Debug, Clone, PartialEq)]
101pub struct Hatch {
102 pub cross: bool,
103 /// Line angle in degrees, measured in pic coordinates.
104 pub angle: f64,
105 /// Distance between hatch lines in inches.
106 pub sep: f64,
107 /// Hatch stroke width in points.
108 pub width: f64,
109 pub color: String,
110}
111
112/// Two-stop linear gradient fill (rpic extension, PSTricks-inspired).
113#[derive(Debug, Clone, PartialEq)]
114pub struct Gradient {
115 pub from: String,
116 pub to: String,
117 /// Angle in degrees, measured in pic coordinates: 0 = left to right,
118 /// 90 = bottom to top (matching how `hatchangle` measures).
119 pub angle: f64,
120}
121
122/// Visual style shared by all shapes.
123#[derive(Debug, Clone, PartialEq)]
124pub struct Style {
125 /// Stroke color (CSS), or `None` to use the default.
126 pub stroke: Option<String>,
127 pub fill: Option<Fill>,
128 pub hatch: Option<Hatch>,
129 pub gradient: Option<Gradient>,
130 /// Fill opacity, applied only to filled or hatched regions.
131 pub fill_opacity: Option<f64>,
132 /// Whether open paths/splines/arcs should emit a filled area. `color` on an
133 /// open object only changes the stroke; `fill` and `shaded` fill.
134 pub fill_open: bool,
135 pub dash: Dash,
136 /// Stroke thickness in points; `None` = backend default.
137 pub thick: Option<f64>,
138 /// Invisible (used by `move` and `invis`): geometry is still available for
139 /// placement and anchors, but is not drawn.
140 pub invis: bool,
141 /// Invisible geometry that still contributes to output bounds. Dpic uses
142 /// this for `move`; explicit `invis` helpers remain bounds-neutral.
143 pub invis_bounds: bool,
144 /// Arrowhead dimensions in inches (`arrowht`/`arrowwid`), used when this
145 /// shape carries arrowheads.
146 pub arrow_ht: f64,
147 pub arrow_wid: f64,
148 /// Filled (solid triangle, `arrowhead=2`) vs open (two strokes,
149 /// `arrowhead=0`).
150 pub arrow_filled: bool,
151}
152
153impl Default for Style {
154 fn default() -> Self {
155 Style {
156 stroke: None,
157 fill: None,
158 hatch: None,
159 gradient: None,
160 fill_opacity: None,
161 fill_open: false,
162 dash: Dash::Solid,
163 thick: None,
164 invis: false,
165 invis_bounds: false,
166 arrow_ht: 0.1,
167 arrow_wid: 0.05,
168 arrow_filled: true,
169 }
170 }
171}
172
173/// Which ends of a path carry an arrowhead.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum Arrowheads {
176 #[default]
177 None,
178 Start,
179 End,
180 Both,
181}
182
183/// A line of attached text with placement hints.
184#[derive(Debug, Clone, PartialEq)]
185pub struct TextLine {
186 pub s: String,
187 /// rpic `texlabels` extension: when set, this line is a typeset math
188 /// formula and `s` keeps the original literal for fallback/diagnostics.
189 pub math: Option<crate::math::MathSpan>,
190 /// horizontal: -1 = ljust, 0 = center, +1 = rjust.
191 pub halign: i8,
192 /// vertical: +1 = above, 0 = center, -1 = below.
193 pub valign: i8,
194 /// Extra text-position offset in inches (`textoffset`).
195 pub text_offset: f64,
196 /// rpic extension: bold face (`bold`).
197 pub bold: bool,
198 /// rpic extension: italic face (`italic`).
199 pub italic: bool,
200 /// rpic extension: font family — `Some("monospace")` from `mono`, or the
201 /// family given to `font "…"`. `None` follows the root `<svg>` family.
202 pub family: Option<String>,
203 /// rpic extension: explicit size in points (`fontsize`); `None` keeps
204 /// classic sizing (11 pt attached, height-derived standalone).
205 pub size_pt: Option<f64>,
206 /// rpic extension: rotation in degrees, CCW (`rotated`); applied about
207 /// the line's anchor point in the SVG.
208 pub rotate: Option<f64>,
209}
210
211/// Classic label size in points — the baseline `fontsize` scales against.
212pub(crate) const FONT_PT_CLASSIC: f64 = 11.0;
213
214impl TextLine {
215 /// Width scale vs. the classic 11 pt regular estimate: explicit
216 /// `fontsize` scales linearly; bold glyphs run ~5% wider.
217 pub(crate) fn width_factor(&self) -> f64 {
218 let size = self.size_pt.map_or(1.0, |pt| pt / FONT_PT_CLASSIC);
219 let weight = if self.bold { 1.05 } else { 1.0 };
220 size * weight
221 }
222
223 /// Height scale (explicit `fontsize` vs. classic 11 pt).
224 pub(crate) fn height_factor(&self) -> f64 {
225 self.size_pt.map_or(1.0, |pt| pt / FONT_PT_CLASSIC)
226 }
227}
228
229/// A placed drawing primitive.
230#[derive(Debug, Clone, PartialEq)]
231pub enum Shape {
232 Box {
233 c: Point,
234 w: f64,
235 h: f64,
236 rad: f64,
237 style: Style,
238 text: Vec<TextLine>,
239 },
240 Circle {
241 c: Point,
242 r: f64,
243 style: Style,
244 text: Vec<TextLine>,
245 },
246 Ellipse {
247 c: Point,
248 w: f64,
249 h: f64,
250 style: Style,
251 text: Vec<TextLine>,
252 },
253 /// Straight polyline (line / arrow / move).
254 Path {
255 pts: Vec<Point>,
256 /// rpic extension: this path was closed with the `close` attribute.
257 closed: bool,
258 arrows: Arrowheads,
259 style: Style,
260 text: Vec<TextLine>,
261 },
262 Spline {
263 pts: Vec<Point>,
264 /// dpic spline tension. `None` = the classic pic quadratic B-spline
265 /// (straight first/last half-segments, tangent at segment midpoints);
266 /// `Some(t)` = dpic's tensioned cubic spline through `t`.
267 tension: Option<f64>,
268 arrows: Arrowheads,
269 style: Style,
270 text: Vec<TextLine>,
271 },
272 Arc {
273 c: Point,
274 r: f64,
275 /// start and end angles in radians.
276 a0: f64,
277 a1: f64,
278 cw: bool,
279 arrows: Arrowheads,
280 style: Style,
281 text: Vec<TextLine>,
282 },
283 Brace {
284 a: Point,
285 b: Point,
286 cubics: Vec<[Point; 4]>,
287 label_at: Point,
288 style: Style,
289 text: Vec<TextLine>,
290 },
291 Text {
292 at: Point,
293 text: Vec<TextLine>,
294 bbox: Bbox,
295 w: f64,
296 h: f64,
297 standalone: bool,
298 },
299}