mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 12:00:50 -06:00
Merge #1031 (Latin accents in the stroke font) onto main
#1031's SHX reader, TextFont::Shx wiring and doc edits are superseded by main's SHX support (#2751, which landed #1944) and resolved to main's. Kept from #1031: the accented Latin letters of CADCraft Stroke (stroke.rs, composed from its own letter bodies and marks), their test (fonts/tests/latin_accents.rs) and a synthetic SHX text-through-DXF test (io/tests/shx_text.rs), which passes against main's reader.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Synthetic DXF STYLE -> TEXT/MTEXT -> display list, without external font fixtures.
|
||||
|
||||
use cadcraft_doc::Space;
|
||||
|
||||
fn font() -> Vec<u8> {
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user