feat(fonts): system TrueType support with shaping and fallback

Add a TrueType/OpenType render path alongside the LFF stroke engine so
users can pick fonts installed on their machine for text and MText.

- Discover system families via fontdb (sysfont) and list them in the
  text-style picker.
- Extract glyph outlines with ttf-parser, flattened into wire strokes
  normalized to the same 9-unit cap-height space as LFF glyphs.
- Shape runs with cosmic-text (ligatures, Arabic joining, kerning, bidi)
  plus automatic font fallback: a glyph missing from the chosen family
  is taken from another installed font.
- Unify both sources behind a single Face type; a shared run tokenizer
  drives the stroke (per-glyph) and shaped (TTF) renderers identically.
- Stroke fonts now fall back to a system TTF (cosmic-text picks it) for
  characters no LFF font covers, replacing the unicode/iso3098 chain.

Also trim the embedded LFF set from 47 to 26, keeping the AutoCAD-SHX
equivalents and dropping LibreCAD-only fonts now covered by TrueType.

LFF builtins keep priority over same-named system fonts so DXF text
renders identically across machines.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-16 11:18:17 +03:00
1 parent eff6c7c730
commit cd048165a9
31 files changed
+1037 -763578

No files matched your search

+51 -2
View File
@@ -71,7 +71,7 @@ const LIST: Color = Color {
const BUILTIN_FONTS: &[&str] = &[
"Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT",
"ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "Cursive", "GreekC", "Symbol", "ISO",
"ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol",
"ISO3098", "Unicode",
];
@@ -255,7 +255,56 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
left: 8.0,
});
let editor = row![font_panel, vsep(), props_panel].height(Fill);
// ── System TrueType fonts (read from the user's machine via fontdb) ──
let ttf_items: Vec<Element<'_, Message>> = crate::scene::sysfont::families()
.iter()
.map(|fam| {
let is_sel = ttf_buf.eq_ignore_ascii_case(fam);
button(text(fam).size(10))
.on_press(Message::TextStyleEdit {
field: "ttf",
value: fam.clone(),
})
.style(list_item(is_sel))
.padding([3, 8])
.width(Fill)
.into()
})
.collect();
let ttf_panel = container(
column![
text("TrueType (system)").size(10).color(DIM),
container(scrollable(column(ttf_items).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(LIST)),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
})
.width(Fill)
.height(Fill)
.padding(2),
text_input("TrueType font…", ttf_buf)
.on_input(|v| Message::TextStyleEdit {
field: "ttf",
value: v
})
.style(field_style)
.size(11)
.width(Fill),
]
.spacing(6)
.height(Fill),
)
.width(190)
.height(Fill)
.padding([12, 8]);
let editor = row![font_panel, vsep(), ttf_panel, vsep(), props_panel].height(Fill);
crate::ui::style_manager::view(crate::ui::style_manager::Scaffold {
kind: StyleKind::Text,