Skip to main content

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}