forked from 1b-agent/opencadstudio-upstream
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:
1 parent
7bae75f5d6
commit
392f4503d8
4 files changed
+131
-14
No files matched your search
+16
-1
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user