From ed177cbf2535e68e89483dfab04de5e504ba1696 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Wed, 9 Sep 2026 14:23:10 +0100 Subject: [PATCH] Host wide polyline quads on their 3D plane instead of billboarding in screen space --- src/scene/pipeline/gpu_tests.rs | 19 ++++ src/scene/pipeline/wire_gpu.rs | 30 +++++- src/shaders/block_wire.wgsl | 116 +++++++++++++++-------- src/shaders/block_wire_storage.wgsl | 116 +++++++++++++++-------- src/shaders/wire.wgsl | 139 +++++++++++++++++----------- src/shaders/wire_indexed.wgsl | 136 +++++++++++++++++---------- 6 files changed, 376 insertions(+), 180 deletions(-) diff --git a/src/scene/pipeline/gpu_tests.rs b/src/scene/pipeline/gpu_tests.rs index 3aeacb28c..c2dfac225 100644 --- a/src/scene/pipeline/gpu_tests.rs +++ b/src/scene/pipeline/gpu_tests.rs @@ -662,6 +662,25 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() { assert_eq!(circles[0].instance_count, 2); pipeline.gpu_circles = std::sync::Arc::new(circles); + // Wide straight polyline segment on the 3D plane + let mut straight_wide = WireModel::default(); + straight_wide.name = "wide_straight".into(); + straight_wide.points = vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]]; + straight_wide.world_width = 10.0; + straight_wide.color = [0.9, 0.2, 0.3, 1.0]; + + let wide_wires = vec![straight_wide]; + let gpu_wires = wire_gpu::WireGpu::from_run( + &device, + &queue, + &wide_wires, + &depth_map, + false, + pipeline.wire_const_bgl.as_ref(), + ); + assert!(!gpu_wires.is_empty()); + pipeline.gpu_wires = std::sync::Arc::new(gpu_wires); + pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512)); let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("tilted_donut_target"), diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 0e2884af4..e3901ee3d 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -532,7 +532,35 @@ fn finite3(p: [f32; 3]) -> bool { fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) { let Some(marker) = wire.point_marker else { - return ([0.0; 4], [0.0; 4], [0.0; 4]); + for tg in &wire.tangent_geoms { + match tg { + crate::scene::model::wire_model::TangentGeom::Arc { axis_x, axis_y, .. } + | crate::scene::model::wire_model::TangentGeom::PlanarCircle { axis_x, axis_y, .. } => { + let ax = glam::DVec3::from_array(*axis_x); + let ay = glam::DVec3::from_array(*axis_y); + let n = ax.cross(ay).normalize_or(glam::DVec3::Z); + return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]); + } + crate::scene::model::wire_model::TangentGeom::PlanarEllipse { normal, .. } => { + let n = glam::DVec3::from_array(*normal).normalize_or(glam::DVec3::Z); + return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]); + } + _ => {} + } + } + if wire.points.len() >= 3 { + let p0 = glam::Vec3::from_array(wire.points[0]); + for i in 1..wire.points.len() - 1 { + let v1 = glam::Vec3::from_array(wire.points[i]) - p0; + let v2 = glam::Vec3::from_array(wire.points[i + 1]) - p0; + let cross = v1.cross(v2); + if cross.length_squared() > 1e-6 { + let n = cross.normalize(); + return ([0.0; 4], [0.0; 4], [n.x, n.y, n.z, 0.0]); + } + } + } + return ([0.0; 4], [0.0; 4], [0.0, 0.0, 1.0, 0.0]); }; let origin = marker.origin; let (hx, lx) = WireModel::split_ds(origin.x); diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index 3847bbaa5..d70301806 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -121,36 +121,83 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut let rel_a = marker_relative(in.pos_a, in.pos_a_low, in.translation, in.translation_low); let rel_b = marker_relative(in.pos_b, in.pos_b_low, in.translation, in.translation_low); - let clip_a = u.view_rot * vec4(rel_a, 1.0); - let clip_b = u.view_rot * vec4(rel_b, 1.0); - let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; - let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; - let segment = screen_b - screen_a; - let segment_length = length(segment); - var direction = vec2(1.0, 0.0); - if segment_length > 1e-4 { - direction = segment / segment_length; - } - let perpendicular = vec2(-direction.y, direction.x); - let clip_position = mix(clip_a, clip_b, which_end); - let half_width_a = resolve_hw( - in.taper_ratio.x, - wire_const.world_half_width, - wire_const.half_width, - ); - let half_width_b = resolve_hw( - in.taper_ratio.y, - wire_const.world_half_width, - wire_const.half_width, - ); - let half_width = mix(half_width_a, half_width_b, which_end); let is_world = wire_const.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0; - let extension = select(which_end * 2.0 - 1.0, 0.0, is_world); - let offset_px = perpendicular * half_width * side - + direction * half_width * extension; - let ndc_offset = offset_px / (u.viewport_size * 0.5); - let final_clip = clip_position - + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + + var final_clip: vec4; + var out_dist: f32; + var out_cap: vec2; + var out_cap_ends: vec3; + + if is_world { + // Wide polylines have physical world-unit width (world_half_width or taper > 0). + // Expand the quad in 3D world space on the entity's plane (perpendicular to + // the segment and the plane normal), so the rectangle remains hosted rigidly + // on the 3D plane when the camera rotates, orbits, or tilts, matching CAD + // behavior and circle.wgsl planar arcs. + let world_hw_a = select(wire_const.world_half_width, in.taper_ratio.x * wire_const.world_half_width, in.taper_ratio.x > 0.0); + let world_hw_b = select(wire_const.world_half_width, in.taper_ratio.y * wire_const.world_half_width, in.taper_ratio.y > 0.0); + let cur_world_hw = mix(world_hw_a, world_hw_b, which_end); + let eff_hw = max(cur_world_hw, 0.5 * u.world_per_pixel); + + let world_delta = rel_b - rel_a; + let world_len = length(world_delta); + var world_dir = vec3(1.0, 0.0, 0.0); + if world_len > 1e-6 { + world_dir = world_delta / world_len; + } + + var norm = vec3(0.0, 0.0, 1.0); + if length(wire_const.marker_normal_scale.xyz) > 1e-4 { + norm = normalize(wire_const.marker_normal_scale.xyz); + } + + var perp_world = cross(norm, world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(0.0, 1.0, 0.0), world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(1.0, 0.0, 0.0), world_dir); + } + } + perp_world = normalize(perp_world); + + let pos_rel = mix(rel_a, rel_b, which_end); + let world_pos = pos_rel + perp_world * (eff_hw * side); + var clip_pos = u.view_rot * vec4(world_pos, 1.0); + clip_pos.z = clip_pos.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos.w; + + final_clip = clip_pos; + out_dist = mix(in.distances.x, in.distances.y, which_end); + out_cap = vec2(which_end * world_len, eff_hw * side); + out_cap_ends = vec3(world_len, world_hw_a, world_hw_b); + } else { + // Thin wires expand in screen pixels facing the camera with rounded end caps / joints. + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; + let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; + let segment = screen_b - screen_a; + let segment_length = length(segment); + var direction = vec2(1.0, 0.0); + if segment_length > 1e-4 { + direction = segment / segment_length; + } + let perpendicular = vec2(-direction.y, direction.x); + let clip_position = mix(clip_a, clip_b, which_end); + + let half_width = resolve_hw(0.0, 0.0, wire_const.half_width); + let extension = which_end * 2.0 - 1.0; + let offset_px = perpendicular * half_width * side + + direction * half_width * extension; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + var clip_pos_out = clip_position + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + clip_pos_out.z = clip_pos_out.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos_out.w; + + final_clip = clip_pos_out; + out_dist = mix(in.distances.x, in.distances.y, which_end) + + extension * half_width * u.world_per_pixel; + out_cap = vec2(which_end * segment_length + extension * half_width, half_width * side); + out_cap_ends = vec3(segment_length, half_width, half_width); + } let scale = u.linetype_scale; var min_element = wire_const.pattern_length * scale; @@ -173,21 +220,16 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut var out: VertexOut; out.clip_pos = final_clip; - out.clip_pos.z -= in.depth.x * DRAW_ORDER_BIAS * out.clip_pos.w; out.color = wire_const.color; - out.distance = mix(in.distances.x, in.distances.y, which_end) - + extension * half_width * u.world_per_pixel; + out.distance = out_dist; + out.cap = out_cap; + out.cap_ends = out_cap_ends; out.pattern_length = wire_const.pattern_length * scale; out.pat0 = wire_const.pat0 * scale; out.pat1 = wire_const.pat1 * scale; out.min_elem = min_element; out.align_end = wire_const.align_end * scale; out.align_total = wire_const.align_total; - out.cap = vec2( - which_end * segment_length + extension * half_width, - half_width * side, - ); - out.cap_ends = vec3(segment_length, half_width_a, half_width_b); return out; } diff --git a/src/shaders/block_wire_storage.wgsl b/src/shaders/block_wire_storage.wgsl index a11ac646d..3b3544e22 100644 --- a/src/shaders/block_wire_storage.wgsl +++ b/src/shaders/block_wire_storage.wgsl @@ -152,36 +152,83 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut let rel_a = marker_relative(seg_pos_a, seg_pos_a_low, in.translation, in.translation_low); let rel_b = marker_relative(seg_pos_b, seg_pos_b_low, in.translation, in.translation_low); - let clip_a = u.view_rot * vec4(rel_a, 1.0); - let clip_b = u.view_rot * vec4(rel_b, 1.0); - let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; - let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; - let segment = screen_b - screen_a; - let segment_length = length(segment); - var direction = vec2(1.0, 0.0); - if segment_length > 1e-4 { - direction = segment / segment_length; - } - let perpendicular = vec2(-direction.y, direction.x); - let clip_position = mix(clip_a, clip_b, which_end); - let half_width_a = resolve_hw( - seg_taper_ratio.x, - wire_const.world_half_width, - wire_const.half_width, - ); - let half_width_b = resolve_hw( - seg_taper_ratio.y, - wire_const.world_half_width, - wire_const.half_width, - ); - let half_width = mix(half_width_a, half_width_b, which_end); let is_world = wire_const.world_half_width > 0.0 || seg_taper_ratio.x > 0.0 || seg_taper_ratio.y > 0.0; - let extension = select(which_end * 2.0 - 1.0, 0.0, is_world); - let offset_px = perpendicular * half_width * side - + direction * half_width * extension; - let ndc_offset = offset_px / (u.viewport_size * 0.5); - let final_clip = clip_position - + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + + var final_clip: vec4; + var out_dist: f32; + var out_cap: vec2; + var out_cap_ends: vec3; + + if is_world { + // Wide polylines have physical world-unit width (world_half_width or taper > 0). + // Expand the quad in 3D world space on the entity's plane (perpendicular to + // the segment and the plane normal), so the rectangle remains hosted rigidly + // on the 3D plane when the camera rotates, orbits, or tilts, matching CAD + // behavior and circle.wgsl planar arcs. + let world_hw_a = select(wire_const.world_half_width, seg_taper_ratio.x * wire_const.world_half_width, seg_taper_ratio.x > 0.0); + let world_hw_b = select(wire_const.world_half_width, seg_taper_ratio.y * wire_const.world_half_width, seg_taper_ratio.y > 0.0); + let cur_world_hw = mix(world_hw_a, world_hw_b, which_end); + let eff_hw = max(cur_world_hw, 0.5 * u.world_per_pixel); + + let world_delta = rel_b - rel_a; + let world_len = length(world_delta); + var world_dir = vec3(1.0, 0.0, 0.0); + if world_len > 1e-6 { + world_dir = world_delta / world_len; + } + + var norm = vec3(0.0, 0.0, 1.0); + if length(wire_const.marker_normal_scale.xyz) > 1e-4 { + norm = normalize(wire_const.marker_normal_scale.xyz); + } + + var perp_world = cross(norm, world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(0.0, 1.0, 0.0), world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(1.0, 0.0, 0.0), world_dir); + } + } + perp_world = normalize(perp_world); + + let pos_rel = mix(rel_a, rel_b, which_end); + let world_pos = pos_rel + perp_world * (eff_hw * side); + var clip_pos = u.view_rot * vec4(world_pos, 1.0); + clip_pos.z = clip_pos.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos.w; + + final_clip = clip_pos; + out_dist = mix(seg_distances.x, seg_distances.y, which_end); + out_cap = vec2(which_end * world_len, eff_hw * side); + out_cap_ends = vec3(world_len, world_hw_a, world_hw_b); + } else { + // Thin wires expand in screen pixels facing the camera with rounded end caps / joints. + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; + let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; + let segment = screen_b - screen_a; + let segment_length = length(segment); + var direction = vec2(1.0, 0.0); + if segment_length > 1e-4 { + direction = segment / segment_length; + } + let perpendicular = vec2(-direction.y, direction.x); + let clip_position = mix(clip_a, clip_b, which_end); + + let half_width = resolve_hw(0.0, 0.0, wire_const.half_width); + let extension = which_end * 2.0 - 1.0; + let offset_px = perpendicular * half_width * side + + direction * half_width * extension; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + var clip_pos_out = clip_position + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + clip_pos_out.z = clip_pos_out.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos_out.w; + + final_clip = clip_pos_out; + out_dist = mix(seg_distances.x, seg_distances.y, which_end) + + extension * half_width * u.world_per_pixel; + out_cap = vec2(which_end * segment_length + extension * half_width, half_width * side); + out_cap_ends = vec3(segment_length, half_width, half_width); + } let scale = u.linetype_scale; var min_element = wire_const.pattern_length * scale; @@ -204,21 +251,16 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut var out: VertexOut; out.clip_pos = final_clip; - out.clip_pos.z -= in.depth.x * DRAW_ORDER_BIAS * out.clip_pos.w; out.color = wire_const.color; - out.distance = mix(seg_distances.x, seg_distances.y, which_end) - + extension * half_width * u.world_per_pixel; + out.distance = out_dist; + out.cap = out_cap; + out.cap_ends = out_cap_ends; out.pattern_length = wire_const.pattern_length * scale; out.pat0 = wire_const.pat0 * scale; out.pat1 = wire_const.pat1 * scale; out.min_elem = min_element; out.align_end = wire_const.align_end * scale; out.align_total = wire_const.align_total; - out.cap = vec2( - which_end * segment_length + extension * half_width, - half_width * side, - ); - out.cap_ends = vec3(segment_length, half_width_a, half_width_b); return out; } diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index b89067aea..afa7a0d75 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -156,54 +156,89 @@ fn marker_relative(position_high: vec3, position_low: vec3, instance: // UTM-scale coordinates and after a cross-drawing paste, with no jitter. let rel_a = marker_relative(in.pos_a, in.pos_a_low, in); let rel_b = marker_relative(in.pos_b, in.pos_b_low, in); - let clip_a = u.view_rot * vec4(rel_a, 1.0); - let clip_b = u.view_rot * vec4(rel_b, 1.0); - - // NDC of both endpoints. - let ndc_a = clip_a.xy / clip_a.w; - let ndc_b = clip_b.xy / clip_b.w; - - // Screen-space pixel positions. - let screen_a = ndc_a * u.viewport_size * 0.5; - let screen_b = ndc_b * u.viewport_size * 0.5; - - // Screen-space direction / perpendicular of the segment. - let seg = screen_b - screen_a; - let seg_len = length(seg); - var dir: vec2; - if seg_len > 1e-4 { - dir = seg / seg_len; - } else { - dir = vec2(1.0, 0.0); - } - let perp = vec2(-dir.y, dir.x); - - // Select the clip-space position for this vertex's endpoint. - let clip_pos = mix(clip_a, clip_b, which_end); - - // A wide polyline carries its band width in world units: expand the quad by - // `world_half_width / world_per_pixel` (pixels) so the band tracks zoom. A - // normal wire (world_half_width == 0) uses the screen-pixel half-width, - // honouring the LWDISPLAY toggle (off → collapse to a 1-pixel line). - // A tapered band interpolates a per-endpoint world half-width across the - // segment; a constant band uses `world_half_width`. Both clamp to a - // half-pixel so a zoomed-out band stays a hairline instead of vanishing. - let hw_a = resolve_hw(in.taper.x, in.misc.w, in.dists.z); - let hw_b = resolve_hw(in.taper.y, in.misc.w, in.dists.z); - let hw = mix(hw_a, hw_b, which_end); - - // Extend the quad longitudinally by the end half-width and let the - // fragment stage round the overhang off: adjoining segments then meet in - // overlapping round joints, closing the wedge gaps a perpendicular-only - // expansion leaves on the outside of corners and along tessellated arcs. - // Wide polylines have physical world-unit width (in.misc.w or in.taper > 0); - // in CAD, wide polyline segments are flat/square-ended 2D rectangles (or - // trapezoids), not rounded pill shapes. let is_world = in.misc.w > 0.0 || in.taper.x > 0.0 || in.taper.y > 0.0; - let ext = select(which_end * 2.0 - 1.0, 0.0, is_world); - let offset_px = perp * hw * side + dir * hw * ext; - let ndc_offset = offset_px / (u.viewport_size * 0.5); - let final_clip = clip_pos + vec4(ndc_offset * clip_pos.w, 0.0, 0.0); + + var final_clip: vec4; + var out_dist: f32; + var out_cap: vec2; + var out_cap_ends: vec3; + + if is_world { + // Wide polylines have physical world-unit width (in.misc.w or in.taper > 0). + // Expand the quad in 3D world space on the entity's plane (perpendicular to + // the segment and the plane normal), so the rectangle remains hosted rigidly + // on the 3D plane when the camera rotates, orbits, or tilts, matching CAD + // behavior and circle.wgsl planar arcs. + let world_hw_a = select(in.misc.w, in.taper.x, in.taper.x > 0.0); + let world_hw_b = select(in.misc.w, in.taper.y, in.taper.y > 0.0); + let cur_world_hw = mix(world_hw_a, world_hw_b, which_end); + let eff_hw = max(cur_world_hw, 0.5 * u.world_per_pixel); + + let world_delta = rel_b - rel_a; + let world_len = length(world_delta); + var world_dir = vec3(1.0, 0.0, 0.0); + if world_len > 1e-6 { + world_dir = world_delta / world_len; + } + + var norm = vec3(0.0, 0.0, 1.0); + if length(in.marker_normal_scale.xyz) > 1e-4 { + norm = normalize(in.marker_normal_scale.xyz); + } + + var perp_world = cross(norm, world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(0.0, 1.0, 0.0), world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(1.0, 0.0, 0.0), world_dir); + } + } + perp_world = normalize(perp_world); + + let pos_rel = mix(rel_a, rel_b, which_end); + let world_pos = pos_rel + perp_world * (eff_hw * side); + var clip_pos = u.view_rot * vec4(world_pos, 1.0); + clip_pos.z = clip_pos.z - in.misc.x * DRAW_ORDER_BIAS * clip_pos.w; + + final_clip = clip_pos; + out_dist = mix(in.dists.x, in.dists.y, which_end); + out_cap = vec2(which_end * world_len, eff_hw * side); + out_cap_ends = vec3(world_len, world_hw_a, world_hw_b); + } else { + // Thin wires expand in screen pixels facing the camera with rounded end caps / joints. + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + + let ndc_a = clip_a.xy / clip_a.w; + let ndc_b = clip_b.xy / clip_b.w; + + let screen_a = ndc_a * u.viewport_size * 0.5; + let screen_b = ndc_b * u.viewport_size * 0.5; + + let seg = screen_b - screen_a; + let seg_len = length(seg); + var dir: vec2; + if seg_len > 1e-4 { + dir = seg / seg_len; + } else { + dir = vec2(1.0, 0.0); + } + let perp = vec2(-dir.y, dir.x); + + let clip_pos = mix(clip_a, clip_b, which_end); + + let hw = resolve_hw(0.0, 0.0, in.dists.z); + let ext = which_end * 2.0 - 1.0; + let offset_px = perp * hw * side + dir * hw * ext; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + var clip_pos_out = clip_pos + vec4(ndc_offset * clip_pos.w, 0.0, 0.0); + clip_pos_out.z = clip_pos_out.z - in.misc.x * DRAW_ORDER_BIAS * clip_pos_out.w; + + final_clip = clip_pos_out; + out_dist = mix(in.dists.x, in.dists.y, which_end) + ext * hw * u.world_per_pixel; + out_cap = vec2(which_end * seg_len + ext * hw, hw * side); + out_cap_ends = vec3(seg_len, hw, hw); + } // Smallest non-zero dash / gap element, in world units. Used by // the fragment stage to decide when the pattern's finest feature @@ -223,14 +258,10 @@ fn marker_relative(position_high: vec3, position_low: vec3, instance: var out: VertexOut; out.clip_pos = final_clip; - out.clip_pos.z = out.clip_pos.z - in.misc.x * DRAW_ORDER_BIAS * out.clip_pos.w; out.color = in.color; - // Dash arc-length, extrapolated over the cap overhang so the pattern - // stays continuous through a joint. - out.distance = mix(in.dists.x, in.dists.y, which_end) - + ext * hw * u.world_per_pixel; - out.cap = vec2(which_end * seg_len + ext * hw, hw * side); - out.cap_ends = vec3(seg_len, hw_a, hw_b); + out.distance = out_dist; + out.cap = out_cap; + out.cap_ends = out_cap_ends; out.pattern_length = in.dists.w * lt_scale; out.pat0 = in.pat0 * lt_scale; out.pat1 = in.pat1 * lt_scale; diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index e474c8088..d739d6e15 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -128,51 +128,89 @@ fn marker_relative(position_high: vec3, position_low: vec3, c: WireCon let rel_a = marker_relative(in.pos_a, in.pos_a_low, c); let rel_b = marker_relative(in.pos_b, in.pos_b_low, c); - let clip_a = u.view_rot * vec4(rel_a, 1.0); - let clip_b = u.view_rot * vec4(rel_b, 1.0); - - let ndc_a = clip_a.xy / clip_a.w; - let ndc_b = clip_b.xy / clip_b.w; - - let screen_a = ndc_a * u.viewport_size * 0.5; - let screen_b = ndc_b * u.viewport_size * 0.5; - - let seg = screen_b - screen_a; - let seg_len = length(seg); - var dir: vec2; - if seg_len > 1e-4 { - dir = seg / seg_len; - } else { - dir = vec2(1.0, 0.0); - } - let perp = vec2(-dir.y, dir.x); - - let clip_pos = mix(clip_a, clip_b, which_end); - - // A wide polyline carries its band width in world units: expand the quad - // by `world_half_width / world_per_pixel` (screen pixels) so the band grows - // and shrinks with zoom. A normal wire (world_half_width == 0) uses the - // screen-pixel half-width, honouring the LWDISPLAY toggle. - // A tapered band carries normalized endpoint widths on the instance: - // interpolate across the segment so the band narrows/widens smoothly. A - // constant band uses the per-wire `world_half_width`. Both clamp to a - // half-pixel so a zoomed-out band stays a hairline instead of vanishing. - let hw_a = resolve_hw(in.taper_ratio.x, c.world_half_width, c.half_width); - let hw_b = resolve_hw(in.taper_ratio.y, c.world_half_width, c.half_width); - let hw = mix(hw_a, hw_b, which_end); - - // Extend the quad longitudinally by the end half-width and let the - // fragment stage round the overhang off: adjoining segments then meet in - // overlapping round joints, closing the wedge gaps a perpendicular-only - // expansion leaves on the outside of corners and along tessellated arcs. - // Wide polylines have physical world-unit width (world_half_width or taper > 0); - // in CAD, wide polyline segments are flat/square-ended 2D rectangles (or - // trapezoids), not rounded pill shapes. let is_world = c.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0; - let ext = select(which_end * 2.0 - 1.0, 0.0, is_world); - let offset_px = perp * hw * side + dir * hw * ext; - let ndc_offset = offset_px / (u.viewport_size * 0.5); - let final_clip = clip_pos + vec4(ndc_offset * clip_pos.w, 0.0, 0.0); + + var final_clip: vec4; + var out_dist: f32; + var out_cap: vec2; + var out_cap_ends: vec3; + + if is_world { + // Wide polylines have physical world-unit width (world_half_width or taper > 0). + // Expand the quad in 3D world space on the entity's plane (perpendicular to + // the segment and the plane normal), so the rectangle remains hosted rigidly + // on the 3D plane when the camera rotates, orbits, or tilts, matching CAD + // behavior and circle.wgsl planar arcs. + let world_hw_a = select(c.world_half_width, in.taper_ratio.x * c.world_half_width, in.taper_ratio.x > 0.0); + let world_hw_b = select(c.world_half_width, in.taper_ratio.y * c.world_half_width, in.taper_ratio.y > 0.0); + let cur_world_hw = mix(world_hw_a, world_hw_b, which_end); + let eff_hw = max(cur_world_hw, 0.5 * u.world_per_pixel); + + let world_delta = rel_b - rel_a; + let world_len = length(world_delta); + var world_dir = vec3(1.0, 0.0, 0.0); + if world_len > 1e-6 { + world_dir = world_delta / world_len; + } + + var norm = vec3(0.0, 0.0, 1.0); + if length(c.marker_normal_scale.xyz) > 1e-4 { + norm = normalize(c.marker_normal_scale.xyz); + } + + var perp_world = cross(norm, world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(0.0, 1.0, 0.0), world_dir); + if length(perp_world) < 1e-4 { + perp_world = cross(vec3(1.0, 0.0, 0.0), world_dir); + } + } + perp_world = normalize(perp_world); + + let pos_rel = mix(rel_a, rel_b, which_end); + let world_pos = pos_rel + perp_world * (eff_hw * side); + var clip_pos = u.view_rot * vec4(world_pos, 1.0); + clip_pos.z = clip_pos.z - c.draw_depth * DRAW_ORDER_BIAS * clip_pos.w; + + final_clip = clip_pos; + out_dist = mix(in.distance_a, in.distance_b, which_end); + out_cap = vec2(which_end * world_len, eff_hw * side); + out_cap_ends = vec3(world_len, world_hw_a, world_hw_b); + } else { + // Thin wires expand in screen pixels facing the camera with rounded end caps / joints. + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + + let ndc_a = clip_a.xy / clip_a.w; + let ndc_b = clip_b.xy / clip_b.w; + + let screen_a = ndc_a * u.viewport_size * 0.5; + let screen_b = ndc_b * u.viewport_size * 0.5; + + let seg = screen_b - screen_a; + let seg_len = length(seg); + var dir: vec2; + if seg_len > 1e-4 { + dir = seg / seg_len; + } else { + dir = vec2(1.0, 0.0); + } + let perp = vec2(-dir.y, dir.x); + + let clip_pos = mix(clip_a, clip_b, which_end); + + let hw = resolve_hw(0.0, 0.0, c.half_width); + let ext = which_end * 2.0 - 1.0; + let offset_px = perp * hw * side + dir * hw * ext; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + var clip_pos_out = clip_pos + vec4(ndc_offset * clip_pos.w, 0.0, 0.0); + clip_pos_out.z = clip_pos_out.z - c.draw_depth * DRAW_ORDER_BIAS * clip_pos_out.w; + + final_clip = clip_pos_out; + out_dist = mix(in.distance_a, in.distance_b, which_end) + ext * hw * u.world_per_pixel; + out_cap = vec2(which_end * seg_len + ext * hw, hw * side); + out_cap_ends = vec3(seg_len, hw, hw); + } let lt_scale = u.linetype_scale; var min_elem: f32 = c.pattern_length * lt_scale; @@ -189,14 +227,10 @@ fn marker_relative(position_high: vec3, position_low: vec3, c: WireCon var out: VertexOut; out.clip_pos = final_clip; - out.clip_pos.z = out.clip_pos.z - c.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w; out.color = c.color; - // Dash arc-length, extrapolated over the cap overhang so the pattern - // stays continuous through a joint. - out.distance = mix(in.distance_a, in.distance_b, which_end) - + ext * hw * u.world_per_pixel; - out.cap = vec2(which_end * seg_len + ext * hw, hw * side); - out.cap_ends = vec3(seg_len, hw_a, hw_b); + out.distance = out_dist; + out.cap = out_cap; + out.cap_ends = out_cap_ends; out.pattern_length = c.pattern_length * lt_scale; out.pat0 = c.pat0 * lt_scale; out.pat1 = c.pat1 * lt_scale;