diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index f3fa2fb6..995def90 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1236,6 +1236,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_neg = base - dir * far; let guide = crate::scene::WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "__polar_guide__".into(), points: vec![ @@ -1271,6 +1272,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_neg = h.base - h.dir * far; previews.push(crate::scene::WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "__otrack_guide__".into(), points: vec![ diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index f7b2240d..b8f19876 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1353,6 +1353,7 @@ fn tessellate_dimension_inner( if !ext1.is_empty() { wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: ext1, @@ -1376,6 +1377,7 @@ fn tessellate_dimension_inner( if !ext2.is_empty() { wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: ext2, @@ -1399,6 +1401,7 @@ fn tessellate_dimension_inner( } else { wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: geom.ext_lines, @@ -1423,6 +1426,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: geom.dim_lines, @@ -1463,6 +1467,7 @@ fn tessellate_dimension_inner( }; wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: vec![], diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 6a4af99d..f02c4a78 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -590,6 +590,7 @@ impl LeaderTess for Leader { if verts.len() < 2 { return WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: vec![], @@ -693,6 +694,7 @@ impl LeaderTess for Leader { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points, diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index bbe95346..608465ec 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1400,6 +1400,7 @@ impl MultiLeaderTess for MultiLeader { let mut wires: Vec = Vec::new(); wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points, @@ -1684,6 +1685,7 @@ impl MultiLeaderTess for MultiLeader { } wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: sdf_verts, name: name.clone(), points: vec![], @@ -1748,6 +1750,7 @@ impl MultiLeaderTess for MultiLeader { ]; wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: vec![], @@ -1780,6 +1783,7 @@ impl MultiLeaderTess for MultiLeader { ]; wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: frame_points, diff --git a/src/entities/table.rs b/src/entities/table.rs index 7bba123e..b4794b17 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -749,6 +749,7 @@ pub fn tessellate_table( |color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points, diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index bd154cd0..97c14f4d 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -187,6 +187,7 @@ impl CadCommand for AlignedDimensionCommand { let p2 = p2.as_vec3(); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimaligned_preview".into(), points: vec![[p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z]], @@ -235,6 +236,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { let d2 = d2.as_vec3(); WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimaligned_preview".into(), points: vec![ diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 4410ff0f..38c8ecd1 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -131,6 +131,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimangular_preview".to_string(), points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 6362e4ce..21a5a185 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -181,6 +181,7 @@ fn v3(p: DVec3) -> Vector3 { fn preview_line(a: Vec3, b: Vec3) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimdia_preview".into(), points: vec![[a.x, a.y, a.z], [b.x, b.y, b.z]], diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 75678310..8e45d0ed 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -180,6 +180,7 @@ impl CadCommand for DimBaselineCommand { let pt = pt.as_vec3(); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimbase_preview".into(), points: vec![ diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index bdb197b4..05ef9304 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -160,6 +160,7 @@ impl CadCommand for DimContinueCommand { let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp)); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimcont_preview".into(), points: vec![ diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 75cc87e5..834a2c50 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -86,6 +86,7 @@ impl CadCommand for DimJogLineCommand { let d = 0.3_f32; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimjog_preview".into(), // Jog zigzag in the XY drawing plane (Z is elevation, ~0). The old diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index d5774b6e..aedfb0c9 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -101,6 +101,7 @@ impl CadCommand for DimTeditCommand { let d = 0.2_f32; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimtedit_preview".into(), // Marker box in the XY drawing plane (Z is elevation, ~0). The old diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index a7301a8e..f2932a10 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -164,6 +164,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { } WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "leader_preview".into(), points, diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index ff63ecde..8ab13b65 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -219,6 +219,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimlinear_preview".to_string(), points: points diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index a39f81f0..b7d55d61 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -151,6 +151,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { } WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "mleader_preview".into(), points, diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index 1d724afe..dbf6689a 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -459,6 +459,7 @@ impl CadCommand for MLeaderCollectCommand { fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "mleader_edit_preview".into(), points: pts.iter().map(|p| [p.x, p.y, p.z]).collect(), diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index bb845bcf..8e20fe32 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -122,6 +122,7 @@ fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 { fn preview_wire(points: Vec) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimordinate_preview".into(), points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index ec216fab..4b9a1689 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -91,6 +91,7 @@ impl CadCommand for QdimCommand { let d = 0.5_f32; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "qdim_preview".into(), points: vec![[pt.x - d * 3.0, pt.y, pt.z], [pt.x + d * 3.0, pt.y, pt.z]], diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 8ac7e5b0..11ba80be 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -186,6 +186,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dimradius_preview".to_string(), points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 5bab500e..7ce6abb4 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -140,6 +140,7 @@ impl CadCommand for TableCommand { let z = pt.z; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "table_preview".into(), points: vec![ diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 61f80ee7..b7dd939d 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -85,6 +85,7 @@ impl CadCommand for ToleranceCommand { let d = 0.15_f32; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "tolerance_preview".into(), points: vec![ diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index 5d5fdb08..e81d24d2 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -161,6 +161,7 @@ impl CadCommand for AttdefCommand { let d = 0.15_f32; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "attdef_preview".into(), points: vec![ diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 2ae5090a..bbba18e6 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -137,6 +137,7 @@ impl CadCommand for MlineCommand { pts.push([pt.x, pt.y, pt.z]); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "mline_preview".into(), points: pts, diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 7f2c27b3..d428a7e6 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -109,6 +109,7 @@ impl CadCommand for ImageCommand { Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "image_preview".into(), points: vec![p0, p1, p2, p3, p0], diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index e1d9de5c..f8eaa595 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -85,6 +85,7 @@ impl CadCommand for RayCommand { let far = base + dir * DISPLAY_EXTENT; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "ray_preview".into(), points: vec![[base.x, base.y, base.z], [far.x, far.y, far.z]], @@ -177,6 +178,7 @@ impl CadCommand for XLineCommand { let far_neg = base - dir * DISPLAY_EXTENT; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "xline_preview".into(), points: vec![ diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 66ac436c..5d7cfdb6 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -90,6 +90,7 @@ impl CadCommand for RevCloudCommand { ]); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "revcloud_preview".into(), points: preview_pts, diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index ea4289be..1cdb78b8 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -115,6 +115,7 @@ impl CadCommand for WipeoutCommand { let max = p1.max(pt); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "wipeout_preview".into(), points: vec![ @@ -158,6 +159,7 @@ impl CadCommand for WipeoutCommand { ]); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "wipeout_poly_preview".into(), points: pts, diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 099d41e0..e70b19ee 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -70,6 +70,7 @@ impl CadCommand for AreaCommand { pts.push(f(self.points[0])); Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "area_preview".into(), points: pts, diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index bde5cf27..14e7a5f1 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -64,6 +64,7 @@ impl CadCommand for DistCommand { // The preview wire is purely visual, so f32 vertices are fine here. Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "dist_preview".into(), points: vec![ diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index cebbc3e8..ad5dd452 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -68,6 +68,7 @@ impl MeasureGeomCommand { fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.to_string(), points: pts, diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 77959c53..aed18704 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3060,6 +3060,7 @@ fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.into(), points, diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index df338e1e..e45f2fad 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -82,6 +82,7 @@ impl CadCommand for MviewCommand { let c1 = self.corner1?; Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "mview_preview".to_string(), points: vec![ diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 589808d0..7c415853 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -306,6 +306,7 @@ inventory::submit!(crate::command::CommandRegistration { fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.into(), points, diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index bb036799..44d358d7 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -49,6 +49,7 @@ impl CadCommand for PlotWindowCommand { // Draw the selection rectangle. Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "plotwindow_preview".into(), points: vec![ diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index a71ac431..abb98f3c 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -66,6 +66,7 @@ impl CadCommand for ZoomWindowCommand { // Draw a rectangle preview Some(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: "zoom_window_preview".into(), points: vec![ diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 258064e7..1682f7eb 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -827,6 +827,7 @@ impl Batches { let color = crate::scene::view::render::adapt_to_bg(b.color, bg_color); WireModel { dash_from_start: false, + dash_align_end: None, text_verts: b.text_verts, name: name.to_string(), points: b.points, diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 25a55d23..3f3c4e82 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -482,6 +482,7 @@ pub(crate) fn tessellate_entity( ); let marker = WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: h.value().to_string(), points: vec![], @@ -693,6 +694,9 @@ pub(crate) fn tessellate_entity( entity_color, sel, base.line_weight_px, + // Single (non-MLINE) entity: keep the from-start tiling (no shared + // A-type reference — there are no sibling elements to align with). + None, ); if !wires.is_empty() { for w in &mut wires { @@ -856,6 +860,7 @@ fn lod_stub_wire( let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, // Diagonal of the entity's 3D AABB so depth tests against @@ -920,6 +925,7 @@ fn lod_stub_wire_3d( let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 02094121..78979783 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -226,6 +226,20 @@ pub fn tessellate( m.vertices[0].position.y, m.vertices[0].position.z, ]; + // Centre-line (vertex path) length — the shared "A"-type reference so + // every parallel element uses the same end-dash length and thus the same + // interior phase (perpendicular dashes line up). f64 deltas so it stays + // precise at UTM coordinates. + let ref_total: f32 = { + let mut acc = 0.0_f64; + for w in m.vertices.windows(2) { + let dx = w[1].position.x - w[0].position.x; + let dy = w[1].position.y - w[0].position.y; + let dz = w[1].position.z - w[0].position.z; + acc += (dx * dx + dy * dy + dz * dz).sqrt(); + } + acc as f32 + }; let mut out: Vec = Vec::with_capacity(lines.len()); let mut snap_attached = false; for l in lines { @@ -261,9 +275,29 @@ pub fn tessellate( // Selection forces a plain solid highlight, so skip dashing then. let clt = if selected { None - } else { + } else if let Some(doc_seg) = crate::io::linetypes::document_lt_segments(document, &l.linetype) - .or_else(|| crate::io::linetypes::complex_lt(&l.linetype).cloned()) + { + // In-document linetype: CPU-expand (apply_along) only when it + // embeds TEXT glyphs that must be laid out along the curve. Pure + // dash / space / dot (and undrawn shape) elements fall through to + // the GPU dash shader below — cheaper (one WireModel + pattern + // instead of N CPU segments) and now UTM-precise, so they stay in + // phase with any glyph-bearing sibling element. + if doc_seg + .segments + .iter() + .any(|s| matches!(s, crate::io::linetypes::LtSegment::Text { .. })) + { + Some(doc_seg) + } else { + None + } + } else { + // Not in the document: bundled-catalog linetype (may embed + // text / shape) → keep the CPU path; `resolve_pattern` below can't + // see it, so GPU-dashing would drop the pattern to solid. + crate::io::linetypes::complex_lt(&l.linetype).cloned() }; let mut elem_wires: Vec = if let Some(clt) = clt { @@ -288,6 +322,10 @@ pub fn tessellate( wcolor, selected, line_weight_px, + // Shared "A"-type reference: this text-bearing element aligns + // with the GPU-dashed sibling elements (same centre-line + // reference) instead of tiling independently from the start. + Some(ref_total), ); for wm in &mut w { wm.aci = aci; @@ -312,8 +350,32 @@ pub fn tessellate( lt_scale, ) }; + // Shared "A"-type: derive the begin/end solid-dash length ONCE + // from the multiline centre-line (`ref_total`) so every parallel + // element runs the same interior phase and its dashes line up + // perpendicular; `align_total` stays each element's own length in + // the shader so each still ends on a dash. Dash-first patterns + // only (`+dash, -gap, …`); shorter-than-a-period lines fall back + // to the per-wire path (solid). + let dash_align_end = if pattern_length > 1e-6 + && pattern[0] > 0.0 + && pattern[1] < 0.0 + && ref_total > pattern_length + { + let a = pattern[0]; + let p = pattern_length; + let k = ((ref_total - a) / p).round().max(1.0); + Some(((ref_total - k * p + a) * 0.5).max(1e-4)) + } else { + None + }; elem_wires.push(WireModel { + // MLINE dashes: A-type aligned, but the end-dash length is + // shared across all parallel elements (`dash_align_end`) so + // their interiors stay in phase (perpendicular dashes line up) + // while each still ends on a dash at its own endpoint. dash_from_start: false, + dash_align_end, text_verts: Vec::new(), name: name.clone(), points: pts, @@ -589,6 +651,7 @@ pub fn tessellate( } wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: vec![], @@ -623,6 +686,7 @@ pub fn tessellate( } wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: fp, @@ -664,6 +728,7 @@ pub fn tessellate( } wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: pts, @@ -686,6 +751,7 @@ pub fn tessellate( } wires.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: sdf_verts, name, points: Vec::new(), @@ -738,6 +804,7 @@ pub fn tessellate( }; out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: bin.pts, @@ -772,6 +839,7 @@ pub fn tessellate( }; out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: Vec::new(), @@ -803,6 +871,7 @@ pub fn tessellate( if !sdf_verts.is_empty() { out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: sdf_verts, name: name.clone(), points: Vec::new(), @@ -827,6 +896,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: Vec::new(), @@ -873,6 +943,7 @@ pub fn tessellate( .collect(); return vec![WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: vec![ @@ -918,6 +989,7 @@ pub fn tessellate( .collect(); return vec![WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points, @@ -952,6 +1024,7 @@ pub fn tessellate( .collect(); return vec![WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points, @@ -1005,6 +1078,7 @@ pub fn tessellate( }; out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: local_pts, @@ -1038,6 +1112,7 @@ pub fn tessellate( }; out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.clone(), points: Vec::new(), @@ -1062,6 +1137,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: Vec::new(), @@ -1096,6 +1172,7 @@ pub fn tessellate( .collect(); return vec![WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points: local_pts, @@ -1161,6 +1238,7 @@ pub fn tessellate( snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect(); vec![WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name, points, diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 3030cecc..6f48a7b8 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -79,6 +79,13 @@ pub struct WireModel { /// from the START vertex with continuous phase and no A-type end forcing — /// DGN line styles are not end-aligned. pub dash_from_start: bool, + /// Shared "A"-type end-dash length for MLINE elements. `Some(len)` makes the + /// dash shader use `len` as the begin/end solid-dash length for EVERY + /// parallel element (derived once from the multiline's centre-line length), + /// while `align_total` stays each element's own length — so all elements + /// share one interior phase (perpendicular dashes line up) yet each still + /// ends on a dash. `None` (the default) = per-wire A-type / from-start. + pub dash_align_end: Option, /// Paper-space bounding box [x0, y0, x1, y1] for GPU scissor clipping. /// Set only for viewport-projected wires in paper-space layouts. pub vp_scissor: Option<[f32; 4]>, @@ -158,6 +165,7 @@ impl WireModel { aabb: Self::UNBOUNDED_AABB, plinegen: true, dash_from_start: false, + dash_align_end: None, vp_scissor: None, fill_tris: vec![], fill_tris_low: Vec::new(), @@ -367,6 +375,7 @@ impl Default for WireModel { aabb: Self::UNBOUNDED_AABB, plinegen: true, dash_from_start: false, + dash_align_end: None, vp_scissor: None, fill_tris: Vec::new(), fill_tris_low: Vec::new(), diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index cfa3ac4c..bb544883 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -258,22 +258,30 @@ fn wire_distances(wire: &WireModel) -> (Vec, f32, f32) { let n = wire.points.len(); let mut dists = vec![0.0_f32; n]; let mut has_break = false; + // Accumulate arc-length in f64 from double-single deltas (high + low). An + // f32-high-only delta `q[0] - p[0]` quantises ~0.1 at UTM coordinates + // (−1.2M), which shifts the dash phase and drifts parallel lines out of sync + // — the reason MLINE elements were CPU-dashed. `points_low` may be empty for + // non-RTE wires → treat the low half as zero (same as the old behaviour). + let mut acc = 0.0_f64; for i in 1..n { let p = wire.points[i - 1]; let q = wire.points[i]; if !p[0].is_finite() || !q[0].is_finite() { has_break = true; // plinegen=false: reset to 0 at the first real point after a NaN separator. - dists[i] = if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() { - 0.0 - } else { - dists[i - 1] - }; + if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() { + acc = 0.0; + } + dists[i] = acc as f32; } else { - let dx = q[0] - p[0]; - let dy = q[1] - p[1]; - let dz = q[2] - p[2]; - dists[i] = dists[i - 1] + (dx * dx + dy * dy + dz * dz).sqrt(); + let pl = wire.points_low.get(i - 1).copied().unwrap_or([0.0; 3]); + let ql = wire.points_low.get(i).copied().unwrap_or([0.0; 3]); + let dx = (q[0] as f64 - p[0] as f64) + (ql[0] as f64 - pl[0] as f64); + let dy = (q[1] as f64 - p[1] as f64) + (ql[1] as f64 - pl[1] as f64); + let dz = (q[2] as f64 - p[2] as f64) + (ql[2] as f64 - pl[2] as f64); + acc += (dx * dx + dy * dy + dz * dz).sqrt(); + dists[i] = acc as f32; } } @@ -295,6 +303,20 @@ fn wire_distances(wire: &WireModel) -> (Vec, f32, f32) { return (dists, 0.0, 0.0); } + // Shared "A"-type for MLINE elements: the caller fixes the begin/end + // solid-dash length (`dash_align_end`, derived once from the multiline's + // centre-line length) so every parallel element runs the SAME interior phase + // — the shader's interior walk depends on `align_end`, not on the wire's own + // length — while `align_total` stays this wire's own length, so each element + // still ends on a dash at its own endpoint. + if let Some(ae) = wire.dash_align_end { + if total <= pat_len { + // Shorter than one full period → solid (matches the per-wire path). + return (dists, total, total); + } + return (dists, ae.clamp(1e-4, total * 0.5), total); + } + // Align only a proper alternating pattern that BEGINS with a dash followed // by a gap — every standard linetype does (DASHED/DASHDOT/CENTER/HIDDEN/…). // Gap-first, dot-first, single-element, or consecutive-dash patterns keep diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index d59e0536..5382c827 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -18,6 +18,55 @@ use crate::scene::model::wire_model::WireModel; /// Returns one `WireModel` per continuous stroke. Pass the entity's `name`, /// `color`, `selected` flag, and `line_weight_px` so the WireModels inherit /// the entity's visual properties. +/// Build the "A"-type segment list for one path: a solid begin/end dash of +/// length `align_end`, and between them the pattern's interior tiled to fill the +/// rest — starting at the element AFTER the first dash (so embedded text lands +/// in the same gaps the GPU dash shader phases to). Length-carrying elements are +/// truncated to fit exactly; zero-length glyphs (Dot / Text / Shape) pass +/// through at their interior position. Returns `None` if the interior would +/// exceed a sane repeat count (caller keeps the from-start tiling). +fn atype_segments(scaled: &[LtSeg], align_end: f32, total: f32) -> Option> { + let pat_len: f32 = scaled + .iter() + .map(|s| match s { + LtSeg::Dash(l) | LtSeg::Space(l) => *l, + _ => 0.0, + }) + .sum(); + if pat_len < 1e-10 || scaled.len() < 2 { + return None; + } + let ae = align_end.clamp(1e-4, total * 0.5); + let interior = total - 2.0 * ae; + if interior / pat_len > 50_000.0 { + return None; // too many repeats to materialise — keep cycling walk + } + let mut segs: Vec = Vec::new(); + segs.push(LtSeg::Dash(ae)); + if interior > 1e-6 { + let mut filled = 0.0f32; + let mut idx = 1usize; // element after the leading dash (the gap) + while filled < interior - 1e-6 { + match &scaled[idx % scaled.len()] { + LtSeg::Dash(l) => { + let take = l.min(interior - filled); + segs.push(LtSeg::Dash(take)); + filled += take; + } + LtSeg::Space(l) => { + let take = l.min(interior - filled); + segs.push(LtSeg::Space(take)); + filled += take; + } + other => segs.push(other.clone()), // Dot / Text / Shape (0-length) + } + idx += 1; + } + } + segs.push(LtSeg::Dash(ae)); + Some(segs) +} + pub fn apply_along( name: &str, path_pts: &[[f32; 3]], @@ -26,6 +75,12 @@ pub fn apply_along( color: [f32; 4], selected: bool, line_weight_px: f32, + // Shared "A"-type reference length (the MLINE centre-line). `Some` end-aligns + // the pattern: a solid begin/end dash whose length is derived from `ref_total` + // (so every parallel element shares one interior phase and its dashes line up) + // while this element still ends on a dash at its own endpoint. `None` tiles + // the pattern from the start vertex (legacy phase, unchanged). + ref_total: Option, ) -> Vec { if path_pts.len() < 2 || lt.segments.is_empty() { return vec![]; @@ -67,6 +122,26 @@ pub fn apply_along( return vec![]; } + // "A"-type end alignment (MLINE): with a shared reference length, replace the + // from-start tiling with a solid begin/end dash and a phased interior so this + // element lines up with its siblings and ends on a dash. Dash-first patterns + // only; align_end is derived from `ref_total` (shared) but the trailing dash + // lands at this element's own end (`path_len`). + let atype_segs: Option> = ref_total.and_then(|rt| { + let a = match scaled.first() { + Some(LtSeg::Dash(a)) if *a > 0.0 => *a, + _ => return None, + }; + if rt <= pattern_len || path_len <= pattern_len { + return None; + } + let k = ((rt - a) / pattern_len).round().max(1.0); + let align_end = (rt - k * pattern_len + a) * 0.5; + atype_segments(&scaled, align_end, path_len) + }); + let atype_on = atype_segs.is_some(); + let walk_segs: &[LtSeg] = atype_segs.as_deref().unwrap_or(&scaled); + let mut strokes: Vec> = Vec::new(); let mut cur_stroke: Vec<[f32; 3]> = Vec::new(); // Embedded linetype text (LtSeg::Text) renders as SDF glyph quads collected @@ -76,7 +151,7 @@ pub fn apply_along( let mut elem_idx: usize = 0; let mut elem_consumed: f32 = 0.0; - for i in 0..path_pts.len() - 1 { + 'path: for i in 0..path_pts.len() - 1 { let ps = path_pts[i]; let pe = path_pts[i + 1]; @@ -94,9 +169,18 @@ pub fn apply_along( let mut pos = 0.0f32; while pos < seg_len - 1e-6 { - let idx = elem_idx % scaled.len(); + let idx = if atype_on { + // A-type walks a finite, path-sized segment list once (no cycling); + // once consumed the whole path is drawn. + if elem_idx >= walk_segs.len() { + break 'path; + } + elem_idx + } else { + elem_idx % walk_segs.len() + }; - match &scaled[idx] { + match &walk_segs[idx] { LtSeg::Dash(dash_len) => { let remaining_dash = dash_len - elem_consumed; let remaining_seg = seg_len - pos; @@ -222,6 +306,7 @@ pub fn apply_along( .filter(|s| s.len() >= 2) .map(|pts| WireModel { dash_from_start: false, + dash_align_end: None, text_verts: Vec::new(), name: name.to_string(), points: pts, @@ -258,6 +343,7 @@ pub fn apply_along( } out.push(WireModel { dash_from_start: false, + dash_align_end: None, text_verts, name: name.to_string(), points: Vec::new(),