feat(shx): real SHX glyphs for text fonts and linetype shapes

The SHX reader now serves all three consumers instead of just SHAPE
entities:

- Complex-linetype shape elements stopped being skipped: a document
  linetype's Shape{number} resolves its style's shape file (shared path
  fallbacks) and draws the real glyph; `.lin` catalog references keep
  the file token and look the shape up by name. The converted-LFF
  substitute set remains the fallback for anything unresolved.

- Face::Shx renders resolvable on-disk .SHX text FONTS with their real
  stroke glyphs: character code → shape lookup, cap height normalised
  to the 9-unit space every stroke-font consumer expects, advance from
  the interpreter's final pen position, line spacing from the shape #0
  above/below header, TTF fallback per missing glyph.
  resolve_text_style resolves a style's .shx next to the drawing and
  passes the absolute path through; unresolvable names keep their LFF
  substitutes. Shapes-only files (no #0 header) are refused as fonts.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-23 06:44:48 +03:00
1 parent 1de4344280
commit bcd1fc15ea
5 files changed
+241 -23

No files matched your search

+45 -6
View File
@@ -22,11 +22,15 @@ pub enum LtSegment {
Space(f32),
/// Draw a dot (zero-length element, rendered as a tiny mark).
Dot,
/// Draw a shape at the current pen position. Embedded SHAPE elements are
/// no longer rendered (the LFF font set ships no shape file).
/// Draw a shape at the current pen position.
Shape {
/// Shape name (retained for round-tripping; not drawn).
/// Shape name (`.lin` catalog reference; empty for document shapes).
name: String,
/// Resolved .SHX shape-file path ("" when none resolved — the LFF
/// substitute set is the fallback for catalog names).
shx_file: String,
/// Shape number inside the file (0 = look up by name).
number: u16,
/// X offset along the linetype direction (can be negative).
x: f32,
/// Y offset perpendicular to the linetype direction.
@@ -130,9 +134,38 @@ pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option<Comple
rot_deg: c.rotation as f32,
});
}
// Embedded SHAPE glyphs need a .shp set we don't ship; skip the
// draw (matches the bundled-catalog behaviour).
LineTypeComplexContent::Shape { .. } => {}
LineTypeComplexContent::Shape { shape_number } => {
// Resolve the style's shape file with the shared path
// fallbacks (as stored / next to the drawing) so the real
// SHX glyph draws; unresolved files simply skip.
let font = document
.text_styles
.iter()
.find(|s| s.handle == c.style_handle)
.map(|s| s.font_file.trim().to_string())
.unwrap_or_default();
if font.is_empty() || *shape_number <= 0 {
continue;
}
let base = document
.source_path
.as_deref()
.map(std::path::Path::new)
.and_then(|p| p.parent());
let Some(resolved) = crate::io::resolve_image_file(&font, base) else {
continue;
};
let scale = c.scale as f32;
segments.push(LtSegment::Shape {
name: String::new(),
shx_file: resolved,
number: *shape_number as u16,
x: c.offset[0] as f32,
y: c.offset[1] as f32,
scale: if scale.abs() < 1e-6 { 1.0 } else { scale },
rot_deg: c.rotation as f32,
});
}
}
}
}
@@ -447,6 +480,7 @@ fn parse_shape_element(inner: &str) -> Option<LtSegment> {
let mut y = 0.0f32;
let mut scale = 1.0f32;
let mut rot_deg = 0.0f32;
let mut shx_file = String::new();
for part in parts {
let p = part.trim();
@@ -463,11 +497,16 @@ fn parse_shape_element(inner: &str) -> Option<LtSegment> {
.or_else(|| p.strip_prefix("U="))
{
rot_deg = v.parse().unwrap_or(0.0);
} else if p.to_ascii_lowercase().ends_with(".shx") {
// `[NAME,ltypeshp.shx,…]` — the shape file reference.
shx_file = p.to_string();
}
}
Some(LtSegment::Shape {
name,
shx_file,
number: 0,
x,
y,
scale,