write_mesh.rs

The Rust file of the “Meshes of triangles, every corner its own colour” example. One triangle running red to green to blue, a grid of sixty with its edges stroked over the paint, the same grid in the four printing inks, and the mesh once more named as a fill and painted inside a disc.

Rust 407 lines

What this example is for

A gradient runs in a straight line or out from a point, and that covers most of what a page asks for. It does not cover a background whose colour turns a corner, a map shaded from readings taken in twenty places, or a picture of heat where every spot has its own value. The usual way out is to build the picture in a drawing program and drop it in — and then it is a photograph of colour: heavy inside the file, fixed at the size it was made at, and soft at the edges the moment somebody prints the page larger.

What this example shows

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Paints meshes of triangles, each corner carrying its own colour.
//!
//! A mesh states its own shape. Every triangle names three corners, and every
//! corner names a point and a colour; what lies between them is worked out from
//! the three. So one triangle already runs from red to green to blue, and a
//! grid of them carries a gradient a straight axis and a circle cannot.
//!
//! Four panels: a single triangle, a grid of sixty in red, green and blue with
//! its edges traced over it, the same grid in the four printing inks, and the
//! grid once more named as a fill and painted inside a disc.
//!
//! The title, the four labels and the legend are held in `Words`, once per
//! language, and `HQF_PDF_LANG` picks which set is written.
//!
//! Usage: `cargo run --example write_mesh -- tmp/mesh.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_mesh -- tmp/maillage.pdf`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::content::Content;
use hqf_pdf::layout::TextFlow;
use hqf_pdf::shading::TriangleMesh;
use hqf_pdf::{Cmyk, Document, Font, FontHandle, Page, Rgb};

#[path = "shared/out.rs"]
mod out;

#[path = "shared/licence.rs"]
mod licence;

#[path = "shared/language.rs"]
mod language;

#[path = "shared/failure.rs"]
mod failure;

use language::Language;

/// The font the example draws with when none is given on the command line.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The width and height of every panel, in points.
const BOX: (f64, f64) = (210.0, 200.0);

/// How many cells the grid runs across.
const COLUMNS: u32 = 6;

/// How many cells the grid runs up.
const ROWS: u32 = 5;

/// How many triangles the page states: the single one, and two per cell in each
/// of the three grids.
const TRIANGLES: u32 = 1 + 3 * COLUMNS * ROWS * 2;

/// How far the legend runs across, between the margins.
const WIDTH: f64 = 455.0;

/// How far a circle's control points stand from its quarter marks, as a share
/// of the radius: the number that turns four cubics into a circle.
const KAPPA: f64 = 0.552_284_749_830_793_4;

/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// The title across the top of the page.
    title: &'static str,
    /// The label under the single triangle.
    one: &'static str,
    /// The label under the grid in red, green and blue.
    grid: &'static str,
    /// The label under the grid in the four printing inks.
    inks: &'static str,
    /// The label under the disc the mesh fills.
    fill: &'static str,
    /// The paragraph at the foot of the page, saying what is above it.
    legend: &'static str,
}

impl Words {
    /// The words the page is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }
}

/// The page in English.
const ENGLISH: Words = Words {
    title: "Meshes of triangles, corner by corner",
    one: "One triangle, three corners",
    grid: "Sixty triangles, edges traced",
    inks: "The same sixty, in the four inks",
    fill: "The mesh as a fill, inside a disc",
    legend: "Every corner above carries a point and a colour, and the colour \
             between three corners is worked out from those three. The single \
             triangle at the top left holds red, green and blue at its corners \
             and nothing in between; the grid beside it cuts each of its thirty \
             cells in two, and its edges are stroked over the paint so the \
             triangles can be counted. The grid at the bottom left states the \
             same corners in cyan, magenta, yellow and black, which is what a \
             press is given. The last one is the same mesh again, named as a \
             fill rather than painted straight onto the page, so the disc it \
             sits under is what decides where it shows.",
};

/// The page in French.
const FRENCH: Words = Words {
    title: "Maillages de triangles, sommet par sommet",
    one: "Un triangle, trois sommets",
    grid: "Soixante triangles, arêtes tracées",
    inks: "Les mêmes soixante, dans les quatre encres",
    fill: "Le maillage en remplissage, dans un disque",
    legend: "Chaque sommet ci-dessus porte un point et une couleur, et la \
             couleur entre trois sommets se déduit de ces trois-là. Le triangle \
             seul, en haut à gauche, tient le rouge, le vert et le bleu à ses \
             sommets et rien entre eux ; la grille à côté coupe en deux \
             chacune de ses trente cases, et ses arêtes sont tracées par-dessus \
             la couleur pour qu'on puisse compter les triangles. La grille en \
             bas à gauche énonce les mêmes sommets en cyan, magenta, jaune et \
             noir, ce qu'on remet à une presse. La dernière est encore le même \
             maillage, nommé comme remplissage au lieu d'être peint à même la \
             page : c'est alors le disque qui décide où il se montre.",
};

