diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 739c676e7..5d0353792 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -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![], } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 3cd149942..5ccbb4899 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -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; diff --git a/src/linetypes.rs b/src/linetypes.rs index 04619623b..ac62804ca 100644 --- a/src/linetypes.rs +++ b/src/linetypes.rs @@ -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 { 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 { 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 { }) } +/// Parse `"TEXT",STYLE,x=v,y=v,s=v,r=v` into an `LtSegment::Text`. +fn parse_text_element(inner: &str) -> Option { + // 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) { let t = token.trim(); if t.is_empty() { diff --git a/src/scene/complex_lt.rs b/src/scene/complex_lt.rs index e3ce5975f..dc41cbaea 100644 --- a/src/scene/complex_lt.rs +++ b/src/scene/complex_lt.rs @@ -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 { @@ -188,19 +236,21 @@ fn scale_segments(segs: &[LtSegment], scale: f32) -> Vec { 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() }