diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index e51bc8bb8..f74588c50 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -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; } diff --git a/src/scene/pipeline/uniforms.rs b/src/scene/pipeline/uniforms.rs index c493081bc..29326992c 100644 --- a/src/scene/pipeline/uniforms.rs +++ b/src/scene/pipeline/uniforms.rs @@ -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 ─────────────────────────── diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 29a9f25cc..b09fb5dbd 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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() diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index 29e5bd256..79231f87f 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -5,7 +5,7 @@ struct Uniforms { flat_shade: f32, transparency_enable: f32, linetype_scale: f32, - _pad: f32, + fillmode_enable: f32, view_rot: mat4x4, eye_high: vec3, _pad_eh: f32, @@ -58,6 +58,7 @@ struct VertexOut { @location(7) @interpolate(flat) align_total: f32, @location(8) cap: vec2, @location(9) @interpolate(flat) cap_ends: vec3, + @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(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, ends: vec3) -> 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, diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index c8fac6521..747fb5740 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -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, // (segment pixel length, end half-width at A, end half-width at B). @location(9) @interpolate(flat) cap_ends: vec3, + @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, position_low: vec3, instance: + 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.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, cap_ends: vec3) -> 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 { // 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, cap_ends: vec3) -> 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, cap_ends: vec3) -> 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) { diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index c62d92bd6..0e0acfdac 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -12,7 +12,7 @@ struct Uniforms { flat_shade: f32, transparency_enable: f32, linetype_scale: f32, - _pad: f32, + fillmode_enable: f32, view_rot: mat4x4, eye_high: vec3, _pad_eh: f32, @@ -71,6 +71,7 @@ struct VertexOut { @location(8) cap: vec2, // (segment pixel length, end half-width at A, end half-width at B). @location(9) @interpolate(flat) cap_ends: vec3, + @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, position_low: vec3, c: WireCon + 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.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, cap_ends: vec3) -> 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 { // 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, cap_ends: vec3) -> 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, cap_ends: vec3) -> 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) {