/// Every language the example is written in. A language is added by writing its
/// own set of words and naming it here.
static WORDS: [(Language, &Words); 2] =
    [(Language::English, &ENGLISH), (Language::French, &FRENCH)];

/// A corner of a mesh: where it stands, and the colour the mesh holds there.
type Corner<T> = ((f64, f64), T);

/// A triangle of a mesh, corner by corner.
type Triangle<T> = [Corner<T>; 3];

/// The one triangle of the panel at `(x, y)`: green at its foot on the left,
/// blue at its foot on the right, red at its apex.
fn one_triangle(x: f64, y: f64) -> Triangle<Rgb> {
    [
        ((x + 10.0, y + 15.0), Rgb::new(0.1, 0.7, 0.2)),
        ((x + BOX.0 - 10.0, y + 15.0), Rgb::new(0.1, 0.2, 0.9)),
        ((x + BOX.0 / 2.0, y + BOX.1 - 15.0), Rgb::new(0.9, 0.1, 0.1)),
    ]
}

/// The point and the colour a grid corner carries in red, green and blue, `u`
/// of the way across the panel at `(x, y)` and `v` of the way up it.
#[expect(
    clippy::suboptimal_flops,
    reason = "a fused multiply-add rounds once, and the Python twin rounds twice"
)]
fn rgb_corner(x: f64, y: f64, u: f64, v: f64) -> Corner<Rgb> {
    (
        (x + u * BOX.0, y + v * BOX.1),
        Rgb::new(0.15 + 0.8 * u, 0.15 + 0.8 * v, 0.95 - 0.7 * u - 0.2 * v),
    )
}

/// The point and the colour a grid corner carries in the four printing inks,
/// `u` of the way across the panel at `(x, y)` and `v` of the way up it.
#[expect(
    clippy::suboptimal_flops,
    reason = "a fused multiply-add rounds once, and the Python twin rounds twice"
)]
fn cmyk_corner(x: f64, y: f64, u: f64, v: f64) -> Corner<Cmyk> {
    (
        (x + u * BOX.0, y + v * BOX.1),
        Cmyk::new(
            0.8 * u,
            0.75 * v,
            0.1 + 0.6 * (1.0 - u),
            0.05 + 0.15 * u * v,
        ),
    )
}

/// The triangles of a grid of [`COLUMNS`] by [`ROWS`] cells over the panel at
/// `(x, y)`, each cell cut in two along the diagonal that rises to the right,
/// every corner carrying what `corner` gives it.
fn grid<T>(x: f64, y: f64, corner: fn(f64, f64, f64, f64) -> Corner<T>) -> Vec<Triangle<T>> {
    let mut triangles = Vec::new();
    for row in 0..ROWS {
        let foot = f64::from(row) / f64::from(ROWS);
        let head = f64::from(row + 1) / f64::from(ROWS);
        for column in 0..COLUMNS {
            let left = f64::from(column) / f64::from(COLUMNS);
            let right = f64::from(column + 1) / f64::from(COLUMNS);
            triangles.push([
                corner(x, y, left, foot),
                corner(x, y, right, foot),
                corner(x, y, right, head),
            ]);
            triangles.push([
                corner(x, y, left, foot),
                corner(x, y, right, head),
                corner(x, y, left, head),
            ]);
        }
    }
    triangles
}

/// Strokes the edges of the grid over the panel at `(x, y)`: the lines between
/// the rows, those between the columns, and the diagonal of each cell.
#[expect(
    clippy::suboptimal_flops,
    reason = "a fused multiply-add rounds once, and the Python twin rounds twice"
)]
fn edges(content: &mut Content, x: f64, y: f64) -> Result<(), hqf_pdf::Error> {
    for row in 0..=ROWS {
        let v = f64::from(row) / f64::from(ROWS);
        content.move_to(x, y + v * BOX.1)?;
        content.line_to(x + BOX.0, y + v * BOX.1)?;
    }
    for column in 0..=COLUMNS {
        let u = f64::from(column) / f64::from(COLUMNS);
        content.move_to(x + u * BOX.0, y)?;
        content.line_to(x + u * BOX.0, y + BOX.1)?;
    }
    for row in 0..ROWS {
        let foot = f64::from(row) / f64::from(ROWS);
        let head = f64::from(row + 1) / f64::from(ROWS);
        for column in 0..COLUMNS {
            let left = f64::from(column) / f64::from(COLUMNS);
            let right = f64::from(column + 1) / f64::from(COLUMNS);
            content.move_to(x + left * BOX.0, y + foot * BOX.1)?;
            content.line_to(x + right * BOX.0, y + head * BOX.1)?;
        }
    }
    content.stroke();
    Ok(())
}

