fix(polyline): honor global width and fill mode

This commit is contained in:
ramox81 2026-08-24 09:36:57 +03:00
commit 8c56c03387
6 changed files with 107 additions and 7 deletions

View file

@ -964,7 +964,16 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
};
match field {
"elevation" => pline.elevation = v,
"global_width" => pline.constant_width = v,
"global_width" if v.is_finite() && v >= 0.0 => {
pline.constant_width = v;
// Global width replaces every segment override. Keeping old
// start/end values would make this editable row appear to work
// while the renderer continued using the stale per-segment data.
for vertex in &mut pline.vertices {
vertex.start_width = 0.0;
vertex.end_width = 0.0;
}
}
"vertex_x" => {
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.location.x = v;
@ -975,12 +984,28 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
vtx.location.y = v;
}
}
"start_width" => {
"start_width" if v.is_finite() && v >= 0.0 => {
if pline.constant_width > 0.0 {
let width = pline.constant_width;
for vertex in &mut pline.vertices {
vertex.start_width = width;
vertex.end_width = width;
}
pline.constant_width = 0.0;
}
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.start_width = v;
}
}
"end_width" => {
"end_width" if v.is_finite() && v >= 0.0 => {
if pline.constant_width > 0.0 {
let width = pline.constant_width;
for vertex in &mut pline.vertices {
vertex.start_width = width;
vertex.end_width = width;
}
pline.constant_width = 0.0;
}
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.end_width = v;
}

View file

@ -25,7 +25,8 @@ pub struct Uniforms {
/// zooming inside MSPACE changes one scalar instead of re-tessellating and
/// re-uploading every dashed wire.
pub linetype_scale: f32,
/// Pads the struct to the uniform alignment required by wgpu.
/// FILLMODE toggle. Wire shaders use this slot to draw wide polylines as
/// boundary outlines when fills are disabled.
pub _pad: f32,
// ── Relative-to-eye (double-single) additions ───────────────────────────

View file

@ -3527,6 +3527,7 @@ impl Scene {
};
let mut uniforms =
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
uniforms._pad = if self.document.header.fill_mode { 1.0 } else { 0.0 };
if self.document.header.paper_space_linetype_scaling
&& !inst.paper_sheet
&& inst.tile_idx.is_none()

View file

@ -5,7 +5,7 @@ struct Uniforms {
flat_shade: f32,
transparency_enable: f32,
linetype_scale: f32,
_pad: f32,
fillmode_enable: f32,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
@ -58,6 +58,7 @@ struct VertexOut {
@location(7) @interpolate(flat) align_total: f32,
@location(8) cap: vec2<f32>,
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
@ -175,6 +176,11 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
half_width * side,
);
out.cap_ends = vec3<f32>(segment_length, half_width_a, half_width_b);
out.world_band = select(
0.0,
1.0,
wire_const.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0
);
return out;
}
@ -244,10 +250,28 @@ fn clipped_cap(cap: vec2<f32>, ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
fn visible_fragment(in: VertexOut) -> bool {
if clipped_cap(in.cap, in.cap_ends) {
return false;
}
if band_interior_clipped(in) {
return false;
}
if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel {
return in_dash(
in.distance,

View file

@ -29,7 +29,7 @@ struct Uniforms {
// every line opaque.
transparency_enable: f32,
linetype_scale: f32,
_pad: f32,
fillmode_enable: f32,
// ── Relative-to-eye (double-single) ──────────────────────────────────
// view_rot is the rotation-only view-projection; vertices subtract the eye
// (eye_high + eye_low, two f32 emulating f64) before transforming, so the
@ -94,6 +94,7 @@ struct VertexOut {
@location(8) cap: vec2<f32>,
// (segment pixel length, end half-width at A, end half-width at B).
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
// Half-width of one segment end: a tapered band's own end width wins, then a
@ -215,6 +216,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
+ 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.world_band = select(0.0, 1.0, in.misc.w > 0.0 || in.taper.x > 0.0 || in.taper.y > 0.0);
out.pattern_length = in.dists.w * lt_scale;
out.pat0 = in.pat0 * lt_scale;
out.pat1 = in.pat1 * lt_scale;
@ -287,6 +289,24 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
// Wide-polyline fill off: retain a one-pixel line at both band edges.
// Segment cap overhangs are discarded; adjacent sampled segments meet at
// their shared endpoint without drawing an interior seam across the band.
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
// Negative pattern length is the persistent-arena tombstone sentinel.
// Discard before cap/alpha work so deleted slabs cannot write color/depth.
@ -296,6 +316,9 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
// LOD: once the pattern's smallest feature drops below ~1 px
// on screen, dash gaps alias / shimmer (or vanish completely)
@ -323,6 +346,9 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {

View file

@ -12,7 +12,7 @@ struct Uniforms {
flat_shade: f32,
transparency_enable: f32,
linetype_scale: f32,
_pad: f32,
fillmode_enable: f32,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
@ -71,6 +71,7 @@ struct VertexOut {
@location(8) cap: vec2<f32>,
// (segment pixel length, end half-width at A, end half-width at B).
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
// Half-width of one segment end: a tapered band's own end width wins, then a
@ -181,6 +182,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
+ 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.world_band = select(0.0, 1.0, c.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0);
out.pattern_length = c.pattern_length * lt_scale;
out.pat0 = c.pat0 * lt_scale;
out.pat1 = c.pat1 * lt_scale;
@ -244,6 +246,21 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
// Negative pattern length is the persistent-arena tombstone sentinel.
// Discard before cap/alpha work so deleted slabs cannot write color/depth.
@ -253,6 +270,9 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {
@ -274,6 +294,9 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {