fix(text): render TTF glyphs as solid fills, fix font fallback, correct Z-order
Text elements previously rendered as hollow wireframe outlines and sometimes fell back to the wrong system font due to case-sensitive exact matching. Also, the viewport background grid drew on top of solid entities. Case-insensitive and partial-name fallback matching (e.g., mapping "arialn" to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring text resolves to the correct installed system font. TrueType font contours are now triangulated into solid fills (`fill_tris`) using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying this 2D text as a 3D mesh (which broke flat shading), text tessellation splits the output into separate outline and fill `WireModel`s. The background grid is extracted from `SelectionCanvas` into `GridCanvas` and moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState` is enabled on all face/fill pipelines to apply a polygon offset, perfectly resolving Z-fighting between coplanar wireframe lines and solid faces.
This commit is contained in:
parent
c599c2ba09
commit
627761b629
20 changed files with 624 additions and 191 deletions
|
|
@ -278,7 +278,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 = lff::tessellate_text_ex(
|
||||
let (strokes, fill_tris) = lff::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
input.height as f32,
|
||||
rotation,
|
||||
|
|
@ -291,6 +291,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
|
|||
strokes,
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
});
|
||||
}
|
||||
let _ = input.line_count; // round-trip only — recomputed above
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub mod solid3d;
|
|||
pub mod spline;
|
||||
pub mod table;
|
||||
pub mod text;
|
||||
pub(crate) mod text_support;
|
||||
pub mod text_support;
|
||||
pub mod tolerance;
|
||||
pub mod traits;
|
||||
pub mod underlay;
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
|
||||
// Text strokes, drawn from the layout computed up front (centred on the
|
||||
// text grip). The snap node is the grip itself.
|
||||
let mut fill_tris = Vec::new();
|
||||
if let Some(layout) = &text_layout {
|
||||
snap_pts.push(node([text_loc.x, text_loc.y, text_loc.z]));
|
||||
for ts in &layout.strokes {
|
||||
|
|
@ -281,6 +282,9 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
points.push([ox + x as f64, oy + y as f64, text_loc.z]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
fill_tris.push([ox + x as f64, oy + y as f64, text_loc.z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +297,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
snap_pts,
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
fill_tris: vec![],
|
||||
fill_tris,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1597,6 +1601,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
// local glyph space with its world origin (already offset-
|
||||
// relative because we passed local_ins_x/y) stored as f64.
|
||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut text_fill_tris: Vec<[f32; 3]> = Vec::new();
|
||||
for ts in &layout.strokes {
|
||||
let ox = ts.origin[0] as f32;
|
||||
let oy = ts.origin[1] as f32;
|
||||
|
|
@ -1609,9 +1614,19 @@ impl MultiLeaderTess for MultiLeader {
|
|||
text_points.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
text_fill_tris.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut is_first = true;
|
||||
if !text_points.is_empty() {
|
||||
let snap = if is_first {
|
||||
is_first = false;
|
||||
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
|
|
@ -1622,7 +1637,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
|
||||
snap_pts: snap,
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
|
|
@ -1632,6 +1647,32 @@ impl MultiLeaderTess for MultiLeader {
|
|||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
if !text_fill_tris.is_empty() {
|
||||
let snap = if is_first {
|
||||
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: snap,
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: text_fill_tris,
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ impl TruckConvertible for Table {
|
|||
let total_h = *row_offsets.last().unwrap_or(&0.0);
|
||||
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut tris_pts: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
// Per-cell borders. When a cell carries a CellStyle, honour the
|
||||
// visibility / `invisible` flag of each of its four borders so
|
||||
|
|
@ -161,15 +162,28 @@ impl TruckConvertible for Table {
|
|||
|
||||
let font_for_handle = |handle: Option<acadrust::Handle>| -> Option<String> {
|
||||
handle.and_then(|h| lookup_style(h)).and_then(|s| {
|
||||
let file = s.font_file.trim();
|
||||
if !file.is_empty() {
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
if !stem.is_empty() {
|
||||
return Some(stem.to_string());
|
||||
let mut font_name = if !s.true_type_font.trim().is_empty() {
|
||||
s.true_type_font.trim().to_string()
|
||||
} else {
|
||||
let file = s.font_file.trim();
|
||||
if !file.is_empty() {
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
if !stem.is_empty() {
|
||||
stem.to_string()
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !crate::scene::text::lff::is_builtin(&font_name) {
|
||||
if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) {
|
||||
font_name = canonical;
|
||||
}
|
||||
}
|
||||
None
|
||||
Some(font_name)
|
||||
})
|
||||
};
|
||||
// Build a ResolvedTextStyle for the cell — needed by the shared MText
|
||||
|
|
@ -304,6 +318,9 @@ impl TruckConvertible for Table {
|
|||
pts.push([x + ox, y + oy, origin.z]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
tris_pts.push([x + ox, y + oy, origin.z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -321,12 +338,18 @@ impl TruckConvertible for Table {
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
let fill_tris_f64: Vec<[f64; 3]> = tris_pts
|
||||
.into_iter()
|
||||
.map(|[x, y, z]| {
|
||||
[x as f64 + base[0], y as f64 + base[1], z as f64 + base[2]]
|
||||
})
|
||||
.collect();
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(pts_f64),
|
||||
snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
fill_tris: fill_tris_f64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -435,10 +458,24 @@ pub fn tessellate_table(
|
|||
};
|
||||
let font_for_handle = |handle: Option<acadrust::Handle>| -> Option<String> {
|
||||
handle.and_then(lookup_style).and_then(|s| {
|
||||
let file = s.font_file.trim();
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
(!stem.is_empty()).then(|| stem.to_string())
|
||||
let mut font_name = if !s.true_type_font.trim().is_empty() {
|
||||
s.true_type_font.trim().to_string()
|
||||
} else {
|
||||
let file = s.font_file.trim();
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
if !stem.is_empty() {
|
||||
stem.to_string()
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !crate::scene::text::lff::is_builtin(&font_name) {
|
||||
if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) {
|
||||
font_name = canonical;
|
||||
}
|
||||
}
|
||||
Some(font_name)
|
||||
})
|
||||
};
|
||||
let resolved_style_for_handle =
|
||||
|
|
@ -455,7 +492,7 @@ pub fn tessellate_table(
|
|||
|
||||
// Accumulators keyed by quantised colour (+ weight for borders).
|
||||
let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>, Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default();
|
||||
let sel_col = WireModel::SELECTED;
|
||||
|
|
@ -672,10 +709,11 @@ pub fn tessellate_table(
|
|||
} else {
|
||||
entity_color
|
||||
};
|
||||
let buf = &mut texts
|
||||
let entry = texts
|
||||
.entry(key4(tcol))
|
||||
.or_insert_with(|| (tcol, Vec::new()))
|
||||
.1;
|
||||
.or_insert_with(|| (tcol, Vec::new(), Vec::new()));
|
||||
let buf = &mut entry.1;
|
||||
let tris_buf = &mut entry.2;
|
||||
for ts in &layout.strokes {
|
||||
let sx = ts.origin[0] as f32;
|
||||
let sy = ts.origin[1] as f32;
|
||||
|
|
@ -690,6 +728,9 @@ pub fn tessellate_table(
|
|||
buf.push([x + sx, y + sy, (to.z as f64) as f32]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
tris_buf.push([x + sx, y + sy, (to.z as f64) as f32]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -730,10 +771,13 @@ pub fn tessellate_table(
|
|||
out.push(mk(color, pts, vec![], lw));
|
||||
}
|
||||
}
|
||||
for (_, (color, pts)) in texts {
|
||||
for (_, (color, pts, tris)) in texts {
|
||||
if !pts.is_empty() {
|
||||
out.push(mk(color, pts, vec![], line_weight_px));
|
||||
}
|
||||
if !tris.is_empty() {
|
||||
out.push(mk(color, vec![], tris, line_weight_px));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,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 = lff::tessellate_text_ex(
|
||||
let (strokes, fill_tris) = lff::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
t.height as f32,
|
||||
rotation,
|
||||
|
|
@ -151,6 +151,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
|||
strokes,
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
}]),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
|
|
|
|||
|
|
@ -19,22 +19,20 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
|||
|| (style_name.trim().is_empty() && entry.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
|
||||
let font_name = if let Some(style) = style {
|
||||
if !style.font_file.trim().is_empty() {
|
||||
let mut font_name = if let Some(style) = style {
|
||||
if !style.true_type_font.trim().is_empty() {
|
||||
style.true_type_font.trim().to_string()
|
||||
} else if !style.font_file.trim().is_empty() {
|
||||
let file = style.font_file.trim();
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
if !stem.is_empty() {
|
||||
stem.to_string()
|
||||
} else if !style.true_type_font.trim().is_empty() {
|
||||
style.true_type_font.trim().to_string()
|
||||
} else if !style.name.trim().is_empty() {
|
||||
style.name.trim().to_string()
|
||||
} else {
|
||||
"Standard".to_string()
|
||||
}
|
||||
} else if !style.true_type_font.trim().is_empty() {
|
||||
style.true_type_font.trim().to_string()
|
||||
} else if !style.name.trim().is_empty() {
|
||||
style.name.trim().to_string()
|
||||
} else {
|
||||
|
|
@ -46,8 +44,18 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
|||
style_name.trim().to_string()
|
||||
};
|
||||
|
||||
if !lff::is_builtin(&font_name) {
|
||||
if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) {
|
||||
font_name = canonical;
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedTextStyle {
|
||||
font_name,
|
||||
font_name: {
|
||||
eprintln!("[resolve_text_style] style={:?} font_file={:?} true_type_font={:?} → font_name={:?}",
|
||||
style.map(|s| &s.name), style.map(|s| &s.font_file), style.map(|s| &s.true_type_font), &font_name);
|
||||
font_name
|
||||
},
|
||||
width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0),
|
||||
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
||||
is_backward: style.map(|s| s.is_backward()).unwrap_or(false),
|
||||
|
|
@ -1420,7 +1428,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 = lff::tessellate_text_run(
|
||||
let (strokes, fill_tris) = lff::tessellate_text_run(
|
||||
[0.0, 0.0],
|
||||
run_h,
|
||||
rot,
|
||||
|
|
@ -1434,6 +1442,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||
strokes,
|
||||
origin,
|
||||
color,
|
||||
fill_tris,
|
||||
});
|
||||
if opts.want_glyph_boxes {
|
||||
// Per-character boxes, advancing exactly as
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
|||
let text_w = cell.len() as f32 * char_w;
|
||||
let tx = cell_x + (cw - text_w) * 0.5;
|
||||
// Tessellate text in local frame then transform
|
||||
let local_strokes =
|
||||
let (local_strokes, _) =
|
||||
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
|
||||
|
|
@ -248,6 +248,7 @@ impl TruckConvertible for Tolerance {
|
|||
strokes,
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris: vec![],
|
||||
}]),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
|
|
|
|||
Loading…
Reference in a new issue