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}