forked from 1b-agent/opencadstudio-upstream
feat(render): render thick and tapered analytical arcs directly on GPU
- Repurpose CircleInstance center_high.w and params.w for start_width and end_width (preserving exact 128-byte stride) - Route wide polylines, tapered polylines with bulges, and donuts through split_mixed_polyline into GPU analytical arc instances - Compute physical annular bands, width tapers, and round end caps in circle.wgsl using world-space distances to prevent 3D perspective foreshortening distortion - Expand bounding quad margins dynamically with grazing-angle compensation to eliminate clipping - Relax pick_tris check in wire classification and partition routines so wide entities retain CPU picking while drawing analytically - Add unit and hardware GPU integration tests, including tilted 3D perspective rendering verification
This commit is contained in:
1 parent
50e4f2bfe3
commit
9bbc5c3182
8 files changed
+714
-72
No files matched your search
@@ -253,7 +253,7 @@ fn donut_wire(
|
|||||||
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
|
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
|
pub(crate) fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
|
||||||
use acadrust::types::Vector2;
|
use acadrust::types::Vector2;
|
||||||
let r_avg = (inner_r + outer_r) / 2.0;
|
let r_avg = (inner_r + outer_r) / 2.0;
|
||||||
let width = outer_r - inner_r;
|
let width = outer_r - inner_r;
|
||||||
|
|||||||
+159
-31
@@ -202,6 +202,66 @@ pub(crate) fn points_to_ds(
|
|||||||
(high, low)
|
(high, low)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn polyline_segment_widths(entity: &EntityType) -> Vec<(f32, f32)> {
|
||||||
|
match entity {
|
||||||
|
EntityType::LwPolyline(p) => {
|
||||||
|
let count = p.vertices.len();
|
||||||
|
let seg_count = if p.is_closed {
|
||||||
|
count
|
||||||
|
} else {
|
||||||
|
count.saturating_sub(1)
|
||||||
|
};
|
||||||
|
let c = p.constant_width;
|
||||||
|
(0..seg_count)
|
||||||
|
.map(|i| {
|
||||||
|
let v = &p.vertices[i];
|
||||||
|
let sw = if v.start_width > 1e-9 {
|
||||||
|
v.start_width
|
||||||
|
} else {
|
||||||
|
c
|
||||||
|
} as f32;
|
||||||
|
let ew = if v.end_width > 1e-9 {
|
||||||
|
v.end_width
|
||||||
|
} else {
|
||||||
|
c
|
||||||
|
} as f32;
|
||||||
|
(sw, ew)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
EntityType::Polyline2D(p) => {
|
||||||
|
let filtered = crate::entities::polyline::drawn_vertices2d(p);
|
||||||
|
let verts: &[acadrust::entities::Vertex2D] =
|
||||||
|
filtered.as_deref().unwrap_or(&p.vertices);
|
||||||
|
let count = verts.len();
|
||||||
|
let seg_count = if p.is_closed() {
|
||||||
|
count
|
||||||
|
} else {
|
||||||
|
count.saturating_sub(1)
|
||||||
|
};
|
||||||
|
let def_start = p.start_width;
|
||||||
|
let def_end = p.end_width;
|
||||||
|
(0..seg_count)
|
||||||
|
.map(|i| {
|
||||||
|
let v = &verts[i];
|
||||||
|
let sw = if v.start_width > 1e-9 {
|
||||||
|
v.start_width
|
||||||
|
} else {
|
||||||
|
def_start
|
||||||
|
} as f32;
|
||||||
|
let ew = if v.end_width > 1e-9 {
|
||||||
|
v.end_width
|
||||||
|
} else {
|
||||||
|
def_end
|
||||||
|
} as f32;
|
||||||
|
(sw, ew)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn split_mixed_polyline(
|
fn split_mixed_polyline(
|
||||||
tangent_geoms: &[TangentGeom],
|
tangent_geoms: &[TangentGeom],
|
||||||
key_vertices: &[[f64; 3]],
|
key_vertices: &[[f64; 3]],
|
||||||
@@ -214,11 +274,15 @@ fn split_mixed_polyline(
|
|||||||
snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
||||||
point_marker: Option<crate::scene::model::wire_model::PointMarker>,
|
point_marker: Option<crate::scene::model::wire_model::PointMarker>,
|
||||||
plinegen: bool,
|
plinegen: bool,
|
||||||
|
seg_widths: &[(f32, f32)],
|
||||||
|
global_world_width: f32,
|
||||||
|
mut pick_tris: Vec<[f32; 3]>,
|
||||||
|
mut pick_tris_low: Vec<[f32; 3]>,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
// Emit each arc segment as an analytical wire (CircleGpu target)
|
// Emit each arc segment as an analytical wire (CircleGpu target)
|
||||||
for tg in tangent_geoms {
|
for (i, tg) in tangent_geoms.iter().enumerate() {
|
||||||
if let TangentGeom::Arc {
|
if let TangentGeom::Arc {
|
||||||
center,
|
center,
|
||||||
axis_x,
|
axis_x,
|
||||||
@@ -246,20 +310,48 @@ fn split_mixed_polyline(
|
|||||||
arc_pts.push(p);
|
arc_pts.push(p);
|
||||||
}
|
}
|
||||||
let (points, points_low) = points_to_ds(arc_pts);
|
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]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
let world_width = sw.max(ew);
|
||||||
|
|
||||||
|
let (arc_pt, arc_ptl) = if out.is_empty() && !pick_tris.is_empty() {
|
||||||
|
(std::mem::take(&mut pick_tris), std::mem::take(&mut pick_tris_low))
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
|
|
||||||
out.push(WireModel {
|
out.push(WireModel {
|
||||||
bg_adapt: None,
|
bg_adapt: None,
|
||||||
point_marker: None,
|
point_marker: None,
|
||||||
taper_widths: Vec::new(),
|
taper_widths,
|
||||||
pattern_stations: Vec::new(),
|
pattern_stations: Vec::new(),
|
||||||
world_width: 0.0,
|
world_width,
|
||||||
depth_override: None,
|
depth_override: None,
|
||||||
display_visible: true,
|
display_visible: true,
|
||||||
plot_visible: true,
|
plot_visible: true,
|
||||||
fill_is_3d: false,
|
fill_is_3d: false,
|
||||||
fill_is_2d_solid: false,
|
fill_is_2d_solid: false,
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
pick_tris: Vec::new(),
|
pick_tris: arc_pt,
|
||||||
pick_tris_low: Vec::new(),
|
pick_tris_low: arc_ptl,
|
||||||
dash_from_start: false,
|
dash_from_start: false,
|
||||||
dash_align_end: None,
|
dash_align_end: None,
|
||||||
text_verts: Vec::new(),
|
text_verts: Vec::new(),
|
||||||
@@ -330,20 +422,25 @@ fn split_mixed_polyline(
|
|||||||
|
|
||||||
let (line_pts, line_pts_low) = points_to_ds(straight_pts);
|
let (line_pts, line_pts_low) = points_to_ds(straight_pts);
|
||||||
if !line_pts.is_empty() {
|
if !line_pts.is_empty() {
|
||||||
|
let (pt, ptl) = if !pick_tris.is_empty() {
|
||||||
|
(pick_tris, pick_tris_low)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
out.push(WireModel {
|
out.push(WireModel {
|
||||||
bg_adapt: None,
|
bg_adapt: None,
|
||||||
point_marker,
|
point_marker,
|
||||||
taper_widths: Vec::new(),
|
taper_widths: Vec::new(),
|
||||||
pattern_stations: Vec::new(),
|
pattern_stations: Vec::new(),
|
||||||
world_width: 0.0,
|
world_width: global_world_width,
|
||||||
depth_override: None,
|
depth_override: None,
|
||||||
display_visible: true,
|
display_visible: true,
|
||||||
plot_visible: true,
|
plot_visible: true,
|
||||||
fill_is_3d: false,
|
fill_is_3d: false,
|
||||||
fill_is_2d_solid: false,
|
fill_is_2d_solid: false,
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
pick_tris: Vec::new(),
|
pick_tris: pt,
|
||||||
pick_tris_low: Vec::new(),
|
pick_tris_low: ptl,
|
||||||
dash_from_start: false,
|
dash_from_start: false,
|
||||||
dash_align_end: None,
|
dash_align_end: None,
|
||||||
text_verts: Vec::new(),
|
text_verts: Vec::new(),
|
||||||
@@ -367,6 +464,10 @@ fn split_mixed_polyline(
|
|||||||
} else if let Some(first_arc) = out.first_mut() {
|
} else if let Some(first_arc) = out.first_mut() {
|
||||||
first_arc.snap_pts = snap_pts;
|
first_arc.snap_pts = snap_pts;
|
||||||
first_arc.key_vertices = key_vertices.to_vec();
|
first_arc.key_vertices = key_vertices.to_vec();
|
||||||
|
if first_arc.pick_tris.is_empty() && !pick_tris.is_empty() {
|
||||||
|
first_arc.pick_tris = pick_tris;
|
||||||
|
first_arc.pick_tris_low = pick_tris_low;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out
|
out
|
||||||
@@ -1744,17 +1845,13 @@ pub fn tessellate(
|
|||||||
.tangent_geoms
|
.tangent_geoms
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
||||||
let has_line = te
|
let can_split = !is_thick_extrusion
|
||||||
.tangent_geoms
|
|
||||||
.iter()
|
|
||||||
.any(|tg| matches!(tg, TangentGeom::Line { .. }));
|
|
||||||
let is_thin_polyline = !is_thick_extrusion
|
|
||||||
&& polyline_band_width(entity, document.header.fill_mode) <= 1e-9
|
|
||||||
&& fill_tris.is_empty()
|
&& fill_tris.is_empty()
|
||||||
&& pick_tris.is_empty()
|
|
||||||
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
||||||
|
|
||||||
if has_arc && has_line && is_thin_polyline {
|
if has_arc && can_split {
|
||||||
|
let seg_widths = polyline_segment_widths(entity);
|
||||||
|
let ww = polyline_band_width(entity, document.header.fill_mode);
|
||||||
out.extend(split_mixed_polyline(
|
out.extend(split_mixed_polyline(
|
||||||
&te.tangent_geoms,
|
&te.tangent_geoms,
|
||||||
&keys,
|
&keys,
|
||||||
@@ -1767,6 +1864,10 @@ pub fn tessellate(
|
|||||||
snap,
|
snap,
|
||||||
point_marker,
|
point_marker,
|
||||||
true,
|
true,
|
||||||
|
&seg_widths,
|
||||||
|
ww,
|
||||||
|
pick_tris,
|
||||||
|
pick_tris_low,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
out.push(WireModel {
|
out.push(WireModel {
|
||||||
@@ -1966,20 +2067,14 @@ pub fn tessellate(
|
|||||||
entity,
|
entity,
|
||||||
EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10
|
EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10
|
||||||
);
|
);
|
||||||
let is_thin_polyline = !is_thick_extrusion
|
let can_split = !is_thick_extrusion
|
||||||
&& polyline_band_width(entity, document.header.fill_mode) <= 1e-9
|
|
||||||
&& pick_tris.is_empty()
|
|
||||||
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
||||||
let has_arc = te
|
let has_arc = te
|
||||||
.tangent_geoms
|
.tangent_geoms
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
||||||
let has_line = te
|
|
||||||
.tangent_geoms
|
|
||||||
.iter()
|
|
||||||
.any(|tg| matches!(tg, TangentGeom::Line { .. }));
|
|
||||||
|
|
||||||
if has_arc && has_line && is_thin_polyline {
|
if has_arc && can_split {
|
||||||
let edge_color = if is_thick_extrusion {
|
let edge_color = if is_thick_extrusion {
|
||||||
[0.0, 0.0, 0.0, 1.0]
|
[0.0, 0.0, 0.0, 1.0]
|
||||||
} else {
|
} else {
|
||||||
@@ -1987,6 +2082,8 @@ pub fn tessellate(
|
|||||||
};
|
};
|
||||||
let point_marker =
|
let point_marker =
|
||||||
crate::entities::point::relative_marker_spec(entity, document);
|
crate::entities::point::relative_marker_spec(entity, document);
|
||||||
|
let seg_widths = polyline_segment_widths(entity);
|
||||||
|
let ww = polyline_band_width(entity, document.header.fill_mode);
|
||||||
return split_mixed_polyline(
|
return split_mixed_polyline(
|
||||||
&te.tangent_geoms,
|
&te.tangent_geoms,
|
||||||
&key_vertices,
|
&key_vertices,
|
||||||
@@ -1999,6 +2096,10 @@ pub fn tessellate(
|
|||||||
snap_pts,
|
snap_pts,
|
||||||
point_marker,
|
point_marker,
|
||||||
false,
|
false,
|
||||||
|
&seg_widths,
|
||||||
|
ww,
|
||||||
|
pick_tris,
|
||||||
|
pick_tris_low,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return vec![WireModel {
|
return vec![WireModel {
|
||||||
@@ -2038,6 +2139,40 @@ pub fn tessellate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
RenderObject::TaperedLines(points, widths) => {
|
RenderObject::TaperedLines(points, widths) => {
|
||||||
|
let has_arc = te
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
||||||
|
let can_split = matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
||||||
|
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
||||||
|
let world_width = widths.iter().copied().fold(0.0f32, f32::max);
|
||||||
|
let key_vertices: Vec<[f64; 3]> = te
|
||||||
|
.key_vertices
|
||||||
|
.into_iter()
|
||||||
|
.map(|[x, y, z]| [x, y, z])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if has_arc && can_split {
|
||||||
|
let seg_widths = polyline_segment_widths(entity);
|
||||||
|
return split_mixed_polyline(
|
||||||
|
&te.tangent_geoms,
|
||||||
|
&key_vertices,
|
||||||
|
&name,
|
||||||
|
color,
|
||||||
|
selected,
|
||||||
|
pattern_length,
|
||||||
|
pattern,
|
||||||
|
line_weight_px,
|
||||||
|
te.snap_pts,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
&seg_widths,
|
||||||
|
world_width,
|
||||||
|
pick_tris,
|
||||||
|
pick_tris_low,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// A wide polyline whose width varies: one continuous band wire
|
// A wide polyline whose width varies: one continuous band wire
|
||||||
// carrying a per-point width; the shader interpolates each
|
// carrying a per-point width; the shader interpolates each
|
||||||
// segment's two endpoint widths. `world_width` (the widest edge)
|
// segment's two endpoint widths. `world_width` (the widest edge)
|
||||||
@@ -2045,13 +2180,6 @@ pub fn tessellate(
|
|||||||
// floor when zoomed out.
|
// floor when zoomed out.
|
||||||
let (local_pts, local_pts_low) = points_to_ds(points);
|
let (local_pts, local_pts_low) = points_to_ds(points);
|
||||||
let snap_pts = te.snap_pts;
|
let snap_pts = te.snap_pts;
|
||||||
let key_vertices: Vec<[f64; 3]> = te
|
|
||||||
.key_vertices
|
|
||||||
.into_iter()
|
|
||||||
.map(|[x, y, z]| [x, y, z])
|
|
||||||
.collect();
|
|
||||||
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
|
||||||
let world_width = widths.iter().copied().fold(0.0f32, f32::max);
|
|
||||||
return vec![WireModel {
|
return vec![WireModel {
|
||||||
bg_adapt: None,
|
bg_adapt: None,
|
||||||
point_marker: None,
|
point_marker: None,
|
||||||
|
|||||||
@@ -12038,4 +12038,129 @@ mod layout_cache_tests {
|
|||||||
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
|
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
|
||||||
println!("============================================================================\n");
|
println!("============================================================================\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "benchmark"]
|
||||||
|
fn bench_wide_and_tapered_arc_rendering() {
|
||||||
|
use std::time::Instant;
|
||||||
|
use acadrust::entities::LwPolyline;
|
||||||
|
use acadrust::types::Vector2;
|
||||||
|
|
||||||
|
let n_wide_arcs = 2500;
|
||||||
|
let n_tapered_arcs = 2500;
|
||||||
|
let n_donuts = 2500;
|
||||||
|
let total_entities = n_wide_arcs + n_tapered_arcs + n_donuts;
|
||||||
|
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
|
||||||
|
// 1. Wide polyline arcs (constant width 6.0)
|
||||||
|
for i in 0..n_wide_arcs {
|
||||||
|
let x = (i % 50) as f64 * 80.0;
|
||||||
|
let y = (i / 50) as f64 * 80.0;
|
||||||
|
let mut pline = LwPolyline::new();
|
||||||
|
pline.constant_width = 6.0;
|
||||||
|
pline.vertices = vec![
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: Vector2::new(x, y),
|
||||||
|
bulge: 0.5,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: Vector2::new(x + 40.0, y),
|
||||||
|
bulge: 0.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
scene.add_entity(EntityType::LwPolyline(pline));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Tapered arcs (width: 2.0 -> 12.0)
|
||||||
|
for i in 0..n_tapered_arcs {
|
||||||
|
let x = (i % 50) as f64 * 80.0;
|
||||||
|
let y = (i / 50) as f64 * 80.0 + 4500.0;
|
||||||
|
let mut pline = LwPolyline::new();
|
||||||
|
pline.vertices = vec![
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: Vector2::new(x, y),
|
||||||
|
bulge: 0.7,
|
||||||
|
start_width: 2.0,
|
||||||
|
end_width: 12.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: Vector2::new(x + 35.0, y + 10.0),
|
||||||
|
bulge: 0.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
scene.add_entity(EntityType::LwPolyline(pline));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Donuts (2 semicircular arc segments, constant width 16.0)
|
||||||
|
for i in 0..n_donuts {
|
||||||
|
let cx = (i % 50) as f64 * 80.0;
|
||||||
|
let cy = (i / 50) as f64 * 80.0 + 9000.0;
|
||||||
|
let donut = crate::modules::draw::draw::donut::make_donut(cx, cy, 0.0, 10.0, 26.0);
|
||||||
|
scene.add_entity(donut);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cam = Camera::default();
|
||||||
|
let t_tess_start = Instant::now();
|
||||||
|
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
|
||||||
|
let tess_duration = t_tess_start.elapsed();
|
||||||
|
|
||||||
|
let total_wires = wires.len();
|
||||||
|
let mut chord_points = 0usize;
|
||||||
|
let mut analytical_tangents = 0usize;
|
||||||
|
for w in wires.iter() {
|
||||||
|
chord_points += w.points.len();
|
||||||
|
analytical_tangents += w.tangent_geoms.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let t_part_start = Instant::now();
|
||||||
|
let iters = 100;
|
||||||
|
let mut part = None;
|
||||||
|
for _ in 0..iters {
|
||||||
|
part = Some(crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths));
|
||||||
|
}
|
||||||
|
let part_duration = t_part_start.elapsed() / (iters as u32);
|
||||||
|
let p = part.unwrap();
|
||||||
|
|
||||||
|
// Zoom simulation
|
||||||
|
let zoom_levels = [0.1f32, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1000.0];
|
||||||
|
let t_zoom_start = Instant::now();
|
||||||
|
let mut dynamic_cam = Camera::default();
|
||||||
|
for &zoom in zoom_levels.iter().cycle().take(100) {
|
||||||
|
dynamic_cam.distance = 1000.0 / zoom;
|
||||||
|
let _w = scene.model_tile_wires_arc(0, &dynamic_cam, 1.0, 1000.0);
|
||||||
|
}
|
||||||
|
let zoom_100_duration = t_zoom_start.elapsed();
|
||||||
|
let per_zoom_ms = zoom_100_duration.as_secs_f64() * 1000.0 / 100.0;
|
||||||
|
|
||||||
|
let line_vram_mb = (chord_points * 36) as f64 / (1024.0 * 1024.0);
|
||||||
|
let inst_vram_mb = (p.circle_instances.len() * 128) as f64 / (1024.0 * 1024.0);
|
||||||
|
|
||||||
|
println!("\n=== BENCHMARK REPORT: THICK & TAPERED ANALYTICAL ARCS ===");
|
||||||
|
println!("Thick & Tapered Entities: {total_entities}");
|
||||||
|
println!("Initial Wire Build Time: {:.2?}", tess_duration);
|
||||||
|
println!("Total Wires: {total_wires}");
|
||||||
|
println!("Tessellated Chord Vertices: {chord_points}");
|
||||||
|
println!("Analytical Tangent Geometries: {analytical_tangents}");
|
||||||
|
println!("Partitioning Time (per frame): {:.3?}", part_duration);
|
||||||
|
println!("Regular Line Wires: {}", p.regular.len());
|
||||||
|
println!("GPU Circle/Arc Instances: {}", p.circle_instances.len());
|
||||||
|
println!("Line Arena VRAM (chord lines): {:.2} MB", line_vram_mb);
|
||||||
|
println!("GPU Analytical VRAM: {:.2} MB", inst_vram_mb);
|
||||||
|
println!("VRAM Reduction Ratio: {:.1}x", if inst_vram_mb > 0.0 { line_vram_mb / inst_vram_mb } else { 0.0 });
|
||||||
|
println!("100 Camera Zoom Navigations: {:.2?}", zoom_100_duration);
|
||||||
|
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
|
||||||
|
println!("========================================================\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ use iced::wgpu;
|
|||||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct CircleInstance {
|
pub struct CircleInstance {
|
||||||
pub center_high: [f32; 3],
|
pub center_high: [f32; 3],
|
||||||
pub _pad0: f32,
|
pub start_width: f32,
|
||||||
pub center_low: [f32; 3],
|
pub center_low: [f32; 3],
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
pub axis_x: [f32; 3],
|
pub axis_x: [f32; 3],
|
||||||
@@ -19,7 +19,7 @@ pub struct CircleInstance {
|
|||||||
pub axis_y: [f32; 3],
|
pub axis_y: [f32; 3],
|
||||||
pub end_angle: f32,
|
pub end_angle: f32,
|
||||||
pub color: [f32; 4],
|
pub color: [f32; 4],
|
||||||
/// `[half_width, pattern_length, draw_depth, padding]`
|
/// `[half_width, pattern_length, draw_depth, end_width]`
|
||||||
pub params: [f32; 4],
|
pub params: [f32; 4],
|
||||||
pub pat0: [f32; 4],
|
pub pat0: [f32; 4],
|
||||||
pub pat1: [f32; 4],
|
pub pat1: [f32; 4],
|
||||||
@@ -204,6 +204,16 @@ pub fn extract_circle_instance_from_geom(
|
|||||||
geom: &crate::scene::model::wire_model::TangentGeom,
|
geom: &crate::scene::model::wire_model::TangentGeom,
|
||||||
wire: &crate::scene::WireModel,
|
wire: &crate::scene::WireModel,
|
||||||
draw_depth: f32,
|
draw_depth: f32,
|
||||||
|
) -> Option<CircleInstance> {
|
||||||
|
extract_circle_instance_from_geom_indexed(geom, wire, draw_depth, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract an analytical circle or circular arc instance at a specific segment index.
|
||||||
|
pub fn extract_circle_instance_from_geom_indexed(
|
||||||
|
geom: &crate::scene::model::wire_model::TangentGeom,
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
geom_index: usize,
|
||||||
) -> Option<CircleInstance> {
|
) -> Option<CircleInstance> {
|
||||||
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match *geom {
|
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match *geom {
|
||||||
crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => (
|
crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => (
|
||||||
@@ -246,9 +256,25 @@ pub fn extract_circle_instance_from_geom(
|
|||||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||||
|
|
||||||
|
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])
|
||||||
|
} 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);
|
||||||
|
(sw, ew)
|
||||||
|
} else {
|
||||||
|
(wire.world_width, wire.world_width)
|
||||||
|
}
|
||||||
|
} else if wire.world_width > 0.0 {
|
||||||
|
(wire.world_width, wire.world_width)
|
||||||
|
} else {
|
||||||
|
(0.0, 0.0)
|
||||||
|
};
|
||||||
|
|
||||||
Some(CircleInstance {
|
Some(CircleInstance {
|
||||||
center_high: ch,
|
center_high: ch,
|
||||||
_pad0: 0.0,
|
start_width,
|
||||||
center_low: cl,
|
center_low: cl,
|
||||||
radius: radius as f32,
|
radius: radius as f32,
|
||||||
axis_x: [axis_x[0] as f32, axis_x[1] as f32, axis_x[2] as f32],
|
axis_x: [axis_x[0] as f32, axis_x[1] as f32, axis_x[2] as f32],
|
||||||
@@ -256,7 +282,7 @@ pub fn extract_circle_instance_from_geom(
|
|||||||
axis_y: [axis_y[0] as f32, axis_y[1] as f32, axis_y[2] as f32],
|
axis_y: [axis_y[0] as f32, axis_y[1] as f32, axis_y[2] as f32],
|
||||||
end_angle,
|
end_angle,
|
||||||
color: wire.color,
|
color: wire.color,
|
||||||
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
params: [hw, wire.pattern_length, draw_depth, end_width],
|
||||||
pat0,
|
pat0,
|
||||||
pat1,
|
pat1,
|
||||||
})
|
})
|
||||||
@@ -265,14 +291,14 @@ pub fn extract_circle_instance_from_geom(
|
|||||||
/// Helper to extract all analytical circle/arc instances from a `WireModel`.
|
/// Helper to extract all analytical circle/arc instances from a `WireModel`.
|
||||||
///
|
///
|
||||||
/// Returns `Some(instances)` if the wire is non-empty, contains solely analytical
|
/// Returns `Some(instances)` if the wire is non-empty, contains solely analytical
|
||||||
/// circle/arc tangent geometries, and has no mesh/pick fills, text, or block instance.
|
/// circle/arc tangent geometries, and has no mesh fills, text, or block instance.
|
||||||
pub fn extract_circle_instances(
|
pub fn extract_circle_instances(
|
||||||
wire: &crate::scene::WireModel,
|
wire: &crate::scene::WireModel,
|
||||||
draw_depth: f32,
|
draw_depth: f32,
|
||||||
) -> Option<Vec<CircleInstance>> {
|
) -> Option<Vec<CircleInstance>> {
|
||||||
if wire.tangent_geoms.is_empty()
|
if wire.tangent_geoms.is_empty()
|
||||||
|| !wire.fill_tris.is_empty()
|
|| !wire.fill_tris.is_empty()
|
||||||
|| !wire.pick_tris.is_empty()
|
|| wire.fill_is_3d
|
||||||
|| !wire.text_verts.is_empty()
|
|| !wire.text_verts.is_empty()
|
||||||
|| wire.render_instance.is_some()
|
|| wire.render_instance.is_some()
|
||||||
{
|
{
|
||||||
@@ -280,8 +306,8 @@ pub fn extract_circle_instances(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
|
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
|
||||||
for geom in &wire.tangent_geoms {
|
for (i, geom) in wire.tangent_geoms.iter().enumerate() {
|
||||||
if let Some(inst) = extract_circle_instance_from_geom(geom, wire, draw_depth) {
|
if let Some(inst) = extract_circle_instance_from_geom_indexed(geom, wire, draw_depth, i) {
|
||||||
instances.push(inst);
|
instances.push(inst);
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
@@ -410,4 +436,100 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_constant_wide_arc() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [100.0, 50.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 40.0,
|
||||||
|
start_angle: 0.0,
|
||||||
|
end_angle: std::f64::consts::PI,
|
||||||
|
});
|
||||||
|
wire.world_width = 12.0;
|
||||||
|
wire.color = [1.0, 0.5, 0.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_circle_instance(&wire, 0.0).expect("extract constant wide arc");
|
||||||
|
assert_eq!(inst.start_width, 12.0);
|
||||||
|
assert_eq!(inst.params[3], 12.0); // end_width
|
||||||
|
assert_eq!(inst.radius, 40.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_tapered_arc() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [0.0, 0.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 30.0,
|
||||||
|
start_angle: 0.2,
|
||||||
|
end_angle: 1.8,
|
||||||
|
});
|
||||||
|
wire.world_width = 20.0;
|
||||||
|
wire.taper_widths = vec![4.0, 20.0];
|
||||||
|
wire.color = [0.2, 0.8, 1.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_circle_instance(&wire, 0.0).expect("extract tapered arc");
|
||||||
|
assert_eq!(inst.start_width, 4.0);
|
||||||
|
assert_eq!(inst.params[3], 20.0); // end_width
|
||||||
|
assert_eq!(inst.radius, 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_accepts_pick_tris() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Circle {
|
||||||
|
center: [5.0, 5.0, 0.0],
|
||||||
|
radius: 15.0,
|
||||||
|
});
|
||||||
|
wire.world_width = 6.0;
|
||||||
|
wire.pick_tris = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
|
||||||
|
wire.pick_tris_low = vec![[0.0; 3]; 3];
|
||||||
|
|
||||||
|
let insts = extract_circle_instances(&wire, 0.0).expect("wide curve with pick_tris should extract");
|
||||||
|
assert_eq!(insts.len(), 1);
|
||||||
|
assert_eq!(insts[0].start_width, 6.0);
|
||||||
|
assert_eq!(insts[0].params[3], 6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_donut_polyline_tessellates_to_analytical_arcs() {
|
||||||
|
use acadrust::CadDocument;
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
let donut = crate::modules::draw::draw::donut::make_donut(10.0, 20.0, 0.0, 10.0, 30.0);
|
||||||
|
let h = doc.add_entity(donut).unwrap();
|
||||||
|
let entity = doc.get_entity(h).unwrap();
|
||||||
|
let selected = rustc_hash::FxHashSet::default();
|
||||||
|
let wires = crate::scene::convert::tess::tessellate_entity(
|
||||||
|
&doc,
|
||||||
|
&selected,
|
||||||
|
None,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
1.0,
|
||||||
|
None,
|
||||||
|
entity,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert_eq!(wires.len(), 2, "donut should split into 2 analytical arc wires");
|
||||||
|
for w in &wires {
|
||||||
|
assert_eq!(w.world_width, 20.0);
|
||||||
|
assert_eq!(w.tangent_geoms.len(), 1);
|
||||||
|
assert!(matches!(w.tangent_geoms[0], crate::scene::model::wire_model::TangentGeom::Arc { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth_map = rustc_hash::FxHashMap::default();
|
||||||
|
let partitioned = crate::scene::pipeline::wire_arena::partition_wires(&wires, &depth_map);
|
||||||
|
assert_eq!(partitioned.circle_instances.len(), 2);
|
||||||
|
assert_eq!(partitioned.regular.len(), 0);
|
||||||
|
assert_eq!(partitioned.circle_instances[0].start_width, 20.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[0].params[3], 20.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[1].start_width, 20.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[1].params[3], 20.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ pub fn extract_ellipse_instances(
|
|||||||
) -> Option<Vec<EllipseInstance>> {
|
) -> Option<Vec<EllipseInstance>> {
|
||||||
if wire.tangent_geoms.is_empty()
|
if wire.tangent_geoms.is_empty()
|
||||||
|| !wire.fill_tris.is_empty()
|
|| !wire.fill_tris.is_empty()
|
||||||
|| !wire.pick_tris.is_empty()
|
|| wire.fill_is_3d
|
||||||
|| !wire.text_verts.is_empty()
|
|| !wire.text_verts.is_empty()
|
||||||
|| wire.render_instance.is_some()
|
|| wire.render_instance.is_some()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -521,6 +521,180 @@ fn test_selected_circle_arc_ellipse_highlight_overlay() {
|
|||||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires a GPU adapter"]
|
||||||
|
fn test_thick_and_tapered_arc_gpu_rendering() {
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||||
|
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||||
|
.expect("GPU adapter");
|
||||||
|
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_limits: adapter.limits(),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.expect("GPU device");
|
||||||
|
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
|
||||||
|
|
||||||
|
// 1. Wide circular arc with pick triangles
|
||||||
|
let mut wide_arc = WireModel::default();
|
||||||
|
wide_arc.name = "wide_arc".into();
|
||||||
|
wide_arc.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [50.0, 50.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 30.0,
|
||||||
|
start_angle: 0.0,
|
||||||
|
end_angle: std::f64::consts::PI,
|
||||||
|
});
|
||||||
|
wide_arc.world_width = 10.0;
|
||||||
|
wide_arc.color = [1.0, 0.2, 0.2, 1.0];
|
||||||
|
wide_arc.pick_tris = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
|
||||||
|
wide_arc.pick_tris_low = vec![[0.0; 3]; 3];
|
||||||
|
|
||||||
|
// 2. Tapered circular arc
|
||||||
|
let mut tapered_arc = WireModel::default();
|
||||||
|
tapered_arc.name = "tapered_arc".into();
|
||||||
|
tapered_arc.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [150.0, 50.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 40.0,
|
||||||
|
start_angle: 0.2,
|
||||||
|
end_angle: 2.8,
|
||||||
|
});
|
||||||
|
tapered_arc.world_width = 16.0;
|
||||||
|
tapered_arc.taper_widths = vec![2.0, 16.0];
|
||||||
|
tapered_arc.color = [0.2, 0.8, 1.0, 1.0];
|
||||||
|
|
||||||
|
let wires = vec![wide_arc, tapered_arc];
|
||||||
|
let depth_map = rustc_hash::FxHashMap::default();
|
||||||
|
let circles = pipeline.upload_circles(&device, &queue, &wires, &depth_map);
|
||||||
|
|
||||||
|
// Both curves must be routed to GPU analytical circle instances!
|
||||||
|
assert!(!circles.is_empty(), "analytical circles must be uploaded");
|
||||||
|
assert_eq!(circles[0].instance_count, 2);
|
||||||
|
pipeline.gpu_circles = std::sync::Arc::new(circles);
|
||||||
|
assert_eq!(pipeline.gpu_circles.len(), 1);
|
||||||
|
assert_eq!(pipeline.gpu_circles[0].instance_count, 2);
|
||||||
|
|
||||||
|
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("thick_arc_target"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("thick_arc_encoder"),
|
||||||
|
});
|
||||||
|
pipeline.render(
|
||||||
|
&mut encoder,
|
||||||
|
&target,
|
||||||
|
iced::Size::new(512, 512),
|
||||||
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires a GPU adapter"]
|
||||||
|
fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||||
|
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||||
|
.expect("GPU adapter");
|
||||||
|
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_limits: adapter.limits(),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.expect("GPU device");
|
||||||
|
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
|
||||||
|
|
||||||
|
// Setup 3D perspective camera with acute tilt
|
||||||
|
let mut camera = crate::scene::view::camera::Camera::default();
|
||||||
|
camera.projection = crate::scene::view::camera::Projection::Perspective;
|
||||||
|
camera.distance = 200.0;
|
||||||
|
camera.target = glam::DVec3::new(50.0, 50.0, 0.0);
|
||||||
|
camera.rotation = glam::Quat::from_axis_angle(glam::Vec3::X, 1.1); // ~63 degree tilt (acute perspective)
|
||||||
|
let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(512.0, 512.0));
|
||||||
|
let uniforms = Uniforms::new(&camera, bounds, false);
|
||||||
|
pipeline.upload_uniforms(&device, &queue, &uniforms);
|
||||||
|
|
||||||
|
// Donut circular arc segments
|
||||||
|
let mut donut = WireModel::default();
|
||||||
|
donut.name = "donut".into();
|
||||||
|
donut.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [50.0, 50.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 30.0,
|
||||||
|
start_angle: 0.0,
|
||||||
|
end_angle: std::f64::consts::PI,
|
||||||
|
});
|
||||||
|
donut.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [50.0, 50.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 30.0,
|
||||||
|
start_angle: std::f64::consts::PI,
|
||||||
|
end_angle: std::f64::consts::TAU,
|
||||||
|
});
|
||||||
|
donut.world_width = 12.0;
|
||||||
|
donut.color = [0.2, 0.9, 0.3, 1.0];
|
||||||
|
|
||||||
|
let wires = vec![donut];
|
||||||
|
let depth_map = rustc_hash::FxHashMap::default();
|
||||||
|
let circles = pipeline.upload_circles(&device, &queue, &wires, &depth_map);
|
||||||
|
|
||||||
|
assert_eq!(circles.len(), 1);
|
||||||
|
assert_eq!(circles[0].instance_count, 2);
|
||||||
|
pipeline.gpu_circles = std::sync::Arc::new(circles);
|
||||||
|
|
||||||
|
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("tilted_donut_target"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("tilted_donut_encoder"),
|
||||||
|
});
|
||||||
|
pipeline.render(
|
||||||
|
&mut encoder,
|
||||||
|
&target,
|
||||||
|
iced::Size::new(512, 512),
|
||||||
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires a GPU adapter"]
|
#[ignore = "requires a GPU adapter"]
|
||||||
fn test_selected_ellipse_overlay() {
|
fn test_selected_ellipse_overlay() {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ pub fn classify_wire(wire: &WireModel) -> WireKind {
|
|||||||
}
|
}
|
||||||
if !wire.tangent_geoms.is_empty()
|
if !wire.tangent_geoms.is_empty()
|
||||||
&& wire.fill_tris.is_empty()
|
&& wire.fill_tris.is_empty()
|
||||||
&& wire.pick_tris.is_empty()
|
&& !wire.fill_is_3d
|
||||||
&& wire.text_verts.is_empty()
|
&& wire.text_verts.is_empty()
|
||||||
{
|
{
|
||||||
if super::circle_gpu::extract_circle_instances(wire, 0.0).is_some() {
|
if super::circle_gpu::extract_circle_instances(wire, 0.0).is_some() {
|
||||||
@@ -219,7 +219,7 @@ pub fn partition_wires<'a>(
|
|||||||
// Resolve draw depth only for wires eligible for analytical extraction.
|
// Resolve draw depth only for wires eligible for analytical extraction.
|
||||||
if !wire.tangent_geoms.is_empty()
|
if !wire.tangent_geoms.is_empty()
|
||||||
&& wire.fill_tris.is_empty()
|
&& wire.fill_tris.is_empty()
|
||||||
&& wire.pick_tris.is_empty()
|
&& !wire.fill_is_3d
|
||||||
&& wire.text_verts.is_empty()
|
&& wire.text_verts.is_empty()
|
||||||
{
|
{
|
||||||
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
|
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
@@ -1709,4 +1709,47 @@ mod tests {
|
|||||||
assert_eq!(partitioned.ellipse_instances.len(), 1);
|
assert_eq!(partitioned.ellipse_instances.len(), 1);
|
||||||
assert_eq!(partitioned.regular.len(), 1);
|
assert_eq!(partitioned.regular.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partition_wires_partitions_thick_and_tapered_arcs() {
|
||||||
|
use crate::scene::model::wire_model::TangentGeom;
|
||||||
|
|
||||||
|
// Wide arc with pick triangles
|
||||||
|
let mut wide_arc = WireModel::default();
|
||||||
|
wide_arc.tangent_geoms.push(TangentGeom::Arc {
|
||||||
|
center: [10.0, 20.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 25.0,
|
||||||
|
start_angle: 0.0,
|
||||||
|
end_angle: std::f64::consts::PI,
|
||||||
|
});
|
||||||
|
wide_arc.world_width = 8.0;
|
||||||
|
wide_arc.pick_tris = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
|
||||||
|
wide_arc.pick_tris_low = vec![[0.0; 3]; 3];
|
||||||
|
|
||||||
|
// Tapered arc
|
||||||
|
let mut tapered_arc = WireModel::default();
|
||||||
|
tapered_arc.tangent_geoms.push(TangentGeom::Arc {
|
||||||
|
center: [50.0, 60.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 15.0,
|
||||||
|
start_angle: 0.5,
|
||||||
|
end_angle: 2.5,
|
||||||
|
});
|
||||||
|
tapered_arc.world_width = 10.0;
|
||||||
|
tapered_arc.taper_widths = vec![2.0, 10.0];
|
||||||
|
|
||||||
|
let wires = vec![wide_arc, tapered_arc];
|
||||||
|
let depth_map = rustc_hash::FxHashMap::default();
|
||||||
|
let partitioned = partition_wires(&wires, &depth_map);
|
||||||
|
|
||||||
|
assert_eq!(partitioned.circle_instances.len(), 2);
|
||||||
|
assert_eq!(partitioned.regular.len(), 0);
|
||||||
|
assert_eq!(partitioned.circle_instances[0].start_width, 8.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[0].params[3], 8.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[1].start_width, 2.0);
|
||||||
|
assert_eq!(partitioned.circle_instances[1].params[3], 10.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+79
-29
@@ -32,12 +32,12 @@ const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
|
|||||||
const TAU: f32 = 6.283185307179586;
|
const TAU: f32 = 6.283185307179586;
|
||||||
|
|
||||||
struct InstanceIn {
|
struct InstanceIn {
|
||||||
@location(0) center_high: vec4<f32>, // xyz = center_high, w = unused
|
@location(0) center_high: vec4<f32>, // xyz = center_high, w = start_width
|
||||||
@location(1) center_low: vec4<f32>, // xyz = center_low, w = radius
|
@location(1) center_low: vec4<f32>, // xyz = center_low, w = radius
|
||||||
@location(2) axis_x: vec4<f32>, // xyz = axis_x, w = start_angle
|
@location(2) axis_x: vec4<f32>, // xyz = axis_x, w = start_angle
|
||||||
@location(3) axis_y: vec4<f32>, // xyz = axis_y, w = end_angle
|
@location(3) axis_y: vec4<f32>, // xyz = axis_y, w = end_angle
|
||||||
@location(4) color: vec4<f32>, // rgba
|
@location(4) color: vec4<f32>, // rgba
|
||||||
@location(5) params: vec4<f32>, // half_width_px, pattern_length, draw_depth, unused
|
@location(5) params: vec4<f32>, // half_width_px, pattern_length, draw_depth, end_width
|
||||||
@location(6) pat0: vec4<f32>,
|
@location(6) pat0: vec4<f32>,
|
||||||
@location(7) pat1: vec4<f32>,
|
@location(7) pat1: vec4<f32>,
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ struct VertexOut {
|
|||||||
@builtin(position) clip_pos: vec4<f32>,
|
@builtin(position) clip_pos: vec4<f32>,
|
||||||
@location(0) local_pos: vec2<f32>,
|
@location(0) local_pos: vec2<f32>,
|
||||||
@location(1) radius: f32,
|
@location(1) radius: f32,
|
||||||
@location(2) hw_px: f32,
|
@location(2) @interpolate(flat) hw_ends: vec2<f32>,
|
||||||
@location(3) color: vec4<f32>,
|
@location(3) color: vec4<f32>,
|
||||||
@location(4) pattern_length: f32,
|
@location(4) pattern_length: f32,
|
||||||
@location(5) pat0: vec4<f32>,
|
@location(5) pat0: vec4<f32>,
|
||||||
@@ -54,9 +54,10 @@ struct VertexOut {
|
|||||||
@location(7) @interpolate(flat) min_elem: f32,
|
@location(7) @interpolate(flat) min_elem: f32,
|
||||||
@location(8) @interpolate(flat) start_angle: f32,
|
@location(8) @interpolate(flat) start_angle: f32,
|
||||||
@location(9) @interpolate(flat) end_angle: f32,
|
@location(9) @interpolate(flat) end_angle: f32,
|
||||||
|
@location(10) @interpolate(flat) is_world: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_hw(px_hw: f32) -> f32 {
|
fn resolve_px_hw(px_hw: f32) -> f32 {
|
||||||
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
||||||
if u.lineweight_scale < 0.0 {
|
if u.lineweight_scale < 0.0 {
|
||||||
let scale = -u.lineweight_scale;
|
let scale = -u.lineweight_scale;
|
||||||
@@ -85,14 +86,32 @@ fn resolve_hw(px_hw: f32) -> f32 {
|
|||||||
let axis_x = in.axis_x.xyz;
|
let axis_x = in.axis_x.xyz;
|
||||||
let axis_y = in.axis_y.xyz;
|
let axis_y = in.axis_y.xyz;
|
||||||
let color = in.color;
|
let color = in.color;
|
||||||
let hw_px = resolve_hw(in.params.x);
|
let world_w_a = in.center_high.w;
|
||||||
|
let world_w_b = in.params.w;
|
||||||
|
let is_world = select(0.0, 1.0, world_w_a > 0.0 || world_w_b > 0.0);
|
||||||
|
|
||||||
|
var hw_a: f32;
|
||||||
|
var hw_b: f32;
|
||||||
|
var ext: f32;
|
||||||
|
|
||||||
|
if is_world > 0.5 {
|
||||||
|
hw_a = world_w_a * 0.5;
|
||||||
|
hw_b = world_w_b * 0.5;
|
||||||
|
let max_hw = max(hw_a, hw_b);
|
||||||
|
// Expand bounding quad so anti-aliasing margin and physical width never clip under 3D tilt
|
||||||
|
let margin_world = max_hw + 16.0 * max(u.world_per_pixel, 1e-6);
|
||||||
|
ext = radius + margin_world;
|
||||||
|
} else {
|
||||||
|
let px_hw = resolve_px_hw(in.params.x);
|
||||||
|
hw_a = px_hw;
|
||||||
|
hw_b = px_hw;
|
||||||
|
let margin_world = (px_hw + 2.0) * max(u.world_per_pixel, 1e-6);
|
||||||
|
ext = radius + margin_world;
|
||||||
|
}
|
||||||
|
|
||||||
let pattern_length = in.params.y;
|
let pattern_length = in.params.y;
|
||||||
let draw_depth = in.params.z;
|
let draw_depth = in.params.z;
|
||||||
|
|
||||||
// Expand bounding quad so anti-aliasing margin and lineweight never clip
|
|
||||||
let margin_world = (hw_px + 2.0) * max(u.world_per_pixel, 1e-6);
|
|
||||||
let ext = radius + margin_world;
|
|
||||||
|
|
||||||
let center_rel = (center_high - u.eye_high) + (center_low - u.eye_low);
|
let center_rel = (center_high - u.eye_high) + (center_low - u.eye_low);
|
||||||
let world_pos_rel = center_rel + (u_val * axis_x + v_val * axis_y) * ext;
|
let world_pos_rel = center_rel + (u_val * axis_x + v_val * axis_y) * ext;
|
||||||
|
|
||||||
@@ -103,7 +122,7 @@ fn resolve_hw(px_hw: f32) -> f32 {
|
|||||||
out.clip_pos = clip_pos;
|
out.clip_pos = clip_pos;
|
||||||
out.local_pos = vec2<f32>(u_val * ext, v_val * ext);
|
out.local_pos = vec2<f32>(u_val * ext, v_val * ext);
|
||||||
out.radius = radius;
|
out.radius = radius;
|
||||||
out.hw_px = hw_px;
|
out.hw_ends = vec2<f32>(hw_a, hw_b);
|
||||||
out.color = color;
|
out.color = color;
|
||||||
out.pattern_length = pattern_length;
|
out.pattern_length = pattern_length;
|
||||||
out.pat0 = in.pat0;
|
out.pat0 = in.pat0;
|
||||||
@@ -122,6 +141,7 @@ fn resolve_hw(px_hw: f32) -> f32 {
|
|||||||
|
|
||||||
out.start_angle = in.axis_x.w;
|
out.start_angle = in.axis_x.w;
|
||||||
out.end_angle = in.axis_y.w;
|
out.end_angle = in.axis_y.w;
|
||||||
|
out.is_world = is_world;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -181,30 +201,60 @@ fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
|||||||
let theta = select(angle, angle + TAU, angle < 0.0);
|
let theta = select(angle, angle + TAU, angle < 0.0);
|
||||||
let d_theta = mod_tau(theta - sa);
|
let d_theta = mod_tau(theta - sa);
|
||||||
|
|
||||||
var d_world: f32;
|
var alpha_cov: f32 = 0.0;
|
||||||
var arc_dist: f32;
|
var arc_dist: f32 = 0.0;
|
||||||
|
|
||||||
if sweep >= TAU - 1e-5 || d_theta <= sweep {
|
let is_full_or_in_sweep = (sweep >= TAU - 1e-5) || (d_theta <= sweep);
|
||||||
// Inside arc sweep or full circle
|
|
||||||
d_world = abs(r - in.radius);
|
if in.is_world > 0.5 {
|
||||||
arc_dist = in.radius * d_theta;
|
// Physical world-unit width (thick analytical circular arcs, donuts, tapered segments).
|
||||||
} else {
|
// Distance-to-edge calculations are strictly performed in the circle plane's world units
|
||||||
// Outside sweep: measure distance to start and end endpoints for round end caps
|
// so that 3D perspective projection yields exact concentric ellipses with invariant
|
||||||
let p_start = in.radius * vec2<f32>(cos(sa), sin(sa));
|
// physical stroke widths across any camera tilt or acute perspective angle.
|
||||||
let p_end = in.radius * vec2<f32>(cos(ea), sin(ea));
|
if is_full_or_in_sweep {
|
||||||
let d_start = length(in.local_pos - p_start);
|
let t = select(0.0, clamp(d_theta / sweep, 0.0, 1.0), sweep > 1e-5);
|
||||||
let d_end = length(in.local_pos - p_end);
|
let world_hw = mix(in.hw_ends.x, in.hw_ends.y, t);
|
||||||
if d_start < d_end {
|
let eff_hw = max(world_hw, 0.5 * fw);
|
||||||
d_world = d_start;
|
let d_world = abs(r - in.radius);
|
||||||
arc_dist = 0.0;
|
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);
|
||||||
|
alpha_cov = cov * subpixel_fade;
|
||||||
|
arc_dist = in.radius * d_theta;
|
||||||
} else {
|
} else {
|
||||||
d_world = d_end;
|
// Outside sweep: evaluate distance to start and end endpoints for smooth round end caps
|
||||||
arc_dist = in.radius * sweep;
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Lineweight stroke (screen-pixel width)
|
||||||
|
if is_full_or_in_sweep {
|
||||||
|
let t = select(0.0, clamp(d_theta / sweep, 0.0, 1.0), sweep > 1e-5);
|
||||||
|
let cur_hw = mix(in.hw_ends.x, in.hw_ends.y, t);
|
||||||
|
let d_world = abs(r - in.radius);
|
||||||
|
let d_px = d_world / fw;
|
||||||
|
alpha_cov = clamp(0.5 + cur_hw - d_px, 0.0, 1.0);
|
||||||
|
arc_dist = in.radius * d_theta;
|
||||||
|
} else {
|
||||||
|
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_px = length(in.local_pos - p_start) / fw;
|
||||||
|
let d_end_px = length(in.local_pos - p_end) / fw;
|
||||||
|
let cov_start = clamp(0.5 + in.hw_ends.x - d_start_px, 0.0, 1.0);
|
||||||
|
let cov_end = clamp(0.5 + in.hw_ends.y - d_end_px, 0.0, 1.0);
|
||||||
|
alpha_cov = max(cov_start, cov_end);
|
||||||
|
arc_dist = select(in.radius * sweep, 0.0, cov_start >= cov_end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let d_px = d_world / fw;
|
|
||||||
let alpha_cov = clamp(0.5 + in.hw_px - d_px, 0.0, 1.0);
|
|
||||||
if alpha_cov <= 0.0 {
|
if alpha_cov <= 0.0 {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user