forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
32136dd9dd
commit
c1b33677e2
98 files changed
+886126
-47228
No files matched your search
+30
-34
@@ -223,7 +223,7 @@ pub fn visible_spans(raw: &str) -> Vec<(usize, usize)> {
|
||||
let is_sp = |c: char| c == ' ' || c == '\u{00A0}';
|
||||
let mut result: Vec<(usize, usize)> = Vec::new();
|
||||
let mut para: Vec<(usize, usize, char)> = Vec::new();
|
||||
let mut flush = |para: &mut Vec<(usize, usize, char)>, result: &mut Vec<(usize, usize)>| {
|
||||
let flush = |para: &mut Vec<(usize, usize, char)>, result: &mut Vec<(usize, usize)>| {
|
||||
let s = para.iter().position(|t| !is_sp(t.2)).unwrap_or(para.len());
|
||||
let e = para.iter().rposition(|t| !is_sp(t.2)).map(|i| i + 1).unwrap_or(s);
|
||||
for t in ¶[s..e] {
|
||||
@@ -305,22 +305,6 @@ pub fn visible_spans(raw: &str) -> Vec<(usize, usize)> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Prefix/suffix inline codes for a character-level toggle. `Uppercase` /
|
||||
/// `Lowercase` return `None` (they transform the selected text instead).
|
||||
pub fn fmt_wrap(kind: MTextFmt) -> Option<(&'static str, &'static str)> {
|
||||
match kind {
|
||||
// Bold / italic ride on a font run; the renderer reads the `b`/`i`
|
||||
// flags of a `\f...;` code. Use a neutral family so any styled font
|
||||
// still picks up the flag.
|
||||
MTextFmt::Bold => Some(("\\fArial|b1;", "\\fArial|b0;")),
|
||||
MTextFmt::Italic => Some(("\\fArial|i1;", "\\fArial|i0;")),
|
||||
MTextFmt::Underline => Some(("\\L", "\\l")),
|
||||
MTextFmt::Overline => Some(("\\O", "\\o")),
|
||||
MTextFmt::Strike => Some(("\\K", "\\k")),
|
||||
MTextFmt::Uppercase | MTextFmt::Lowercase => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── App-side editor driver ──────────────────────────────────────────────────
|
||||
|
||||
use crate::scene::tessellate;
|
||||
@@ -432,26 +416,38 @@ impl super::OpenCADStudio {
|
||||
}
|
||||
|
||||
/// Apply a character-format toggle to the preview selection (preferred) or
|
||||
/// the Edit-box selection.
|
||||
/// the Edit-box selection. The stroke-font renderer has no true bold /
|
||||
/// italic, so Bold switches the run to the heavier Gothic stroke font and
|
||||
/// Italic applies an oblique slant — both produce a visible effect.
|
||||
pub(super) fn mtext_apply_fmt(&mut self, kind: MTextFmt) {
|
||||
let applied = match kind {
|
||||
MTextFmt::Uppercase => self.mtext_splice_sel("", "", Some(true)),
|
||||
MTextFmt::Lowercase => self.mtext_splice_sel("", "", Some(false)),
|
||||
_ => {
|
||||
let (pre, suf) = fmt_wrap(kind).unwrap();
|
||||
self.mtext_splice_sel(pre, suf, None)
|
||||
}
|
||||
// Font to restore to after a Bold run (the current global font).
|
||||
let restore = self
|
||||
.mtext_editor
|
||||
.as_ref()
|
||||
.map(|e| {
|
||||
if e.font.trim().is_empty() {
|
||||
"Standard".to_string()
|
||||
} else {
|
||||
e.font.clone()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
let (pre, suf, case): (String, String, Option<bool>) = match kind {
|
||||
MTextFmt::Bold => ("\\fGothic;".into(), format!("\\f{restore};"), None),
|
||||
MTextFmt::Italic => ("\\Q15;".into(), "\\Q0;".into(), None),
|
||||
MTextFmt::Underline => ("\\L".into(), "\\l".into(), None),
|
||||
MTextFmt::Overline => ("\\O".into(), "\\o".into(), None),
|
||||
MTextFmt::Strike => ("\\K".into(), "\\k".into(), None),
|
||||
MTextFmt::Uppercase => (String::new(), String::new(), Some(true)),
|
||||
MTextFmt::Lowercase => (String::new(), String::new(), Some(false)),
|
||||
};
|
||||
if !applied {
|
||||
if !self.mtext_splice_sel(&pre, &suf, case) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let sel = ed.content.selection();
|
||||
let text = match kind {
|
||||
MTextFmt::Uppercase => sel.as_deref().unwrap_or("").to_uppercase(),
|
||||
MTextFmt::Lowercase => sel.as_deref().unwrap_or("").to_lowercase(),
|
||||
_ => {
|
||||
let (pre, suf) = fmt_wrap(kind).unwrap();
|
||||
format!("{pre}{}{suf}", sel.as_deref().unwrap_or(""))
|
||||
}
|
||||
let sel = ed.content.selection().unwrap_or_default();
|
||||
let text = match case {
|
||||
Some(true) => sel.to_uppercase(),
|
||||
Some(false) => sel.to_lowercase(),
|
||||
None => format!("{pre}{sel}{suf}"),
|
||||
};
|
||||
ed.content.perform(Action::Edit(Edit::Paste(Arc::new(text))));
|
||||
}
|
||||
|
||||
+30
-20
@@ -1323,12 +1323,18 @@ fn position_canvas_overlay<'a>(
|
||||
|
||||
// ── In-place MText editor overlay ───────────────────────────────────────────
|
||||
|
||||
const MTEXT_FONTS: [&str; 5] = [
|
||||
// Stroke-font families the renderer ships (LibreCAD LFF; see scene/lff.rs).
|
||||
const MTEXT_FONTS: [&str; 10] = [
|
||||
"[Style default]",
|
||||
"Arial",
|
||||
"Times New Roman",
|
||||
"Courier New",
|
||||
"ISOCPEUR",
|
||||
"Standard",
|
||||
"ISO",
|
||||
"Simplex",
|
||||
"RomanS",
|
||||
"RomanD",
|
||||
"ItalicC",
|
||||
"ScriptS",
|
||||
"GothGBT",
|
||||
"Cursive",
|
||||
];
|
||||
/// (label, ACI). 256 = ByLayer.
|
||||
const MTEXT_COLORS: [(&str, u16); 8] = [
|
||||
@@ -1348,8 +1354,8 @@ const MTEXT_COLORS: [(&str, u16); 8] = [
|
||||
const MTEXT_PREVIEW_PAD: f32 = 12.0;
|
||||
|
||||
struct MTextPreview {
|
||||
/// Disconnected polylines as (x, y) world points (NaN-split already done).
|
||||
segments: Vec<Vec<(f32, f32)>>,
|
||||
/// Disconnected polylines as (x, y) world points + colour (NaN-split done).
|
||||
segments: Vec<(Vec<(f32, f32)>, Color)>,
|
||||
/// Per-visible-character boxes (world frame) for click-to-select.
|
||||
boxes: Vec<crate::entities::text_support::GlyphBox>,
|
||||
/// Current selection as a visible-char range.
|
||||
@@ -1477,7 +1483,7 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
|
||||
}
|
||||
}
|
||||
}
|
||||
for seg in &self.segments {
|
||||
for (seg, col) in &self.segments {
|
||||
if seg.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
@@ -1487,31 +1493,35 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
|
||||
p.line_to(map(x, y));
|
||||
}
|
||||
});
|
||||
frame.stroke(
|
||||
&path,
|
||||
Stroke::default()
|
||||
.with_color(Color { r: 0.93, g: 0.93, b: 0.93, a: 1.0 })
|
||||
.with_width(1.4),
|
||||
);
|
||||
frame.stroke(&path, Stroke::default().with_color(*col).with_width(1.4));
|
||||
}
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
||||
/// Split every preview WireModel into finite (x, y) polyline runs.
|
||||
fn mtext_preview_segments(ed: &super::mtext_editor::MTextEditorState) -> Vec<Vec<(f32, f32)>> {
|
||||
let mut out: Vec<Vec<(f32, f32)>> = Vec::new();
|
||||
/// Split every preview WireModel into finite (x, y) polyline runs, each
|
||||
/// carrying its wire's colour so inline `\C` / the colour dropdown shows.
|
||||
fn mtext_preview_segments(
|
||||
ed: &super::mtext_editor::MTextEditorState,
|
||||
) -> Vec<(Vec<(f32, f32)>, Color)> {
|
||||
let mut out: Vec<(Vec<(f32, f32)>, Color)> = Vec::new();
|
||||
for w in &ed.preview_wires {
|
||||
let col = Color {
|
||||
r: w.color[0],
|
||||
g: w.color[1],
|
||||
b: w.color[2],
|
||||
a: 1.0,
|
||||
};
|
||||
let mut run: Vec<(f32, f32)> = Vec::new();
|
||||
for p in &w.points {
|
||||
if p[0].is_finite() && p[1].is_finite() {
|
||||
run.push((p[0], p[1]));
|
||||
} else if !run.is_empty() {
|
||||
out.push(std::mem::take(&mut run));
|
||||
out.push((std::mem::take(&mut run), col));
|
||||
}
|
||||
}
|
||||
if !run.is_empty() {
|
||||
out.push(run);
|
||||
out.push((run, col));
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -1690,7 +1700,7 @@ fn mtext_editor_overlay<'a>(
|
||||
let body: Element<'a, Message> = if ed.show_preview {
|
||||
let segments = mtext_preview_segments(ed);
|
||||
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
||||
for seg in &segments {
|
||||
for (seg, _) in &segments {
|
||||
for &(x, y) in seg {
|
||||
minx = minx.min(x);
|
||||
miny = miny.min(y);
|
||||
|
||||
Reference in new issue
Block a user