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
|
|
@ -1,3 +1,6 @@
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
|
||||||
|
|
||||||
# getrandom 0.3 (pulled by ahash via acadrust) needs this cfg to select its
|
# getrandom 0.3 (pulled by ahash via acadrust) needs this cfg to select its
|
||||||
# browser backend on wasm32-unknown-unknown, alongside its `wasm_js` feature
|
# browser backend on wasm32-unknown-unknown, alongside its `wasm_js` feature
|
||||||
# (enabled in Cargo.toml for the wasm target). Without it the web build fails
|
# (enabled in Cargo.toml for the wasm target). Without it the web build fails
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -30,6 +30,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"inventory",
|
"inventory",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"lyon_tessellation",
|
||||||
"lzma-sys",
|
"lzma-sys",
|
||||||
"ocs_plugin_api",
|
"ocs_plugin_api",
|
||||||
"open",
|
"open",
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ ttf-parser = "0.25"
|
||||||
# obtain positioned (font, glyph-id) runs; outlines come from ttf-parser and
|
# obtain positioned (font, glyph-id) runs; outlines come from ttf-parser and
|
||||||
# render through our own wire pipeline.
|
# render through our own wire pipeline.
|
||||||
cosmic-text = "0.15"
|
cosmic-text = "0.15"
|
||||||
|
lyon_tessellation = "1.0.20"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
# Win32_System_Com — COM behind "Set as default app"
|
# Win32_System_Com — COM behind "Set as default app"
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,46 @@ impl OpenCADStudio {
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let grid_overlay = {
|
||||||
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||||
|
let model_basis = {
|
||||||
|
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
||||||
|
(o.as_dvec3(), (ux, uy, uz))
|
||||||
|
};
|
||||||
|
let grid: Vec<overlay::GridParams> = tab
|
||||||
|
.scene
|
||||||
|
.grid_views(vw, vh)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(bounds, cam, handle)| {
|
||||||
|
let (origin, axes): (glam::DVec3, _) = if is_paper {
|
||||||
|
match tab.ucs_from_viewport(handle) {
|
||||||
|
Some(u) => {
|
||||||
|
let (o, ux, uy, uz) =
|
||||||
|
super::helpers::UcsXform::from_ucs(&u).axes();
|
||||||
|
(o.as_dvec3(), (ux, uy, uz))
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
glam::DVec3::ZERO,
|
||||||
|
(glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model_basis
|
||||||
|
};
|
||||||
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||||||
|
overlay::GridParams {
|
||||||
|
view_rot: cam.view_proj_rte(bounds),
|
||||||
|
eye: cam.eye(),
|
||||||
|
bounds,
|
||||||
|
plane,
|
||||||
|
origin,
|
||||||
|
axes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
overlay::grid_overlay(grid)
|
||||||
|
};
|
||||||
|
|
||||||
let selection_overlay = {
|
let selection_overlay = {
|
||||||
let sel = tab.scene.selection.borrow().clone();
|
let sel = tab.scene.selection.borrow().clone();
|
||||||
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
||||||
|
|
@ -151,49 +191,6 @@ impl OpenCADStudio {
|
||||||
// the correct place and scale.
|
// the correct place and scale.
|
||||||
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
||||||
|
|
||||||
// Each view draws its own grid, clipped to its own bounds, so they
|
|
||||||
// stay independent: model tiles in model space; the sheet plus each
|
|
||||||
// floating viewport (clipped to its rectangle) in paper space. One
|
|
||||||
// enumeration for both, shared with the renderer.
|
|
||||||
// Align each grid to its pane's UCS (origin in wire space, world
|
|
||||||
// axis directions): model tiles to the tab's model UCS, each content
|
|
||||||
// viewport to its own per-viewport UCS, the paper sheet to plain WCS.
|
|
||||||
let model_basis = {
|
|
||||||
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
|
||||||
(o.as_dvec3(), (ux, uy, uz))
|
|
||||||
};
|
|
||||||
let grid: Vec<overlay::GridParams> = tab
|
|
||||||
.scene
|
|
||||||
.grid_views(vw, vh)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(bounds, cam, handle)| {
|
|
||||||
let (origin, axes): (glam::DVec3, _) = if is_paper {
|
|
||||||
match tab.ucs_from_viewport(handle) {
|
|
||||||
Some(u) => {
|
|
||||||
let (o, ux, uy, uz) =
|
|
||||||
super::helpers::UcsXform::from_ucs(&u).axes();
|
|
||||||
(o.as_dvec3(), (ux, uy, uz))
|
|
||||||
}
|
|
||||||
None => (
|
|
||||||
glam::DVec3::ZERO,
|
|
||||||
(glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
model_basis
|
|
||||||
};
|
|
||||||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
||||||
overlay::GridParams {
|
|
||||||
view_rot: cam.view_proj_rte(bounds),
|
|
||||||
eye: cam.eye(),
|
|
||||||
bounds,
|
|
||||||
plane,
|
|
||||||
origin,
|
|
||||||
axes,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// The UCS icon shows the active pane's UCS tripod: the model view, or
|
// The UCS icon shows the active pane's UCS tripod: the model view, or
|
||||||
// (inside a floating viewport) projected through the viewport camera
|
// (inside a floating viewport) projected through the viewport camera
|
||||||
// at the viewport's rect so it tracks the in-viewport UCS.
|
// at the viewport's rect so it tracks the in-viewport UCS.
|
||||||
|
|
@ -257,7 +254,6 @@ impl OpenCADStudio {
|
||||||
sel,
|
sel,
|
||||||
snap_info,
|
snap_info,
|
||||||
grips,
|
grips,
|
||||||
grid,
|
|
||||||
ucs_icon,
|
ucs_icon,
|
||||||
ost_points,
|
ost_points,
|
||||||
tab.last_cursor_screen,
|
tab.last_cursor_screen,
|
||||||
|
|
@ -396,13 +392,14 @@ impl OpenCADStudio {
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
};
|
};
|
||||||
stack![
|
stack![
|
||||||
container(viewport_3d)
|
container(grid_overlay)
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(DESK)),
|
background: Some(Background::Color(DESK)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill),
|
.height(Fill),
|
||||||
|
viewport_3d,
|
||||||
selection_overlay,
|
selection_overlay,
|
||||||
viewport_mouse,
|
viewport_mouse,
|
||||||
]
|
]
|
||||||
|
|
@ -410,13 +407,14 @@ impl OpenCADStudio {
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
} else {
|
} else {
|
||||||
stack![
|
stack![
|
||||||
container(viewport_3d)
|
container(grid_overlay)
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(bg_color)),
|
background: Some(Background::Color(bg_color)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill),
|
.height(Fill),
|
||||||
|
viewport_3d,
|
||||||
selection_overlay,
|
selection_overlay,
|
||||||
viewport_mouse,
|
viewport_mouse,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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[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),
|
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],
|
[0.0, 0.0],
|
||||||
input.height as f32,
|
input.height as f32,
|
||||||
rotation,
|
rotation,
|
||||||
|
|
@ -291,6 +291,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
|
||||||
strokes,
|
strokes,
|
||||||
origin,
|
origin,
|
||||||
color: None,
|
color: None,
|
||||||
|
fill_tris,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let _ = input.line_count; // round-trip only — recomputed above
|
let _ = input.line_count; // round-trip only — recomputed above
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub mod solid3d;
|
||||||
pub mod spline;
|
pub mod spline;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub(crate) mod text_support;
|
pub mod text_support;
|
||||||
pub mod tolerance;
|
pub mod tolerance;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
pub mod underlay;
|
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 strokes, drawn from the layout computed up front (centred on the
|
||||||
// text grip). The snap node is the grip itself.
|
// text grip). The snap node is the grip itself.
|
||||||
|
let mut fill_tris = Vec::new();
|
||||||
if let Some(layout) = &text_layout {
|
if let Some(layout) = &text_layout {
|
||||||
snap_pts.push(node([text_loc.x, text_loc.y, text_loc.z]));
|
snap_pts.push(node([text_loc.x, text_loc.y, text_loc.z]));
|
||||||
for ts in &layout.strokes {
|
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]);
|
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,
|
snap_pts,
|
||||||
tangent_geoms: tangents,
|
tangent_geoms: tangents,
|
||||||
key_vertices: key_verts,
|
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-
|
// local glyph space with its world origin (already offset-
|
||||||
// relative because we passed local_ins_x/y) stored as f64.
|
// relative because we passed local_ins_x/y) stored as f64.
|
||||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||||
|
let mut text_fill_tris: Vec<[f32; 3]> = Vec::new();
|
||||||
for ts in &layout.strokes {
|
for ts in &layout.strokes {
|
||||||
let ox = ts.origin[0] as f32;
|
let ox = ts.origin[0] as f32;
|
||||||
let oy = ts.origin[1] 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]);
|
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() {
|
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 {
|
wires.push(WireModel {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
points: text_points,
|
points: text_points,
|
||||||
|
|
@ -1622,7 +1637,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
line_weight_px,
|
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![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
|
@ -1632,6 +1647,32 @@ impl MultiLeaderTess for MultiLeader {
|
||||||
fill_tris_low: Vec::new(),
|
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.
|
// 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 total_h = *row_offsets.last().unwrap_or(&0.0);
|
||||||
|
|
||||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
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
|
// Per-cell borders. When a cell carries a CellStyle, honour the
|
||||||
// visibility / `invisible` flag of each of its four borders so
|
// 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> {
|
let font_for_handle = |handle: Option<acadrust::Handle>| -> Option<String> {
|
||||||
handle.and_then(|h| lookup_style(h)).and_then(|s| {
|
handle.and_then(|h| lookup_style(h)).and_then(|s| {
|
||||||
|
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 file = s.font_file.trim();
|
||||||
if !file.is_empty() {
|
if !file.is_empty() {
|
||||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||||
if !stem.is_empty() {
|
if !stem.is_empty() {
|
||||||
return Some(stem.to_string());
|
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
|
// 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]);
|
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();
|
.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 {
|
Some(TruckEntity {
|
||||||
object: TruckObject::Lines(pts_f64),
|
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)],
|
snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: 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> {
|
let font_for_handle = |handle: Option<acadrust::Handle>| -> Option<String> {
|
||||||
handle.and_then(lookup_style).and_then(|s| {
|
handle.and_then(lookup_style).and_then(|s| {
|
||||||
|
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 file = s.font_file.trim();
|
||||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||||
(!stem.is_empty()).then(|| stem.to_string())
|
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 =
|
let resolved_style_for_handle =
|
||||||
|
|
@ -455,7 +492,7 @@ pub fn tessellate_table(
|
||||||
|
|
||||||
// Accumulators keyed by quantised colour (+ weight for borders).
|
// Accumulators keyed by quantised colour (+ weight for borders).
|
||||||
let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default();
|
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 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 mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default();
|
||||||
let sel_col = WireModel::SELECTED;
|
let sel_col = WireModel::SELECTED;
|
||||||
|
|
@ -672,10 +709,11 @@ pub fn tessellate_table(
|
||||||
} else {
|
} else {
|
||||||
entity_color
|
entity_color
|
||||||
};
|
};
|
||||||
let buf = &mut texts
|
let entry = texts
|
||||||
.entry(key4(tcol))
|
.entry(key4(tcol))
|
||||||
.or_insert_with(|| (tcol, Vec::new()))
|
.or_insert_with(|| (tcol, Vec::new(), Vec::new()));
|
||||||
.1;
|
let buf = &mut entry.1;
|
||||||
|
let tris_buf = &mut entry.2;
|
||||||
for ts in &layout.strokes {
|
for ts in &layout.strokes {
|
||||||
let sx = ts.origin[0] as f32;
|
let sx = ts.origin[0] as f32;
|
||||||
let sy = ts.origin[1] 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]);
|
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));
|
out.push(mk(color, pts, vec![], lw));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (_, (color, pts)) in texts {
|
for (_, (color, pts, tris)) in texts {
|
||||||
if !pts.is_empty() {
|
if !pts.is_empty() {
|
||||||
out.push(mk(color, pts, vec![], line_weight_px));
|
out.push(mk(color, pts, vec![], line_weight_px));
|
||||||
}
|
}
|
||||||
|
if !tris.is_empty() {
|
||||||
|
out.push(mk(color, vec![], tris, line_weight_px));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out
|
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),
|
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]).
|
// 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],
|
[0.0, 0.0],
|
||||||
t.height as f32,
|
t.height as f32,
|
||||||
rotation,
|
rotation,
|
||||||
|
|
@ -151,6 +151,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
||||||
strokes,
|
strokes,
|
||||||
origin,
|
origin,
|
||||||
color: None,
|
color: None,
|
||||||
|
fill_tris,
|
||||||
}]),
|
}]),
|
||||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
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"))
|
|| (style_name.trim().is_empty() && entry.name.eq_ignore_ascii_case("Standard"))
|
||||||
});
|
});
|
||||||
|
|
||||||
let font_name = if let Some(style) = style {
|
let mut font_name = if let Some(style) = style {
|
||||||
if !style.font_file.trim().is_empty() {
|
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 file = style.font_file.trim();
|
||||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||||
if !stem.is_empty() {
|
if !stem.is_empty() {
|
||||||
stem.to_string()
|
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() {
|
} else if !style.name.trim().is_empty() {
|
||||||
style.name.trim().to_string()
|
style.name.trim().to_string()
|
||||||
} else {
|
} else {
|
||||||
"Standard".to_string()
|
"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() {
|
} else if !style.name.trim().is_empty() {
|
||||||
style.name.trim().to_string()
|
style.name.trim().to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -46,8 +44,18 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
||||||
style_name.trim().to_string()
|
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 {
|
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),
|
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),
|
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),
|
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_x + (line_base_x + world_dx) as f64,
|
||||||
ins_y + (line_base_y + world_dy) 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],
|
[0.0, 0.0],
|
||||||
run_h,
|
run_h,
|
||||||
rot,
|
rot,
|
||||||
|
|
@ -1434,6 +1442,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||||
strokes,
|
strokes,
|
||||||
origin,
|
origin,
|
||||||
color,
|
color,
|
||||||
|
fill_tris,
|
||||||
});
|
});
|
||||||
if opts.want_glyph_boxes {
|
if opts.want_glyph_boxes {
|
||||||
// Per-character boxes, advancing exactly as
|
// 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 text_w = cell.len() as f32 * char_w;
|
||||||
let tx = cell_x + (cw - text_w) * 0.5;
|
let tx = cell_x + (cw - text_w) * 0.5;
|
||||||
// Tessellate text in local frame then transform
|
// 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);
|
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
|
||||||
for polyline in local_strokes {
|
for polyline in local_strokes {
|
||||||
let transformed: Vec<[f32; 2]> = polyline
|
let transformed: Vec<[f32; 2]> = polyline
|
||||||
|
|
@ -248,6 +248,7 @@ impl TruckConvertible for Tolerance {
|
||||||
strokes,
|
strokes,
|
||||||
origin,
|
origin,
|
||||||
color: None,
|
color: None,
|
||||||
|
fill_tris: vec![],
|
||||||
}]),
|
}]),
|
||||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub struct TextStroke {
|
||||||
pub strokes: Vec<Vec<[f32; 2]>>,
|
pub strokes: Vec<Vec<[f32; 2]>>,
|
||||||
pub origin: [f64; 2],
|
pub origin: [f64; 2],
|
||||||
pub color: Option<[f32; 3]>,
|
pub color: Option<[f32; 3]>,
|
||||||
|
pub fill_tris: Vec<[f32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
|
||||||
|
|
@ -160,17 +160,28 @@ pub fn tessellate(
|
||||||
// Selection forces a single uniform colour — never split.
|
// Selection forces a single uniform colour — never split.
|
||||||
let split_by_color = !selected;
|
let split_by_color = !selected;
|
||||||
|
|
||||||
// Bins: key = Some(rgb), parallel high/low f32 buffers — the
|
// Bins: key = Some(rgb)
|
||||||
// low buffer is index-for-index with high so the renderer's
|
struct TextBin {
|
||||||
// double-single RTE shader survives at UTM-scale anchors.
|
color: Option<[f32; 3]>,
|
||||||
let mut bins: Vec<(Option<[f32; 3]>, Vec<[f32; 3]>, Vec<[f32; 3]>)> = Vec::new();
|
pts: Vec<[f32; 3]>,
|
||||||
|
pts_low: Vec<[f32; 3]>,
|
||||||
|
fill_tris: Vec<[f32; 3]>,
|
||||||
|
fill_tris_low: Vec<[f32; 3]>,
|
||||||
|
}
|
||||||
|
let mut bins: Vec<TextBin> = Vec::new();
|
||||||
let mut bin_first: Vec<bool> = Vec::new();
|
let mut bin_first: Vec<bool> = Vec::new();
|
||||||
let find_or_make =
|
let find_or_make =
|
||||||
|key: Option<[f32; 3]>, bins: &mut Vec<(Option<[f32; 3]>, Vec<[f32; 3]>, Vec<[f32; 3]>)>, firsts: &mut Vec<bool>| -> usize {
|
|key: Option<[f32; 3]>, bins: &mut Vec<TextBin>, firsts: &mut Vec<bool>| -> usize {
|
||||||
if let Some(i) = bins.iter().position(|(k, _, _)| *k == key) {
|
if let Some(i) = bins.iter().position(|b| b.color == key) {
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
bins.push((key, Vec::new(), Vec::new()));
|
bins.push(TextBin {
|
||||||
|
color: key,
|
||||||
|
pts: Vec::new(),
|
||||||
|
pts_low: Vec::new(),
|
||||||
|
fill_tris: Vec::new(),
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
firsts.push(true);
|
firsts.push(true);
|
||||||
bins.len() - 1
|
bins.len() - 1
|
||||||
}
|
}
|
||||||
|
|
@ -184,28 +195,34 @@ pub fn tessellate(
|
||||||
let sly_v = (ly_v - ref_ly_v) * anno + ref_ly_v;
|
let sly_v = (ly_v - ref_ly_v) * anno + ref_ly_v;
|
||||||
let bin_key = if split_by_color { group.color } else { None };
|
let bin_key = if split_by_color { group.color } else { None };
|
||||||
let bi = find_or_make(bin_key, &mut bins, &mut bin_first);
|
let bi = find_or_make(bin_key, &mut bins, &mut bin_first);
|
||||||
let (_k, pts, pts_low) = {
|
|
||||||
let b = &mut bins[bi];
|
// 1. Process outline strokes
|
||||||
(&b.0, &mut b.1, &mut b.2)
|
|
||||||
};
|
|
||||||
let _ = _k;
|
|
||||||
for stroke in &group.strokes {
|
for stroke in &group.strokes {
|
||||||
if stroke.len() < 2 {
|
if stroke.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !bin_first[bi] && !pts.is_empty() {
|
if !bin_first[bi] && !bins[bi].pts.is_empty() {
|
||||||
pts.push([f32::NAN, f32::NAN, f32::NAN]);
|
bins[bi].pts.push([f32::NAN, f32::NAN, f32::NAN]);
|
||||||
pts_low.push([0.0; 3]);
|
bins[bi].pts_low.push([0.0; 3]);
|
||||||
}
|
}
|
||||||
bin_first[bi] = false;
|
bin_first[bi] = false;
|
||||||
for &[x, y] in stroke {
|
for &[x, y] in stroke {
|
||||||
let xv = x as f64 * anno + slx_v;
|
let xv = x as f64 * anno + slx_v;
|
||||||
let yv = y as f64 * anno + sly_v;
|
let yv = y as f64 * anno + sly_v;
|
||||||
let (h, l) = split_ds_xyz(xv, yv, elev_v);
|
let (h, l) = split_ds_xyz(xv, yv, elev_v);
|
||||||
pts.push(h);
|
bins[bi].pts.push(h);
|
||||||
pts_low.push(l);
|
bins[bi].pts_low.push(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Process fill triangles
|
||||||
|
for &[x, y] in &group.fill_tris {
|
||||||
|
let xv = x as f64 * anno + slx_v;
|
||||||
|
let yv = y as f64 * anno + sly_v;
|
||||||
|
let (h, l) = split_ds_xyz(xv, yv, elev_v);
|
||||||
|
bins[bi].fill_tris.push(h);
|
||||||
|
bins[bi].fill_tris_low.push(l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let snap_pts = te.snap_pts;
|
let snap_pts = te.snap_pts;
|
||||||
|
|
@ -239,17 +256,17 @@ pub fn tessellate(
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
let bin_count = bins.len();
|
let mut out: Vec<WireModel> = Vec::new();
|
||||||
let mut out: Vec<WireModel> = Vec::with_capacity(bin_count);
|
let mut is_first = true;
|
||||||
for (idx, (override_rgb, pts, pts_low)) in bins.into_iter().enumerate() {
|
for bin in bins {
|
||||||
let wire_color = match override_rgb {
|
let wire_color = match bin.color {
|
||||||
Some([r, g, b]) => [r, g, b, color[3]],
|
Some([r, g, b]) => [r, g, b, color[3]],
|
||||||
None => color,
|
None => color,
|
||||||
};
|
};
|
||||||
// Snap points and key vertices belong to the entity as a
|
|
||||||
// whole — attach them only to the first emitted wire so
|
if !bin.pts.is_empty() {
|
||||||
// pickers / hover don't double-count.
|
let (snap, keys, tangents) = if is_first {
|
||||||
let (snap, keys, tangents) = if idx == 0 {
|
is_first = false;
|
||||||
(
|
(
|
||||||
snap_pts.clone(),
|
snap_pts.clone(),
|
||||||
key_vertices.clone(),
|
key_vertices.clone(),
|
||||||
|
|
@ -260,8 +277,8 @@ pub fn tessellate(
|
||||||
};
|
};
|
||||||
out.push(WireModel {
|
out.push(WireModel {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
points: pts,
|
points: bin.pts,
|
||||||
points_low: pts_low,
|
points_low: bin.pts_low,
|
||||||
color: wire_color,
|
color: wire_color,
|
||||||
selected,
|
selected,
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
|
|
@ -278,6 +295,61 @@ pub fn tessellate(
|
||||||
fill_tris_low: Vec::new(),
|
fill_tris_low: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !bin.fill_tris.is_empty() {
|
||||||
|
let (snap, keys, tangents) = if is_first {
|
||||||
|
is_first = false;
|
||||||
|
(
|
||||||
|
snap_pts.clone(),
|
||||||
|
key_vertices.clone(),
|
||||||
|
te.tangent_geoms.clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
|
out.push(WireModel {
|
||||||
|
name: name.clone(),
|
||||||
|
points: Vec::new(),
|
||||||
|
points_low: Vec::new(),
|
||||||
|
color: wire_color,
|
||||||
|
selected,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px,
|
||||||
|
snap_pts: snap,
|
||||||
|
tangent_geoms: tangents,
|
||||||
|
aci: 0,
|
||||||
|
key_vertices: keys,
|
||||||
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: bin.fill_tris,
|
||||||
|
fill_tris_low: bin.fill_tris_low,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.is_empty() {
|
||||||
|
out.push(WireModel {
|
||||||
|
name,
|
||||||
|
points: Vec::new(),
|
||||||
|
points_low: Vec::new(),
|
||||||
|
color,
|
||||||
|
selected,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px,
|
||||||
|
snap_pts,
|
||||||
|
tangent_geoms: te.tangent_geoms,
|
||||||
|
aci: 0,
|
||||||
|
key_vertices,
|
||||||
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,8 +486,22 @@ pub fn tessellate(
|
||||||
.map(|[x, y, z]| [x, y, z])
|
.map(|[x, y, z]| [x, y, z])
|
||||||
.collect();
|
.collect();
|
||||||
let (fill_tris, fill_tris_low) = points_to_ds(te.fill_tris);
|
let (fill_tris, fill_tris_low) = points_to_ds(te.fill_tris);
|
||||||
return vec![WireModel {
|
let mut out = Vec::new();
|
||||||
name,
|
let mut is_first = true;
|
||||||
|
|
||||||
|
if !local_pts.is_empty() {
|
||||||
|
let (snap, keys, tangents) = if is_first {
|
||||||
|
is_first = false;
|
||||||
|
(
|
||||||
|
snap_pts.clone(),
|
||||||
|
key_vertices.clone(),
|
||||||
|
te.tangent_geoms.clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
|
out.push(WireModel {
|
||||||
|
name: name.clone(),
|
||||||
points: local_pts,
|
points: local_pts,
|
||||||
points_low: local_pts_low,
|
points_low: local_pts_low,
|
||||||
color,
|
color,
|
||||||
|
|
@ -423,6 +509,59 @@ pub fn tessellate(
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
line_weight_px,
|
line_weight_px,
|
||||||
|
snap_pts: snap,
|
||||||
|
tangent_geoms: tangents,
|
||||||
|
aci: 0,
|
||||||
|
key_vertices: keys,
|
||||||
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fill_tris.is_empty() {
|
||||||
|
let (snap, keys, tangents) = if is_first {
|
||||||
|
(
|
||||||
|
snap_pts.clone(),
|
||||||
|
key_vertices.clone(),
|
||||||
|
te.tangent_geoms.clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
|
out.push(WireModel {
|
||||||
|
name: name.clone(),
|
||||||
|
points: Vec::new(),
|
||||||
|
points_low: Vec::new(),
|
||||||
|
color,
|
||||||
|
selected,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px,
|
||||||
|
snap_pts: snap,
|
||||||
|
tangent_geoms: tangents,
|
||||||
|
aci: 0,
|
||||||
|
key_vertices: keys,
|
||||||
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris,
|
||||||
|
fill_tris_low,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.is_empty() {
|
||||||
|
out.push(WireModel {
|
||||||
|
name,
|
||||||
|
points: Vec::new(),
|
||||||
|
points_low: Vec::new(),
|
||||||
|
color,
|
||||||
|
selected,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px,
|
||||||
snap_pts,
|
snap_pts,
|
||||||
tangent_geoms: te.tangent_geoms,
|
tangent_geoms: te.tangent_geoms,
|
||||||
aci: 0,
|
aci: 0,
|
||||||
|
|
@ -430,9 +569,12 @@ pub fn tessellate(
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
plinegen: true,
|
plinegen: true,
|
||||||
vp_scissor: None,
|
vp_scissor: None,
|
||||||
fill_tris,
|
fill_tris: vec![],
|
||||||
fill_tris_low,
|
fill_tris_low: Vec::new(),
|
||||||
}];
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
TruckObject::SegmentedLines(points) => {
|
TruckObject::SegmentedLines(points) => {
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
@ -401,7 +405,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
@ -457,7 +465,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
@ -546,7 +558,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
@ -600,7 +616,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
@ -643,7 +663,11 @@ impl Pipeline {
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
count: MSAA_SAMPLES,
|
count: MSAA_SAMPLES,
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ pub fn apply_along(
|
||||||
let insert = offset_pt(insert, fwd, perp, *x, *y);
|
let insert = offset_pt(insert, fwd, perp, *x, *y);
|
||||||
let fwd_angle = fwd[1].atan2(fwd[0]) + rot_deg.to_radians();
|
let fwd_angle = fwd[1].atan2(fwd[0]) + rot_deg.to_radians();
|
||||||
let resolved = resolve_dxf_special_chars(text);
|
let resolved = resolve_dxf_special_chars(text);
|
||||||
let text_strokes = lff::tessellate_text_ex(
|
let (text_strokes, _) = lff::tessellate_text_ex(
|
||||||
[insert[0], insert[1]],
|
[insert[0], insert[1]],
|
||||||
*tx_scale,
|
*tx_scale,
|
||||||
fwd_angle,
|
fwd_angle,
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,17 @@ impl Face {
|
||||||
/// Resolve a style's font name to a concrete face. Embedded stroke fonts
|
/// Resolve a style's font name to a concrete face. Embedded stroke fonts
|
||||||
/// take priority; only otherwise-unknown names try the system fonts.
|
/// take priority; only otherwise-unknown names try the system fonts.
|
||||||
pub fn resolve(font_name: &str) -> Face {
|
pub fn resolve(font_name: &str) -> Face {
|
||||||
if !lff::is_builtin(font_name) && sysfont::has_family(font_name) {
|
let is_builtin = lff::is_builtin(font_name);
|
||||||
let word = ttf_glyph::glyph(font_name, ' ')
|
let has_sys = sysfont::has_family(font_name);
|
||||||
|
if !is_builtin && has_sys {
|
||||||
|
let canonical = sysfont::canonical_family_name(font_name).unwrap_or_else(|| font_name.to_string());
|
||||||
|
let word = ttf_glyph::glyph(&canonical, ' ')
|
||||||
.map(|g| g.advance)
|
.map(|g| g.advance)
|
||||||
// Fall back to a sensible blank-width if the font has no space.
|
// Fall back to a sensible blank-width if the font has no space.
|
||||||
.filter(|w| *w > 0.0)
|
.filter(|w| *w > 0.0)
|
||||||
.unwrap_or(4.5);
|
.unwrap_or(4.5);
|
||||||
return Face::Ttf {
|
return Face::Ttf {
|
||||||
family: font_name.to_string(),
|
family: canonical,
|
||||||
word,
|
word,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +133,7 @@ mod tests {
|
||||||
.find(|f| ttf_glyph::glyph(f, 'A').is_some())
|
.find(|f| ttf_glyph::glyph(f, 'A').is_some())
|
||||||
.expect("a system family with an 'A'");
|
.expect("a system family with an 'A'");
|
||||||
assert!(matches!(Face::resolve(fam), Face::Ttf { .. }));
|
assert!(matches!(Face::resolve(fam), Face::Ttf { .. }));
|
||||||
let strokes =
|
let (strokes, _) =
|
||||||
lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "ABC");
|
lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "ABC");
|
||||||
assert!(!strokes.is_empty(), "TTF run produced no strokes");
|
assert!(!strokes.is_empty(), "TTF run produced no strokes");
|
||||||
}
|
}
|
||||||
|
|
@ -168,8 +171,8 @@ mod tests {
|
||||||
.expect("a system family with an 'A'");
|
.expect("a system family with an 'A'");
|
||||||
// Underlined shaped text: glyph contours plus exactly one underline
|
// Underlined shaped text: glyph contours plus exactly one underline
|
||||||
// segment (a 2-point polyline) emitted on the \l toggle.
|
// segment (a 2-point polyline) emitted on the \l toggle.
|
||||||
let plain = lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "AB");
|
let (plain, _) = lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "AB");
|
||||||
let deco = lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "\\LAB\\l");
|
let (deco, _) = lff::tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, fam, "\\LAB\\l");
|
||||||
assert!(!plain.is_empty());
|
assert!(!plain.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
deco.len(),
|
deco.len(),
|
||||||
|
|
@ -177,4 +180,32 @@ mod tests {
|
||||||
"underline should add exactly one segment on the TTF path"
|
"underline should add exactly one segment on the TTF path"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn case_insensitive_ttf_resolution() {
|
||||||
|
let fams = sysfont::families();
|
||||||
|
if fams.is_empty() {
|
||||||
|
eprintln!("no system fonts; skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arial and Cambria are typical on Windows/Mac/Linux
|
||||||
|
for test_name in &["arial", "ARIAL", "ARIALN", "cambria"] {
|
||||||
|
let resolved = Face::resolve(test_name);
|
||||||
|
match resolved {
|
||||||
|
Face::Ttf { family, .. } => {
|
||||||
|
assert!(
|
||||||
|
family == "Arial" || family == "Arial Narrow" || family == "Cambria",
|
||||||
|
"Resolved to unexpected family name: {}", family
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Face::Lff(_) => {
|
||||||
|
// It's possible some test environments don't have Arial or Cambria,
|
||||||
|
// but if they are present in sysfont::families() (or we can fallback),
|
||||||
|
// they should resolve to Ttf. If they aren't installed, Lff fallback is accepted.
|
||||||
|
eprintln!("Font {} resolved to Lff (probably not installed)", test_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ pub struct Glyph {
|
||||||
pub strokes: Vec<Vec<[f32; 2]>>,
|
pub strokes: Vec<Vec<[f32; 2]>>,
|
||||||
/// Advance width in glyph units (rightmost X of all strokes).
|
/// Advance width in glyph units (rightmost X of all strokes).
|
||||||
pub advance: f32,
|
pub advance: f32,
|
||||||
|
pub fill_tris: Vec<[f32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A parsed LFF font.
|
/// A parsed LFF font.
|
||||||
|
|
@ -329,7 +330,7 @@ pub fn tessellate_text_ex(
|
||||||
oblique_angle: f32,
|
oblique_angle: f32,
|
||||||
font_name: &str,
|
font_name: &str,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Vec<Vec<[f32; 2]>> {
|
) -> (Vec<Vec<[f32; 2]>>, Vec<[f32; 2]>) {
|
||||||
tessellate_text_run(
|
tessellate_text_run(
|
||||||
origin,
|
origin,
|
||||||
height,
|
height,
|
||||||
|
|
@ -352,9 +353,9 @@ pub fn tessellate_text_run(
|
||||||
tracking: f32,
|
tracking: f32,
|
||||||
font_name: &str,
|
font_name: &str,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Vec<Vec<[f32; 2]>> {
|
) -> (Vec<Vec<[f32; 2]>>, Vec<[f32; 2]>) {
|
||||||
if text.is_empty() || height <= 0.0 {
|
if text.is_empty() || height <= 0.0 {
|
||||||
return vec![];
|
return (vec![], vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let face = crate::scene::text::font_face::Face::resolve(font_name);
|
let face = crate::scene::text::font_face::Face::resolve(font_name);
|
||||||
|
|
@ -377,6 +378,7 @@ pub fn tessellate_text_run(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
|
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
|
||||||
|
let mut fill_tris: Vec<[f32; 2]> = Vec::new();
|
||||||
let mut cursor_x: f32 = 0.0;
|
let mut cursor_x: f32 = 0.0;
|
||||||
let mut underline: Option<f32> = None;
|
let mut underline: Option<f32> = None;
|
||||||
let mut overline: Option<f32> = None;
|
let mut overline: Option<f32> = None;
|
||||||
|
|
@ -399,10 +401,16 @@ pub fn tessellate_text_run(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let emit_fill = |fill_tris: &mut Vec<[f32; 2]>, tris: &[[f32; 2]], cx: f32| {
|
||||||
|
for &v in tris {
|
||||||
|
fill_tris.push(xform(v[0], v[1], cx));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Flush a buffered TTF segment: shape it, emit the positioned glyph
|
// Flush a buffered TTF segment: shape it, emit the positioned glyph
|
||||||
// contours, and advance the pen by the shaped run width. Falls back to
|
// contours, and advance the pen by the shaped run width. Falls back to
|
||||||
// per-glyph outlines if shaping is unavailable.
|
// per-glyph outlines if shaping is unavailable.
|
||||||
let flush_ttf = |seg: &mut String, cursor_x: &mut f32, out: &mut Vec<Vec<[f32; 2]>>| {
|
let flush_ttf = |seg: &mut String, cursor_x: &mut f32, out: &mut Vec<Vec<[f32; 2]>>, fill_tris: &mut Vec<[f32; 2]>| {
|
||||||
if seg.is_empty() {
|
if seg.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -410,6 +418,7 @@ pub fn tessellate_text_run(
|
||||||
if let Some(run) = crate::scene::text::ttf_glyph::shape_run(family, seg) {
|
if let Some(run) = crate::scene::text::ttf_glyph::shape_run(family, seg) {
|
||||||
for g in &run.glyphs {
|
for g in &run.glyphs {
|
||||||
emit_glyph(out, &g.strokes, *cursor_x);
|
emit_glyph(out, &g.strokes, *cursor_x);
|
||||||
|
emit_fill(fill_tris, &g.fill_tris, *cursor_x);
|
||||||
}
|
}
|
||||||
*cursor_x += run.advance * wf;
|
*cursor_x += run.advance * wf;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -417,6 +426,7 @@ pub fn tessellate_text_run(
|
||||||
match face.glyph(ch) {
|
match face.glyph(ch) {
|
||||||
Some(glyph) => {
|
Some(glyph) => {
|
||||||
emit_glyph(out, &glyph.strokes, *cursor_x);
|
emit_glyph(out, &glyph.strokes, *cursor_x);
|
||||||
|
emit_fill(fill_tris, &glyph.fill_tris, *cursor_x);
|
||||||
*cursor_x += (glyph.advance + face.letter_spacing() * tracking) * wf;
|
*cursor_x += (glyph.advance + face.letter_spacing() * tracking) * wf;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -435,7 +445,7 @@ pub fn tessellate_text_run(
|
||||||
// decoration toggle, end of run) flushes the buffer first so pen
|
// decoration toggle, end of run) flushes the buffer first so pen
|
||||||
// positions stay correct for decorations.
|
// positions stay correct for decorations.
|
||||||
if ttf_family.is_some() && !matches!(tok, Tok::Glyph(_)) {
|
if ttf_family.is_some() && !matches!(tok, Tok::Glyph(_)) {
|
||||||
flush_ttf(&mut seg, &mut cursor_x, &mut out);
|
flush_ttf(&mut seg, &mut cursor_x, &mut out, &mut fill_tris);
|
||||||
}
|
}
|
||||||
match tok {
|
match tok {
|
||||||
Tok::Glyph(c) => {
|
Tok::Glyph(c) => {
|
||||||
|
|
@ -445,6 +455,7 @@ pub fn tessellate_text_run(
|
||||||
match face.glyph(*c) {
|
match face.glyph(*c) {
|
||||||
Some(glyph) => {
|
Some(glyph) => {
|
||||||
emit_glyph(&mut out, &glyph.strokes, cursor_x);
|
emit_glyph(&mut out, &glyph.strokes, cursor_x);
|
||||||
|
emit_fill(&mut fill_tris, &glyph.fill_tris, cursor_x);
|
||||||
cursor_x += (glyph.advance + face.letter_spacing() * tracking) * wf;
|
cursor_x += (glyph.advance + face.letter_spacing() * tracking) * wf;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -487,7 +498,7 @@ pub fn tessellate_text_run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ttf_family.is_some() {
|
if ttf_family.is_some() {
|
||||||
flush_ttf(&mut seg, &mut cursor_x, &mut out);
|
flush_ttf(&mut seg, &mut cursor_x, &mut out, &mut fill_tris);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(start) = underline {
|
if let Some(start) = underline {
|
||||||
|
|
@ -500,7 +511,7 @@ pub fn tessellate_text_run(
|
||||||
out.push(vec![xform(start, STRIKE_Y, 0.0), xform(cursor_x, STRIKE_Y, 0.0)]);
|
out.push(vec![xform(start, STRIKE_Y, 0.0), xform(cursor_x, STRIKE_Y, 0.0)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
out
|
(out, fill_tris)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Parser ────────────────────────────────────────────────────────────────
|
// ── Parser ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -641,11 +652,11 @@ fn parse_lff(src: &str) -> Font {
|
||||||
};
|
};
|
||||||
for (c, g) in raw {
|
for (c, g) in raw {
|
||||||
let advance = advance_of(&g.strokes);
|
let advance = advance_of(&g.strokes);
|
||||||
font.glyphs.insert(c, Glyph { strokes: g.strokes, advance });
|
font.glyphs.insert(c, Glyph { strokes: g.strokes, advance, fill_tris: Vec::new() });
|
||||||
}
|
}
|
||||||
for (n, g) in raw_shapes {
|
for (n, g) in raw_shapes {
|
||||||
let advance = advance_of(&g.strokes);
|
let advance = advance_of(&g.strokes);
|
||||||
font.shapes.insert(n, Glyph { strokes: g.strokes, advance });
|
font.shapes.insert(n, Glyph { strokes: g.strokes, advance, fill_tris: Vec::new() });
|
||||||
}
|
}
|
||||||
font
|
font
|
||||||
}
|
}
|
||||||
|
|
@ -735,7 +746,7 @@ mod tests {
|
||||||
("Hello, World!", 15, 113, 5078.5248),
|
("Hello, World!", 15, 113, 5078.5248),
|
||||||
];
|
];
|
||||||
for &(t, segs, verts, sum_ref) in cases {
|
for &(t, segs, verts, sum_ref) in cases {
|
||||||
let st = tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, "txt", t);
|
let (st, _) = tessellate_text_ex([0.0, 0.0], 10.0, 0.0, 1.0, 0.0, "txt", t);
|
||||||
let nv: usize = st.iter().map(|s| s.len()).sum();
|
let nv: usize = st.iter().map(|s| s.len()).sum();
|
||||||
let sum: f64 = st
|
let sum: f64 = st
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -780,7 +791,7 @@ mod tests {
|
||||||
assert!(!is_builtin("amiri-regular"));
|
assert!(!is_builtin("amiri-regular"));
|
||||||
assert!(get_font("kochigothic").glyph('A').is_some());
|
assert!(get_font("kochigothic").glyph('A').is_some());
|
||||||
// Unicode fallback covers a non-ASCII letter via the renderer path.
|
// Unicode fallback covers a non-ASCII letter via the renderer path.
|
||||||
let strokes = tessellate_text_run([0.0, 0.0], 2.5, 0.0, 1.0, 0.0, 1.0, "Standard", "Aб");
|
let (strokes, _) = tessellate_text_run([0.0, 0.0], 2.5, 0.0, 1.0, 0.0, 1.0, "Standard", "Aб");
|
||||||
assert!(!strokes.is_empty());
|
assert!(!strokes.is_empty());
|
||||||
// The bulge belongs to the segment ENDING at the vertex (LibreCAD
|
// The bulge belongs to the segment ENDING at the vertex (LibreCAD
|
||||||
// convention): the standard/iso/unicode 'O' must come out as an
|
// convention): the standard/iso/unicode 'O' must come out as an
|
||||||
|
|
@ -803,7 +814,7 @@ mod tests {
|
||||||
// Turkish letters absent from simplex/unicode still render via the
|
// Turkish letters absent from simplex/unicode still render via the
|
||||||
// iso3098 fallback (ı/U+0131 is the one exception iso3098 lacks).
|
// iso3098 fallback (ı/U+0131 is the one exception iso3098 lacks).
|
||||||
for ch in ['Ğ', 'ş', 'İ', 'Ş', 'ğ'] {
|
for ch in ['Ğ', 'ş', 'İ', 'Ş', 'ğ'] {
|
||||||
let s = tessellate_text_run(
|
let (s, _) = tessellate_text_run(
|
||||||
[0.0, 0.0],
|
[0.0, 0.0],
|
||||||
2.5,
|
2.5,
|
||||||
0.0,
|
0.0,
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,60 @@ pub fn families() -> &'static [String] {
|
||||||
&fonts().families
|
&fonts().families
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a requested family name to the canonical installed system family name (with exact case).
|
||||||
|
pub fn canonical_family_name(family: &str) -> Option<String> {
|
||||||
|
let db = &fonts().db;
|
||||||
|
|
||||||
|
// 1. Try exact match first
|
||||||
|
let query = fontdb::Query {
|
||||||
|
families: &[fontdb::Family::Name(family)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if db.query(&query).is_some() {
|
||||||
|
if let Some(canonical) = fonts().families.iter().find(|&f| f.eq_ignore_ascii_case(family)) {
|
||||||
|
return Some(canonical.clone());
|
||||||
|
}
|
||||||
|
return Some(family.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try case-insensitive match on the families we have
|
||||||
|
if let Some(matched) = fonts().families.iter().find(|&f| f.eq_ignore_ascii_case(family)) {
|
||||||
|
return Some(matched.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match common prefixes / variations
|
||||||
|
let family_lower = family.to_lowercase();
|
||||||
|
let alias = match family_lower.as_str() {
|
||||||
|
"arialn" => Some("Arial Narrow"),
|
||||||
|
"gothic" => Some("Century Gothic"),
|
||||||
|
"times" => Some("Times New Roman"),
|
||||||
|
"cour" => Some("Courier New"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(alias_name) = alias {
|
||||||
|
if let Some(matched) = fonts().families.iter().find(|&f| f.eq_ignore_ascii_case(alias_name)) {
|
||||||
|
return Some(matched.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try matching prefix/subset case-insensitively
|
||||||
|
if let Some(matched) = fonts().families.iter().find(|&f| {
|
||||||
|
let f_low = f.to_lowercase();
|
||||||
|
f_low.starts_with(&family_lower) || family_lower.starts_with(&f_low)
|
||||||
|
}) {
|
||||||
|
return Some(matched.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a family name to a concrete face id (regular weight/style).
|
/// Resolve a family name to a concrete face id (regular weight/style).
|
||||||
fn face_id(family: &str) -> Option<fontdb::ID> {
|
fn face_id(family: &str) -> Option<fontdb::ID> {
|
||||||
let db = &fonts().db;
|
let db = &fonts().db;
|
||||||
|
let canonical = canonical_family_name(family)?;
|
||||||
let query = fontdb::Query {
|
let query = fontdb::Query {
|
||||||
families: &[fontdb::Family::Name(family)],
|
families: &[fontdb::Family::Name(&canonical)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
db.query(&query)
|
db.query(&query)
|
||||||
|
|
@ -61,3 +110,4 @@ pub fn with_face_data<T>(family: &str, f: impl FnOnce(&[u8], u32) -> T) -> Optio
|
||||||
pub fn has_family(family: &str) -> bool {
|
pub fn has_family(family: &str) -> bool {
|
||||||
face_id(family).is_some()
|
face_id(family).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ use crate::scene::text::sysfont;
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use lyon_tessellation::math::point;
|
||||||
|
use lyon_tessellation::path::Path;
|
||||||
|
use lyon_tessellation::{FillTessellator, FillOptions, BuffersBuilder, VertexBuffers, FillVertex};
|
||||||
|
|
||||||
/// Bézier flattening step counts. Outlines are small on screen most of the
|
/// Bézier flattening step counts. Outlines are small on screen most of the
|
||||||
/// time; these are a fixed budget that keeps curves smooth without exploding
|
/// time; these are a fixed budget that keeps curves smooth without exploding
|
||||||
/// vertex counts. Cubic gets more steps because OTF/CFF curves swing wider.
|
/// vertex counts. Cubic gets more steps because OTF/CFF curves swing wider.
|
||||||
|
|
@ -156,10 +160,12 @@ pub fn glyph(family: &str, ch: char) -> Option<Arc<Glyph>> {
|
||||||
// A glyph with no outline (e.g. space) still has a valid advance.
|
// A glyph with no outline (e.g. space) still has a valid advance.
|
||||||
face.outline_glyph(gid, &mut fl);
|
face.outline_glyph(gid, &mut fl);
|
||||||
fl.flush();
|
fl.flush();
|
||||||
|
let fill_tris = triangulate_contours(&fl.contours);
|
||||||
|
|
||||||
Some(Arc::new(Glyph {
|
Some(Arc::new(Glyph {
|
||||||
strokes: fl.contours,
|
strokes: fl.contours,
|
||||||
advance,
|
advance,
|
||||||
|
fill_tris,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
@ -180,6 +186,46 @@ fn cap_scale(face: &ttf_parser::Face) -> f32 {
|
||||||
CAP_UNITS / cap
|
CAP_UNITS / cap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn triangulate_contours(contours: &[Vec<[f32; 2]>]) -> Vec<[f32; 2]> {
|
||||||
|
if contours.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = Path::builder();
|
||||||
|
for contour in contours {
|
||||||
|
if contour.len() < 3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
builder.begin(point(contour[0][0], contour[0][1]));
|
||||||
|
for p in &contour[1..] {
|
||||||
|
builder.line_to(point(p[0], p[1]));
|
||||||
|
}
|
||||||
|
builder.end(true);
|
||||||
|
}
|
||||||
|
let path = builder.build();
|
||||||
|
|
||||||
|
let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
|
||||||
|
let mut tessellator = FillTessellator::new();
|
||||||
|
if let Err(e) = tessellator.tessellate_path(
|
||||||
|
&path,
|
||||||
|
&FillOptions::default(),
|
||||||
|
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
|
||||||
|
vertex.position().to_array()
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
eprintln!("[ttf_glyph] Tessellation error: {:?}", e);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tris = Vec::with_capacity(geometry.indices.len());
|
||||||
|
for &idx in &geometry.indices {
|
||||||
|
if let Some(&p) = geometry.vertices.get(idx as usize) {
|
||||||
|
tris.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tris
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shaping ────────────────────────────────────────────────────────────────
|
// ── Shaping ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// One shaped glyph, positioned within its run. Strokes are in 9-unit space and
|
/// One shaped glyph, positioned within its run. Strokes are in 9-unit space and
|
||||||
|
|
@ -187,6 +233,7 @@ fn cap_scale(face: &ttf_parser::Face) -> f32 {
|
||||||
/// so the caller only applies the run's own transform.
|
/// so the caller only applies the run's own transform.
|
||||||
pub struct PlacedGlyph {
|
pub struct PlacedGlyph {
|
||||||
pub strokes: Vec<Vec<[f32; 2]>>,
|
pub strokes: Vec<Vec<[f32; 2]>>,
|
||||||
|
pub fill_tris: Vec<[f32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fully shaped run.
|
/// A fully shaped run.
|
||||||
|
|
@ -289,9 +336,11 @@ fn build_fallback(ch: char) -> Option<Arc<Glyph>> {
|
||||||
if fl.contours.is_empty() {
|
if fl.contours.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let fill_tris = triangulate_contours(&fl.contours);
|
||||||
Some(Arc::new(Glyph {
|
Some(Arc::new(Glyph {
|
||||||
strokes: fl.contours,
|
strokes: fl.contours,
|
||||||
advance,
|
advance,
|
||||||
|
fill_tris,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,9 +370,11 @@ fn build_fallback(ch: char) -> Option<Arc<Glyph>> {
|
||||||
let mut fl = OutlineFlattener::new(k);
|
let mut fl = OutlineFlattener::new(k);
|
||||||
face.outline_glyph(gid, &mut fl);
|
face.outline_glyph(gid, &mut fl);
|
||||||
fl.flush();
|
fl.flush();
|
||||||
|
let fill_tris = triangulate_contours(&fl.contours);
|
||||||
return Some(Arc::new(Glyph {
|
return Some(Arc::new(Glyph {
|
||||||
strokes: fl.contours,
|
strokes: fl.contours,
|
||||||
advance,
|
advance,
|
||||||
|
fill_tris,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,8 +442,10 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
||||||
face.outline_glyph(ttf_parser::GlyphId(g.glyph_id), &mut fl);
|
face.outline_glyph(ttf_parser::GlyphId(g.glyph_id), &mut fl);
|
||||||
fl.flush();
|
fl.flush();
|
||||||
if !fl.contours.is_empty() {
|
if !fl.contours.is_empty() {
|
||||||
|
let fill_tris = triangulate_contours(&fl.contours);
|
||||||
glyphs.push(PlacedGlyph {
|
glyphs.push(PlacedGlyph {
|
||||||
strokes: fl.contours,
|
strokes: fl.contours,
|
||||||
|
fill_tris,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,60 @@ pub struct OstTrackPoint {
|
||||||
pub screen: Point,
|
pub screen: Point,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grid_overlay<'a>(
|
||||||
|
grid: Vec<GridParams>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
canvas(GridCanvas { grid })
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GridCanvas {
|
||||||
|
grid: Vec<GridParams>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl canvas::Program<Message> for GridCanvas {
|
||||||
|
type State = ();
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &(),
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Vec<canvas::Geometry> {
|
||||||
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
|
||||||
|
for g in &self.grid {
|
||||||
|
let gb = g.bounds;
|
||||||
|
let cx0 = gb.x.max(0.0);
|
||||||
|
let cy0 = gb.y.max(0.0);
|
||||||
|
let cx1 = (gb.x + gb.width).min(bounds.width);
|
||||||
|
let cy1 = (gb.y + gb.height).min(bounds.height);
|
||||||
|
if cx1 <= cx0 || cy1 <= cy0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let clip = iced::Rectangle {
|
||||||
|
x: cx0,
|
||||||
|
y: cy0,
|
||||||
|
width: cx1 - cx0,
|
||||||
|
height: cy1 - cy0,
|
||||||
|
};
|
||||||
|
frame.with_clip(clip, |f| {
|
||||||
|
draw_grid(f, g.view_rot, g.eye, g.plane, gb, g.origin, g.axes)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![frame.into_geometry()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection_overlay<'a>(
|
pub fn selection_overlay<'a>(
|
||||||
selection: SelectionState,
|
selection: SelectionState,
|
||||||
snap: Option<(Point, SnapType)>,
|
snap: Option<(Point, SnapType)>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
grid: Vec<GridParams>,
|
|
||||||
ucs_icon: Option<UcsIconParams>,
|
ucs_icon: Option<UcsIconParams>,
|
||||||
ost_points: Vec<OstTrackPoint>,
|
ost_points: Vec<OstTrackPoint>,
|
||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
|
|
@ -135,7 +184,6 @@ pub fn selection_overlay<'a>(
|
||||||
selection,
|
selection,
|
||||||
snap,
|
snap,
|
||||||
grips,
|
grips,
|
||||||
grid,
|
|
||||||
ucs_icon,
|
ucs_icon,
|
||||||
ost_points,
|
ost_points,
|
||||||
cursor_screen,
|
cursor_screen,
|
||||||
|
|
@ -152,7 +200,6 @@ struct SelectionCanvas {
|
||||||
selection: SelectionState,
|
selection: SelectionState,
|
||||||
snap: Option<(Point, SnapType)>,
|
snap: Option<(Point, SnapType)>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
grid: Vec<GridParams>,
|
|
||||||
ucs_icon: Option<UcsIconParams>,
|
ucs_icon: Option<UcsIconParams>,
|
||||||
ost_points: Vec<OstTrackPoint>,
|
ost_points: Vec<OstTrackPoint>,
|
||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
|
|
@ -313,32 +360,6 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid display ──────────────────────────────────────────────────
|
|
||||||
// One entry per pane with its grid on. Each is clipped to its own
|
|
||||||
// rectangle so lines never spill into neighbouring panes — and the clip
|
|
||||||
// is intersected with the widget so a zoomed-in viewport whose rectangle
|
|
||||||
// overflows the canvas never paints grid outside the 3-D view.
|
|
||||||
for g in &self.grid {
|
|
||||||
let gb = g.bounds;
|
|
||||||
let cx0 = gb.x.max(0.0);
|
|
||||||
let cy0 = gb.y.max(0.0);
|
|
||||||
let cx1 = (gb.x + gb.width).min(bounds.width);
|
|
||||||
let cy1 = (gb.y + gb.height).min(bounds.height);
|
|
||||||
if cx1 <= cx0 || cy1 <= cy0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let clip = iced::Rectangle {
|
|
||||||
x: cx0,
|
|
||||||
y: cy0,
|
|
||||||
width: cx1 - cx0,
|
|
||||||
height: cy1 - cy0,
|
|
||||||
};
|
|
||||||
// Project with the full viewport rect `gb` so the grid stays aligned;
|
|
||||||
// only the clip is clamped.
|
|
||||||
frame.with_clip(clip, |f| {
|
|
||||||
draw_grid(f, g.view_rot, g.eye, g.plane, gb, g.origin, g.axes)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
|
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
|
||||||
let (fill, stroke) = if self.selection.box_crossing {
|
let (fill, stroke) = if self.selection.box_crossing {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue