fix(scene): per-line text greek honors word-wrap subline count
`text_obb_corners_native` derived MText line count from explicit `\n`
matches in `m.value`, so any MText that relied on `\P`/`\N` breaks or
word-wrap (rectangle_width-driven) reported a single line. The greek
emitter then split the OBB into one row and the user saw a single box
on top of a 5-line wrapped paragraph.
- text_support: new `mtext_line_count` — strip codes, split on
\n/\P/\N, word-wrap each paragraph against `rectangle_width`,
sum. Same line splitting the MText/MultiLeader renderers already
perform; centralized for reuse.
- text_obb_corners_native: takes `mtext_lines_override: Option<usize>`.
When supplied, OBB height = h_world × n × line_spacing_factor; this
also overrides a possibly stale `rectangle_height` on resaved DWGs.
- Top-level greek paths (scene/mod.rs) compute the line count via
`mtext_line_count` and pass it through to baseline / rect emission.
- block_cache: tessellate_sub_local computes the count at defn build
so the stored `text_obb_local` is wrap-aware; emit_text_baseline
and emit_greeked_text recover n_lines from `ulen / h_local` as
before and now produce the correct per-row split inside Inserts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ddd0a2de16
commit
f4ae0e7773
3 changed files with 274 additions and 80 deletions
|
|
@ -341,6 +341,35 @@ pub fn measure_mtext_chars(text: &str, scale: f32, font: &cxf::CxfFile) -> f32 {
|
||||||
width
|
width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Total number of visible lines an MText renders to — explicit `\P` /
|
||||||
|
/// `\n` / `\N` breaks plus word-wrap induced sublines when
|
||||||
|
/// `rectangle_width > 0`. Mirrors the line-splitting `entities/mtext.rs`
|
||||||
|
/// and `tessellate_multileader` perform; lets renderers split the OBB
|
||||||
|
/// into per-row LOD primitives without re-running the wrap measurement.
|
||||||
|
pub fn mtext_line_count(
|
||||||
|
m: &acadrust::entities::MText,
|
||||||
|
document: &CadDocument,
|
||||||
|
anno_scale: f32,
|
||||||
|
) -> usize {
|
||||||
|
let resolved = resolve_text_style(&m.style, document);
|
||||||
|
let font = cxf::get_font(&resolved.font_name);
|
||||||
|
let width_factor = resolved.width_factor.max(0.01);
|
||||||
|
let height = m.height as f32 * anno_scale;
|
||||||
|
let plain = strip_mtext_codes(&m.value);
|
||||||
|
let explicit = split_mtext_lines(&plain);
|
||||||
|
let total: usize = if m.rectangle_width > 0.0 && height > 0.0 {
|
||||||
|
let scale = height / 9.0 * width_factor;
|
||||||
|
let max_w = m.rectangle_width as f32 * anno_scale;
|
||||||
|
explicit
|
||||||
|
.iter()
|
||||||
|
.map(|l| word_wrap(l, max_w, scale, font).len())
|
||||||
|
.sum()
|
||||||
|
} else {
|
||||||
|
explicit.len()
|
||||||
|
};
|
||||||
|
total.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn word_wrap(text: &str, max_w: f32, scale: f32, font: &'static cxf::CxfFile) -> Vec<String> {
|
pub fn word_wrap(text: &str, max_w: f32, scale: f32, font: &'static cxf::CxfFile) -> Vec<String> {
|
||||||
if max_w <= 0.0 || text.is_empty() {
|
if max_w <= 0.0 || text.is_empty() {
|
||||||
return vec![text.to_string()];
|
return vec![text.to_string()];
|
||||||
|
|
|
||||||
|
|
@ -423,9 +423,17 @@ fn tessellate_sub_local(
|
||||||
// so the greek path can emit a rect that matches the entity's rotation
|
// so the greek path can emit a rect that matches the entity's rotation
|
||||||
// instead of falling back to the axis-aligned `aabb_local`. Subtract the
|
// instead of falling back to the axis-aligned `aabb_local`. Subtract the
|
||||||
// defn's `local_offset` in f64 first so distant text retains precision.
|
// defn's `local_offset` in f64 first so distant text retains precision.
|
||||||
|
// For MText the OBB height is built from the wrap-expanded line count
|
||||||
|
// so emit_greeked_text can split it into per-line rows.
|
||||||
let [lo_x, lo_y, lo_z] = local_offset;
|
let [lo_x, lo_y, lo_z] = local_offset;
|
||||||
|
let mtext_lines = match sub {
|
||||||
|
EntityType::MText(m) => Some(
|
||||||
|
crate::entities::text_support::mtext_line_count(m, doc, anno_scale),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
let text_obb_local: Option<[[f32; 3]; 4]> =
|
let text_obb_local: Option<[[f32; 3]; 4]> =
|
||||||
crate::scene::text_obb_corners_native(sub, anno_scale).map(|c| {
|
crate::scene::text_obb_corners_native(sub, anno_scale, mtext_lines).map(|c| {
|
||||||
[
|
[
|
||||||
[(c[0][0] - lo_x) as f32, (c[0][1] - lo_y) as f32, (c[0][2] - lo_z) as f32],
|
[(c[0][0] - lo_x) as f32, (c[0][1] - lo_y) as f32, (c[0][2] - lo_z) as f32],
|
||||||
[(c[1][0] - lo_x) as f32, (c[1][1] - lo_y) as f32, (c[1][2] - lo_z) as f32],
|
[(c[1][0] - lo_x) as f32, (c[1][1] - lo_y) as f32, (c[1][2] - lo_z) as f32],
|
||||||
|
|
@ -656,46 +664,69 @@ fn emit_greeked_text(
|
||||||
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
||||||
};
|
};
|
||||||
|
|
||||||
let corners: Option<[[f32; 3]; 4]> = if let (Some(obb), Some(h_local)) =
|
// Per-line rects: split the OBB by `h_local` so each visible wrap
|
||||||
|
// line gets its own box. Falls back to the axis-aligned AABB when no
|
||||||
|
// OBB / height was cached.
|
||||||
|
let tris: Vec<[f32; 3]> = if let (Some(obb), Some(h_local)) =
|
||||||
(lw.text_obb_local, lw.text_height_local)
|
(lw.text_obb_local, lw.text_height_local)
|
||||||
{
|
{
|
||||||
if h_local <= 0.0 {
|
if h_local <= 0.0 {
|
||||||
None
|
return;
|
||||||
} else {
|
|
||||||
// OBB[3] - OBB[0] = up direction in local frame, full-block
|
|
||||||
// height for MText; normalize and rescale to a single line.
|
|
||||||
let dx = obb[3][0] - obb[0][0];
|
|
||||||
let dy = obb[3][1] - obb[0][1];
|
|
||||||
let dz = obb[3][2] - obb[0][2];
|
|
||||||
let dlen = (dx * dx + dy * dy + dz * dz).sqrt();
|
|
||||||
if dlen < 1e-9 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let nx = dx / dlen * h_local;
|
|
||||||
let ny = dy / dlen * h_local;
|
|
||||||
let nz = dz / dlen * h_local;
|
|
||||||
let bl = obb[0];
|
|
||||||
let br = obb[1];
|
|
||||||
let tl = [bl[0] + nx, bl[1] + ny, bl[2] + nz];
|
|
||||||
let tr = [br[0] + nx, br[1] + ny, br[2] + nz];
|
|
||||||
Some([xf(bl), xf(br), xf(tr), xf(tl)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let (ux, uy, uz) = (
|
||||||
|
obb[3][0] - obb[0][0],
|
||||||
|
obb[3][1] - obb[0][1],
|
||||||
|
obb[3][2] - obb[0][2],
|
||||||
|
);
|
||||||
|
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||||
|
if ulen < 1e-9 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n_lines = ((ulen / h_local).round() as usize).max(1);
|
||||||
|
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
||||||
|
let bl_local = obb[0];
|
||||||
|
let br_local = obb[1];
|
||||||
|
let mut acc = Vec::with_capacity(n_lines * 6);
|
||||||
|
for i in 0..n_lines {
|
||||||
|
let top_off = (i as f32) * h_local;
|
||||||
|
let bot_off = ((i + 1) as f32) * h_local;
|
||||||
|
let top_along = (ulen - top_off).max(0.0);
|
||||||
|
let bot_along = (ulen - bot_off).max(0.0);
|
||||||
|
let tl = xf([
|
||||||
|
bl_local[0] + nx * top_along,
|
||||||
|
bl_local[1] + ny * top_along,
|
||||||
|
bl_local[2] + nz * top_along,
|
||||||
|
]);
|
||||||
|
let tr = xf([
|
||||||
|
br_local[0] + nx * top_along,
|
||||||
|
br_local[1] + ny * top_along,
|
||||||
|
br_local[2] + nz * top_along,
|
||||||
|
]);
|
||||||
|
let bl = xf([
|
||||||
|
bl_local[0] + nx * bot_along,
|
||||||
|
bl_local[1] + ny * bot_along,
|
||||||
|
bl_local[2] + nz * bot_along,
|
||||||
|
]);
|
||||||
|
let br = xf([
|
||||||
|
br_local[0] + nx * bot_along,
|
||||||
|
br_local[1] + ny * bot_along,
|
||||||
|
br_local[2] + nz * bot_along,
|
||||||
|
]);
|
||||||
|
acc.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
|
||||||
|
}
|
||||||
|
acc
|
||||||
} else {
|
} else {
|
||||||
// OBB missing — fall back to the axis-aligned local AABB. Already
|
|
||||||
// a tight per-entity rect (no multi-line block to clamp).
|
|
||||||
let [x0, y0, x1, y1] = local_aabb;
|
let [x0, y0, x1, y1] = local_aabb;
|
||||||
let z = 0.0_f32;
|
let z = 0.0_f32;
|
||||||
Some([
|
let bl = xf([x0, y0, z]);
|
||||||
xf([x0, y0, z]),
|
let br = xf([x1, y0, z]);
|
||||||
xf([x1, y0, z]),
|
let tr = xf([x1, y1, z]);
|
||||||
xf([x1, y1, z]),
|
let tl = xf([x0, y1, z]);
|
||||||
xf([x0, y1, z]),
|
vec![bl, br, tr, bl, tr, tl]
|
||||||
])
|
|
||||||
};
|
};
|
||||||
let Some([bl, br, tr, tl]) = corners else {
|
if tris.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
let final_color = if ctx.selected {
|
let final_color = if ctx.selected {
|
||||||
WireModel::SELECTED
|
WireModel::SELECTED
|
||||||
|
|
@ -716,7 +747,7 @@ fn emit_greeked_text(
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, true));
|
.or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, true));
|
||||||
|
|
||||||
for p in [bl, br, tr, bl, tr, tl] {
|
for p in tris {
|
||||||
if p[0] < entry.min_x {
|
if p[0] < entry.min_x {
|
||||||
entry.min_x = p[0];
|
entry.min_x = p[0];
|
||||||
}
|
}
|
||||||
|
|
@ -748,6 +779,12 @@ fn emit_text_baseline(
|
||||||
let Some(obb) = lw.text_obb_local else {
|
let Some(obb) = lw.text_obb_local else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(h_local) = lw.text_height_local else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if h_local <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let [ox, oy, oz] = ctx.world_offset;
|
let [ox, oy, oz] = ctx.world_offset;
|
||||||
let [lo_x, lo_y, lo_z] = defn_lo;
|
let [lo_x, lo_y, lo_z] = defn_lo;
|
||||||
let xf = |p: [f32; 3]| -> [f32; 3] {
|
let xf = |p: [f32; 3]| -> [f32; 3] {
|
||||||
|
|
@ -758,17 +795,39 @@ fn emit_text_baseline(
|
||||||
));
|
));
|
||||||
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
||||||
};
|
};
|
||||||
let p0 = xf(obb[0]);
|
|
||||||
let p1 = xf(obb[1]);
|
|
||||||
|
|
||||||
// Skip the baseline too if the line itself projects under 2 px on screen.
|
// Single line: just obb[0] → obb[1]. Skip when the projected length
|
||||||
let dx = p1[0] - p0[0];
|
// falls under 2 px (single-char text seen edge-on). All wrap lines
|
||||||
let dy = p1[1] - p0[1];
|
// share the same baseline length, so checking once is enough.
|
||||||
|
let p0_world = xf(obb[0]);
|
||||||
|
let p1_world = xf(obb[1]);
|
||||||
|
let dx = p1_world[0] - p0_world[0];
|
||||||
|
let dy = p1_world[1] - p0_world[1];
|
||||||
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
||||||
if len_px < 2.0 {
|
if len_px < 2.0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the per-line baseline endpoints in *local* frame, then
|
||||||
|
// transform — keeps precision for distant text and matches the
|
||||||
|
// greek/baseline math used by the rect path.
|
||||||
|
let (ux, uy, uz) = (
|
||||||
|
obb[3][0] - obb[0][0],
|
||||||
|
obb[3][1] - obb[0][1],
|
||||||
|
obb[3][2] - obb[0][2],
|
||||||
|
);
|
||||||
|
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||||
|
let n_lines = if ulen < 1e-9 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
((ulen / h_local).round() as usize).max(1)
|
||||||
|
};
|
||||||
|
let (nx, ny, nz) = if ulen < 1e-9 {
|
||||||
|
(0.0, 0.0, 0.0)
|
||||||
|
} else {
|
||||||
|
(ux / ulen, uy / ulen, uz / ulen)
|
||||||
|
};
|
||||||
|
|
||||||
let final_color = if ctx.selected {
|
let final_color = if ctx.selected {
|
||||||
WireModel::SELECTED
|
WireModel::SELECTED
|
||||||
} else if lw.color_is_byblock {
|
} else if lw.color_is_byblock {
|
||||||
|
|
@ -788,25 +847,45 @@ fn emit_text_baseline(
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, false));
|
.or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, false));
|
||||||
|
|
||||||
let needs_sep = !entry.points.is_empty()
|
let bl_local = obb[0];
|
||||||
&& !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false);
|
let br_local = obb[1];
|
||||||
if needs_sep {
|
for i in 0..n_lines {
|
||||||
entry.points.push([f32::NAN; 3]);
|
let along = if ulen < 1e-9 {
|
||||||
}
|
0.0
|
||||||
for q in [p0, p1] {
|
} else {
|
||||||
if q[0] < entry.min_x {
|
(ulen - ((i + 1) as f32) * h_local).max(0.0)
|
||||||
entry.min_x = q[0];
|
};
|
||||||
|
let p0 = xf([
|
||||||
|
bl_local[0] + nx * along,
|
||||||
|
bl_local[1] + ny * along,
|
||||||
|
bl_local[2] + nz * along,
|
||||||
|
]);
|
||||||
|
let p1 = xf([
|
||||||
|
br_local[0] + nx * along,
|
||||||
|
br_local[1] + ny * along,
|
||||||
|
br_local[2] + nz * along,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let needs_sep = !entry.points.is_empty()
|
||||||
|
&& !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false);
|
||||||
|
if needs_sep {
|
||||||
|
entry.points.push([f32::NAN; 3]);
|
||||||
}
|
}
|
||||||
if q[1] < entry.min_y {
|
for q in [p0, p1] {
|
||||||
entry.min_y = q[1];
|
if q[0] < entry.min_x {
|
||||||
|
entry.min_x = q[0];
|
||||||
|
}
|
||||||
|
if q[1] < entry.min_y {
|
||||||
|
entry.min_y = q[1];
|
||||||
|
}
|
||||||
|
if q[0] > entry.max_x {
|
||||||
|
entry.max_x = q[0];
|
||||||
|
}
|
||||||
|
if q[1] > entry.max_y {
|
||||||
|
entry.max_y = q[1];
|
||||||
|
}
|
||||||
|
entry.points.push(q);
|
||||||
}
|
}
|
||||||
if q[0] > entry.max_x {
|
|
||||||
entry.max_x = q[0];
|
|
||||||
}
|
|
||||||
if q[1] > entry.max_y {
|
|
||||||
entry.max_y = q[1];
|
|
||||||
}
|
|
||||||
entry.points.push(q);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
162
src/scene/mod.rs
162
src/scene/mod.rs
|
|
@ -4162,13 +4162,22 @@ fn tessellate_entity(
|
||||||
};
|
};
|
||||||
if let Some(h_world) = text_height {
|
if let Some(h_world) = text_height {
|
||||||
let h_px = (h_world as f32) / wpp;
|
let h_px = (h_world as f32) / wpp;
|
||||||
|
// Wrap-expanded line count for MText (Text = 1).
|
||||||
|
let n_lines = match e {
|
||||||
|
EntityType::MText(m) => {
|
||||||
|
crate::entities::text_support::mtext_line_count(m, document, anno_scale)
|
||||||
|
}
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
if h_px < 1.0 {
|
if h_px < 1.0 {
|
||||||
let pts = text_baseline_points(e, anno_scale, world_offset);
|
let pts = text_baseline_points(e, anno_scale, world_offset, n_lines);
|
||||||
if pts.len() < 2 {
|
if pts.len() < 2 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
// Skip the baseline too if the line itself projects under
|
// Skip the baseline too if the line itself projects under
|
||||||
// 2 px (e.g. a 1-char text seen edge-on).
|
// 2 px (e.g. a 1-char text seen edge-on). All wrap lines
|
||||||
|
// share the same baseline length, so the first segment is
|
||||||
|
// a representative sample.
|
||||||
let dx = pts[1][0] - pts[0][0];
|
let dx = pts[1][0] - pts[0][0];
|
||||||
let dy = pts[1][1] - pts[0][1];
|
let dy = pts[1][1] - pts[0][1];
|
||||||
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
||||||
|
|
@ -4194,7 +4203,7 @@ fn tessellate_entity(
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB {
|
if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB {
|
||||||
let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset);
|
let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset, n_lines);
|
||||||
if fill_tris.is_empty() {
|
if fill_tris.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -4260,9 +4269,15 @@ fn tessellate_entity(
|
||||||
/// for block-defn subs it's block-local. No offset/transform applied.
|
/// for block-defn subs it's block-local. No offset/transform applied.
|
||||||
/// Width is approximated from glyph height × character count (TEXT) or
|
/// Width is approximated from glyph height × character count (TEXT) or
|
||||||
/// from `rectangle_width` (MTEXT). Returns `None` for non-text entities.
|
/// from `rectangle_width` (MTEXT). Returns `None` for non-text entities.
|
||||||
|
///
|
||||||
|
/// `mtext_lines_override` lets the caller plug in a wrap-aware line count
|
||||||
|
/// (from `text_support::mtext_line_count`). Without it, MText's OBB
|
||||||
|
/// height collapses to a single line when the file omits `rectangle_height`,
|
||||||
|
/// which makes downstream per-line LOD math degenerate.
|
||||||
pub(crate) fn text_obb_corners_native(
|
pub(crate) fn text_obb_corners_native(
|
||||||
e: &EntityType,
|
e: &EntityType,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
|
mtext_lines_override: Option<usize>,
|
||||||
) -> Option<[[f64; 3]; 4]> {
|
) -> Option<[[f64; 3]; 4]> {
|
||||||
use acadrust::entities::{
|
use acadrust::entities::{
|
||||||
AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment,
|
AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment,
|
||||||
|
|
@ -4317,15 +4332,26 @@ pub(crate) fn text_obb_corners_native(
|
||||||
}
|
}
|
||||||
EntityType::MText(m) => {
|
EntityType::MText(m) => {
|
||||||
let h_world = m.height * anno;
|
let h_world = m.height * anno;
|
||||||
let lines = (m.value.matches('\n').count() + 1) as f64;
|
let raw_lines = (m.value.matches('\n').count() + 1) as f64;
|
||||||
|
let effective_lines = match mtext_lines_override {
|
||||||
|
Some(n) => n.max(1) as f64,
|
||||||
|
None => raw_lines,
|
||||||
|
};
|
||||||
let w_world = if m.rectangle_width > 0.0 {
|
let w_world = if m.rectangle_width > 0.0 {
|
||||||
m.rectangle_width
|
m.rectangle_width
|
||||||
} else {
|
} else {
|
||||||
h_world * 8.0 * lines.max(1.0)
|
h_world * 8.0 * effective_lines.max(1.0)
|
||||||
|
};
|
||||||
|
// Wrap-aware override beats `rectangle_height` — the stored
|
||||||
|
// height can be stale on DWGs that were re-saved without
|
||||||
|
// updating the bounds.
|
||||||
|
let total_h = if mtext_lines_override.is_some() {
|
||||||
|
h_world * effective_lines.max(1.0) * m.line_spacing_factor.max(0.5)
|
||||||
|
} else {
|
||||||
|
m.rectangle_height.unwrap_or(
|
||||||
|
h_world * raw_lines.max(1.0) * m.line_spacing_factor.max(0.5),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let total_h = m
|
|
||||||
.rectangle_height
|
|
||||||
.unwrap_or(h_world * lines.max(1.0) * m.line_spacing_factor.max(0.5));
|
|
||||||
// MText `attachment_point` puts `insertion_point` at one of the
|
// MText `attachment_point` puts `insertion_point` at one of the
|
||||||
// 9 corners/midpoints of the text bbox. h_anchor / v_anchor are
|
// 9 corners/midpoints of the text bbox. h_anchor / v_anchor are
|
||||||
// fractions from (left, bottom) of the bbox.
|
// fractions from (left, bottom) of the bbox.
|
||||||
|
|
@ -4374,35 +4400,18 @@ pub(crate) fn text_obb_corners_native(
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Baseline endpoints (`p00 → p10` of the OBB) for a sub-pixel Text / MText,
|
/// NaN-separated baseline segments — one per visible wrap line — for a
|
||||||
/// in world-offset-subtracted f32 space. Used as the lowest LOD step before
|
/// sub-pixel Text / MText, in world-offset-subtracted f32 space. `n_lines`
|
||||||
/// the entity is dropped entirely.
|
/// must be the actual rendered line count (1 for Text, the wrap-expanded
|
||||||
|
/// count for MText). Lines are emitted top → bottom along the OBB's up
|
||||||
|
/// direction, each at the bottom edge of its slab.
|
||||||
fn text_baseline_points(
|
fn text_baseline_points(
|
||||||
e: &EntityType,
|
e: &EntityType,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
|
n_lines: usize,
|
||||||
) -> Vec<[f32; 3]> {
|
) -> Vec<[f32; 3]> {
|
||||||
let Some(corners) = text_obb_corners_native(e, anno_scale) else {
|
let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else {
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
let [ox, oy, oz] = world_offset;
|
|
||||||
let cast = |p: [f64; 3]| -> [f32; 3] {
|
|
||||||
[(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32]
|
|
||||||
};
|
|
||||||
vec![cast(corners[0]), cast(corners[1])]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 6-vertex filled rect (2 triangles) for a greeked top-level Text / MText.
|
|
||||||
/// Clamped to a single line's height — multi-line MText's OBB spans the
|
|
||||||
/// whole block and using its full height would emit a misleadingly tall
|
|
||||||
/// box. The face3d pipeline skips its 0.45 dim for wires with empty
|
|
||||||
/// `points`, so these tris render at the literal text color.
|
|
||||||
fn text_greek_obb_tris(
|
|
||||||
e: &EntityType,
|
|
||||||
anno_scale: f32,
|
|
||||||
world_offset: [f64; 3],
|
|
||||||
) -> Vec<[f32; 3]> {
|
|
||||||
let Some(corners) = text_obb_corners_native(e, anno_scale) else {
|
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
let line_h = match e {
|
let line_h = match e {
|
||||||
|
|
@ -4413,6 +4422,62 @@ fn text_greek_obb_tris(
|
||||||
if line_h <= 0.0 {
|
if line_h <= 0.0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
let n_lines = n_lines.max(1);
|
||||||
|
let [ox, oy, oz] = world_offset;
|
||||||
|
let cast = |p: [f64; 3]| -> [f32; 3] {
|
||||||
|
[(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32]
|
||||||
|
};
|
||||||
|
let bl = cast(corners[0]);
|
||||||
|
let br = cast(corners[1]);
|
||||||
|
let full_tl = cast(corners[3]);
|
||||||
|
|
||||||
|
let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]);
|
||||||
|
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||||
|
if ulen < 1e-9 {
|
||||||
|
return vec![bl, br];
|
||||||
|
}
|
||||||
|
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
||||||
|
|
||||||
|
let mut pts = Vec::with_capacity(n_lines * 3);
|
||||||
|
for i in 0..n_lines {
|
||||||
|
// i = 0 is the topmost line — its bottom sits one line_h below
|
||||||
|
// the OBB top (≈ `ulen`). For n_lines > ulen/line_h the deepest
|
||||||
|
// baselines clamp to the OBB bottom.
|
||||||
|
let bot_off = ((i + 1) as f32) * line_h;
|
||||||
|
let along = (ulen - bot_off).max(0.0);
|
||||||
|
let p0 = [bl[0] + nx * along, bl[1] + ny * along, bl[2] + nz * along];
|
||||||
|
let p1 = [br[0] + nx * along, br[1] + ny * along, br[2] + nz * along];
|
||||||
|
if !pts.is_empty() {
|
||||||
|
pts.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
pts.extend_from_slice(&[p0, p1]);
|
||||||
|
}
|
||||||
|
pts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filled tris for a greeked top-level Text / MText. One 2-triangle rect
|
||||||
|
/// per visible line — `n_lines` is the actual rendered line count from
|
||||||
|
/// `mtext_line_count` (1 for Text). Stacked top → bottom along the OBB's
|
||||||
|
/// up direction. The face3d pipeline skips its 0.45 dim for wires with
|
||||||
|
/// empty `points`, so these tris render at the literal text color.
|
||||||
|
fn text_greek_obb_tris(
|
||||||
|
e: &EntityType,
|
||||||
|
anno_scale: f32,
|
||||||
|
world_offset: [f64; 3],
|
||||||
|
n_lines: usize,
|
||||||
|
) -> Vec<[f32; 3]> {
|
||||||
|
let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let line_h = match e {
|
||||||
|
EntityType::Text(t) => (t.height * anno_scale as f64) as f32,
|
||||||
|
EntityType::MText(m) => (m.height * anno_scale as f64) as f32,
|
||||||
|
_ => return vec![],
|
||||||
|
};
|
||||||
|
if line_h <= 0.0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let n_lines = n_lines.max(1);
|
||||||
let [ox, oy, oz] = world_offset;
|
let [ox, oy, oz] = world_offset;
|
||||||
let cast = |p: [f64; 3]| -> [f32; 3] {
|
let cast = |p: [f64; 3]| -> [f32; 3] {
|
||||||
[(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32]
|
[(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32]
|
||||||
|
|
@ -4421,19 +4486,40 @@ fn text_greek_obb_tris(
|
||||||
let br = cast(corners[1]);
|
let br = cast(corners[1]);
|
||||||
let full_tl = cast(corners[3]);
|
let full_tl = cast(corners[3]);
|
||||||
|
|
||||||
// Up direction = (bl → full_tl) normalized, then scale to a single
|
|
||||||
// line. Replaces the full-block top corners so MText with N lines
|
|
||||||
// still emits a single-line rect.
|
|
||||||
let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]);
|
let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]);
|
||||||
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||||
if ulen < 1e-9 {
|
if ulen < 1e-9 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
||||||
let tl = [bl[0] + nx * line_h, bl[1] + ny * line_h, bl[2] + nz * line_h];
|
|
||||||
let tr = [br[0] + nx * line_h, br[1] + ny * line_h, br[2] + nz * line_h];
|
|
||||||
|
|
||||||
vec![bl, br, tr, bl, tr, tl]
|
let mut tris = Vec::with_capacity(n_lines * 6);
|
||||||
|
for i in 0..n_lines {
|
||||||
|
let top_along = (ulen - (i as f32) * line_h).max(0.0);
|
||||||
|
let bot_along = (ulen - ((i + 1) as f32) * line_h).max(0.0);
|
||||||
|
let tl = [
|
||||||
|
bl[0] + nx * top_along,
|
||||||
|
bl[1] + ny * top_along,
|
||||||
|
bl[2] + nz * top_along,
|
||||||
|
];
|
||||||
|
let tr = [
|
||||||
|
br[0] + nx * top_along,
|
||||||
|
br[1] + ny * top_along,
|
||||||
|
br[2] + nz * top_along,
|
||||||
|
];
|
||||||
|
let lbl = [
|
||||||
|
bl[0] + nx * bot_along,
|
||||||
|
bl[1] + ny * bot_along,
|
||||||
|
bl[2] + nz * bot_along,
|
||||||
|
];
|
||||||
|
let lbr = [
|
||||||
|
br[0] + nx * bot_along,
|
||||||
|
br[1] + ny * bot_along,
|
||||||
|
br[2] + nz * bot_along,
|
||||||
|
];
|
||||||
|
tris.extend_from_slice(&[lbl, lbr, tr, lbl, tr, tl]);
|
||||||
|
}
|
||||||
|
tris
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] {
|
fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue