feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts

Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.

Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.

All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-31 03:31:20 +03:00
commit c1b33677e2
98 changed files with 886126 additions and 47228 deletions

View file

@ -15,7 +15,7 @@ use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckC
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::wire_model::SnapHint;
use crate::scene::{cxf, transform};
use crate::scene::{lff, transform};
// ── Shared helpers ────────────────────────────────────────────────────────────
@ -273,7 +273,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
anchor_f64[0] - (anchor_local_x as f64 * cos_r - local_y_for_line as f64 * sin_r),
anchor_f64[1] - (anchor_local_x as f64 * sin_r + local_y_for_line as f64 * cos_r),
];
let strokes = cxf::tessellate_text_ex(
let strokes = lff::tessellate_text_ex(
[0.0, 0.0],
input.height as f32,
rotation,

View file

@ -8,7 +8,7 @@ use crate::entities::text_support::{
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
use crate::scene::cxf;
use crate::scene::lff;
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::wire_model::SnapHint;
@ -131,7 +131,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r),
];
// Strokes are in glyph-local space (origin = [0,0]).
let strokes = cxf::tessellate_text_ex(
let strokes = lff::tessellate_text_ex(
[0.0, 0.0],
t.height as f32,
rotation,

View file

@ -2,7 +2,7 @@ use acadrust::types::aci_table::aci_to_rgb;
use acadrust::{CadDocument, EntityType};
use crate::scene::acad_to_truck::TextStroke;
use crate::scene::cxf;
use crate::scene::lff;
pub struct ResolvedTextStyle {
pub font_name: String,
@ -65,7 +65,7 @@ pub fn text_local_bounds(
return None;
}
let font = cxf::get_font(font_name);
let font = lff::get_font(font_name);
let scale = height / 9.0;
let wf = width_factor.abs().clamp(0.01, 100.0);
let ob = oblique_angle.tan();
@ -846,7 +846,7 @@ pub fn measure_word(
) -> f32 {
let scale = run_scale(state, entity_h, base_wf);
let font_name = resolve_font(state, base_font);
let font = cxf::get_font(font_name);
let font = lff::get_font(font_name);
let mut w = 0.0_f32;
for ch in text.chars() {
w += match font.glyph(ch) {
@ -860,7 +860,7 @@ pub fn measure_word(
pub fn measure_space(state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
let scale = run_scale(state, entity_h, base_wf);
let font_name = resolve_font(state, base_font);
cxf::get_font(font_name).word_spacing * scale
lff::get_font(font_name).word_spacing * scale
}
pub fn atom_width(atom: &LayoutAtom, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
@ -991,7 +991,7 @@ pub fn resolve_inline_color(c: &InlineColor) -> Option<[f32; 3]> {
}
}
/// Wrap a run's glyph text with MTEXT decoration markers so cxf's
/// Wrap a run's glyph text with MTEXT decoration markers so lff's
/// `tessellate_text_run` emits the underline / overline / strikethrough
/// strokes for us — keeps decoration geometry in one place rather than
/// duplicating the y-position constants.
@ -1115,7 +1115,7 @@ pub struct MTextLayout {
/// text block.
pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
let base_font_name = opts.style.font_name.clone();
let base_font = cxf::get_font(&base_font_name);
let base_font = lff::get_font(&base_font_name);
let base_wf_abs = opts.style.width_factor.max(0.01);
let base_wf = if opts.style.is_backward { -base_wf_abs } else { base_wf_abs };
let base_oblique = opts.style.oblique_angle;
@ -1353,7 +1353,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
ins_x + (line_base_x + world_dx) as f64,
ins_y + (line_base_y + world_dy) as f64,
];
let strokes = cxf::tessellate_text_run(
let strokes = lff::tessellate_text_run(
[0.0, 0.0],
run_h,
rot,
@ -1372,7 +1372,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// Per-character boxes, advancing exactly as
// `measure_word` does so they track the glyphs.
let scale = run_scale(&atom.state, entity_h, base_wf);
let font = cxf::get_font(font_name);
let font = lff::get_font(font_name);
let mut cx = cursor_x;
for ch in text.chars() {
let adv = match font.glyph(ch) {

View file

@ -7,7 +7,7 @@ use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckC
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
use crate::scene::object::{GripApply, GripDef, PropSection};
use crate::scene::wire_model::SnapHint;
use crate::scene::{cxf, transform};
use crate::scene::{lff, transform};
// ── GDT text parser ───────────────────────────────────────────────────────────
@ -207,7 +207,7 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
let tx = cell_x + (cw - text_w) * 0.5;
// Tessellate text in local frame then transform
let local_strokes =
cxf::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
for polyline in local_strokes {
let transformed: Vec<[f32; 2]> = polyline
.into_iter()