/// Fills a disc of radius `radius` about `(cx, cy)` with whatever the fill is
/// set to, out of the four cubics that make a circle.
fn disc(content: &mut Content, cx: f64, cy: f64, radius: f64) -> Result<(), hqf_pdf::Error> {
    let pull = KAPPA * radius;
    content.move_to(cx + radius, cy)?;
    content.curve_to(
        cx + radius,
        cy + pull,
        cx + pull,
        cy + radius,
        cx,
        cy + radius,
    )?;
    content.curve_to(
        cx - pull,
        cy + radius,
        cx - radius,
        cy + pull,
        cx - radius,
        cy,
    )?;
    content.curve_to(
        cx - radius,
        cy - pull,
        cx - pull,
        cy - radius,
        cx,
        cy - radius,
    )?;
    content.curve_to(
        cx + pull,
        cy - radius,
        cx + radius,
        cy - pull,
        cx + radius,
        cy,
    )?;
    content.close_path();
    content.fill();
    Ok(())
}

/// Writes a line of text at `(x, y)`.
fn write(
    content: &mut Content,
    font: &FontHandle,
    x: f64,
    y: f64,
    size: f64,
    text: &str,
) -> Result<(), hqf_pdf::Error> {
    content.begin_text();
    content.set_font(font, size)?;
    content.text_origin(x, y)?;
    content.show_glyphs(&font.glyphs(text));
    content.end_text();
    Ok(())
}

/// Writes the legend in a column the width of the page.
fn legend(
    content: &mut Content,
    font: &FontHandle,
    top: f64,
    color: Rgb,
    text: &str,
) -> Result<(), hqf_pdf::Error> {
    let flow = TextFlow::new(font, 8.5).leading(12.0).color(color);
    let lines = flow.break_lines(text, WIDTH);
    content.begin_text();
    flow.draw(content, &lines, 70.0, top, WIDTH)?;
    content.end_text();
    Ok(())
}

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);

    let mut args = env::args().skip(1);
    // A named file is written as named; the default one carries the language,
    // so the two languages do not overwrite each other in `tmp/`.
    let out = args
        .next()
        .unwrap_or_else(|| language.file_name(&out::default_path("mesh")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);

    // Left column x, right column x, top row foot, bottom row foot.
    let (left, right, top, bottom) = (70.0, 315.0, 560.0, 300.0);

    let single = doc.add_triangle_mesh(&TriangleMesh::rgb([one_triangle(left, top)]))?;
    let colours = doc.add_triangle_mesh(&TriangleMesh::rgb(grid(right, top, rgb_corner)))?;
    let inks = doc.add_triangle_mesh(&TriangleMesh::cmyk(grid(left, bottom, cmyk_corner)))?;
    let fill =
        doc.add_triangle_mesh_pattern(&TriangleMesh::rgb(grid(right, bottom, rgb_corner)))?;

    let mut content = Content::new();
    write(&mut content, &font, left, 790.0, 18.0, words.title)?;

    content.draw_shading(&single);
    write(&mut content, &font, left, top - 15.0, 10.0, words.one)?;

    content.draw_shading(&colours);
    content.save_state();
    content.set_stroke(Rgb::gray(1.0))?;
    content.set_line_width(0.4)?;
    edges(&mut content, right, top)?;
    content.restore_state();
    write(&mut content, &font, right, top - 15.0, 10.0, words.grid)?;

    content.draw_shading(&inks);
    write(&mut content, &font, left, bottom - 15.0, 10.0, words.inks)?;

    content.save_state();
    content.set_fill_pattern(&fill);
    disc(
        &mut content,
        right + BOX.0 / 2.0,
        bottom + BOX.1 / 2.0,
        BOX.1 / 2.0,
    )?;
    content.restore_state();
    write(&mut content, &font, right, bottom - 15.0, 10.0, words.fill)?;

    legend(&mut content, &font, 265.0, Rgb::gray(0.35), words.legend)?;

    let mut page = Page::a4();
    page.content = content.into_bytes();
    doc.add_page(page)?;

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&out).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&out, &bytes)?;
    println!(
        "wrote {out}: {} bytes, {TRIANGLES} triangles in all",
        bytes.len()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{WORDS, language};

    /// The lines two languages are allowed to write the same way. There are
    /// none: every word is a word of the language it is written in.
    const SPARED: [&str; 0] = [];

    #[test]
    fn every_language_draws_the_page_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the page says these in more than one language: {untranslated:?}"
        );
    }
}