feat(mline): GPU-dash multiline elements with shared A-type alignment
MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per dash — to keep the parallel lines in phase. Move pure-dash elements onto the GPU dash shader and end-align every element to the multiline's centre-line so they share one interior phase. - wire_distances now accumulates arc-length from the double-single (high+low) points, so the dash phase stays precise at UTM coordinates. f32-high-only deltas quantised ~0.1 there and drifted the parallel lines apart — the original reason MLINE dashes were CPU-side. - New WireModel.dash_align_end carries a shared begin/end dash length. The shader uses it as align_end for every parallel element (the interior phase depends on align_end, not the wire's own length) while align_total stays the element's own length, so each still ends on a dash at its own endpoint. - MLINE tessellation derives that length once from the centre-line, routes pure dash/space/dot elements to the GPU pattern and text-bearing elements to apply_along. - apply_along gained an "A"-type mode (shared reference length): a solid begin/end dash and a phased interior, so its dashes — and embedded text — line up with the GPU-dashed sibling elements instead of tiling from start. Pure-dash MLINEs now cost one WireModel + pattern per element instead of N segments; single (non-MLINE) entities keep the from-start tiling unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
743dc1026d
commit
5bd683cf4e
41 changed files with 263 additions and 14 deletions
|
|
@ -1236,6 +1236,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||
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<Message> {
|
|||
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![
|
||||
|
|
|
|||
|
|
@ -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![],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1400,6 +1400,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
let mut wires: Vec<WireModel> = 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ fn v3(pt: DVec3) -> Vector3 {
|
|||
fn preview_wire(points: Vec<Vec3>) -> 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(),
|
||||
|
|
|
|||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ fn v3(pt: DVec3) -> Vector3 {
|
|||
fn preview_wire(points: Vec<DVec3>) -> WireModel {
|
||||
WireModel {
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "dimlinear_preview".to_string(),
|
||||
points: points
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 {
|
|||
fn preview_wire(points: Vec<Vec3>) -> 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(),
|
||||
|
|
|
|||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ fn v3(pt: DVec3) -> Vector3 {
|
|||
fn preview_wire(points: Vec<Vec3>) -> 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(),
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
1
src/scene/cache/block_cache.rs
vendored
1
src/scene/cache/block_cache.rs
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<WireModel> = 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<WireModel> = 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,
|
||||
|
|
|
|||
|
|
@ -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<f32>,
|
||||
/// 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(),
|
||||
|
|
|
|||
|
|
@ -258,22 +258,30 @@ fn wire_distances(wire: &WireModel) -> (Vec<f32>, 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, 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
|
||||
|
|
|
|||
|
|
@ -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<Vec<LtSeg>> {
|
||||
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<LtSeg> = 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<f32>,
|
||||
) -> Vec<WireModel> {
|
||||
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<Vec<LtSeg>> = 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<[f32; 3]>> = 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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue