diff --git a/assets/fonts/ltypeshp.lff b/assets/fonts/ltypeshp.lff new file mode 100644 index 000000000..9a64e3799 --- /dev/null +++ b/assets/fonts/ltypeshp.lff @@ -0,0 +1,45 @@ +# Format: LibreCAD Font 1 +# Name: ltypeshp +# LetterSpacing: 3 +# WordSpacing: 6.75 +# LineSpacingFactor: 1 + + +[0042] BAT +0,0;-1,2;3,2,A-1;2,0 + +[0042] BOX +2,1;2,-1;0,-1;0,1;2,1 + +[0043] CIRC1 +1,1;1,-1,A1;1,1,A1 + +[0054] TRACK1 +0,-1;0,1 + +[005a] ZIG +0,0;1,1;2,0 + +[0099] PT1 +0,0;0.577,1;1.154,0 + +[009A] PT2 +0,0;0.577,-1;1.154,0 + +[009B] PLANT1 +0,0;1.82,1.14;2.27,-0.71;3,0 + +[009C] PLANT2 +0,0;1.41,1.61;2.63,-0.59;3.15,0 + +[009C] PLANT3 +0,0;2.42,0.53;3.05,1.24;4.23,0 + +[009D] PLANT4 +0,0;1.76,-0.55;3.64,1;4.63,0 + +[009E] PLANT5 +0,0;0.42,-0.4;3.45,-0.81;2.5,0 + +[009F] PLANT6 +0,0;2.58,-0.5;3.81,1.37;4.42,0 diff --git a/src/scene/complex_lt.rs b/src/scene/complex_lt.rs index ae68beefd..975272346 100644 --- a/src/scene/complex_lt.rs +++ b/src/scene/complex_lt.rs @@ -291,16 +291,44 @@ fn offset_pt(pt: [f32; 3], fwd: [f32; 3], perp: [f32; 3], dx: f32, dy: f32) -> [ ] } -/// Embedded SHAPE elements in complex linetypes are no longer supported: -/// the LFF font set ships no shape file (matching LibreCAD). Shape elements -/// are skipped; text-in-linetype still renders via the LFF fonts. +/// Transform a named linetype shape (from the converted `ltypeshp` LFF font) +/// into world-space strokes at the pen position. fn emit_shape( - _name: &str, - _insert: [f32; 3], - _fwd: [f32; 3], - _perp: [f32; 3], - _scale: f32, - _rot_deg: f32, + name: &str, + insert: [f32; 3], + fwd: [f32; 3], + perp: [f32; 3], + scale: f32, + rot_deg: f32, ) -> Vec> { - Vec::new() + let shape = match lff::shape(name) { + Some(s) => s, + None => return vec![], + }; + + let rot_r = rot_deg.to_radians(); + let (cos_r, sin_r) = (rot_r.cos(), rot_r.sin()); + + shape + .strokes + .iter() + .map(|stroke| { + stroke + .iter() + .map(|&[lx, ly]| { + let scaled_x = lx * scale; + let scaled_y = ly * scale; + // Rotate in the shape's local frame, then place along the + // line tangent (fwd) and perpendicular (perp). + let along_fwd = cos_r * scaled_x - sin_r * scaled_y; + let along_perp = sin_r * scaled_x + cos_r * scaled_y; + [ + insert[0] + fwd[0] * along_fwd + perp[0] * along_perp, + insert[1] + fwd[1] * along_fwd + perp[1] * along_perp, + insert[2] + fwd[2] * along_fwd + perp[2] * along_perp, + ] + }) + .collect() + }) + .collect() } diff --git a/src/scene/lff.rs b/src/scene/lff.rs index 861c3f4f8..ea4c34296 100644 --- a/src/scene/lff.rs +++ b/src/scene/lff.rs @@ -46,6 +46,7 @@ const FONTS_SRC: &[(&str, &str)] = &[ ("kochigothic", include_str!("../../assets/fonts/kochigothic.lff")), ("kochimincho", include_str!("../../assets/fonts/kochimincho.lff")), ("kst32b", include_str!("../../assets/fonts/kst32b.lff")), + ("ltypeshp", include_str!("../../assets/fonts/ltypeshp.lff")), ("lc_opengost-ar", include_str!("../../assets/fonts/lc_opengost-ar.lff")), ("lc_opengost-br", include_str!("../../assets/fonts/lc_opengost-br.lff")), ("opengosttypea-regular", include_str!("../../assets/fonts/opengosttypea-regular.lff")), @@ -122,6 +123,10 @@ pub struct Font { pub word_spacing: f32, pub line_spacing: f32, glyphs: HashMap, + /// Named shapes — blocks whose `[hex] LABEL` label is a word rather than + /// the single codepoint character (used by `ltypeshp` for complex + /// linetype shapes, keyed by name since codepoints collide). + shapes: HashMap, } impl Font { @@ -129,6 +134,15 @@ impl Font { pub fn glyph(&self, c: char) -> Option<&Glyph> { self.glyphs.get(&c) } + /// Look up a named shape (case-insensitive). + pub fn shape(&self, name: &str) -> Option<&Glyph> { + self.shapes.get(&name.to_ascii_uppercase()) + } +} + +/// Look up a complex-linetype shape by name in the `ltypeshp` font. +pub fn shape(name: &str) -> Option<&'static Glyph> { + fonts_map().get("LTYPESHP").and_then(|f| f.shape(name)) } // ── Registry ─────────────────────────────────────────────────────────────── @@ -179,6 +193,12 @@ fn unicode_font() -> &'static Font { .expect("at least one LFF font must be embedded") } +/// Secondary fallback covering Latin-extended / Turkish letters (ğ Ş İ …) +/// that `unicode` lacks. +fn latin_ext_font() -> Option<&'static Font> { + fonts_map().get("ISO3098") +} + /// Return a font by name (case-insensitive). `.shx` suffix is stripped. /// Falls back to Standard → Unicode → any embedded font. pub fn get_font(name: &str) -> &'static Font { @@ -390,10 +410,12 @@ pub fn tessellate_text_run( cursor_x += font.word_spacing; continue; } - // Selected family first, then the broad Unicode fallback. + // Selected family first, then the broad Unicode fallback, then + // iso3098 for Latin-extended/Turkish letters Unicode omits. let glyph = font .glyph(render_ch) - .or_else(|| unicode_font().glyph(render_ch)); + .or_else(|| unicode_font().glyph(render_ch)) + .or_else(|| latin_ext_font().and_then(|f| f.glyph(render_ch))); match glyph { Some(glyph) => { for stroke in &glyph.strokes { @@ -440,19 +462,29 @@ fn parse_lff(src: &str) -> Font { word_spacing: 6.75, line_spacing: 1.0, glyphs: HashMap::new(), + shapes: HashMap::new(), }; let mut raw: HashMap = HashMap::new(); + let mut raw_shapes: HashMap = HashMap::new(); let mut cur: Option = None; + let mut cur_name: Option = None; let mut cur_glyph = RawGlyph::default(); - let flush = |cur: &mut Option, g: &mut RawGlyph, raw: &mut HashMap| { - if let Some(c) = cur.take() { - raw.insert(c, std::mem::take(g)); - } else { - *g = RawGlyph::default(); - } - }; + // Route the just-finished block to the glyph map (by char) or, when it + // carried a shape name, to the shape map (by name). + macro_rules! flush { + () => {{ + if let Some(c) = cur.take() { + raw.insert(c, std::mem::take(&mut cur_glyph)); + } else if let Some(n) = cur_name.take() { + raw_shapes.insert(n, std::mem::take(&mut cur_glyph)); + } else { + // Discard any strokes seen before the first header. + let _ = std::mem::take(&mut cur_glyph); + } + }}; + } for line in src.lines() { let t = line.trim(); @@ -478,16 +510,22 @@ fn parse_lff(src: &str) -> Font { continue; } if t.starts_with('[') { - flush(&mut cur, &mut cur_glyph, &mut raw); + flush!(); if let Some(end) = t.find(']') { let hex = t[1..end].trim(); - if let Some(c) = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) { - cur = Some(c); + let label = t[end + 1..].trim(); + let cp = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32); + // A 0/1-char label is a normal glyph (keyed by codepoint); a + // word label (BOX, CIRC1, …) is a named shape. + if label.chars().count() > 1 { + cur_name = Some(label.to_ascii_uppercase()); + } else { + cur = cp; } } continue; } - if cur.is_none() { + if cur.is_none() && cur_name.is_none() { continue; } // `C` — reference another glyph's strokes. @@ -506,7 +544,7 @@ fn parse_lff(src: &str) -> Font { } } } - flush(&mut cur, &mut cur_glyph, &mut raw); + flush!(); // Resolve `C` references. Each pass folds in targets that are // themselves already reference-free; repeat so ref-to-ref chains settle. @@ -537,20 +575,20 @@ fn parse_lff(src: &str) -> Font { } } - for (c, g) in raw { - let advance = g - .strokes + let advance_of = |strokes: &[Vec<[f32; 2]>]| -> f32 { + strokes .iter() .flat_map(|s| s.iter()) .map(|&[x, _]| x) - .fold(0.0_f32, f32::max); - font.glyphs.insert( - c, - Glyph { - strokes: g.strokes, - advance, - }, - ); + .fold(0.0_f32, f32::max) + }; + for (c, g) in raw { + let advance = advance_of(&g.strokes); + font.glyphs.insert(c, Glyph { strokes: g.strokes, advance }); + } + for (n, g) in raw_shapes { + let advance = advance_of(&g.strokes); + font.shapes.insert(n, Glyph { strokes: g.strokes, advance }); } font } @@ -662,6 +700,27 @@ mod tests { let (w, h) = (maxx - minx, maxy - miny); assert!(h > w, "{name} O should be upright (h {h:.1} > w {w:.1})"); } + // Turkish letters absent from simplex/unicode still render via the + // iso3098 fallback (ı/U+0131 is the one exception iso3098 lacks). + for ch in ['Ğ', 'ş', 'İ', 'Ş', 'ğ'] { + let s = tessellate_text_run( + [0.0, 0.0], + 2.5, + 0.0, + 1.0, + 0.0, + 1.0, + "simplex", + &ch.to_string(), + ); + assert!(!s.is_empty(), "Turkish '{ch}' should render via fallback"); + } + // Complex-linetype shapes load by name (codepoints collide, so they + // must be keyed by label). + for sh in ["BOX", "CIRC1", "ZIG", "TRACK1"] { + let g = shape(sh).unwrap_or_else(|| panic!("shape {sh} missing")); + assert!(!g.strokes.is_empty(), "shape {sh} has no strokes"); + } } }