Skip to main content

pfx_trace/
text.rs

1use pfx_materials::{Blend, ContentLayer, Material};
2pub use pfx_text::MSDF_SPREAD;
3use pfx_text::{Atlas, AtlasPage, RichGlyphQuad, RichParagraph, msdf_coverage};
4
5use crate::detail::{ContentFace, ContentText, Transform};
6use crate::stage::{ContentKind, Image, Mesh, PlacementContent};
7
8pub const MAX_TEXELS: u64 = 1 << 26;
9pub const MAX_RECORDS: usize = 1 << 24;
10pub const MAX_CELLS: usize = 1 << 18;
11pub const NOMINAL_LIMIT: f32 = 16384.0;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Glyph {
15    pub rect: [f32; 4],
16    pub uv: [f32; 4],
17    pub color: [f32; 4],
18    pub clip: [f32; 4],
19    pub turn: [f32; 3],
20}
21
22impl From<&RichGlyphQuad> for Glyph {
23    fn from(quad: &RichGlyphQuad) -> Self {
24        Self {
25            rect: quad.rect,
26            uv: quad.uv,
27            color: quad.color,
28            clip: quad.clip,
29            turn: quad.turn,
30        }
31    }
32}
33
34pub fn glyphs(paragraph: &RichParagraph) -> Vec<Glyph> {
35    paragraph
36        .quads
37        .iter()
38        .filter(|quad| quad.atlas_page == AtlasPage::Monochrome)
39        .map(Glyph::from)
40        .collect()
41}
42
43pub fn material(lit: bool) -> Material {
44    let layer = if lit {
45        ContentLayer::default()
46    } else {
47        ContentLayer {
48            blend: Blend::Emit,
49            strength: 1.0,
50            ..ContentLayer::default()
51        }
52    };
53    Material {
54        base: [0.0; 3],
55        roughness: 1.0,
56        specular: 0.0,
57        content_layer: layer,
58        ..Material::default()
59    }
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub struct Print {
64    pub width: u32,
65    pub height: u32,
66    pub density: f32,
67    pub rect: [f32; 4],
68    pub texels: Vec<[u8; 4]>,
69}
70
71pub trait Laid {
72    fn rect(&self) -> [f32; 4];
73    fn kind(&self) -> ContentKind<'_>;
74}
75
76impl Laid for Print {
77    fn rect(&self) -> [f32; 4] {
78        self.rect
79    }
80
81    fn kind(&self) -> ContentKind<'_> {
82        ContentKind::Image(self.image())
83    }
84}
85
86impl Print {
87    pub fn image(&self) -> Image<'_> {
88        Image {
89            width: self.width,
90            height: self.height,
91            texels: &self.texels,
92            srgb: true,
93        }
94    }
95
96    pub fn content(&self) -> PlacementContent<'_> {
97        PlacementContent {
98            cutout: true,
99            ..PlacementContent::new(self.image())
100        }
101    }
102
103    pub fn carrier(&self) -> Carrier {
104        Carrier::over(self.rect)
105    }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct Carrier {
110    pub positions: [[f32; 3]; 4],
111    pub normals: [[f32; 3]; 4],
112    pub tangents: [[f32; 4]; 4],
113    pub uvs: [[f32; 2]; 4],
114    pub indices: [u32; 6],
115}
116
117impl Carrier {
118    pub fn over(rect: [f32; 4]) -> Self {
119        let [x, y, w, h] = rect;
120        Self {
121            positions: [
122                [x, y, 0.0],
123                [x + w, y, 0.0],
124                [x + w, y + h, 0.0],
125                [x, y + h, 0.0],
126            ],
127            normals: [[0.0, 0.0, 1.0]; 4],
128            tangents: [[1.0, 0.0, 0.0, 1.0]; 4],
129            uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
130            indices: [0, 1, 2, 0, 2, 3],
131        }
132    }
133
134    pub fn mesh(&self) -> Mesh<'_> {
135        Mesh {
136            positions: &self.positions,
137            normals: &self.normals,
138            tangents: &self.tangents,
139            uvs: &self.uvs,
140            alpha: None,
141            indices: &self.indices,
142        }
143    }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq)]
147struct Grid {
148    scale: [f32; 2],
149    offset: [f32; 2],
150}
151
152impl Grid {
153    fn texel(&self, point: [f32; 2]) -> [f32; 2] {
154        std::array::from_fn(|k| point[k] * self.scale[k] + self.offset[k])
155    }
156
157    fn layout(&self, texel: [f32; 2]) -> [f32; 2] {
158        std::array::from_fn(|k| (texel[k] - self.offset[k]) / self.scale[k])
159    }
160
161    fn valid(&self) -> bool {
162        self.scale
163            .iter()
164            .chain(&self.offset)
165            .all(|value| value.is_finite())
166            && self.scale.iter().all(|&value| value != 0.0)
167    }
168}
169
170fn turned(turn: [f32; 3], point: [f32; 2], angle: f32) -> [f32; 2] {
171    let (sin, cos) = angle.sin_cos();
172    let dx = point[0] - turn[0];
173    let dy = point[1] - turn[1];
174    [turn[0] + cos * dx - sin * dy, turn[1] + sin * dx + cos * dy]
175}
176
177fn corners(glyph: &Glyph) -> [[f32; 2]; 4] {
178    let [x, y, w, h] = glyph.rect;
179    [[x, y], [x + w, y], [x, y + h], [x + w, y + h]].map(|corner| {
180        if glyph.turn[2] == 0.0 {
181            corner
182        } else {
183            turned(glyph.turn, corner, glyph.turn[2])
184        }
185    })
186}
187
188fn checked(atlas: &Atlas, glyphs: &[Glyph]) -> Result<(), String> {
189    if atlas.channels != 1 && atlas.channels != 3 {
190        return Err("text atlas must be a coverage or an MSDF atlas".into());
191    }
192    let level = atlas.levels.first().ok_or("text atlas has no levels")?;
193    if level.width == 0
194        || level.height == 0
195        || level.bytes.len()
196            != level.width as usize * level.height as usize * atlas.channels as usize
197    {
198        return Err("text atlas level has the wrong size".into());
199    }
200    let finite = glyphs.iter().all(|glyph| {
201        glyph
202            .rect
203            .iter()
204            .chain(&glyph.uv)
205            .chain(&glyph.color)
206            .chain(&glyph.turn)
207            .all(|value| value.is_finite())
208            && !glyph.clip.iter().any(|value| value.is_nan())
209    });
210    if !finite {
211        return Err("text glyph is not finite".into());
212    }
213    Ok(())
214}
215
216fn sample(atlas: &Atlas, uv: [f32; 2]) -> [f32; 3] {
217    let level = &atlas.levels[0];
218    let channels = atlas.channels as usize;
219    let size = [level.width as f32, level.height as f32];
220    let x = uv[0] * size[0] - 0.5;
221    let y = uv[1] * size[1] - 0.5;
222    let (x0, y0) = (x.floor(), y.floor());
223    let (tx, ty) = (x - x0, y - y0);
224    let column = |value: f32| (value.max(0.0) as usize).min(level.width as usize - 1);
225    let row = |value: f32| (value.max(0.0) as usize).min(level.height as usize - 1);
226    let at = |px: usize, py: usize| -> [f32; 3] {
227        let base = (py * level.width as usize + px) * channels;
228        std::array::from_fn(|k| f32::from(level.bytes[base + k.min(channels - 1)]) / 255.0)
229    };
230    let corners = [
231        (at(column(x0), row(y0)), (1.0 - tx) * (1.0 - ty)),
232        (at(column(x0 + 1.0), row(y0)), tx * (1.0 - ty)),
233        (at(column(x0), row(y0 + 1.0)), (1.0 - tx) * ty),
234        (at(column(x0 + 1.0), row(y0 + 1.0)), tx * ty),
235    ];
236    let mut out = [0.0; 3];
237    for (texel, weight) in corners {
238        for k in 0..3 {
239            out[k] += texel[k] * weight;
240        }
241    }
242    out
243}
244
245fn paint(
246    atlas: &Atlas,
247    glyphs: &[Glyph],
248    grid: Grid,
249    width: u32,
250    height: u32,
251    target: &mut [[f32; 4]],
252) -> Result<(), String> {
253    checked(atlas, glyphs)?;
254    if !grid.valid() {
255        return Err("text grid is not finite".into());
256    }
257    if target.len() != width as usize * height as usize {
258        return Err("text target has the wrong size".into());
259    }
260    let level = &atlas.levels[0];
261    let size = [level.width as f32, level.height as f32];
262    let msdf = atlas.channels == 3;
263    for glyph in glyphs {
264        let [x, y, w, h] = glyph.rect;
265        if w <= 0.0 || h <= 0.0 || glyph.color[3] <= 0.0 {
266            continue;
267        }
268        let cell = [
269            (glyph.uv[2] - glyph.uv[0]) * size[0],
270            (glyph.uv[3] - glyph.uv[1]) * size[1],
271        ];
272        if cell[0] <= 0.0 || cell[1] <= 0.0 {
273            continue;
274        }
275        let range = MSDF_SPREAD
276            * 0.5
277            * (grid.scale[0].abs() * w / cell[0] + grid.scale[1].abs() * h / cell[1]);
278        let half = [0.5 / size[0], 0.5 / size[1]];
279        let low = [
280            (glyph.uv[0] + half[0]).min(glyph.uv[2]),
281            (glyph.uv[1] + half[1]).min(glyph.uv[3]),
282        ];
283        let high = [
284            (glyph.uv[2] - half[0]).max(low[0]),
285            (glyph.uv[3] - half[1]).max(low[1]),
286        ];
287        let mut least = [f32::MAX; 2];
288        let mut most = [f32::MIN; 2];
289        for corner in corners(glyph) {
290            let texel = grid.texel(corner);
291            for k in 0..2 {
292                least[k] = least[k].min(texel[k]);
293                most[k] = most[k].max(texel[k]);
294            }
295        }
296        let from = [least[0].floor().max(0.0), least[1].floor().max(0.0)];
297        let to = [
298            most[0].ceil().min(width as f32),
299            most[1].ceil().min(height as f32),
300        ];
301        if from[0] >= to[0] || from[1] >= to[1] {
302            continue;
303        }
304        let color = glyph.color;
305        for row in from[1] as u32..to[1] as u32 {
306            for column in from[0] as u32..to[0] as u32 {
307                let centre = grid.layout([column as f32 + 0.5, row as f32 + 0.5]);
308                let point = if glyph.turn[2] == 0.0 {
309                    centre
310                } else {
311                    turned(glyph.turn, centre, -glyph.turn[2])
312                };
313                let fraction = [(point[0] - x) / w, (point[1] - y) / h];
314                if !(0.0..=1.0).contains(&fraction[0]) || !(0.0..=1.0).contains(&fraction[1]) {
315                    continue;
316                }
317                let inside = point[0] >= glyph.clip[0]
318                    && point[0] <= glyph.clip[2]
319                    && point[1] >= glyph.clip[1]
320                    && point[1] <= glyph.clip[3];
321                if !inside {
322                    continue;
323                }
324                let uv = [
325                    (glyph.uv[0] + (glyph.uv[2] - glyph.uv[0]) * fraction[0])
326                        .clamp(low[0], high[0]),
327                    (glyph.uv[1] + (glyph.uv[3] - glyph.uv[1]) * fraction[1])
328                        .clamp(low[1], high[1]),
329                ];
330                let sampled = sample(atlas, uv);
331                let coverage = if msdf {
332                    msdf_coverage(sampled, range)
333                } else {
334                    sampled[0]
335                };
336                let opacity = coverage * color[3];
337                if opacity <= 0.0 {
338                    continue;
339                }
340                let pixel = &mut target[(row * width + column) as usize];
341                let keep = 1.0 - opacity;
342                for k in 0..3 {
343                    pixel[k] = color[k] * opacity + pixel[k] * keep;
344                }
345                pixel[3] = opacity + pixel[3] * keep;
346            }
347        }
348    }
349    Ok(())
350}
351
352fn encode(value: f32) -> u8 {
353    let value = value.clamp(0.0, 1.0);
354    let encoded = pfx_materials::encode_channel(value);
355    (encoded * 255.0).round() as u8
356}
357
358fn decode(byte: u8) -> f32 {
359    pfx_materials::linear_channel(f32::from(byte) / 255.0)
360}
361
362fn bytes(pixel: [f32; 4], srgb: bool) -> [u8; 4] {
363    let linear = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
364    let colour = |value: f32| if srgb { encode(value) } else { linear(value) };
365    [
366        colour(pixel[0]),
367        colour(pixel[1]),
368        colour(pixel[2]),
369        linear(pixel[3]),
370    ]
371}
372
373pub fn print(atlas: &Atlas, glyphs: &[Glyph], density: f32) -> Result<Print, String> {
374    if !density.is_finite() || density <= 0.0 {
375        return Err("text density must be a positive number of texels per unit".into());
376    }
377    checked(atlas, glyphs)?;
378    let mut least = [f32::MAX; 2];
379    let mut most = [f32::MIN; 2];
380    for glyph in glyphs
381        .iter()
382        .filter(|glyph| glyph.rect[2] > 0.0 && glyph.rect[3] > 0.0)
383    {
384        for corner in corners(glyph) {
385            for k in 0..2 {
386                least[k] = least[k].min(corner[k]);
387                most[k] = most[k].max(corner[k]);
388            }
389        }
390    }
391    if least[0] > most[0] {
392        return Err("text has no glyphs to print".into());
393    }
394    let first = least.map(|value| (value * density).floor());
395    let last = most.map(|value| (value * density).ceil());
396    let columns = (last[0] - first[0]).max(1.0);
397    let rows = (last[1] - first[1]).max(1.0);
398    if (columns as u64).saturating_mul(rows as u64) > MAX_TEXELS {
399        return Err("text print is too large at this density".into());
400    }
401    let (width, height) = (columns as u32, rows as u32);
402    let grid = Grid {
403        scale: [density; 2],
404        offset: [-first[0], -first[1]],
405    };
406    let mut pixels = vec![[0.0; 4]; width as usize * height as usize];
407    paint(atlas, glyphs, grid, width, height, &mut pixels)?;
408    Ok(Print {
409        width,
410        height,
411        density,
412        rect: [
413            first[0] / density,
414            first[1] / density,
415            columns / density,
416            rows / density,
417        ],
418        texels: pixels.into_iter().map(|pixel| bytes(pixel, true)).collect(),
419    })
420}
421
422#[derive(Clone, Debug)]
423pub struct Lettering {
424    pub rect: [f32; 4],
425    pub density: f32,
426    pub glyphs: Vec<Glyph>,
427    pub atlas: Atlas,
428}
429
430fn drawn(glyph: &Glyph, size: [f32; 2]) -> bool {
431    glyph.rect[2] > 0.0
432        && glyph.rect[3] > 0.0
433        && glyph.color[3] > 0.0
434        && glyph.uv[2] > glyph.uv[0]
435        && glyph.uv[3] > glyph.uv[1]
436        && size[0] > 0.0
437        && size[1] > 0.0
438}
439
440pub fn lettering(atlas: &Atlas, glyphs: &[Glyph]) -> Result<Lettering, String> {
441    checked(atlas, glyphs)?;
442    let level = &atlas.levels[0];
443    let size = [level.width as f32, level.height as f32];
444    let mut least = [f32::MAX; 2];
445    let mut most = [f32::MIN; 2];
446    let mut density = 0.0f64;
447    let mut counted = 0usize;
448    for glyph in glyphs.iter().filter(|glyph| drawn(glyph, size)) {
449        for corner in corners(glyph) {
450            for k in 0..2 {
451                least[k] = least[k].min(corner[k]);
452                most[k] = most[k].max(corner[k]);
453            }
454        }
455        let cell = [
456            (glyph.uv[2] - glyph.uv[0]) * size[0],
457            (glyph.uv[3] - glyph.uv[1]) * size[1],
458        ];
459        density += 0.5 * f64::from(cell[0] / glyph.rect[2] + cell[1] / glyph.rect[3]);
460        counted += 1;
461    }
462    if counted == 0 || least[0] >= most[0] || least[1] >= most[1] {
463        return Err("text has no glyphs to letter".into());
464    }
465    let density = (density / counted as f64) as f32;
466    if !density.is_finite() || density <= 0.0 {
467        return Err("text glyphs have no atlas density".into());
468    }
469    Ok(Lettering {
470        rect: [least[0], least[1], most[0] - least[0], most[1] - least[1]],
471        density,
472        glyphs: glyphs.to_vec(),
473        atlas: atlas.clone(),
474    })
475}
476
477impl Laid for Lettering {
478    fn rect(&self) -> [f32; 4] {
479        self.rect
480    }
481
482    fn kind(&self) -> ContentKind<'_> {
483        ContentKind::Text(self.shown())
484    }
485}
486
487impl Lettering {
488    pub fn shown(&self) -> TextContent<'_> {
489        let [x, y, w, h] = self.rect;
490        TextContent {
491            lettering: self,
492            scale: [1.0 / w, 1.0 / h],
493            offset: [-x / w, -y / h],
494            picture: None,
495        }
496    }
497
498    pub fn content(&self) -> PlacementContent<'_> {
499        PlacementContent {
500            cutout: true,
501            ..PlacementContent::text(self.shown())
502        }
503    }
504
505    pub fn carrier(&self) -> Carrier {
506        Carrier::over(self.rect)
507    }
508
509    pub fn sampler(&self) -> impl Fn([f32; 2]) -> [f32; 4] + '_ {
510        let shown = self.shown();
511        let sample = shown.sampler();
512        move |layout| {
513            sample(std::array::from_fn(|k| {
514                layout[k] * shown.scale[k] + shown.offset[k]
515            }))
516        }
517    }
518
519    pub fn at(&self, layout: [f32; 2]) -> [f32; 4] {
520        self.sampler()(layout)
521    }
522}
523
524#[derive(Clone, Copy, Debug)]
525pub struct TextContent<'a> {
526    pub lettering: &'a Lettering,
527    pub scale: [f32; 2],
528    pub offset: [f32; 2],
529    pub picture: Option<Image<'a>>,
530}
531
532impl PartialEq for TextContent<'_> {
533    fn eq(&self, other: &Self) -> bool {
534        self.same(other)
535    }
536}
537
538#[derive(Clone, Copy, Debug, PartialEq)]
539struct Placed {
540    map: [f32; 4],
541    shift: [f32; 2],
542    clip: [f32; 4],
543    uv: [f32; 4],
544    color: [f32; 4],
545    reach: [f32; 4],
546}
547
548impl Placed {
549    fn fraction(&self, uv: [f32; 2]) -> Option<[f32; 2]> {
550        let m = self.map;
551        let f = [
552            m[0] * uv[0] + m[1] * uv[1] + self.shift[0],
553            m[2] * uv[0] + m[3] * uv[1] + self.shift[1],
554        ];
555        let inside =
556            (0..2).all(|k| f[k] >= self.clip[k].max(0.0) && f[k] <= self.clip[k + 2].min(1.0));
557        inside.then_some(f)
558    }
559}
560
561fn over(below: [f32; 4], color: [f32; 4], opacity: f32) -> [f32; 4] {
562    let keep = 1.0 - opacity;
563    [
564        color[0] * opacity + below[0] * keep,
565        color[1] * opacity + below[1] * keep,
566        color[2] * opacity + below[2] * keep,
567        opacity + below[3] * keep,
568    ]
569}
570
571fn texel_at(image: &crate::detail::ContentImage, uv: [f32; 2]) -> [f32; 4] {
572    let size = [image.width as f32, image.height as f32];
573    let p = [0, 1].map(|k| (uv[k] * size[k] - 0.5).clamp(0.0, size[k] - 1.0));
574    let lo = p.map(|v| v.floor() as u32);
575    let hi = [
576        (lo[0] + 1).min(image.width - 1),
577        (lo[1] + 1).min(image.height - 1),
578    ];
579    let f = [p[0] - p[0].floor(), p[1] - p[1].floor()];
580    let at = |column: u32, row: u32| image.texels[(row * image.width + column) as usize];
581    let mix = |a: [f32; 4], b: [f32; 4], t: f32| -> [f32; 4] {
582        std::array::from_fn(|k| a[k] + (b[k] - a[k]) * t)
583    };
584    mix(
585        mix(at(lo[0], lo[1]), at(hi[0], lo[1]), f[0]),
586        mix(at(lo[0], hi[1]), at(hi[0], hi[1]), f[0]),
587        f[1],
588    )
589}
590
591fn coverage(atlas: &Atlas, glyph: &Placed, fraction: [f32; 2]) -> f32 {
592    let level = &atlas.levels[0];
593    let half = [0.5 / level.width as f32, 0.5 / level.height as f32];
594    let uv = std::array::from_fn(|k| {
595        let low = (glyph.uv[k] + half[k]).min(glyph.uv[k + 2]);
596        let high = (glyph.uv[k + 2] - half[k]).max(low);
597        (glyph.uv[k] + (glyph.uv[k + 2] - glyph.uv[k]) * fraction[k]).clamp(low, high)
598    });
599    let sampled = sample(atlas, uv);
600    if atlas.channels == 3 {
601        let median = sampled[0]
602            .min(sampled[1])
603            .max(sampled[0].max(sampled[1]).min(sampled[2]));
604        if median >= 0.5 { 1.0 } else { 0.0 }
605    } else {
606        sampled[0]
607    }
608}
609
610fn whole(value: usize) -> f32 {
611    value as f32
612}
613
614impl<'a> TextContent<'a> {
615    pub fn same(&self, other: &TextContent<'_>) -> bool {
616        std::ptr::eq(self.lettering, other.lettering)
617            && self.scale == other.scale
618            && self.offset == other.offset
619            && match (self.picture, other.picture) {
620                (None, None) => true,
621                (Some(a), Some(b)) => a.same(&b),
622                _ => false,
623            }
624    }
625
626    pub fn valid(&self) -> bool {
627        self.scale
628            .iter()
629            .chain(&self.offset)
630            .all(|value| value.is_finite())
631            && self.scale.iter().all(|&value| value != 0.0)
632            && self.picture.is_none_or(|picture| picture.valid())
633            && checked(&self.lettering.atlas, &self.lettering.glyphs).is_ok()
634    }
635
636    fn placed(&self) -> Vec<Placed> {
637        let level = &self.lettering.atlas.levels[0];
638        let size = [level.width as f32, level.height as f32];
639        let scale = self.scale.map(f64::from);
640        let offset = self.offset.map(f64::from);
641        self.lettering
642            .glyphs
643            .iter()
644            .filter(|glyph| drawn(glyph, size))
645            .map(|glyph| {
646                let [x, y, w, h] = glyph.rect.map(f64::from);
647                let angle = f64::from(glyph.turn[2]);
648                let (sin, cos) = (-angle).sin_cos();
649                let rotation = [[cos, -sin], [sin, cos]];
650                let pivot = [f64::from(glyph.turn[0]), f64::from(glyph.turn[1])];
651                let base = [-offset[0] / scale[0], -offset[1] / scale[1]];
652                let moved: [f64; 2] = std::array::from_fn(|row| {
653                    rotation[row][0] * (base[0] - pivot[0])
654                        + rotation[row][1] * (base[1] - pivot[1])
655                        + pivot[row]
656                });
657                let extent = [w, h];
658                let start = [x, y];
659                let map = [
660                    rotation[0][0] / scale[0] / w,
661                    rotation[0][1] / scale[1] / w,
662                    rotation[1][0] / scale[0] / h,
663                    rotation[1][1] / scale[1] / h,
664                ];
665                let shift: [f64; 2] = std::array::from_fn(|k| (moved[k] - start[k]) / extent[k]);
666                let clip = std::array::from_fn(|k| {
667                    let value = (f64::from(glyph.clip[k]) - start[k % 2]) / extent[k % 2];
668                    value.clamp(-1.0, 2.0) as f32
669                });
670                let mut least = [f32::MAX; 2];
671                let mut most = [f32::MIN; 2];
672                for corner in corners(glyph) {
673                    for k in 0..2 {
674                        let uv = corner[k] * self.scale[k] + self.offset[k];
675                        least[k] = least[k].min(uv);
676                        most[k] = most[k].max(uv);
677                    }
678                }
679                Placed {
680                    map: map.map(|value| value as f32),
681                    shift: shift.map(|value| value as f32),
682                    clip,
683                    uv: glyph.uv,
684                    color: glyph.color,
685                    reach: [least[0], least[1], most[0], most[1]],
686                }
687            })
688            .collect()
689    }
690
691    pub fn sampler(self) -> impl Fn([f32; 2]) -> [f32; 4] + 'a {
692        let placed = self.placed();
693        let picture = self.picture.map(|picture| picture.linear());
694        move |uv| {
695            let mut color = picture
696                .as_ref()
697                .map_or([0.0; 4], |image| texel_at(image, uv));
698            for glyph in &placed {
699                if let Some(fraction) = glyph.fraction(uv) {
700                    let opacity = coverage(&self.lettering.atlas, glyph, fraction) * glyph.color[3];
701                    color = over(color, glyph.color, opacity);
702                }
703            }
704            color
705        }
706    }
707
708    pub fn at(&self, uv: [f32; 2]) -> [f32; 4] {
709        self.sampler()(uv)
710    }
711
712    pub fn records(&self) -> Result<ContentText, String> {
713        if !self.valid() {
714            return Err("text content is not finite or has a broken atlas".into());
715        }
716        let placed = self.placed();
717        if placed.is_empty() {
718            return Err("text content has no glyphs to draw".into());
719        }
720        let mut least = [f32::MAX; 2];
721        let mut most = [f32::MIN; 2];
722        let mut mean = [0.0f64; 2];
723        for glyph in &placed {
724            for k in 0..2 {
725                least[k] = least[k].min(glyph.reach[k]);
726                most[k] = most[k].max(glyph.reach[k + 2]);
727                mean[k] += f64::from(glyph.reach[k + 2] - glyph.reach[k]);
728            }
729        }
730        let extent = [most[0] - least[0], most[1] - least[1]];
731        if !extent.iter().all(|value| value.is_finite() && *value > 0.0) {
732            return Err("text content has no extent".into());
733        }
734        let mut counts = [0, 1].map(|k| {
735            let mean = mean[k] / placed.len() as f64;
736            (f64::from(extent[k]) / mean.max(1e-30))
737                .ceil()
738                .clamp(1.0, 1024.0) as usize
739        });
740        let budget = MAX_CELLS.min(64.max(16 * placed.len()));
741        while counts[0] * counts[1] > budget {
742            counts = counts.map(|count| count.div_ceil(2));
743        }
744        let [columns, rows] = counts;
745        let inverse = [columns as f32 / extent[0], rows as f32 / extent[1]];
746        let mut cells: Vec<Vec<u32>> = vec![Vec::new(); columns * rows];
747        for (index, glyph) in placed.iter().enumerate() {
748            let range = |k: usize, value: f32, nudge: f32| {
749                ((value - least[k]) * inverse[k] + nudge)
750                    .floor()
751                    .clamp(0.0, (counts[k] - 1) as f32) as usize
752            };
753            for row in range(1, glyph.reach[1], -1e-3)..=range(1, glyph.reach[3], 1e-3) {
754                for column in range(0, glyph.reach[0], -1e-3)..=range(0, glyph.reach[2], 1e-3) {
755                    cells[row * columns + column].push(index as u32);
756                }
757            }
758        }
759        let atlas = &self.lettering.atlas;
760        let level = &atlas.levels[0];
761        let channels = atlas.channels as usize;
762        let atlas_texels = level.width as usize * level.height as usize;
763        let picture = self.picture.map(|picture| picture.linear());
764        let total: usize = cells.iter().map(Vec::len).sum();
765        let index_base = 4 + cells.len().div_ceil(2);
766        let glyph_base = index_base + total.div_ceil(4);
767        let atlas_base = glyph_base + 5 * placed.len();
768        let picture_base = atlas_base + atlas_texels.div_ceil(4);
769        let length = picture_base + picture.as_ref().map_or(0, |image| image.texels.len());
770        if length > MAX_RECORDS {
771            return Err("text content is too large for the tracer".into());
772        }
773        let mut records = Vec::with_capacity(length);
774        records.push([least[0], least[1], inverse[0], inverse[1]]);
775        records.push([
776            whole(columns),
777            whole(rows),
778            whole(glyph_base),
779            whole(index_base),
780        ]);
781        records.push([
782            whole(level.width as usize),
783            whole(level.height as usize),
784            whole(atlas_base),
785            whole(channels),
786        ]);
787        records.push(match &picture {
788            Some(image) => [
789                whole(image.width as usize),
790                whole(image.height as usize),
791                whole(picture_base),
792                0.0,
793            ],
794            None => [0.0; 4],
795        });
796        let mut start = 0usize;
797        let mut entries = Vec::with_capacity(cells.len() * 2);
798        for cell in &cells {
799            entries.push(whole(start));
800            entries.push(whole(cell.len()));
801            start += cell.len();
802        }
803        let quads = |values: &[f32], records: &mut Vec<[f32; 4]>| {
804            for chunk in values.chunks(4) {
805                records.push(std::array::from_fn(|k| {
806                    chunk.get(k).copied().unwrap_or(0.0)
807                }));
808            }
809        };
810        quads(&entries, &mut records);
811        let indices: Vec<f32> = cells.iter().flatten().map(|&index| index as f32).collect();
812        quads(&indices, &mut records);
813        for glyph in &placed {
814            records.push(glyph.map);
815            records.push([glyph.shift[0], glyph.shift[1], 0.0, 0.0]);
816            records.push(glyph.clip);
817            records.push(glyph.uv);
818            records.push(glyph.color);
819        }
820        let packed: Vec<f32> = level
821            .bytes
822            .chunks_exact(channels)
823            .map(|texel| {
824                let at = |k: usize| u32::from(texel[k.min(channels - 1)]);
825                (at(0) | at(1) << 8 | at(2) << 16) as f32
826            })
827            .collect();
828        quads(&packed, &mut records);
829        if let Some(image) = &picture {
830            records.extend_from_slice(&image.texels);
831        }
832        debug_assert_eq!(records.len(), length);
833        let (width, height) = match &picture {
834            Some(image) => (image.width, image.height),
835            None => {
836                let nominal = |k: usize| {
837                    (self.lettering.density / self.scale[k].abs())
838                        .round()
839                        .clamp(1.0, NOMINAL_LIMIT) as u32
840                };
841                (nominal(0), nominal(1))
842            }
843        };
844        let text = ContentText {
845            width,
846            height,
847            records,
848        };
849        if !text.valid() {
850            return Err("text content records are not finite".into());
851        }
852        Ok(text)
853    }
854}
855
856pub fn overlay(
857    pixels: &mut [[f32; 4]],
858    width: u32,
859    height: u32,
860    atlas: &Atlas,
861    glyphs: &[Glyph],
862) -> Result<(), String> {
863    let grid = Grid {
864        scale: [1.0; 2],
865        offset: [0.0; 2],
866    };
867    paint(atlas, glyphs, grid, width, height, pixels)
868}
869
870#[derive(Clone, Debug, PartialEq)]
871pub struct Fit {
872    pub scale: [f32; 2],
873    pub offset: [f32; 2],
874    pub face: Vec<usize>,
875}
876
877fn affine_inverse(m: Transform) -> Option<[[f32; 4]; 3]> {
878    let a = |row: usize, column: usize| f64::from(m[column][row]);
879    if m.iter().flatten().any(|value| !value.is_finite()) {
880        return None;
881    }
882    let cofactor = |row: usize, column: usize| {
883        let (r0, r1) = ((row + 1) % 3, (row + 2) % 3);
884        let (c0, c1) = ((column + 1) % 3, (column + 2) % 3);
885        a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0)
886    };
887    let determinant: f64 = (0..3)
888        .map(|column| a(0, column) * cofactor(0, column))
889        .sum();
890    if determinant.abs() < 1e-18 {
891        return None;
892    }
893    let inverse: [[f64; 3]; 3] = std::array::from_fn(|row| {
894        std::array::from_fn(|column| cofactor(column, row) / determinant)
895    });
896    let shift = [a(0, 3), a(1, 3), a(2, 3)];
897    Some(std::array::from_fn(|row| {
898        let moved: f64 = (0..3).map(|k| inverse[row][k] * shift[k]).sum();
899        [
900            inverse[row][0] as f32,
901            inverse[row][1] as f32,
902            inverse[row][2] as f32,
903            -moved as f32,
904        ]
905    }))
906}
907
908fn apply(m: Transform, p: [f32; 3]) -> [f32; 3] {
909    std::array::from_fn(|row| m[0][row] * p[0] + m[1][row] * p[1] + m[2][row] * p[2] + m[3][row])
910}
911
912fn solve(rows: [[f64; 4]; 3]) -> Option<[f64; 3]> {
913    let mut m = rows;
914    for pivot in 0..3 {
915        let best = (pivot..3).max_by(|&a, &b| m[a][pivot].abs().total_cmp(&m[b][pivot].abs()))?;
916        if m[best][pivot].abs() < 1e-18 {
917            return None;
918        }
919        m.swap(pivot, best);
920        for row in 0..3 {
921            if row != pivot {
922                let factor = m[row][pivot] / m[pivot][pivot];
923                let lead = m[pivot];
924                for (value, above) in m[row].iter_mut().zip(lead).skip(pivot) {
925                    *value -= factor * above;
926                }
927            }
928        }
929    }
930    Some(std::array::from_fn(|k| m[k][3] / m[k][k]))
931}
932
933pub fn fit(part: &Mesh<'_>, part_model: Transform, text_model: Transform) -> Result<Fit, String> {
934    let back = affine_inverse(text_model).ok_or("text model is not an invertible affine map")?;
935    if affine_inverse(part_model).is_none() {
936        return Err("part model is not an invertible affine map".into());
937    }
938    if part.uvs.len() != part.positions.len()
939        || !part.indices.len().is_multiple_of(3)
940        || part
941            .indices
942            .iter()
943            .any(|&index| index as usize >= part.positions.len())
944    {
945        return Err("part mesh has mismatched UVs or indices".into());
946    }
947    let layout = |index: u32| {
948        let world = apply(part_model, part.positions[index as usize]);
949        std::array::from_fn::<f32, 3, _>(|row| {
950            back[row][0] * world[0]
951                + back[row][1] * world[1]
952                + back[row][2] * world[2]
953                + back[row][3]
954        })
955    };
956    let mut parallel = Vec::new();
957    let mut reach = 0.0f32;
958    for (triangle, face) in part.indices.chunks_exact(3).enumerate() {
959        let [a, b, c] = [layout(face[0]), layout(face[1]), layout(face[2])];
960        let e1: [f32; 3] = std::array::from_fn(|k| b[k] - a[k]);
961        let e2: [f32; 3] = std::array::from_fn(|k| c[k] - a[k]);
962        let normal = [
963            e1[1] * e2[2] - e1[2] * e2[1],
964            e1[2] * e2[0] - e1[0] * e2[2],
965            e1[0] * e2[1] - e1[1] * e2[0],
966        ];
967        let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt();
968        if length <= 0.0 || normal[2].abs() < 0.999 * length {
969            continue;
970        }
971        for p in [a, b, c] {
972            reach = reach.max(p[0].abs()).max(p[1].abs());
973        }
974        parallel.push((triangle, (a[2] + b[2] + c[2]).abs() / 3.0));
975    }
976    let nearest = parallel
977        .iter()
978        .map(|&(_, distance)| distance)
979        .fold(f32::INFINITY, f32::min);
980    if !nearest.is_finite() {
981        return Err("part has no face parallel to the text".into());
982    }
983    let tolerance = nearest + 1e-4 * reach.max(1e-6);
984    let face: Vec<usize> = parallel
985        .into_iter()
986        .filter(|&(_, distance)| distance <= tolerance)
987        .map(|(triangle, _)| triangle)
988        .collect();
989    let mut sums = [[0.0f64; 4]; 3];
990    let mut targets = [[0.0f64; 3]; 2];
991    let mut points = Vec::new();
992    for &triangle in &face {
993        for &index in &part.indices[triangle * 3..triangle * 3 + 3] {
994            let p = layout(index);
995            let row = [f64::from(p[0]), f64::from(p[1]), 1.0];
996            let uv = part.uvs[index as usize].map(f64::from);
997            for i in 0..3 {
998                for j in 0..3 {
999                    sums[i][j] += row[i] * row[j];
1000                }
1001                for k in 0..2 {
1002                    targets[k][i] += row[i] * uv[k];
1003                }
1004            }
1005            points.push((row, uv));
1006        }
1007    }
1008    let mut maps = [[0.0f64; 3]; 2];
1009    for k in 0..2 {
1010        let rows: [[f64; 4]; 3] =
1011            std::array::from_fn(|i| [sums[i][0], sums[i][1], sums[i][2], targets[k][i]]);
1012        maps[k] = solve(rows).ok_or("part face's UVs do not span it")?;
1013    }
1014    let spread = points
1015        .iter()
1016        .flat_map(|(_, uv)| uv.iter().map(|value| value.abs()))
1017        .fold(1e-6, f64::max);
1018    for (row, uv) in &points {
1019        for k in 0..2 {
1020            let predicted: f64 = (0..3).map(|i| maps[k][i] * row[i]).sum();
1021            if (predicted - uv[k]).abs() > 1e-3 * spread {
1022                return Err("part face's UVs are not a flat map of it".into());
1023            }
1024        }
1025    }
1026    let [u, v] = maps;
1027    if u[0] == 0.0
1028        || v[1] == 0.0
1029        || u[1].abs() > 1e-3 * u[0].abs()
1030        || v[0].abs() > 1e-3 * v[1].abs()
1031    {
1032        return Err(
1033            "part face's UVs turn against the text; only UVs along its axes are fitted".into(),
1034        );
1035    }
1036    Ok(Fit {
1037        scale: [u[0] as f32, v[1] as f32],
1038        offset: [u[2] as f32, v[2] as f32],
1039        face,
1040    })
1041}
1042
1043impl Fit {
1044    pub fn content<'a, L: Laid>(&self, laid: &'a L) -> PlacementContent<'a> {
1045        let [x, y, w, h] = laid.rect();
1046        PlacementContent {
1047            uv_offset: [
1048                -(self.offset[0] / self.scale[0] + x) / w,
1049                -(self.offset[1] / self.scale[1] + y) / h,
1050            ],
1051            uv_scale: [1.0 / (self.scale[0] * w), 1.0 / (self.scale[1] * h)],
1052            crop: [0.0, 0.0, 1.0, 1.0],
1053            face: ContentFace::Both,
1054            cutout: false,
1055            kind: laid.kind(),
1056        }
1057    }
1058
1059    pub fn over<'a>(
1060        &self,
1061        picture: &PlacementContent<'a>,
1062        lettering: &'a Lettering,
1063    ) -> Result<PlacementContent<'a>, String> {
1064        let ContentKind::Image(image) = picture.kind else {
1065            return Err("text goes over a picture, not over other text".into());
1066        };
1067        if !image.valid() {
1068            return Err("picture has the wrong texel count".into());
1069        }
1070        let text = TextContent {
1071            lettering,
1072            scale: std::array::from_fn(|k| self.scale[k] * picture.uv_scale[k]),
1073            offset: std::array::from_fn(|k| {
1074                self.offset[k] * picture.uv_scale[k] + picture.uv_offset[k]
1075            }),
1076            picture: Some(image),
1077        };
1078        if !text.valid() {
1079            return Err("text over the picture is not finite".into());
1080        }
1081        Ok(PlacementContent {
1082            kind: ContentKind::Text(text),
1083            ..*picture
1084        })
1085    }
1086
1087    pub fn elsewhere(&self, part: &Mesh<'_>, content: &PlacementContent<'_>) -> Vec<usize> {
1088        let shown = |uv: [f32; 2]| -> [f32; 2] {
1089            std::array::from_fn(|k| uv[k] * content.uv_scale[k] + content.uv_offset[k])
1090        };
1091        part.indices
1092            .chunks_exact(3)
1093            .enumerate()
1094            .filter(|(triangle, _)| !self.face.contains(triangle))
1095            .filter(|(_, face)| {
1096                let at: Vec<[f32; 2]> = face
1097                    .iter()
1098                    .map(|&index| {
1099                        shown(
1100                            part.uvs
1101                                .get(index as usize)
1102                                .copied()
1103                                .unwrap_or([f32::NAN; 2]),
1104                        )
1105                    })
1106                    .collect();
1107                let low = [0, 1].map(|k| at.iter().map(|p| p[k]).fold(f32::INFINITY, f32::min));
1108                let high =
1109                    [0, 1].map(|k| at.iter().map(|p| p[k]).fold(f32::NEG_INFINITY, f32::max));
1110                low[0] < content.crop[2]
1111                    && high[0] > content.crop[0]
1112                    && low[1] < content.crop[3]
1113                    && high[1] > content.crop[1]
1114            })
1115            .map(|(triangle, _)| triangle)
1116            .collect()
1117    }
1118
1119    pub fn onto(
1120        &self,
1121        picture: &PlacementContent<'_>,
1122        atlas: &Atlas,
1123        glyphs: &[Glyph],
1124    ) -> Result<Vec<[u8; 4]>, String> {
1125        let ContentKind::Image(image) = &picture.kind else {
1126            return Err("text prints onto a picture, not onto other text".into());
1127        };
1128        if !image.valid() {
1129            return Err("picture has the wrong texel count".into());
1130        }
1131        let size = [image.width as f32, image.height as f32];
1132        let grid = Grid {
1133            scale: std::array::from_fn(|k| self.scale[k] * picture.uv_scale[k] * size[k]),
1134            offset: std::array::from_fn(|k| {
1135                (self.offset[k] * picture.uv_scale[k] + picture.uv_offset[k]) * size[k]
1136            }),
1137        };
1138        let unpack = |byte: u8| {
1139            if image.srgb {
1140                decode(byte)
1141            } else {
1142                f32::from(byte) / 255.0
1143            }
1144        };
1145        let mut pixels: Vec<[f32; 4]> = image
1146            .texels
1147            .iter()
1148            .map(|&[r, g, b, a]| [unpack(r), unpack(g), unpack(b), f32::from(a) / 255.0])
1149            .collect();
1150        paint(atlas, glyphs, grid, image.width, image.height, &mut pixels)?;
1151        Ok(pixels
1152            .into_iter()
1153            .map(|pixel| bytes(pixel, image.srgb))
1154            .collect())
1155    }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160    use super::*;
1161    use pfx_text::{Anchor, Face, Representation, Span, TextEngine};
1162
1163    const FONT: &[u8] = pfx_text::fixture::EB_GARAMOND;
1164    const UNITS_PER_METRE: f32 = 8192.0;
1165    const OPEN: [f32; 4] = [-1e9, -1e9, 1e9, 1e9];
1166    const COLOR: [f32; 4] = [1.0, 0.5, 0.25, 1.0];
1167
1168    fn laid(text: &str, size: f32, scale: f32, at: [f32; 2]) -> (Atlas, Vec<Glyph>) {
1169        let representation = if scale == 1.0 {
1170            Representation::Coverage
1171        } else {
1172            Representation::Msdf
1173        };
1174        let mut engine = TextEngine::new(FONT).unwrap();
1175        let spans = [Span {
1176            text: text.into(),
1177            face: Face {
1178                family: "EB Garamond".into(),
1179                size,
1180                line: size * 1.25,
1181                weight: 400,
1182                italic: false,
1183                spacing: 0.0,
1184            },
1185            color: COLOR,
1186        }];
1187        let rich = engine
1188            .render_spans(
1189                &spans,
1190                None,
1191                scale,
1192                Anchor::Start,
1193                representation,
1194                at,
1195                1.0,
1196                OPEN,
1197                [0.0; 3],
1198            )
1199            .unwrap();
1200        let glyphs = glyphs(&rich);
1201        (rich.atlas, glyphs)
1202    }
1203
1204    struct Cube {
1205        positions: Vec<[f32; 3]>,
1206        normals: Vec<[f32; 3]>,
1207        tangents: Vec<[f32; 4]>,
1208        uvs: Vec<[f32; 2]>,
1209        indices: Vec<u32>,
1210    }
1211
1212    impl Cube {
1213        fn new() -> Self {
1214            let mut cube = Self {
1215                positions: Vec::new(),
1216                normals: Vec::new(),
1217                tangents: Vec::new(),
1218                uvs: Vec::new(),
1219                indices: Vec::new(),
1220            };
1221            for axis in 0..3 {
1222                for sign in [-1.0_f32, 1.0] {
1223                    let mut normal = [0.0; 3];
1224                    normal[axis] = sign;
1225                    let start = cube.positions.len() as u32;
1226                    for (a, b) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
1227                        let mut p = normal;
1228                        p[(axis + 1) % 3] = a;
1229                        p[(axis + 2) % 3] = b;
1230                        cube.positions.push(p.map(|v| v * 0.5));
1231                        cube.normals.push(normal);
1232                        cube.tangents.push([1.0, 0.0, 0.0, 1.0]);
1233                        cube.uvs.push([(a + 1.0) * 0.5, (b + 1.0) * 0.5]);
1234                    }
1235                    let quad = if sign > 0.0 {
1236                        [0, 1, 2, 0, 2, 3]
1237                    } else {
1238                        [0, 2, 1, 0, 3, 2]
1239                    };
1240                    cube.indices.extend(quad.map(|k| start + k));
1241                }
1242            }
1243            cube
1244        }
1245
1246        fn mesh(&self) -> Mesh<'_> {
1247            Mesh {
1248                positions: &self.positions,
1249                normals: &self.normals,
1250                tangents: &self.tangents,
1251                uvs: &self.uvs,
1252                alpha: None,
1253                indices: &self.indices,
1254            }
1255        }
1256    }
1257
1258    const IDENTITY: Transform = [
1259        [1.0, 0.0, 0.0, 0.0],
1260        [0.0, 1.0, 0.0, 0.0],
1261        [0.0, 0.0, 1.0, 0.0],
1262        [0.0, 0.0, 0.0, 1.0],
1263    ];
1264    const ON_FRONT: Transform = [
1265        [1.0, 0.0, 0.0, 0.0],
1266        [0.0, -1.0, 0.0, 0.0],
1267        [0.0, 0.0, 1.0, 0.0],
1268        [0.0, 0.0, 0.5005, 1.0],
1269    ];
1270
1271    #[test]
1272    fn a_print_covers_every_glyph_on_whole_texels_with_premultiplied_srgb_colour() {
1273        let (atlas, glyphs) = laid("Hi", 0.1, UNITS_PER_METRE, [0.25, -0.5]);
1274        let density = 2000.0;
1275        let print = print(&atlas, &glyphs, density).unwrap();
1276        assert!(print.image().valid());
1277        let [x, y, w, h] = print.rect;
1278        assert_eq!(print.width as f32, (w * density).round());
1279        assert_eq!(print.height as f32, (h * density).round());
1280        assert_eq!((x * density).fract(), 0.0);
1281        for glyph in &glyphs {
1282            assert!(glyph.rect[0] >= x && glyph.rect[1] >= y);
1283            assert!(glyph.rect[0] + glyph.rect[2] <= x + w + 1e-6);
1284            assert!(glyph.rect[1] + glyph.rect[3] <= y + h + 1e-6);
1285        }
1286        let at = |column: u32, row: u32| print.texels[(row * print.width + column) as usize];
1287        for column in 0..print.width {
1288            assert_eq!(at(column, 0)[3], 0);
1289            assert_eq!(at(column, print.height - 1)[3], 0);
1290        }
1291        let solid: Vec<[u8; 4]> = print
1292            .texels
1293            .iter()
1294            .copied()
1295            .filter(|texel| texel[3] == 255)
1296            .collect();
1297        assert!(solid.len() > 100, "{} solid texels", solid.len());
1298        let want = [encode(1.0), encode(0.5), encode(0.25), 255];
1299        assert!(
1300            solid
1301                .iter()
1302                .all(|texel| texel.iter().zip(want).all(|(a, b)| a.abs_diff(b) <= 1)),
1303            "{:?}",
1304            solid[0]
1305        );
1306        assert!(solid.contains(&want));
1307        let partial = print
1308            .texels
1309            .iter()
1310            .find(|t| (64..192).contains(&t[3]))
1311            .unwrap();
1312        let alpha = f32::from(partial[3]) / 255.0;
1313        assert!(
1314            (decode(partial[1]) - 0.5 * alpha).abs() < 0.01,
1315            "{partial:?}"
1316        );
1317
1318        let content = print.content();
1319        assert!(content.cutout);
1320        assert_eq!(content.kind, ContentKind::Image(print.image()));
1321        assert_eq!(content.crop, [0.0, 0.0, 1.0, 1.0]);
1322        let carrier = print.carrier();
1323        assert_eq!(carrier.positions[0], [x, y, 0.0]);
1324        assert_eq!(carrier.positions[2], [x + w, y + h, 0.0]);
1325        assert_eq!(carrier.uvs[2], [1.0, 1.0]);
1326        let mesh = carrier.mesh();
1327        assert_eq!(mesh.indices.len(), 6);
1328    }
1329
1330    #[test]
1331    fn msdf_edges_stay_about_a_texel_wide_at_any_density() {
1332        let (atlas, glyphs) = laid("l", 0.2, UNITS_PER_METRE, [0.0; 2]);
1333        for density in [400.0, 1500.0, 6000.0] {
1334            let print = print(&atlas, &glyphs, density).unwrap();
1335            let row = print.height / 2;
1336            let line =
1337                &print.texels[(row * print.width) as usize..((row + 1) * print.width) as usize];
1338            let partial = line.iter().filter(|t| t[3] > 8 && t[3] < 247).count();
1339            let solid = line.iter().filter(|t| t[3] >= 247).count();
1340            assert!(solid > 0, "{density}: no solid texel");
1341            assert!(
1342                partial <= 4,
1343                "{density}: {partial} partial texels across a stem"
1344            );
1345        }
1346    }
1347
1348    #[test]
1349    fn a_fit_maps_a_box_face_and_names_the_faces_sharing_its_uvs() {
1350        let cube = Cube::new();
1351        let part = cube.mesh();
1352        let fit = fit(&part, IDENTITY, ON_FRONT).unwrap();
1353        assert_eq!(fit.face, [6 * 2 - 2, 6 * 2 - 1]);
1354        assert!((fit.scale[0] - 1.0).abs() < 1e-5 && (fit.scale[1] + 1.0).abs() < 1e-5);
1355        assert!((fit.offset[0] - 0.5).abs() < 1e-5 && (fit.offset[1] - 0.5).abs() < 1e-5);
1356
1357        let (atlas, glyphs) = laid("on", 0.08, UNITS_PER_METRE, [-0.1, -0.05]);
1358        let print = print(&atlas, &glyphs, 1000.0).unwrap();
1359        let content = fit.content(&print);
1360        assert!(!content.cutout);
1361        let [x, y, w, h] = print.rect;
1362        for layout in [[x, y], [x + w, y + h], [x + 0.3 * w, y + 0.6 * h]] {
1363            let world = [layout[0], -layout[1]];
1364            let uv = [world[0] + 0.5, world[1] + 0.5];
1365            let shown: [f32; 2] =
1366                std::array::from_fn(|k| uv[k] * content.uv_scale[k] + content.uv_offset[k]);
1367            let want = [(layout[0] - x) / w, (layout[1] - y) / h];
1368            for k in 0..2 {
1369                assert!(
1370                    (shown[k] - want[k]).abs() < 1e-4,
1371                    "{shown:?} against {want:?}"
1372                );
1373            }
1374        }
1375        let shared = fit.elsewhere(&part, &content);
1376        assert_eq!(shared.len(), 10);
1377        assert!(shared.contains(&4) && shared.contains(&5));
1378    }
1379
1380    #[test]
1381    fn a_fit_refuses_uvs_turned_against_the_text_and_parts_without_a_face() {
1382        let cube = Cube::new();
1383        let part = cube.mesh();
1384        let quarter = [
1385            [0.0, 1.0, 0.0, 0.0],
1386            [1.0, 0.0, 0.0, 0.0],
1387            [0.0, 0.0, 1.0, 0.0],
1388            [0.0, 0.0, 0.5005, 1.0],
1389        ];
1390        assert!(fit(&part, IDENTITY, quarter).is_err());
1391        let slanted = [
1392            [0.8, 0.0, 0.6, 0.0],
1393            [0.0, -1.0, 0.0, 0.0],
1394            [-0.6, 0.0, 0.8, 0.0],
1395            [0.0, 0.0, 0.5005, 1.0],
1396        ];
1397        assert!(fit(&part, IDENTITY, slanted).is_err());
1398        let flat = [
1399            [0.0; 4],
1400            [0.0, 1.0, 0.0, 0.0],
1401            [0.0, 0.0, 1.0, 0.0],
1402            [0.0, 0.0, 0.0, 1.0],
1403        ];
1404        assert!(fit(&part, IDENTITY, flat).is_err());
1405    }
1406
1407    #[test]
1408    fn text_composes_onto_a_picture_where_the_fit_puts_it() {
1409        let cube = Cube::new();
1410        let part = cube.mesh();
1411        let fit = fit(&part, IDENTITY, ON_FRONT).unwrap();
1412        let (atlas, glyphs) = laid("ab", 0.1, UNITS_PER_METRE, [-0.1, -0.05]);
1413        let grey = vec![[128, 128, 128, 255]; 256 * 256];
1414        let picture = PlacementContent::new(Image {
1415            width: 256,
1416            height: 256,
1417            texels: &grey,
1418            srgb: true,
1419        });
1420        let composed = fit.onto(&picture, &atlas, &glyphs).unwrap();
1421        assert_eq!(composed.len(), grey.len());
1422        let print = print(&atlas, &glyphs, 256.0).unwrap();
1423        let [x, y, w, h] = print.rect;
1424        let box_texels = |lx: f32, ly: f32| [(lx + 0.5) * 256.0, (-ly + 0.5) * 256.0];
1425        let [left, bottom] = box_texels(x, y);
1426        let [right, top] = box_texels(x + w, y + h);
1427        let mut changed = 0;
1428        for (index, texel) in composed.iter().enumerate() {
1429            if *texel == grey[index] {
1430                continue;
1431            }
1432            changed += 1;
1433            let column = (index % 256) as f32 + 0.5;
1434            let row = (index / 256) as f32 + 0.5;
1435            assert!(
1436                column >= left && column <= right && row >= top && row <= bottom,
1437                "{column}, {row} outside {left}..{right}, {top}..{bottom}"
1438            );
1439            assert_eq!(texel[3], 255);
1440        }
1441        assert!(changed > 50, "{changed} texels changed");
1442        let inked = composed
1443            .iter()
1444            .filter(|t| t[0] == encode(1.0) && t[1] == encode(0.5) && t[2] == encode(0.25))
1445            .count();
1446        assert!(inked > 10, "{inked} texels carry the ink");
1447    }
1448
1449    #[test]
1450    fn overlay_text_blends_premultiplied_over_the_frame_in_pixels() {
1451        let (atlas, glyphs) = laid("Ink", 32.0, 1.0, [10.0, 20.0]);
1452        assert_eq!(atlas.channels, 1);
1453        let (width, height) = (96, 64);
1454        let background = [0.2, 0.3, 0.4, 1.0];
1455        let mut pixels = vec![background; (width * height) as usize];
1456        overlay(&mut pixels, width, height, &atlas, &glyphs).unwrap();
1457        let solid = pixels.iter().filter(|p| **p == COLOR).count();
1458        let mixed = pixels
1459            .iter()
1460            .filter(|p| **p != COLOR && **p != background)
1461            .count();
1462        assert!(solid > 40 && mixed > 40, "{solid} solid, {mixed} mixed");
1463        for pixel in &pixels {
1464            let t = (pixel[0] - background[0]) / (COLOR[0] - background[0]);
1465            for k in 1..3 {
1466                let expected = background[k] + (COLOR[k] - background[k]) * t;
1467                assert!((pixel[k] - expected).abs() < 1e-5, "{pixel:?}");
1468            }
1469        }
1470        let rows_touched = (0..height)
1471            .filter(|row| {
1472                pixels[(row * width) as usize..((row + 1) * width) as usize]
1473                    .iter()
1474                    .any(|p| *p != background)
1475            })
1476            .collect::<Vec<_>>();
1477        assert!(rows_touched[0] >= 20 && *rows_touched.last().unwrap() < 20 + 40);
1478    }
1479
1480    #[test]
1481    fn broken_input_is_refused() {
1482        let (atlas, glyphs) = laid("x", 0.1, UNITS_PER_METRE, [0.0; 2]);
1483        assert!(print(&atlas, &glyphs, 0.0).is_err());
1484        assert!(print(&atlas, &glyphs, f32::NAN).is_err());
1485        assert!(print(&atlas, &[], 100.0).is_err());
1486        let four = Atlas {
1487            channels: 4,
1488            ..atlas.clone()
1489        };
1490        assert!(print(&four, &glyphs, 100.0).is_err());
1491        let bad = Glyph {
1492            rect: [f32::NAN, 0.0, 1.0, 1.0],
1493            ..glyphs[0]
1494        };
1495        assert!(print(&atlas, &[bad], 100.0).is_err());
1496        assert!(print(&atlas, &glyphs, 1e9).is_err());
1497        let mut short = vec![[0.0; 4]; 3];
1498        assert!(overlay(&mut short, 2, 2, &atlas, &glyphs).is_err());
1499    }
1500
1501    fn evaluate(records: &[[f32; 4]], uv: [f32; 2]) -> [f32; 4] {
1502        let grid = records[0];
1503        let shape = records[1].map(|value| value as usize);
1504        let atlas = records[2].map(|value| value as usize);
1505        let picture = records[3].map(|value| value as usize);
1506        let mut color = [0.0; 4];
1507        if picture[0] > 0 {
1508            let image = crate::detail::ContentImage {
1509                width: picture[0] as u32,
1510                height: picture[1] as u32,
1511                texels: records[picture[2]..picture[2] + picture[0] * picture[1]].to_vec(),
1512            };
1513            color = texel_at(&image, uv);
1514        }
1515        let cell = [
1516            ((uv[0] - grid[0]) * grid[2]).floor(),
1517            ((uv[1] - grid[1]) * grid[3]).floor(),
1518        ];
1519        if cell[0] < 0.0
1520            || cell[1] < 0.0
1521            || cell[0] >= shape[0] as f32
1522            || cell[1] >= shape[1] as f32
1523        {
1524            return color;
1525        }
1526        let index = cell[1] as usize * shape[0] + cell[0] as usize;
1527        let pair = records[4 + index / 2];
1528        let (start, count) = if index % 2 == 1 {
1529            (pair[2] as usize, pair[3] as usize)
1530        } else {
1531            (pair[0] as usize, pair[1] as usize)
1532        };
1533        let texel = |x: usize, y: usize| -> [f32; 3] {
1534            let at = y * atlas[0] + x;
1535            let packed = records[atlas[2] + at / 4][at % 4] as u32;
1536            [packed & 255, (packed >> 8) & 255, packed >> 16].map(|v| v as f32 / 255.0)
1537        };
1538        for at in start..start + count {
1539            let glyph = shape[2] + records[shape[3] + at / 4][at % 4] as usize * 5;
1540            let placed = Placed {
1541                map: records[glyph],
1542                shift: [records[glyph + 1][0], records[glyph + 1][1]],
1543                clip: records[glyph + 2],
1544                uv: records[glyph + 3],
1545                color: records[glyph + 4],
1546                reach: [0.0; 4],
1547            };
1548            let Some(f) = placed.fraction(uv) else {
1549                continue;
1550            };
1551            let size = [atlas[0] as f32, atlas[1] as f32];
1552            let a: [f32; 2] = std::array::from_fn(|k| {
1553                let half = 0.5 / size[k];
1554                let low = (placed.uv[k] + half).min(placed.uv[k + 2]);
1555                let high = (placed.uv[k + 2] - half).max(low);
1556                (placed.uv[k] + (placed.uv[k + 2] - placed.uv[k]) * f[k]).clamp(low, high)
1557            });
1558            let p = [a[0] * size[0] - 0.5, a[1] * size[1] - 0.5];
1559            let start = p.map(f32::floor);
1560            let t = [p[0] - start[0], p[1] - start[1]];
1561            let clamp = |value: f32, k: usize| value.clamp(0.0, size[k] - 1.0) as usize;
1562            let (x0, y0) = (clamp(start[0], 0), clamp(start[1], 1));
1563            let (x1, y1) = (clamp(start[0] + 1.0, 0), clamp(start[1] + 1.0, 1));
1564            let mix = |a: [f32; 3], b: [f32; 3], t: f32| -> [f32; 3] {
1565                std::array::from_fn(|k| a[k] + (b[k] - a[k]) * t)
1566            };
1567            let sampled = mix(
1568                mix(texel(x0, y0), texel(x1, y0), t[0]),
1569                mix(texel(x0, y1), texel(x1, y1), t[0]),
1570                t[1],
1571            );
1572            let coverage = if atlas[3] == 3 {
1573                let median = sampled[0]
1574                    .min(sampled[1])
1575                    .max(sampled[0].max(sampled[1]).min(sampled[2]));
1576                if median >= 0.5 { 1.0 } else { 0.0 }
1577            } else {
1578                sampled[0]
1579            };
1580            color = over(color, placed.color, coverage * placed.color[3]);
1581        }
1582        color
1583    }
1584
1585    fn agree(text: &TextContent<'_>, from: [f32; 2], to: [f32; 2], steps: usize) -> usize {
1586        let records = text.records().unwrap().records;
1587        let reference = text.sampler();
1588        let mut inked = 0;
1589        for row in 0..steps {
1590            for column in 0..steps {
1591                let uv = [
1592                    from[0] + (to[0] - from[0]) * (column as f32 + 0.37) / steps as f32,
1593                    from[1] + (to[1] - from[1]) * (row as f32 + 0.61) / steps as f32,
1594                ];
1595                let ours = evaluate(&records, uv);
1596                let theirs = reference(uv);
1597                for k in 0..4 {
1598                    assert!(
1599                        (ours[k] - theirs[k]).abs() < 1e-5,
1600                        "{uv:?}: {ours:?} against {theirs:?}"
1601                    );
1602                }
1603                if ours[3] > 0.0 {
1604                    inked += 1;
1605                }
1606            }
1607        }
1608        inked
1609    }
1610
1611    #[test]
1612    fn lettering_records_find_every_glyph_through_the_grid() {
1613        let (atlas, mut glyphs) = laid("Lettering at any zoom", 0.05, UNITS_PER_METRE, [0.1, 0.2]);
1614        let pivot = glyphs[3].rect;
1615        glyphs[3].turn = [pivot[0] + pivot[2] * 0.5, pivot[1] + pivot[3] * 0.5, 0.4];
1616        glyphs[5].clip = [-1e9, -1e9, glyphs[5].rect[0] + glyphs[5].rect[2] * 0.5, 1e9];
1617        glyphs[7].color = [0.2, 0.9, 0.4, 0.5];
1618        let lettering = lettering(&atlas, &glyphs).unwrap();
1619        let shown = lettering.shown();
1620        let records = shown.records().unwrap();
1621        let sample = lettering.sampler();
1622        assert_eq!(records.records[3], [0.0; 4]);
1623        assert_eq!(
1624            records.width as f32,
1625            (lettering.rect[2] * lettering.density).round()
1626        );
1627        let inked = agree(&shown, [-0.02, -0.03], [1.02, 1.03], 300);
1628        assert!(inked > 3000, "{inked} inked samples");
1629        let values: Vec<[f32; 4]> = (0..2000)
1630            .map(|k| {
1631                let t = k as f32 / 2000.0;
1632                sample([
1633                    lettering.rect[0] + lettering.rect[2] * t,
1634                    lettering.rect[1] + lettering.rect[3] * 0.6,
1635                ])
1636            })
1637            .collect();
1638        assert!(
1639            values.iter().all(|v| [0.0, 0.5, 0.75, 1.0]
1640                .iter()
1641                .any(|a| (v[3] - a).abs() < 1e-6)),
1642            "binary coverage"
1643        );
1644    }
1645
1646    #[test]
1647    fn lettering_matches_a_fine_print_inside_the_glyphs() {
1648        let (atlas, glyphs) = laid("Bold", 0.2, UNITS_PER_METRE, [0.0; 2]);
1649        let lettering = lettering(&atlas, &glyphs).unwrap();
1650        let print = print(&atlas, &glyphs, 8000.0).unwrap();
1651        let sample = lettering.sampler();
1652        let [x, y, w, h] = print.rect;
1653        let (mut both, mut either) = (0, 0);
1654        for row in 0..print.height {
1655            for column in 0..print.width {
1656                let printed = print.texels[(row * print.width + column) as usize][3] >= 128;
1657                let point = [
1658                    x + w * (column as f32 + 0.5) / print.width as f32,
1659                    y + h * (row as f32 + 0.5) / print.height as f32,
1660                ];
1661                let lettered = sample(point)[3] >= 0.5;
1662                both += usize::from(printed && lettered);
1663                either += usize::from(printed || lettered);
1664            }
1665        }
1666        let overlap = both as f64 / either as f64;
1667        assert!(both > 10_000, "{both} texels inside");
1668        assert!(overlap > 0.995, "IoU {overlap:.4}");
1669        let content = lettering.content();
1670        assert!(content.cutout);
1671        assert!(matches!(content.kind, ContentKind::Text(_)));
1672        assert_eq!(
1673            lettering.carrier().positions[0],
1674            [lettering.rect[0], lettering.rect[1], 0.0]
1675        );
1676    }
1677
1678    #[test]
1679    fn text_over_a_picture_lands_where_the_print_onto_it_does() {
1680        let cube = Cube::new();
1681        let part = cube.mesh();
1682        let fit = fit(&part, IDENTITY, ON_FRONT).unwrap();
1683        let (atlas, glyphs) = laid("ab", 0.3, UNITS_PER_METRE, [-0.3, -0.15]);
1684        let grey = vec![[128, 128, 128, 255]; 128 * 128];
1685        let picture = PlacementContent {
1686            uv_offset: [0.1, -0.05],
1687            uv_scale: [0.9, 1.1],
1688            ..PlacementContent::new(Image {
1689                width: 128,
1690                height: 128,
1691                texels: &grey,
1692                srgb: true,
1693            })
1694        };
1695        let lettering = lettering(&atlas, &glyphs).unwrap();
1696        let content = fit.over(&picture, &lettering).unwrap();
1697        assert_eq!(content.uv_offset, picture.uv_offset);
1698        assert_eq!(content.uv_scale, picture.uv_scale);
1699        let ContentKind::Text(text) = content.kind else {
1700            panic!("text over a picture is text content");
1701        };
1702        let inked = agree(&text, [0.0; 2], [1.0; 2], 200);
1703        assert_eq!(inked, 40_000, "the picture shows everywhere");
1704        let composed = fit.onto(&picture, &atlas, &glyphs).unwrap();
1705        let reference = text.sampler();
1706        let mut sum = [[0.0f64; 3]; 2];
1707        for row in 0..128u32 {
1708            for column in 0..128u32 {
1709                let uv = [(column as f32 + 0.5) / 128.0, (row as f32 + 0.5) / 128.0];
1710                let printed = composed[(row * 128 + column) as usize][0] > 200;
1711                let ours = reference(uv);
1712                for (k, weight) in [
1713                    (0, f64::from(u8::from(printed))),
1714                    (1, f64::from(ours[0] > 0.5)),
1715                ] {
1716                    sum[k][0] += weight * f64::from(column);
1717                    sum[k][1] += weight * f64::from(row);
1718                    sum[k][2] += weight;
1719                }
1720            }
1721        }
1722        assert!(sum[0][2] > 200.0 && sum[1][2] > 200.0, "{sum:?}");
1723        for axis in 0..2 {
1724            let apart = sum[0][axis] / sum[0][2] - sum[1][axis] / sum[1][2];
1725            assert!(apart.abs() < 0.5, "{apart} texels apart on axis {axis}");
1726        }
1727        let words = lettering.content();
1728        assert!(fit.over(&words, &lettering).is_err());
1729        assert!(fit.onto(&words, &atlas, &glyphs).is_err());
1730    }
1731
1732    #[test]
1733    fn a_fit_puts_lettering_where_it_puts_the_print() {
1734        let cube = Cube::new();
1735        let part = cube.mesh();
1736        let fit = fit(&part, IDENTITY, ON_FRONT).unwrap();
1737        let (atlas, glyphs) = laid("on", 0.08, UNITS_PER_METRE, [-0.1, -0.05]);
1738        let lettering = lettering(&atlas, &glyphs).unwrap();
1739        let content = fit.content(&lettering);
1740        assert!(!content.cutout);
1741        let [x, y, w, h] = lettering.rect;
1742        for layout in [[x, y], [x + w, y + h], [x + 0.3 * w, y + 0.6 * h]] {
1743            let uv = [layout[0] + 0.5, -layout[1] + 0.5];
1744            let shown: [f32; 2] =
1745                std::array::from_fn(|k| uv[k] * content.uv_scale[k] + content.uv_offset[k]);
1746            let want = [(layout[0] - x) / w, (layout[1] - y) / h];
1747            for k in 0..2 {
1748                assert!(
1749                    (shown[k] - want[k]).abs() < 1e-4,
1750                    "{shown:?} against {want:?}"
1751                );
1752            }
1753        }
1754        assert_eq!(fit.elsewhere(&part, &content).len(), 10);
1755    }
1756
1757    #[test]
1758    fn broken_lettering_is_refused() {
1759        let (atlas, glyphs) = laid("x", 0.1, UNITS_PER_METRE, [0.0; 2]);
1760        assert!(lettering(&atlas, &[]).is_err());
1761        let four = Atlas {
1762            channels: 4,
1763            ..atlas.clone()
1764        };
1765        assert!(lettering(&four, &glyphs).is_err());
1766        let hidden = Glyph {
1767            color: [1.0, 1.0, 1.0, 0.0],
1768            ..glyphs[0]
1769        };
1770        assert!(lettering(&atlas, &[hidden]).is_err());
1771        let lettering = lettering(&atlas, &glyphs).unwrap();
1772        let flat = TextContent {
1773            scale: [0.0, 1.0],
1774            ..lettering.shown()
1775        };
1776        assert!(!flat.valid());
1777        assert!(flat.records().is_err());
1778    }
1779
1780    #[test]
1781    fn staged_lettering_rides_its_own_slot_beside_images() {
1782        use crate::gpu::{Camera, Sun};
1783        use crate::stage::{Placement, Stage};
1784        let (atlas, glyphs) = laid("slot", 0.1, UNITS_PER_METRE, [0.0; 2]);
1785        let lettering = lettering(&atlas, &glyphs).unwrap();
1786        let carrier = lettering.carrier();
1787        let red = [[255, 0, 0, 255]; 4];
1788        let image = Image {
1789            width: 2,
1790            height: 2,
1791            texels: &red,
1792            srgb: true,
1793        };
1794        let meshes = [carrier.mesh()];
1795        let placements = [
1796            Placement {
1797                content: Some(lettering.content()),
1798                ..Placement::new(0, IDENTITY, 0)
1799            },
1800            Placement {
1801                content: Some(PlacementContent::new(image)),
1802                ..Placement::new(0, IDENTITY, 0)
1803            },
1804            Placement {
1805                content: Some(lettering.content()),
1806                ..Placement::new(0, IDENTITY, 0)
1807            },
1808        ];
1809        let materials = [material(false)];
1810        let staged = Stage {
1811            meshes: &meshes,
1812            instances: &placements,
1813            materials: &materials,
1814            sky: pfx_load::Sky {
1815                width: 1,
1816                height: 1,
1817                texels: vec![[0.0; 4]],
1818            },
1819            sun: Sun {
1820                direction: [0.0, 1.0, 0.0],
1821                color: [1.0; 3],
1822                intensity: 0.0,
1823            },
1824            camera: Camera {
1825                origin: [0.0, 0.0, 1.0],
1826                forward: [0.0, 0.0, -1.0],
1827                right: [1.0, 0.0, 0.0],
1828                up: [0.0, 1.0, 0.0],
1829            },
1830            projection: crate::gpu::Projection::Perspective,
1831            lens: crate::detail::Lens::default(),
1832        }
1833        .build()
1834        .unwrap();
1835        let detail = &staged.detail;
1836        let slots: Vec<i32> = detail
1837            .instances
1838            .iter()
1839            .map(|instance| instance.surface.layer.slot)
1840            .collect();
1841        assert_eq!(slots, [0, 1, 0]);
1842        assert!(detail.instances[0].surface.cutout);
1843        assert_eq!(detail.content_slots.len(), 2);
1844        assert_eq!(detail.text_slots.len(), 2);
1845        assert!(detail.content_slots[0].is_none() && detail.content_slots[1].is_some());
1846        assert!(detail.text_slots[1].is_none());
1847        let text = detail.text_slots[0].as_ref().unwrap();
1848        assert_eq!(*text, lettering.shown().records().unwrap());
1849        let buffers = crate::gpu::SceneBuffers::build(&[], &[], &materials, detail).unwrap();
1850        let info: &[u32] = bytemuck::cast_slice(&buffers.content_info);
1851        assert_eq!(&info[..4], [4, text.width, text.height, 1]);
1852        assert_eq!(&info[4..], [0, 2, 2, 0]);
1853        assert_eq!(buffers.content.len(), (4 + text.records.len()) * 16);
1854    }
1855}