diff --git a/crates/fonts/src/stroke.rs b/crates/fonts/src/stroke.rs index e6d38b7db..43aa2dd3f 100644 --- a/crates/fonts/src/stroke.rs +++ b/crates/fonts/src/stroke.rs @@ -110,6 +110,61 @@ pub(crate) fn glyph(c: char) -> Option<(f64, &'static str)> { 'Ø' | 'ø' | '⌀' => (4.0, "1c0d0h1i3i4h4d3c1c 0c4i"), // List bullet: a small crossed diamond at mid x-height. '•' => (2.0, "1d0e1f2e1d 0e2e 1d1f"), + // Original Latin diacritics, composed from our own letter bodies and marks. + '\u{00C0}' => (4.0, concat!("0c2i4c 1f3f", " 1k3j")), + '\u{00C1}' => (4.0, concat!("0c2i4c 1f3f", " 1j3k")), + '\u{00C2}' => (4.0, concat!("0c2i4c 1f3f", " 0j2k4j")), + '\u{00C3}' => (4.0, concat!("0c2i4c 1f3f", " 0j1k3j4k")), + '\u{00C4}' => (4.0, concat!("0c2i4c 1f3f", " 1j1k 3j3k")), + '\u{00C5}' => (4.0, concat!("0c2i4c 1f3f", " 2j1k2l3k2j")), + '\u{00C7}' => (4.0, concat!("4h3i1i0h0d1c3c4d", " 2c3b2a1a")), + '\u{00C8}' => (4.0, concat!("4i0i0c4c 0f3f", " 1k3j")), + '\u{00C9}' => (4.0, concat!("4i0i0c4c 0f3f", " 1j3k")), + '\u{00CA}' => (4.0, concat!("4i0i0c4c 0f3f", " 0j2k4j")), + '\u{00CB}' => (4.0, concat!("4i0i0c4c 0f3f", " 1j1k 3j3k")), + '\u{00CC}' => (2.0, concat!("0i2i 1i1c 0c2c", " 0k2j")), + '\u{00CD}' => (2.0, concat!("0i2i 1i1c 0c2c", " 0j2k")), + '\u{00CE}' => (2.0, concat!("0i2i 1i1c 0c2c", " 0j1k2j")), + '\u{00CF}' => (2.0, concat!("0i2i 1i1c 0c2c", " 0j0k 2j2k")), + '\u{00D1}' => (4.0, concat!("0c0i4c4i", " 0j1k3j4k")), + '\u{00D2}' => (4.0, concat!("1c0d0h1i3i4h4d3c1c", " 1k3j")), + '\u{00D3}' => (4.0, concat!("1c0d0h1i3i4h4d3c1c", " 1j3k")), + '\u{00D4}' => (4.0, concat!("1c0d0h1i3i4h4d3c1c", " 0j2k4j")), + '\u{00D5}' => (4.0, concat!("1c0d0h1i3i4h4d3c1c", " 0j1k3j4k")), + '\u{00D6}' => (4.0, concat!("1c0d0h1i3i4h4d3c1c", " 1j1k 3j3k")), + '\u{00D9}' => (4.0, concat!("0i0d1c3c4d4i", " 1k3j")), + '\u{00DA}' => (4.0, concat!("0i0d1c3c4d4i", " 1j3k")), + '\u{00DB}' => (4.0, concat!("0i0d1c3c4d4i", " 0j2k4j")), + '\u{00DC}' => (4.0, concat!("0i0d1c3c4d4i", " 1j1k 3j3k")), + '\u{00DD}' => (4.0, concat!("0i2f4i 2f2c", " 1j3k")), + '\u{00E0}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 1i3h")), + '\u{00E1}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 1h3i")), + '\u{00E2}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 0h2i4h")), + '\u{00E3}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 0h1i3h4i")), + '\u{00E4}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 1h1i 3h3i")), + '\u{00E5}' => (4.0, concat!("4g4c 4f3g1g0f0d1c3c4d", " 2h1i2j3i2h")), + '\u{00E7}' => (4.0, concat!("4f3g1g0f0d1c3c4d", " 2c3b2a1a")), + '\u{00E8}' => (4.0, concat!("0e4e4f3g1g0f0d1c3c4d", " 1i3h")), + '\u{00E9}' => (4.0, concat!("0e4e4f3g1g0f0d1c3c4d", " 1h3i")), + '\u{00EA}' => (4.0, concat!("0e4e4f3g1g0f0d1c3c4d", " 0h2i4h")), + '\u{00EB}' => (4.0, concat!("0e4e4f3g1g0f0d1c3c4d", " 1h1i 3h3i")), + '\u{00EC}' => (1.0, concat!("0c0g", " 0i1h")), + '\u{00ED}' => (1.0, concat!("0c0g", " 0h1i")), + '\u{00EE}' => (1.0, concat!("0c0g", " 0h0i1h")), + '\u{00EF}' => (1.0, concat!("0c0g", " 0h0i 1h1i")), + '\u{00F1}' => (4.0, concat!("0c0g 0f1g3g4f4c", " 0h1i3h4i")), + '\u{00F2}' => (4.0, concat!("1c0d0f1g3g4f4d3c1c", " 1i3h")), + '\u{00F3}' => (4.0, concat!("1c0d0f1g3g4f4d3c1c", " 1h3i")), + '\u{00F4}' => (4.0, concat!("1c0d0f1g3g4f4d3c1c", " 0h2i4h")), + '\u{00F5}' => (4.0, concat!("1c0d0f1g3g4f4d3c1c", " 0h1i3h4i")), + '\u{00F6}' => (4.0, concat!("1c0d0f1g3g4f4d3c1c", " 1h1i 3h3i")), + '\u{00F9}' => (4.0, concat!("0g0d1c3c4d 4g4c", " 1i3h")), + '\u{00FA}' => (4.0, concat!("0g0d1c3c4d 4g4c", " 1h3i")), + '\u{00FB}' => (4.0, concat!("0g0d1c3c4d 4g4c", " 0h2i4h")), + '\u{00FC}' => (4.0, concat!("0g0d1c3c4d 4g4c", " 1h1i 3h3i")), + '\u{00FD}' => (4.0, concat!("0g2c 4g1a0a", " 1h3i")), + '\u{00FF}' => (4.0, concat!("0g2c 4g1a0a", " 1h1i 3h3i")), + '\u{0178}' => (4.0, concat!("0i2f4i 2f2c", " 1j1k 3j3k")), _ => return None, }; Some(g) diff --git a/crates/fonts/tests/latin_accents.rs b/crates/fonts/tests/latin_accents.rs new file mode 100644 index 000000000..79d4a37c1 --- /dev/null +++ b/crates/fonts/tests/latin_accents.rs @@ -0,0 +1,54 @@ +use cadcraft_fonts::{MTextParams, TextFont, char_advance, layout_mtext_with, line_width, shape_stroke}; +use cadcraft_geom::Vec2; + +#[test] +fn latin_accents_keep_single_strokes_and_the_base_letter_spacing() { + let groups = [ + ('A', "ÀÁÂÃÄÅ"), + ('C', "Ç"), + ('E', "ÈÉÊË"), + ('I', "ÌÍÎÏ"), + ('N', "Ñ"), + ('O', "ÒÓÔÕÖ"), + ('U', "ÙÚÛÜ"), + ('Y', "ÝŸ"), + ('a', "àáâãäå"), + ('c', "ç"), + ('e', "èéêë"), + ('i', "ìíîï"), + ('n', "ñ"), + ('o', "òóôõö"), + ('u', "ùúûü"), + ('y', "ýÿ"), + ]; + for (base, accents) in groups { + for c in accents.chars() { + let shaped = shape_stroke(&c.to_string(), 6.0, 0.8, 0.2); + assert!(shaped.glyphs.is_empty(), "{c} must not become a filled system-font outline"); + assert!(!shaped.strokes.is_empty()); + assert_eq!(char_advance(c), char_advance(base)); + assert!((line_width(&c.to_string(), 6.0, 0.8) - shaped.width).abs() < 1e-9); + assert!(shaped.strokes.iter().flatten().all(|p| p.is_finite())); + let next = shape_stroke(&format!("{c}B"), 6.0, 0.8, 0.2); + let plain = shape_stroke(&format!("{base}B"), 6.0, 0.8, 0.2); + assert_eq!(next.width, plain.width, "accent does not shift the next letter"); + } + } + for (accent, base) in [('Ç', 'C'), ('Ã', 'A'), ('Á', 'A'), ('Ó', 'O')] { + let plain = shape_stroke(&base.to_string(), 6.0, 1.0, 0.0); + let marked = shape_stroke(&accent.to_string(), 6.0, 1.0, 0.0); + assert!(marked.strokes.starts_with(&plain.strokes), "same letter body for {accent}"); + assert!(marked.strokes.len() > plain.strokes.len()); + } +} + +#[test] +fn portuguese_mtext_wraps_with_consistent_stroke_metrics() { + let text = "A CONCEPÇÃO SERÁ APROVADA APÓS O PROCESSAMENTO DA ESTRUTURA"; + let params = MTextParams { width: 32.0, font: TextFont::Stroke, ..MTextParams::new(Vec2::ZERO, 1.0) }; + let layout = layout_mtext_with(text, ¶ms); + assert!(layout.lines.len() >= 2); + assert_eq!(layout.lines.join(" "), text); + assert!(layout.pieces.iter().all(|p| p.shaped.glyphs.is_empty())); + assert!(layout.strokes.iter().flatten().all(|p| p.is_finite())); +} diff --git a/crates/io/tests/shx_text.rs b/crates/io/tests/shx_text.rs new file mode 100644 index 000000000..bd04a031b --- /dev/null +++ b/crates/io/tests/shx_text.rs @@ -0,0 +1,35 @@ +//! Synthetic DXF STYLE -> TEXT/MTEXT -> display list, without external font fixtures. + +use cadcraft_doc::Space; + +fn font() -> Vec { + let mut bytes = b"AutoCAD-86 unifont 1.0\r\n\x1a".to_vec(); + bytes.extend_from_slice(&[4, 0, 0, 0, 8, 0]); + bytes.extend_from_slice(&[b's', 0, 6, 2, 2, 0, 0, 0]); + for (id, program) in + [(32u16, vec![2, 8, 3, 0, 0]), (65, vec![8, 0, 6, 8, 4, 250, 2, 8, 3, 0, 0]), (193, vec![7, 0, 65, 5, 2, 8, 252, 7, 1, 8, 2, 1, 6, 0])] + { + bytes.extend_from_slice(&id.to_le_bytes()); + bytes.extend_from_slice(&(program.len() as u16 + 1).to_le_bytes()); + bytes.push(0); + bytes.extend(program); + } + bytes +} + +#[test] +fn shx_style_and_unicode_escapes_render_as_native_strokes_and_roundtrip() { + cadcraft_fonts::shx::register("cadcraft-io-synthetic.shx", &font()).unwrap(); + let dxf = "0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nSTYLE\n0\nSTYLE\n2\nR80\n3\ncadcraft-io-synthetic.shx\n0\nENDTAB\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nTEXT\n7\nR80\n10\n0\n20\n0\n40\n6\n1\nA\\U+00C1\n0\nMTEXT\n7\nR80\n10\n0\n20\n-20\n40\n6\n41\n18\n1\nA\\U+00C1 A\\U+00C1\n0\nENDSEC\n0\nEOF\n"; + let drawing = cadcraft_io::read(dxf.as_bytes(), "synthetic.dxf").unwrap(); + let display = cadcraft_render::build(&drawing, &Space::Model, &cadcraft_render::Options::default()); + assert!(display.tris.is_empty(), "SHX letters and accents are strokes, not filled TrueType outlines"); + assert!(display.verts.iter().any(|p| (p.x - 11.0).abs() < 1e-9), "native advance of 7, not the built-in advance"); + assert!(display.verts.iter().any(|p| p.y > 6.0), "accent above the cap height"); + assert!(display.verts.iter().any(|p| p.y < -27.0), "MTEXT wraps its second word"); + let saved = cadcraft_io::write_dxf(&drawing); + let reopened = cadcraft_io::read(saved.as_bytes(), "synthetic.dxf").unwrap(); + let again = cadcraft_render::build(&reopened, &Space::Model, &cadcraft_render::Options::default()); + assert_eq!(display.verts, again.verts); + assert!(again.tris.is_empty()); +}