feat: complex linetype text elements and ellipse quadrant snaps

Complex linetypes now render embedded text elements (e.g. GAS_LINE's
"GAS" labels): the LtSegment::Text variant is parsed from LIN files,
scaled along with shape/dash segments, and tessellated via
cxf::tessellate_text_ex aligned to the linetype direction.

Full ellipses gain four Quadrant snap hints at ±major/±minor axis
endpoints in addition to the existing Center snap, matching AutoCAD
snap behaviour.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-27 02:34:51 +03:00
1 parent 7bae75f5d6
commit 392f4503d8
4 files changed
+131 -14

No files matched your search

+16 -1
View File
@@ -71,9 +71,24 @@ fn to_truck(ell: &Ellipse) -> TruckEntity {
let edge_upper = Edge::new(&v_pos, &v_neg, Curve::BSplineCurve(spl_u));
let edge_lower = Edge::new(&v_neg, &v_pos, Curve::BSplineCurve(spl_l));
let wire: Wire = [edge_upper, edge_lower].into_iter().collect();
// Quadrant points at ±major and ±minor axis endpoints.
let q = |lx: f64, lz: f64| {
Vec3::new(
(cx + lx * u.x as f64 + lz * v_axis.x as f64) as f32,
(cy + lx * u.y as f64 + lz * v_axis.y as f64) as f32,
cz as f32,
)
};
let snap_pts = vec![
(center_v3, SnapHint::Center),
(q(r_major, 0.0), SnapHint::Quadrant),
(q(-r_major, 0.0), SnapHint::Quadrant),
(q(0.0, r_minor), SnapHint::Quadrant),
(q(0.0, -r_minor), SnapHint::Quadrant),
];
TruckEntity {
object: TruckObject::Contour(wire),
snap_pts: vec![(center_v3, SnapHint::Center)],
snap_pts,
tangent_geoms: vec![],
key_vertices: vec![],
}
+1 -1
View File
@@ -22,7 +22,7 @@ pub mod shape;
pub mod solid;
pub mod spline;
pub mod text;
mod text_support;
pub(crate) mod text_support;
pub mod table;
pub mod tolerance;
pub mod traits;
+56 -4
View File
@@ -35,6 +35,21 @@ pub enum LtSegment {
/// Optional rotation in degrees (0 = along the line).
rot_deg: f32,
},
/// Draw a text string at the current pen position.
Text {
/// The string to render.
text: String,
/// Text style / font name.
style: String,
/// X offset along the linetype direction.
x: f32,
/// Y offset perpendicular to the linetype direction.
y: f32,
/// Height scale factor.
scale: f32,
/// Rotation in degrees (0 = along the line).
rot_deg: f32,
},
}
/// A complex linetype — ordered elements for one pattern repeat.
@@ -307,13 +322,12 @@ fn parse_complex_elements(s: &str) -> Vec<LtSegment> {
segs
}
/// Try to parse `SHAPENAME,font,x=v,s=v,r=v` into an `LtSegment::Shape`.
/// Text elements (start with `"`) are skipped → `None`.
/// Parse a bracketed linetype element.
/// Handles both shape (`SHAPENAME,...`) and text (`"string",...`) elements.
fn parse_shape_element(inner: &str) -> Option<LtSegment> {
let inner = inner.trim();
// Skip text elements.
if inner.starts_with('"') {
return None;
return parse_text_element(inner);
}
let mut parts = inner.split(',');
@@ -354,6 +368,44 @@ fn parse_shape_element(inner: &str) -> Option<LtSegment> {
})
}
/// Parse `"TEXT",STYLE,x=v,y=v,s=v,r=v` into an `LtSegment::Text`.
fn parse_text_element(inner: &str) -> Option<LtSegment> {
// Find the closing quote.
let rest = inner.strip_prefix('"')?;
let end = rest.find('"')?;
let text = rest[..end].to_string();
let after = rest[end + 1..].trim().trim_start_matches(',');
let mut parts = after.split(',');
let style = parts.next().map(|s| s.trim().to_string()).unwrap_or_default();
let style = if style.is_empty() { "Standard".to_string() } else { style };
let mut x = 0.0f32;
let mut y = 0.0f32;
let mut scale = 1.0f32;
let mut rot_deg = 0.0f32;
for part in parts {
let p = part.trim();
if let Some(v) = p.strip_prefix("x=").or_else(|| p.strip_prefix("X=")) {
x = v.parse().unwrap_or(0.0);
} else if let Some(v) = p.strip_prefix("y=").or_else(|| p.strip_prefix("Y=")) {
y = v.parse().unwrap_or(0.0);
} else if let Some(v) = p.strip_prefix("s=").or_else(|| p.strip_prefix("S=")) {
scale = v.parse().unwrap_or(1.0);
} else if let Some(v) = p
.strip_prefix("r=")
.or_else(|| p.strip_prefix("R="))
.or_else(|| p.strip_prefix("u="))
.or_else(|| p.strip_prefix("U="))
{
rot_deg = v.parse().unwrap_or(0.0);
}
}
Some(LtSegment::Text { text, style, x, y, scale, rot_deg })
}
fn push_lt_segment(token: &str, out: &mut Vec<LtSegment>) {
let t = token.trim();
if t.is_empty() {
+58 -8
View File
@@ -9,6 +9,7 @@
use crate::linetypes::{ComplexLt, LtSegment};
use crate::scene::cxf;
use crate::scene::wire_model::WireModel;
use crate::entities::text_support::resolve_dxf_special_chars;
// ── Public entry point ────────────────────────────────────────────────────
@@ -39,7 +40,7 @@ pub fn apply_along(
.iter()
.map(|s| match s {
LtSeg::Dash(l) | LtSeg::Space(l) => *l,
LtSeg::Dot | LtSeg::Shape { .. } => 0.0,
LtSeg::Dot | LtSeg::Shape { .. } | LtSeg::Text { .. } => 0.0,
})
.sum();
if pattern_len < 1e-10 {
@@ -134,6 +135,45 @@ pub fn apply_along(
let shape_strokes = emit_shape(sh_name, insert, fwd, perp, *sh_scale, *rot_deg);
strokes.extend(shape_strokes);
elem_idx += 1;
elem_consumed = 0.0;
if pos < 1e-6 {
break;
}
}
LtSeg::Text {
text,
style,
x,
y,
scale: tx_scale,
rot_deg,
} => {
flush(&mut cur_stroke, &mut strokes);
let insert = lerp(ps, pe, pos / seg_len);
let insert = offset_pt(insert, fwd, perp, *x, *y);
let fwd_angle = fwd[1].atan2(fwd[0]) + rot_deg.to_radians();
let resolved = resolve_dxf_special_chars(text);
let text_strokes = cxf::tessellate_text_ex(
[insert[0], insert[1]],
*tx_scale,
fwd_angle,
1.0,
0.0,
style,
&resolved,
);
for stroke in &text_strokes {
if stroke.len() >= 2 {
let pts: Vec<[f32; 3]> = stroke
.iter()
.map(|&[sx, sy]| [sx, sy, insert[2]])
.collect();
strokes.push(pts);
}
}
elem_idx += 1;
elem_consumed = 0.0;
if pos < 1e-6 {
@@ -180,6 +220,14 @@ enum LtSeg {
scale: f32,
rot_deg: f32,
},
Text {
text: String,
style: String,
x: f32,
y: f32,
scale: f32,
rot_deg: f32,
},
}
fn scale_segments(segs: &[LtSegment], scale: f32) -> Vec<LtSeg> {
@@ -188,19 +236,21 @@ fn scale_segments(segs: &[LtSegment], scale: f32) -> Vec<LtSeg> {
LtSegment::Dash(l) => LtSeg::Dash(l * scale),
LtSegment::Space(l) => LtSeg::Space(l * scale),
LtSegment::Dot => LtSeg::Dot,
LtSegment::Shape {
name,
x,
y,
scale: sh_scale,
rot_deg,
} => LtSeg::Shape {
LtSegment::Shape { name, x, y, scale: sh_scale, rot_deg } => LtSeg::Shape {
name: name.clone(),
x: x * scale,
y: y * scale,
scale: *sh_scale * scale,
rot_deg: *rot_deg,
},
LtSegment::Text { text, style, x, y, scale: tx_scale, rot_deg } => LtSeg::Text {
text: text.clone(),
style: style.clone(),
x: x * scale,
y: y * scale,
scale: *tx_scale * scale,
rot_deg: *rot_deg,
},
})
.collect()
}