Host wide polyline quads on their 3D plane instead of billboarding in screen space

This commit is contained in:
Karim Jerbi committed 2026-09-09 14:23:10 +01:00
1 parent 76e1610dcc
commit ed177cbf25
6 files changed
+376 -180

No files matched your search

+19
View File
@@ -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"),
+29 -1
View File
@@ -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);
+79 -37
View File
@@ -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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>(1.0, 0.0);
if segment_length > 1e-4 {
direction = segment / segment_length;
}
let perpendicular = vec2<f32>(-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<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
var final_clip: vec4<f32>;
var out_dist: f32;
var out_cap: vec2<f32>;
var out_cap_ends: vec3<f32>;
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<f32>(1.0, 0.0, 0.0);
if world_len > 1e-6 {
world_dir = world_delta / world_len;
}
var norm = vec3<f32>(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<f32>(0.0, 1.0, 0.0), world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(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<f32>(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<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>(1.0, 0.0);
if segment_length > 1e-4 {
direction = segment / segment_length;
}
let perpendicular = vec2<f32>(-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<f32>(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<f32>(which_end * segment_length + extension * half_width, half_width * side);
out_cap_ends = vec3<f32>(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<f32>(
which_end * segment_length + extension * half_width,
half_width * side,
);
out.cap_ends = vec3<f32>(segment_length, half_width_a, half_width_b);
return out;
}
+79 -37
View File
@@ -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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>(1.0, 0.0);
if segment_length > 1e-4 {
direction = segment / segment_length;
}
let perpendicular = vec2<f32>(-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<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
var final_clip: vec4<f32>;
var out_dist: f32;
var out_cap: vec2<f32>;
var out_cap_ends: vec3<f32>;
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<f32>(1.0, 0.0, 0.0);
if world_len > 1e-6 {
world_dir = world_delta / world_len;
}
var norm = vec3<f32>(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<f32>(0.0, 1.0, 0.0), world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(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<f32>(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<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>(1.0, 0.0);
if segment_length > 1e-4 {
direction = segment / segment_length;
}
let perpendicular = vec2<f32>(-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<f32>(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<f32>(which_end * segment_length + extension * half_width, half_width * side);
out_cap_ends = vec3<f32>(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<f32>(
which_end * segment_length + extension * half_width,
half_width * side,
);
out.cap_ends = vec3<f32>(segment_length, half_width_a, half_width_b);
return out;
}
+85 -54
View File
@@ -156,54 +156,89 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, 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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>;
if seg_len > 1e-4 {
dir = seg / seg_len;
} else {
dir = vec2<f32>(1.0, 0.0);
}
let perp = vec2<f32>(-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<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
var final_clip: vec4<f32>;
var out_dist: f32;
var out_cap: vec2<f32>;
var out_cap_ends: vec3<f32>;
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<f32>(1.0, 0.0, 0.0);
if world_len > 1e-6 {
world_dir = world_delta / world_len;
}
var norm = vec3<f32>(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<f32>(0.0, 1.0, 0.0), world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(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<f32>(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<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>;
if seg_len > 1e-4 {
dir = seg / seg_len;
} else {
dir = vec2<f32>(1.0, 0.0);
}
let perp = vec2<f32>(-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<f32>(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<f32>(which_end * seg_len + ext * hw, hw * side);
out_cap_ends = vec3<f32>(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<f32>, position_low: vec3<f32>, 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<f32>(which_end * seg_len + ext * hw, hw * side);
out.cap_ends = vec3<f32>(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;
+85 -51
View File
@@ -128,51 +128,89 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, 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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>;
if seg_len > 1e-4 {
dir = seg / seg_len;
} else {
dir = vec2<f32>(1.0, 0.0);
}
let perp = vec2<f32>(-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<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
var final_clip: vec4<f32>;
var out_dist: f32;
var out_cap: vec2<f32>;
var out_cap_ends: vec3<f32>;
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<f32>(1.0, 0.0, 0.0);
if world_len > 1e-6 {
world_dir = world_delta / world_len;
}
var norm = vec3<f32>(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<f32>(0.0, 1.0, 0.0), world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(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<f32>(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<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(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<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(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<f32>;
if seg_len > 1e-4 {
dir = seg / seg_len;
} else {
dir = vec2<f32>(1.0, 0.0);
}
let perp = vec2<f32>(-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<f32>(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<f32>(which_end * seg_len + ext * hw, hw * side);
out_cap_ends = vec3<f32>(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<f32>, position_low: vec3<f32>, 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<f32>(which_end * seg_len + ext * hw, hw * side);
out.cap_ends = vec3<f32>(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;