forked from 1b-agent/opencadstudio-upstream
fix(render): correct polyline segment widths, flat arc end caps, and zero-width endpoints
This commit is contained in:
1 parent
6ca16738f0
commit
0ad31ece6f
8 files changed
+346
-73
No files matched your search
+276
-24
@@ -282,7 +282,53 @@ fn split_mixed_polyline(
|
||||
end_angle,
|
||||
} = *tg
|
||||
{
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Check if the CCW start_angle of this TangentGeom::Arc corresponds
|
||||
// to the segment's end vertex (clockwise bulge arc in CAD where
|
||||
// bulge_arc_to_tangent inverted start_angle and end_angle to enforce CCW).
|
||||
let (p_seg_start, p_seg_end) = if i + 1 < key_vertices.len() {
|
||||
(key_vertices[i], key_vertices[i + 1])
|
||||
} else if !key_vertices.is_empty() {
|
||||
(
|
||||
key_vertices[i % key_vertices.len()],
|
||||
key_vertices[(i + 1) % key_vertices.len()],
|
||||
)
|
||||
} else {
|
||||
([0.0; 3], [0.0; 3])
|
||||
};
|
||||
|
||||
let p_sa = [
|
||||
center[0] + radius * (start_angle.cos() * axis_x[0] + start_angle.sin() * axis_y[0]),
|
||||
center[1] + radius * (start_angle.cos() * axis_x[1] + start_angle.sin() * axis_y[1]),
|
||||
center[2] + radius * (start_angle.cos() * axis_x[2] + start_angle.sin() * axis_y[2]),
|
||||
];
|
||||
let is_reversed = if !key_vertices.is_empty() {
|
||||
let d_start_sq = (p_sa[0] - p_seg_start[0]).powi(2)
|
||||
+ (p_sa[1] - p_seg_start[1]).powi(2)
|
||||
+ (p_sa[2] - p_seg_start[2]).powi(2);
|
||||
let d_end_sq = (p_sa[0] - p_seg_end[0]).powi(2)
|
||||
+ (p_sa[1] - p_seg_end[1]).powi(2)
|
||||
+ (p_sa[2] - p_seg_end[2]).powi(2);
|
||||
d_end_sq < d_start_sq
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let (w_at_sa, w_at_ea) = if is_reversed {
|
||||
(ew, sw)
|
||||
} else {
|
||||
(sw, ew)
|
||||
};
|
||||
|
||||
let mut arc_pts = Vec::with_capacity(17);
|
||||
let mut arc_widths = Vec::with_capacity(17);
|
||||
let n = 16;
|
||||
let sweep = if end_angle >= start_angle {
|
||||
end_angle - start_angle
|
||||
@@ -298,25 +344,12 @@ fn split_mixed_polyline(
|
||||
center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]),
|
||||
];
|
||||
arc_pts.push(p);
|
||||
arc_widths.push(w_at_sa + (w_at_ea - w_at_sa) * frac as f32);
|
||||
}
|
||||
let (points, points_low) = points_to_ds(arc_pts);
|
||||
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
if w0 > 1e-9 || w1 > 1e-9 {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
let taper_widths = if (sw - ew).abs() > 1e-6 {
|
||||
vec![sw, ew]
|
||||
let taper_widths = if (w_at_sa - w_at_ea).abs() > 1e-6 {
|
||||
arc_widths
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -367,43 +400,77 @@ fn split_mixed_polyline(
|
||||
|
||||
// Collect straight lines into a line wire
|
||||
let mut straight_pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut straight_widths: Vec<f32> = Vec::new();
|
||||
let mut straight_tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut last_end: Option<[f64; 3]> = None;
|
||||
let mut last_end_w: f32 = 0.0;
|
||||
|
||||
for (i, tg) in tangent_geoms.iter().enumerate() {
|
||||
if let TangentGeom::Line { p1, p2 } = tg {
|
||||
straight_tangents.push(tg.clone());
|
||||
let (p_start, p_end) = if i + 1 < key_vertices.len() {
|
||||
(key_vertices[i], key_vertices[i + 1])
|
||||
let p_start = if i + 1 < key_vertices.len()
|
||||
&& (key_vertices[i][0] as f32 - p1[0]).abs() < 1e-3
|
||||
&& (key_vertices[i][1] as f32 - p1[1]).abs() < 1e-3
|
||||
&& (key_vertices[i][2] as f32 - p1[2]).abs() < 1e-3
|
||||
{
|
||||
key_vertices[i]
|
||||
} else {
|
||||
(
|
||||
[p1[0] as f64, p1[1] as f64, p1[2] as f64],
|
||||
[p2[0] as f64, p2[1] as f64, p2[2] as f64],
|
||||
)
|
||||
[p1[0] as f64, p1[1] as f64, p1[2] as f64]
|
||||
};
|
||||
let p_end = if i + 1 < key_vertices.len()
|
||||
&& (key_vertices[i + 1][0] as f32 - p2[0]).abs() < 1e-3
|
||||
&& (key_vertices[i + 1][1] as f32 - p2[1]).abs() < 1e-3
|
||||
&& (key_vertices[i + 1][2] as f32 - p2[2]).abs() < 1e-3
|
||||
{
|
||||
key_vertices[i + 1]
|
||||
} else {
|
||||
[p2[0] as f64, p2[1] as f64, p2[2] as f64]
|
||||
};
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
if !plinegen {
|
||||
if !straight_pts.is_empty() {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
}
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
} else {
|
||||
if let Some(prev) = last_end {
|
||||
if (prev[0] - p_start[0]).abs() < 1e-7
|
||||
&& (prev[1] - p_start[1]).abs() < 1e-7
|
||||
&& (prev[2] - p_start[2]).abs() < 1e-7
|
||||
&& (last_end_w - sw).abs() < 1e-6
|
||||
{
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
} else {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
}
|
||||
} else {
|
||||
if !straight_pts.is_empty() {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
}
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
}
|
||||
last_end = Some(p_end);
|
||||
last_end_w = ew;
|
||||
}
|
||||
} else {
|
||||
last_end = None;
|
||||
@@ -417,12 +484,24 @@ fn split_mixed_polyline(
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
let line_world_width = straight_widths
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(0.0f32, f32::max);
|
||||
let has_line_taper = straight_widths
|
||||
.first()
|
||||
.map_or(false, |&w0| straight_widths.iter().any(|&w| (w - w0).abs() > 1e-6));
|
||||
let taper_widths = if has_line_taper || straight_widths.iter().any(|&w| w > 1e-6) {
|
||||
straight_widths
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker,
|
||||
taper_widths: Vec::new(),
|
||||
taper_widths,
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: global_world_width,
|
||||
world_width: line_world_width,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
@@ -2979,3 +3058,176 @@ pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
||||
v.normalize()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_straight_lines_have_taper_widths() {
|
||||
let tg_line = TangentGeom::Line {
|
||||
p1: [0.0, 0.0, 0.0],
|
||||
p2: [10.0, 0.0, 0.0],
|
||||
};
|
||||
let tg_arc = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tgs = vec![tg_line, tg_arc];
|
||||
let kv = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [15.0, 5.0, 0.0]];
|
||||
let seg_widths = vec![(0.0f32, 10.0f32), (10.0f32, 20.0f32)];
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"mixed",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
20.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// Arc wire + Line wire
|
||||
assert_eq!(wires.len(), 2);
|
||||
let arc_wire = &wires[0];
|
||||
assert_eq!(arc_wire.taper_widths.first().copied(), Some(10.0));
|
||||
assert_eq!(arc_wire.taper_widths.last().copied(), Some(20.0));
|
||||
|
||||
let line_wire = &wires[1];
|
||||
assert_eq!(line_wire.taper_widths, vec![0.0, 10.0]);
|
||||
assert_eq!(line_wire.world_width, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_zero_width_segments_after_tapered_segment() {
|
||||
let tg_line = TangentGeom::Line {
|
||||
p1: [0.0, 0.0, 0.0],
|
||||
p2: [10.0, 0.0, 0.0],
|
||||
};
|
||||
let tg_arc1 = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tg_arc2 = TangentGeom::Arc {
|
||||
center: [15.0, 10.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tgs = vec![tg_line, tg_arc1, tg_arc2];
|
||||
let kv = vec![
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[15.0, 5.0, 0.0],
|
||||
[20.0, 10.0, 0.0],
|
||||
];
|
||||
// Segment 0 has width (2.0, 0.0), segment 1 and 2 have width (0.0, 0.0)
|
||||
let seg_widths = vec![(2.0f32, 0.0f32), (0.0f32, 0.0f32), (0.0f32, 0.0f32)];
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"taper_then_zero",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
2.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// 2 arc wires + 1 line wire = 3 wires
|
||||
assert_eq!(wires.len(), 3);
|
||||
let arc1 = &wires[0];
|
||||
let arc2 = &wires[1];
|
||||
let line = &wires[2];
|
||||
|
||||
// Arcs must have world_width = 0.0 and empty taper_widths
|
||||
assert_eq!(arc1.world_width, 0.0);
|
||||
assert!(arc1.taper_widths.is_empty());
|
||||
assert_eq!(arc2.world_width, 0.0);
|
||||
assert!(arc2.taper_widths.is_empty());
|
||||
|
||||
let inst1 = crate::scene::pipeline::circle_gpu::extract_circle_instance(arc1, 0.0)
|
||||
.expect("arc1 instance");
|
||||
assert_eq!(inst1.start_width, 0.0);
|
||||
assert_eq!(inst1.params[3], 0.0);
|
||||
|
||||
// Straight line must have world_width = 2.0 and taper_widths = [2.0, 0.0]
|
||||
assert_eq!(line.world_width, 2.0);
|
||||
assert_eq!(line.taper_widths, vec![2.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_reversed_arc_aligns_endpoints() {
|
||||
// Clockwise arc from (10.0, 0.0) to (15.0, 5.0).
|
||||
// bulge_arc_to_tangent inverts angles so start_angle is at (15.0, 5.0) and end_angle at (10.0, 0.0).
|
||||
let tg_arc_reversed = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: 0.0, // at (15.0, 5.0) - physical end!
|
||||
end_angle: -std::f64::consts::FRAC_PI_2, // at (10.0, 0.0) - physical start!
|
||||
};
|
||||
let tgs = vec![tg_arc_reversed];
|
||||
let kv = vec![[10.0, 0.0, 0.0], [15.0, 5.0, 0.0]];
|
||||
let seg_widths = vec![(5.0f32, 25.0f32)]; // sw=5.0 at start, ew=25.0 at end
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"arc_rev",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
25.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(wires.len(), 1);
|
||||
let arc_wire = &wires[0];
|
||||
// In CCW shader angles, start_angle is at (15.0, 5.0) which is physical end (ew = 25.0).
|
||||
// end_angle is at (10.0, 0.0) which is physical start (sw = 5.0).
|
||||
assert_eq!(arc_wire.taper_widths.first().copied(), Some(25.0));
|
||||
assert_eq!(arc_wire.taper_widths.last().copied(), Some(5.0));
|
||||
|
||||
let inst = crate::scene::pipeline::circle_gpu::extract_circle_instance(arc_wire, 0.0)
|
||||
.expect("arc instance extracted");
|
||||
// start_width is at start_angle (physical end, 25.0)
|
||||
assert_eq!(inst.start_width, 25.0);
|
||||
// params.w (end_width) is at end_angle (physical start, 5.0)
|
||||
assert_eq!(inst.params[3], 5.0);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@ pub fn extract_circle_instance_from_geom_indexed(
|
||||
|
||||
let (start_width, end_width) = if !wire.taper_widths.is_empty() {
|
||||
if wire.taper_widths.len() >= 2 && wire.tangent_geoms.len() == 1 {
|
||||
(wire.taper_widths[0], wire.taper_widths[1])
|
||||
(wire.taper_widths[0], *wire.taper_widths.last().unwrap())
|
||||
} else if wire.taper_widths.len() > geom_index {
|
||||
let sw = wire.taper_widths[geom_index];
|
||||
let ew = wire.taper_widths.get(geom_index + 1).copied().unwrap_or(sw);
|
||||
|
||||
@@ -116,7 +116,7 @@ pub struct WireConst {
|
||||
/// the quad by `world_half_width / world_per_pixel` so the band tracks zoom
|
||||
/// in drawing units.
|
||||
pub world_half_width: f32,
|
||||
pub _pad1: f32,
|
||||
pub is_tapered: f32,
|
||||
pub _pad2: f32,
|
||||
/// Point-marker origin as a double-single pair. `marker_normal_scale.w`
|
||||
/// stores the viewport-height percentage; zero disables marker scaling.
|
||||
@@ -592,7 +592,14 @@ pub(crate) fn emit_wire_packed(
|
||||
let (dists, align_end, align_total) = wire_distances(wire);
|
||||
let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 };
|
||||
let is_tapered = !wire.taper_widths.is_empty();
|
||||
let tw = |i: usize| -> f32 {
|
||||
if is_tapered {
|
||||
wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
};
|
||||
let mut instances: Vec<PackedWireInstance> = Vec::with_capacity(seg_count);
|
||||
for i in 0..seg_count {
|
||||
let a = wire.points[i];
|
||||
@@ -636,6 +643,7 @@ pub(crate) fn emit_wire_native(
|
||||
) -> (Vec<WireInstance>, WireConst) {
|
||||
let (dists, align_end, align_total) = wire_distances(wire);
|
||||
let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
|
||||
let is_tapered = !wire.taper_widths.is_empty();
|
||||
let cst = WireConst {
|
||||
color,
|
||||
pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]],
|
||||
@@ -646,7 +654,7 @@ pub(crate) fn emit_wire_native(
|
||||
align_end,
|
||||
align_total,
|
||||
world_half_width: wire.world_width * 0.5,
|
||||
_pad1: 0.0,
|
||||
is_tapered: if is_tapered { 1.0 } else { 0.0 },
|
||||
_pad2: 0.0,
|
||||
marker_origin_high,
|
||||
marker_origin_low,
|
||||
@@ -658,18 +666,12 @@ pub(crate) fn emit_wire_native(
|
||||
return (Vec::new(), cst);
|
||||
}
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
// Store an endpoint/max-width ratio. The shared f32 maximum keeps drawing
|
||||
// units and range out of the packed field; UNORM16 contributes only a
|
||||
// relative error below 1/65535. Preserve zero as the existing constant
|
||||
// width fallback sentinel.
|
||||
let taper_ratio = |i: usize| -> u16 {
|
||||
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||
if width <= 0.0 || wire.world_width <= 0.0 {
|
||||
if !is_tapered || wire.world_width <= 0.0 {
|
||||
0
|
||||
} else {
|
||||
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32)
|
||||
.round()
|
||||
.max(1.0) as u16
|
||||
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32).round() as u16
|
||||
}
|
||||
};
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||
|
||||
@@ -24,7 +24,7 @@ struct WireConst {
|
||||
align_end: f32,
|
||||
align_total: f32,
|
||||
world_half_width: f32,
|
||||
_pad1: f32,
|
||||
is_tapered: f32,
|
||||
_pad2: f32,
|
||||
marker_origin_high: vec4<f32>,
|
||||
marker_origin_low: vec4<f32>,
|
||||
@@ -121,7 +121,8 @@ 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 is_world = wire_const.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0;
|
||||
let is_tapered = wire_const.is_tapered > 0.5;
|
||||
let is_world = wire_const.world_half_width > 0.0;
|
||||
|
||||
var final_clip: vec4<f32>;
|
||||
var out_dist: f32;
|
||||
@@ -134,8 +135,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
|
||||
// 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 world_hw_a = select(wire_const.world_half_width, in.taper_ratio.x * wire_const.world_half_width, is_tapered);
|
||||
let world_hw_b = select(wire_const.world_half_width, in.taper_ratio.y * wire_const.world_half_width, is_tapered);
|
||||
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);
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ struct WireConst {
|
||||
align_end: f32,
|
||||
align_total: f32,
|
||||
world_half_width: f32,
|
||||
_pad1: f32,
|
||||
is_tapered: f32,
|
||||
_pad2: f32,
|
||||
marker_origin_high: vec4<f32>,
|
||||
marker_origin_low: vec4<f32>,
|
||||
@@ -152,7 +152,8 @@ 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 is_world = wire_const.world_half_width > 0.0 || seg_taper_ratio.x > 0.0 || seg_taper_ratio.y > 0.0;
|
||||
let is_tapered = wire_const.is_tapered > 0.5;
|
||||
let is_world = wire_const.world_half_width > 0.0;
|
||||
|
||||
var final_clip: vec4<f32>;
|
||||
var out_dist: f32;
|
||||
@@ -165,8 +166,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
|
||||
// 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 world_hw_a = select(wire_const.world_half_width, seg_taper_ratio.x * wire_const.world_half_width, is_tapered);
|
||||
let world_hw_b = select(wire_const.world_half_width, seg_taper_ratio.y * wire_const.world_half_width, is_tapered);
|
||||
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);
|
||||
|
||||
|
||||
+36
-20
@@ -207,32 +207,48 @@ fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
||||
let is_full_or_in_sweep = (sweep >= TAU - 1e-5) || (d_theta <= sweep);
|
||||
|
||||
if in.is_world > 0.5 {
|
||||
// Physical world-unit width (thick analytical circular arcs, donuts, tapered segments).
|
||||
// Distance-to-edge calculations are strictly performed in the circle plane's world units
|
||||
// so that 3D perspective projection yields exact concentric ellipses with invariant
|
||||
// physical stroke widths across any camera tilt or acute perspective angle.
|
||||
if is_full_or_in_sweep {
|
||||
let t = select(0.0, clamp(d_theta / sweep, 0.0, 1.0), sweep > 1e-5);
|
||||
let world_hw = mix(in.hw_ends.x, in.hw_ends.y, t);
|
||||
let eff_hw = max(world_hw, 0.5 * fw);
|
||||
let is_full = sweep >= TAU - 1e-5;
|
||||
if is_full {
|
||||
let eff_hw = max(in.hw_ends.x, 0.5 * fw);
|
||||
let d_world = abs(r - in.radius);
|
||||
let delta_px = (eff_hw - d_world) / fw;
|
||||
let cov = clamp(0.5 + delta_px, 0.0, 1.0);
|
||||
let subpixel_fade = min(1.0, (2.0 * world_hw) / fw);
|
||||
let subpixel_fade = min(1.0, (2.0 * in.hw_ends.x) / fw);
|
||||
alpha_cov = cov * subpixel_fade;
|
||||
arc_dist = in.radius * d_theta;
|
||||
} else {
|
||||
// Outside sweep: evaluate distance to start and end endpoints for smooth round end caps
|
||||
let p_start = in.radius * vec2<f32>(cos(sa), sin(sa));
|
||||
let p_end = in.radius * vec2<f32>(cos(ea), sin(ea));
|
||||
let d_start = length(in.local_pos - p_start);
|
||||
let d_end = length(in.local_pos - p_end);
|
||||
let eff_hw_a = max(in.hw_ends.x, 0.5 * fw);
|
||||
let eff_hw_b = max(in.hw_ends.y, 0.5 * fw);
|
||||
let cov_start = clamp(0.5 + (eff_hw_a - d_start) / fw, 0.0, 1.0) * min(1.0, (2.0 * in.hw_ends.x) / fw);
|
||||
let cov_end = clamp(0.5 + (eff_hw_b - d_end) / fw, 0.0, 1.0) * min(1.0, (2.0 * in.hw_ends.y) / fw);
|
||||
alpha_cov = max(cov_start, cov_end);
|
||||
arc_dist = select(in.radius * sweep, 0.0, cov_start >= cov_end);
|
||||
// Flat (radial) end caps perpendicular to arc tangent at start_angle and end_angle.
|
||||
let d_start_rad = r * d_theta;
|
||||
let d_end_rad = r * (sweep - d_theta);
|
||||
let d_before_sa = r * mod_tau(sa - theta);
|
||||
let d_after_ea = r * mod_tau(theta - ea);
|
||||
|
||||
var cov_angular: f32 = 0.0;
|
||||
var world_hw: f32 = 0.0;
|
||||
|
||||
if is_full_or_in_sweep {
|
||||
let t = select(0.0, clamp(d_theta / sweep, 0.0, 1.0), sweep > 1e-5);
|
||||
world_hw = mix(in.hw_ends.x, in.hw_ends.y, t);
|
||||
let d_inside = min(d_start_rad, d_end_rad);
|
||||
cov_angular = clamp(0.5 + d_inside / fw, 0.0, 1.0);
|
||||
arc_dist = in.radius * d_theta;
|
||||
} else {
|
||||
let d_outside = min(d_before_sa, d_after_ea);
|
||||
if d_outside <= 0.5 * fw {
|
||||
cov_angular = clamp(0.5 - d_outside / fw, 0.0, 1.0);
|
||||
world_hw = select(in.hw_ends.y, in.hw_ends.x, d_before_sa <= d_after_ea);
|
||||
arc_dist = select(in.radius * sweep, 0.0, d_before_sa <= d_after_ea);
|
||||
}
|
||||
}
|
||||
|
||||
if cov_angular > 0.0 {
|
||||
let eff_hw = max(world_hw, 0.5 * fw);
|
||||
let d_world = abs(r - in.radius);
|
||||
let delta_px = (eff_hw - d_world) / fw;
|
||||
let cov_radial = clamp(0.5 + delta_px, 0.0, 1.0);
|
||||
let subpixel_fade = min(1.0, (2.0 * world_hw) / fw);
|
||||
alpha_cov = cov_radial * cov_angular * subpixel_fade;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Lineweight stroke (screen-pixel width)
|
||||
|
||||
@@ -102,7 +102,7 @@ struct VertexOut {
|
||||
// constant world-unit band, then the screen-pixel lineweight (LWDISPLAY off
|
||||
// collapses to a hairline).
|
||||
fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||
if taper > 0.0 { return max(taper / u.world_per_pixel, 0.5); }
|
||||
if taper >= 0.0 { return max(taper / u.world_per_pixel, 0.5); }
|
||||
if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); }
|
||||
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
||||
if u.lineweight_scale < 0.0 {
|
||||
@@ -156,7 +156,7 @@ 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 is_world = in.misc.w > 0.0 || in.taper.x > 0.0 || in.taper.y > 0.0;
|
||||
let is_world = in.misc.w > 0.0 || in.taper.x >= 0.0 || in.taper.y >= 0.0;
|
||||
|
||||
var final_clip: vec4<f32>;
|
||||
var out_dist: f32;
|
||||
@@ -169,8 +169,8 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
|
||||
// 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 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);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ struct WireConst {
|
||||
align_end: f32,
|
||||
align_total: f32,
|
||||
world_half_width: f32,
|
||||
_pad1: f32,
|
||||
is_tapered: f32,
|
||||
_pad2: f32,
|
||||
marker_origin_high: vec4<f32>,
|
||||
marker_origin_low: vec4<f32>,
|
||||
@@ -128,7 +128,8 @@ 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 is_world = c.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0;
|
||||
let is_tapered = c.is_tapered > 0.5;
|
||||
let is_world = c.world_half_width > 0.0;
|
||||
|
||||
var final_clip: vec4<f32>;
|
||||
var out_dist: f32;
|
||||
@@ -141,8 +142,8 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
|
||||
// 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 world_hw_a = select(c.world_half_width, in.taper_ratio.x * c.world_half_width, is_tapered);
|
||||
let world_hw_b = select(c.world_half_width, in.taper_ratio.y * c.world_half_width, is_tapered);
|
||||
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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user