forked from 1b-agent/opencadstudio-upstream
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
This commit is contained in:
1 parent
2600ae1815
commit
8bfa5088c4
3 files changed
+495
-80
No files matched your search
+194
-24
@@ -1,4 +1,32 @@
|
||||
use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType, TextAttachmentType};
|
||||
use acadrust::entities::{
|
||||
LeaderContentType, MultiLeader, MultiLeaderPathType, TextAttachmentPointType, TextAttachmentType,
|
||||
};
|
||||
|
||||
/// Local-frame vertical baseline offset for line 0 given a vertical attachment
|
||||
/// and the overall block geometry. The baseline is at y=0 for `BottomOfTopLine*`
|
||||
/// (i.e. text_location coincides with line-0 baseline); other variants offset
|
||||
/// the block above/below/around text_location.
|
||||
pub(crate) fn v_offset_for_attachment(
|
||||
attach: TextAttachmentType,
|
||||
n_lines: f32,
|
||||
h: f32,
|
||||
line_h: f32,
|
||||
) -> f32 {
|
||||
match attach {
|
||||
TextAttachmentType::TopOfTopLine => -h,
|
||||
TextAttachmentType::MiddleOfTopLine => -h * 0.5,
|
||||
TextAttachmentType::MiddleOfText
|
||||
| TextAttachmentType::CenterOfText
|
||||
| TextAttachmentType::CenterOfTextOverline => ((n_lines - 1.0) * line_h - h) * 0.5,
|
||||
TextAttachmentType::MiddleOfBottomLine => (n_lines - 1.0) * line_h - h * 0.5,
|
||||
TextAttachmentType::BottomOfBottomLine | TextAttachmentType::BottomLine => {
|
||||
(n_lines - 1.0) * line_h
|
||||
}
|
||||
TextAttachmentType::BottomOfTopLineUnderlineBottomLine
|
||||
| TextAttachmentType::BottomOfTopLineUnderlineTopLine
|
||||
| TextAttachmentType::BottomOfTopLineUnderlineAll => 0.0,
|
||||
}
|
||||
}
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
@@ -146,33 +174,117 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
||||
}
|
||||
}
|
||||
|
||||
// Text strokes (MText content rendered inline)
|
||||
// Text strokes (MText content rendered inline). Mirrors the MText pipeline:
|
||||
// strip inline format codes, split / word-wrap into lines, then place each
|
||||
// line with the multileader's rotation, horizontal & vertical attachment,
|
||||
// line spacing and scale_factor applied. Annotation scale is NOT applied
|
||||
// here — this path is consumed by snap / truck export which work in WCS.
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let height = if ml.context.text_height > 0.0 {
|
||||
ml.context.text_height
|
||||
let ctx = &ml.context;
|
||||
let raw_height = if ctx.text_height > 0.0 {
|
||||
ctx.text_height
|
||||
} else {
|
||||
ml.text_height
|
||||
};
|
||||
let ins = &ml.context.text_location;
|
||||
} as f32;
|
||||
let scale_factor = ml.scale_factor as f32;
|
||||
let height = raw_height * scale_factor;
|
||||
|
||||
let ins = &ctx.text_location;
|
||||
let ins_x = ins.x;
|
||||
let ins_y = ins.y;
|
||||
let z = ins.z as f32;
|
||||
snap_pts.push(node([ins.x as f32, ins.y as f32, z]));
|
||||
let style = crate::entities::text_support::resolve_text_style("STANDARD", document);
|
||||
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||
[ins.x as f32, ins.y as f32],
|
||||
height as f32,
|
||||
0.0,
|
||||
style.width_factor.max(0.01),
|
||||
style.oblique_angle,
|
||||
&style.font_name,
|
||||
&ml.context.text_string,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
points.push([x, y, z]);
|
||||
// Prefer text_direction (carries through rotations/mirrors); fall back
|
||||
// to text_rotation when no direction has been set.
|
||||
let td = ctx.text_direction;
|
||||
let rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(td.y as f32).atan2(td.x as f32)
|
||||
} else {
|
||||
ctx.text_rotation as f32
|
||||
};
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
snap_pts.push(node([ins_x as f32, ins_y as f32, z]));
|
||||
|
||||
// Resolve text style via handle when available, falling back to STANDARD.
|
||||
let style_name = ctx
|
||||
.text_style_handle
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == *h)
|
||||
.map(|s| s.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "STANDARD".to_string());
|
||||
let style = crate::entities::text_support::resolve_text_style(&style_name, document);
|
||||
let font_name = style.font_name;
|
||||
let font = crate::scene::cxf::get_font(&font_name);
|
||||
let width_factor = style.width_factor.max(0.01);
|
||||
let oblique = style.oblique_angle;
|
||||
|
||||
// Strip MText format codes (e.g. `{\fArial Black|b0|i0|c162|p34;...}`),
|
||||
// then split on \P / \n / \N and optionally word-wrap to text_width.
|
||||
let plain = crate::entities::text_support::strip_mtext_codes(&ctx.text_string);
|
||||
let explicit_lines = crate::entities::text_support::split_mtext_lines(&plain);
|
||||
let lines: Vec<String> = if ctx.text_width > 0.0 {
|
||||
let scale = height / 9.0 * width_factor;
|
||||
let max_w = ctx.text_width as f32 * scale_factor;
|
||||
explicit_lines
|
||||
.iter()
|
||||
.flat_map(|line| {
|
||||
crate::entities::text_support::word_wrap(line, max_w, scale, font)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
explicit_lines
|
||||
};
|
||||
|
||||
let ls_factor = if ctx.line_spacing_factor > 0.0 {
|
||||
ctx.line_spacing_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let line_h = height * ls_factor * (5.0 / 3.0) * font.line_spacing;
|
||||
let n_lines = lines.len().max(1) as f32;
|
||||
|
||||
let h_anchor = match ctx.text_attachment_point {
|
||||
TextAttachmentPointType::Left => 0.0_f32,
|
||||
TextAttachmentPointType::Center => 0.5,
|
||||
TextAttachmentPointType::Right => 1.0,
|
||||
};
|
||||
let v_offset =
|
||||
v_offset_for_attachment(ctx.text_left_attachment, n_lines, height, line_h);
|
||||
|
||||
let scale = height / 9.0 * width_factor;
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let line_y_local = -li * line_h + v_offset;
|
||||
let line_w = if h_anchor > 0.0 {
|
||||
crate::entities::text_support::measure_mtext_chars(line, scale, font)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let h_shift_local = -line_w * h_anchor;
|
||||
let wcs_dx = h_shift_local * cos_r - line_y_local * sin_r;
|
||||
let wcs_dy = h_shift_local * sin_r + line_y_local * cos_r;
|
||||
let origin = [ins_x as f32 + wcs_dx, ins_y as f32 + wcs_dy];
|
||||
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||
origin,
|
||||
height,
|
||||
rot,
|
||||
width_factor,
|
||||
oblique,
|
||||
&font_name,
|
||||
line,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
points.push([x, y, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,11 +644,29 @@ fn parse_attachment(s: &str) -> TextAttachmentType {
|
||||
|
||||
fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(ml, t, |entity, p1, p2| {
|
||||
// Reflect every point on the leader (line points, connection points,
|
||||
// break-point endpoints) AND every direction vector that drives the
|
||||
// text orientation. Without the direction reflection text would keep
|
||||
// its original rotation while the leader appears mirrored.
|
||||
for root in &mut entity.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
for p in &mut line.points {
|
||||
crate::scene::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
|
||||
}
|
||||
for bp in &mut line.break_points {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut bp.start_point.x,
|
||||
&mut bp.start_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut bp.end_point.x,
|
||||
&mut bp.end_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut root.connection_point.x,
|
||||
@@ -544,6 +674,21 @@ fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
reflect_xy_direction(&mut root.direction.x, &mut root.direction.y, p1, p2);
|
||||
for bp in &mut root.break_points {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut bp.start_point.x,
|
||||
&mut bp.start_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut bp.end_point.x,
|
||||
&mut bp.end_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.context.text_location.x,
|
||||
@@ -551,9 +696,34 @@ fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
reflect_xy_direction(
|
||||
&mut entity.context.text_direction.x,
|
||||
&mut entity.context.text_direction.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
reflect_xy_direction(
|
||||
&mut entity.context.base_direction.x,
|
||||
&mut entity.context.base_direction.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Reflect a direction (not position) vector across the mirror line p1→p2.
|
||||
/// Reflecting a direction is the same as reflecting `p1 + dir` then subtracting
|
||||
/// the reflection of `p1`, which simplifies to reflecting around the origin.
|
||||
fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: Vec3, p2: Vec3) {
|
||||
let zero = Vec3::ZERO;
|
||||
let p2_rel = Vec3::new(p2.x - p1.x, p2.y - p1.y, 0.0);
|
||||
let mut tip_x = *dx;
|
||||
let mut tip_y = *dy;
|
||||
crate::scene::transform::reflect_xy_point(&mut tip_x, &mut tip_y, zero, p2_rel);
|
||||
*dx = tip_x;
|
||||
*dy = tip_y;
|
||||
}
|
||||
|
||||
// ── Trait impls ────────────────────────────────────────────────────────────
|
||||
|
||||
impl TruckConvertible for MultiLeader {
|
||||
|
||||
@@ -3787,6 +3787,25 @@ fn tessellate_entity(
|
||||
return wires;
|
||||
}
|
||||
|
||||
if let EntityType::MultiLeader(ml) = e {
|
||||
let aabb = entity_aabb(e, world_offset);
|
||||
let mut wires = tessellate::tessellate_multileader(
|
||||
document,
|
||||
h,
|
||||
ml,
|
||||
sel,
|
||||
entity_color,
|
||||
line_weight_px,
|
||||
world_offset,
|
||||
anno_scale,
|
||||
);
|
||||
for w in &mut wires {
|
||||
w.aci = aci;
|
||||
w.aabb = aabb;
|
||||
}
|
||||
return wires;
|
||||
}
|
||||
|
||||
if let EntityType::Insert(ins) = e {
|
||||
// Resolve the INSERT's own style so ByBlock sub-entities can inherit it.
|
||||
let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = render::render_style_for(document, e);
|
||||
|
||||
+282
-56
@@ -13,9 +13,10 @@
|
||||
// Entities not handled by acad_to_truck (Viewport, Hatch, …) are tessellated
|
||||
// by the legacy geometry() path so nothing regresses.
|
||||
|
||||
use crate::entities::multileader::catmull_rom_pts;
|
||||
use crate::entities::multileader::{catmull_rom_pts, v_offset_for_attachment};
|
||||
use acadrust::entities::{
|
||||
Dimension, Leader, LeaderContentType, MultiLeader, MultiLeaderPathType, Text,
|
||||
TextAttachmentPointType,
|
||||
};
|
||||
use acadrust::types::{Color as AcadColor, Vector3};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
@@ -85,19 +86,8 @@ pub fn tessellate(
|
||||
}
|
||||
};
|
||||
|
||||
// MultiLeader/Leader need anno_scale for arrow/dogleg/text — handle before generic path.
|
||||
if let EntityType::MultiLeader(ml) = entity {
|
||||
return tessellate_multileader_single(
|
||||
document,
|
||||
handle,
|
||||
ml,
|
||||
selected,
|
||||
entity_color,
|
||||
line_weight_px,
|
||||
world_offset,
|
||||
anno_scale,
|
||||
);
|
||||
}
|
||||
// MultiLeader is handled by scene/mod.rs since it emits multiple WireModels
|
||||
// (leader, text, frame, fill) with distinct colors.
|
||||
if let EntityType::Leader(leader) = entity {
|
||||
return tessellate_leader_single(
|
||||
handle,
|
||||
@@ -582,7 +572,21 @@ fn tessellate_leader_single(
|
||||
}
|
||||
}
|
||||
|
||||
fn tessellate_multileader_single(
|
||||
/// Convert an acadrust `Color` to RGBA, falling back to `inherited` for
|
||||
/// `ByLayer` / `ByBlock` (assumes those are already resolved upstream).
|
||||
fn color_or_inherit(c: &AcadColor, inherited: [f32; 4]) -> [f32; 4] {
|
||||
match c.rgb() {
|
||||
Some((r, g, b)) => [
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
inherited[3],
|
||||
],
|
||||
None => inherited,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tessellate_multileader(
|
||||
document: &CadDocument,
|
||||
handle: Handle,
|
||||
ml: &MultiLeader,
|
||||
@@ -591,8 +595,8 @@ fn tessellate_multileader_single(
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
) -> WireModel {
|
||||
let color = if selected {
|
||||
) -> Vec<WireModel> {
|
||||
let line_color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
entity_color
|
||||
@@ -604,10 +608,21 @@ fn tessellate_multileader_single(
|
||||
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||
};
|
||||
|
||||
let arrow_size = ml.arrowhead_size as f32 * anno_scale;
|
||||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||||
// ml.scale_factor is always applied; anno_scale is only applied when the
|
||||
// multileader is marked annotative.
|
||||
let effective_scale = (ml.scale_factor as f32)
|
||||
* if ml.enable_annotation_scale {
|
||||
anno_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
|
||||
let draw_arrow = arrow_size > 0.0;
|
||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||
|
||||
// ── Leader / arrow / dogleg points ───────────────────────────────────────
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
||||
@@ -690,7 +705,7 @@ fn tessellate_multileader_single(
|
||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||
let dir = &root.direction;
|
||||
let dl = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
||||
let d = ml.dogleg_length * anno_scale as f64;
|
||||
let d = ml.dogleg_length * effective_scale as f64;
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push([
|
||||
@@ -701,44 +716,14 @@ fn tessellate_multileader_single(
|
||||
}
|
||||
}
|
||||
|
||||
// Text strokes — scale height, keep insertion point fixed.
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let height = if ml.context.text_height > 0.0 {
|
||||
ml.context.text_height
|
||||
} else {
|
||||
ml.text_height
|
||||
};
|
||||
let ins = &ml.context.text_location;
|
||||
let z = (ins.z - oz) as f32;
|
||||
snap_pts.push((
|
||||
Vec3::new((ins.x - ox) as f32, (ins.y - oy) as f32, z),
|
||||
SnapHint::Node,
|
||||
));
|
||||
let style = crate::entities::text_support::resolve_text_style("STANDARD", document);
|
||||
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||
[ins.x as f32, ins.y as f32],
|
||||
(height as f32) * anno_scale,
|
||||
0.0,
|
||||
style.width_factor.max(0.01),
|
||||
style.oblique_angle,
|
||||
&style.font_name,
|
||||
&ml.context.text_string,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
points.push([x - ox as f32, y - oy as f32, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WireModel {
|
||||
name,
|
||||
// The leader/arrow/dogleg wire goes out as a single WireModel. Text, frame,
|
||||
// and background fill (each with their own color) are appended as separate
|
||||
// WireModels so the renderer respects per-piece coloring.
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points,
|
||||
color,
|
||||
color: line_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
@@ -751,7 +736,248 @@ fn tessellate_multileader_single(
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
|
||||
// ── Text strokes / frame / background fill ──────────────────────────────
|
||||
// Strip inline format codes, split / word-wrap into lines, then place each
|
||||
// line according to text_attachment_point (horizontal) and
|
||||
// text_left_attachment (vertical), with text_rotation/text_direction applied.
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let ctx = &ml.context;
|
||||
let raw_height = if ctx.text_height > 0.0 {
|
||||
ctx.text_height
|
||||
} else {
|
||||
ml.text_height
|
||||
} as f32;
|
||||
let height = raw_height * effective_scale;
|
||||
|
||||
let ins = &ctx.text_location;
|
||||
// Subtract world_offset in f64 before casting to f32: drawings often
|
||||
// sit at large absolute coordinates and casting first then subtracting
|
||||
// throws away the precision needed for the rotated sub-glyph offsets.
|
||||
let local_ins_x = (ins.x - ox) as f32;
|
||||
let local_ins_y = (ins.y - oy) as f32;
|
||||
let z = (ins.z - oz) as f32;
|
||||
|
||||
// Rotation: prefer text_direction (transforms survive rotations / mirrors
|
||||
// when acadrust updates it) and fall back to text_rotation.
|
||||
let td = ctx.text_direction;
|
||||
let rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(td.y as f32).atan2(td.x as f32)
|
||||
} else {
|
||||
ctx.text_rotation as f32
|
||||
};
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
|
||||
// Resolve text style via handle when available, falling back to STANDARD.
|
||||
let style_name = ctx
|
||||
.text_style_handle
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == *h)
|
||||
.map(|s| s.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "STANDARD".to_string());
|
||||
let style = crate::entities::text_support::resolve_text_style(&style_name, document);
|
||||
let font_name = style.font_name;
|
||||
let font = crate::scene::cxf::get_font(&font_name);
|
||||
let width_factor = style.width_factor.max(0.01);
|
||||
let oblique = style.oblique_angle;
|
||||
|
||||
// Strip MText format codes (e.g. `{\fArial Black|b0|i0|c162|p34;...}`),
|
||||
// then split on \P / \n / \N and optionally word-wrap to text_width.
|
||||
let plain = crate::entities::text_support::strip_mtext_codes(&ctx.text_string);
|
||||
let explicit_lines = crate::entities::text_support::split_mtext_lines(&plain);
|
||||
let lines: Vec<String> = if ctx.text_width > 0.0 {
|
||||
let scale = height / 9.0 * width_factor;
|
||||
let max_w = ctx.text_width as f32 * effective_scale;
|
||||
explicit_lines
|
||||
.iter()
|
||||
.flat_map(|line| {
|
||||
crate::entities::text_support::word_wrap(line, max_w, scale, font)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
explicit_lines
|
||||
};
|
||||
|
||||
let ls_factor = if ctx.line_spacing_factor > 0.0 {
|
||||
ctx.line_spacing_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let line_h = height * ls_factor * (5.0 / 3.0) * font.line_spacing;
|
||||
let n_lines = lines.len().max(1) as f32;
|
||||
|
||||
let h_anchor = match ctx.text_attachment_point {
|
||||
TextAttachmentPointType::Left => 0.0_f32,
|
||||
TextAttachmentPointType::Center => 0.5,
|
||||
TextAttachmentPointType::Right => 1.0,
|
||||
};
|
||||
// Vertical anchor: use text_left_attachment (matches the leader-to-text
|
||||
// attachment convention for the common case of left-side leaders).
|
||||
let v_offset =
|
||||
v_offset_for_attachment(ctx.text_left_attachment, n_lines, height, line_h);
|
||||
|
||||
let scale = height / 9.0 * width_factor;
|
||||
let line_widths: Vec<f32> = lines
|
||||
.iter()
|
||||
.map(|line| crate::entities::text_support::measure_mtext_chars(line, scale, font))
|
||||
.collect();
|
||||
let max_line_w = line_widths.iter().cloned().fold(0.0_f32, f32::max);
|
||||
|
||||
// Resolve text color (falls back to entity color for ByLayer / ByBlock).
|
||||
let text_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.text_color, entity_color)
|
||||
};
|
||||
|
||||
// Build per-line stroke points in WCS.
|
||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let line_y_local = -li * line_h + v_offset;
|
||||
let line_w = line_widths[i];
|
||||
let h_shift_local = -line_w * h_anchor;
|
||||
let wcs_dx = h_shift_local * cos_r - line_y_local * sin_r;
|
||||
let wcs_dy = h_shift_local * sin_r + line_y_local * cos_r;
|
||||
// Origin already in offset-relative space — tessellator will rotate
|
||||
// glyph offsets around it and produce points directly in render space.
|
||||
let origin = [local_ins_x + wcs_dx, local_ins_y + wcs_dy];
|
||||
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||
origin,
|
||||
height,
|
||||
rot,
|
||||
width_factor,
|
||||
oblique,
|
||||
&font_name,
|
||||
line,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
text_points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
text_points.push([x, y, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !text_points.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
if ml.text_frame || ctx.background_fill_enabled {
|
||||
// Visual gap so the frame/fill doesn't touch glyph caps.
|
||||
let pad = height * 0.25;
|
||||
let block_top = v_offset + height + pad;
|
||||
let block_bottom = v_offset - (n_lines - 1.0) * line_h - pad;
|
||||
let block_left = -max_line_w * h_anchor - pad;
|
||||
let block_right = max_line_w * (1.0 - h_anchor) + pad;
|
||||
let local_corners: [[f32; 2]; 4] = [
|
||||
[block_left, block_bottom],
|
||||
[block_right, block_bottom],
|
||||
[block_right, block_top],
|
||||
[block_left, block_top],
|
||||
];
|
||||
let wcs_corners: [[f32; 3]; 4] = std::array::from_fn(|i| {
|
||||
let lx = local_corners[i][0];
|
||||
let ly = local_corners[i][1];
|
||||
let wx = local_ins_x + lx * cos_r - ly * sin_r;
|
||||
let wy = local_ins_y + lx * sin_r + ly * cos_r;
|
||||
[wx, wy, z]
|
||||
});
|
||||
|
||||
// Background fill — emit two triangles; renders under the text strokes.
|
||||
if ctx.background_fill_enabled {
|
||||
let fill_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||||
};
|
||||
let fill_tris: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[0],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: fill_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris,
|
||||
});
|
||||
}
|
||||
|
||||
// Text frame — closed rectangle, matches text color.
|
||||
if ml.text_frame {
|
||||
let frame_points: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
wcs_corners[0],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name,
|
||||
points: frame_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wires
|
||||
}
|
||||
|
||||
/// Tessellate a Solid3D entity into a MeshModel (truck Shell/Solid path).
|
||||
|
||||
Reference in new issue
Block a user