feat(render): GPU analytical curve extraction for multi-arc polylines and blocks

This commit is contained in:
Karim Jerbi committed 2026-09-07 14:59:50 +01:00
1 parent 6e01d464e5
commit 75ec08dc55
6 files changed
+186 -88

No files matched your search

+17 -10
View File
@@ -2318,19 +2318,26 @@ fn emit_wire(
// and transforms cleanly with accum_xform into an analytical curve, emit it as a standalone // and transforms cleanly with accum_xform into an analytical curve, emit it as a standalone
// wire in extra_wires. This lets partition_wires extract it into CircleGpu / EllipseGpu for // wire in extra_wires. This lets partition_wires extract it into CircleGpu / EllipseGpu for
// pixel-perfect analytical rendering on the GPU instead of segmented lines. // pixel-perfect analytical rendering on the GPU instead of segmented lines.
if lw.tangent_geoms.len() == 1 if !lw.tangent_geoms.is_empty()
&& lw.fill_tris.is_empty() && lw.fill_tris.is_empty()
&& lw.pick_tris.is_empty() && lw.pick_tris.is_empty()
&& lw.text_verts.is_empty() && lw.text_verts.is_empty()
{ {
if let Some(tangent) = transform_tangent(&lw.tangent_geoms[0], accum_xform) { let transformed_tangents: Option<Vec<TangentGeom>> = lw
if matches!( .tangent_geoms
tangent, .iter()
TangentGeom::Circle { .. } .map(|tg| transform_tangent(tg, accum_xform))
| TangentGeom::PlanarCircle { .. } .collect();
| TangentGeom::Arc { .. } if let Some(tangents) = transformed_tangents {
| TangentGeom::PlanarEllipse { .. } if tangents.iter().all(|tangent| {
) { matches!(
tangent,
TangentGeom::Circle { .. }
| TangentGeom::PlanarCircle { .. }
| TangentGeom::Arc { .. }
| TangentGeom::PlanarEllipse { .. }
)
}) {
let contrast_bg = lw.contrast_bg.unwrap_or(ctx.bg_color); let contrast_bg = lw.contrast_bg.unwrap_or(ctx.bg_color);
let color = if lw.canvas_color { let color = if lw.canvas_color {
ctx.bg_color ctx.bg_color
@@ -2421,7 +2428,7 @@ fn emit_wire(
line_weight_px: final_lw_px, line_weight_px: final_lw_px,
aci: final_aci, aci: final_aci,
snap_pts, snap_pts,
tangent_geoms: vec![tangent], tangent_geoms: tangents,
key_vertices, key_vertices,
aabb, aabb,
plinegen: lw.plinegen, plinegen: lw.plinegen,
+40
View File
@@ -11358,4 +11358,44 @@ mod layout_cache_tests {
"Single bulge arc polyline must be extracted as analytical GPU arc instance" "Single bulge arc polyline must be extracted as analytical GPU arc instance"
); );
} }
#[test]
fn multi_bulge_polyline_extracts_as_analytical_gpu_arcs() {
let mut s = Scene::new();
let mut pline = acadrust::entities::LwPolyline::new();
// A circle represented as a closed 2-vertex polyline with two semicircle bulges (standard CAD polyline circle)
pline.is_closed = true;
pline.vertices = vec![
acadrust::entities::LwVertex {
location: acadrust::types::Vector2::new(0.0, 0.0),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
acadrust::entities::LwVertex {
location: acadrust::types::Vector2::new(100.0, 0.0),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
let _ = s.add_entity(EntityType::LwPolyline(pline));
let wires = s.model_tile_wires_arc(0, &Camera::default(), 1.0, 1000.0);
let depths = rustc_hash::FxHashMap::default();
let partitioned = crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths);
assert_eq!(
partitioned.circle_instances.len(),
2,
"Multi-bulge closed polyline circle must be extracted as 2 analytical GPU arc instances"
);
assert_eq!(
partitioned.regular.len(),
0,
"Multi-bulge pure arc polyline must not be sent to regular line arena"
);
}
} }
+44 -13
View File
@@ -199,22 +199,13 @@ pub fn create_pipelines(
(circle_pipeline, circle_xray_pipeline) (circle_pipeline, circle_xray_pipeline)
} }
/// Helper to extract an analytical circle or circular arc instance from a `WireModel`. /// Helper to extract an analytical circle or circular arc instance from a single `TangentGeom`.
pub fn extract_circle_instance( pub fn extract_circle_instance_from_geom(
geom: &crate::scene::model::wire_model::TangentGeom,
wire: &crate::scene::WireModel, wire: &crate::scene::WireModel,
draw_depth: f32, draw_depth: f32,
) -> Option<CircleInstance> { ) -> Option<CircleInstance> {
// Only thin 2D planar circles or circular arcs have exactly 1 PlanarCircle / Arc tangent geometry let (center, axis_x, axis_y, radius, start_angle, end_angle) = match *geom {
// and no 3D mesh fills or text quads.
if wire.tangent_geoms.len() != 1
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
|| !wire.text_verts.is_empty()
|| wire.render_instance.is_some()
{
return None;
}
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match wire.tangent_geoms[0] {
crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => ( crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => (
[center[0] as f64, center[1] as f64, center[2] as f64], [center[0] as f64, center[1] as f64, center[2] as f64],
[1.0, 0.0, 0.0], [1.0, 0.0, 0.0],
@@ -271,6 +262,46 @@ pub fn extract_circle_instance(
}) })
} }
/// Helper to extract all analytical circle/arc instances from a `WireModel`.
///
/// 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.
pub fn extract_circle_instances(
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> Option<Vec<CircleInstance>> {
if wire.tangent_geoms.is_empty()
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
|| !wire.text_verts.is_empty()
|| wire.render_instance.is_some()
{
return None;
}
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
for geom in &wire.tangent_geoms {
if let Some(inst) = extract_circle_instance_from_geom(geom, wire, draw_depth) {
instances.push(inst);
} else {
return None;
}
}
Some(instances)
}
/// Helper to extract a single analytical circle or circular arc instance from a `WireModel`.
pub fn extract_circle_instance(
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> Option<CircleInstance> {
if wire.tangent_geoms.len() == 1 {
extract_circle_instances(wire, draw_depth).and_then(|mut v| v.pop())
} else {
None
}
}
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) { fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
let hx = x as f32; let hx = x as f32;
let lx = (x - hx as f64) as f32; let lx = (x - hx as f64) as f32;
+41 -11
View File
@@ -199,19 +199,12 @@ pub fn create_pipelines(
(ellipse_pipeline, ellipse_xray_pipeline) (ellipse_pipeline, ellipse_xray_pipeline)
} }
/// Helper to extract an analytical ellipse instance from a `WireModel`. /// Helper to extract an analytical ellipse instance from a single `TangentGeom`.
pub fn extract_ellipse_instance( pub fn extract_ellipse_instance_from_geom(
geom: &crate::scene::model::wire_model::TangentGeom,
wire: &crate::scene::WireModel, wire: &crate::scene::WireModel,
draw_depth: f32, draw_depth: f32,
) -> Option<EllipseInstance> { ) -> Option<EllipseInstance> {
if wire.tangent_geoms.len() != 1
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
|| !wire.text_verts.is_empty()
|| wire.render_instance.is_some()
{
return None;
}
let crate::scene::model::wire_model::TangentGeom::PlanarEllipse { let crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
center, center,
major_axis, major_axis,
@@ -219,7 +212,7 @@ pub fn extract_ellipse_instance(
minor_axis_ratio, minor_axis_ratio,
start_param, start_param,
end_param, end_param,
} = wire.tangent_geoms[0] else { } = *geom else {
return None; return None;
}; };
@@ -254,6 +247,43 @@ pub fn extract_ellipse_instance(
}) })
} }
/// Helper to extract all analytical ellipse instances from a `WireModel`.
pub fn extract_ellipse_instances(
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> Option<Vec<EllipseInstance>> {
if wire.tangent_geoms.is_empty()
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
|| !wire.text_verts.is_empty()
|| wire.render_instance.is_some()
{
return None;
}
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
for geom in &wire.tangent_geoms {
if let Some(inst) = extract_ellipse_instance_from_geom(geom, wire, draw_depth) {
instances.push(inst);
} else {
return None;
}
}
Some(instances)
}
/// Helper to extract a single analytical ellipse instance from a `WireModel`.
pub fn extract_ellipse_instance(
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> Option<EllipseInstance> {
if wire.tangent_geoms.len() == 1 {
extract_ellipse_instances(wire, draw_depth).and_then(|mut v| v.pop())
} else {
None
}
}
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) { fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
let hx = x as f32; let hx = x as f32;
let lx = (x - hx as f64) as f32; let lx = (x - hx as f64) as f32;
+30 -22
View File
@@ -2421,8 +2421,8 @@ impl Pipeline {
continue; continue;
} }
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(inst) = circle_gpu::extract_circle_instance(wire, depth) { if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
instances.push(inst); instances.extend(insts);
} }
} }
self.upload_circles_from_instances(device, queue, &instances) self.upload_circles_from_instances(device, queue, &instances)
@@ -2461,8 +2461,8 @@ impl Pipeline {
continue; continue;
} }
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) { if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
instances.push(inst); instances.extend(insts);
} }
} }
self.upload_ellipses_from_instances(device, queue, &instances) self.upload_ellipses_from_instances(device, queue, &instances)
@@ -2656,18 +2656,22 @@ impl Pipeline {
let mut selected_circles: Vec<CircleInstance> = Vec::new(); let mut selected_circles: Vec<CircleInstance> = Vec::new();
for &wire in &selected_wires { for &wire in &selected_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = circle_gpu::extract_circle_instance(wire, depth) { if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
if let Some(tint) = selected_tint { for mut inst in insts {
inst.color = tint; if let Some(tint) = selected_tint {
inst.color = tint;
}
selected_circles.push(inst);
} }
selected_circles.push(inst);
} }
} }
for &wire in &hover_wires { for &wire in &hover_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = circle_gpu::extract_circle_instance(wire, depth) { if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
inst.color = WireModel::HOVER; for mut inst in insts {
selected_circles.push(inst); inst.color = WireModel::HOVER;
selected_circles.push(inst);
}
} }
} }
self.gpu_selected_circles = if selected_circles.is_empty() { self.gpu_selected_circles = if selected_circles.is_empty() {
@@ -2684,18 +2688,22 @@ impl Pipeline {
let mut selected_ellipses: Vec<EllipseInstance> = Vec::new(); let mut selected_ellipses: Vec<EllipseInstance> = Vec::new();
for &wire in &selected_wires { for &wire in &selected_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) { if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
if let Some(tint) = selected_tint { for mut inst in insts {
inst.color = tint; if let Some(tint) = selected_tint {
inst.color = tint;
}
selected_ellipses.push(inst);
} }
selected_ellipses.push(inst);
} }
} }
for &wire in &hover_wires { for &wire in &hover_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map); let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) { if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
inst.color = WireModel::HOVER; for mut inst in insts {
selected_ellipses.push(inst); inst.color = WireModel::HOVER;
selected_ellipses.push(inst);
}
} }
} }
self.gpu_selected_ellipses = if selected_ellipses.is_empty() { self.gpu_selected_ellipses = if selected_ellipses.is_empty() {
@@ -2714,8 +2722,8 @@ impl Pipeline {
.copied() .copied()
.filter(|wire| { .filter(|wire| {
wire.render_instance.is_none() wire.render_instance.is_none()
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none() && circle_gpu::extract_circle_instances(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none() && ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
}) })
.collect(); .collect();
let hover_regular: Vec<&WireModel> = hover_wires let hover_regular: Vec<&WireModel> = hover_wires
@@ -2723,8 +2731,8 @@ impl Pipeline {
.copied() .copied()
.filter(|wire| { .filter(|wire| {
wire.render_instance.is_none() wire.render_instance.is_none()
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none() && circle_gpu::extract_circle_instances(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none() && ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
}) })
.collect(); .collect();
let selected_blocks: Vec<&WireModel> = selected_wires let selected_blocks: Vec<&WireModel> = selected_wires
+14 -32
View File
@@ -144,25 +144,16 @@ pub fn classify_wire(wire: &WireModel) -> WireKind {
if wire.render_instance.is_some() { if wire.render_instance.is_some() {
return WireKind::Block; return WireKind::Block;
} }
if wire.tangent_geoms.len() == 1 if !wire.tangent_geoms.is_empty()
&& wire.fill_tris.is_empty() && wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty() && wire.pick_tris.is_empty()
&& wire.text_verts.is_empty() && wire.text_verts.is_empty()
{ {
match &wire.tangent_geoms[0] { if super::circle_gpu::extract_circle_instances(wire, 0.0).is_some() {
crate::scene::model::wire_model::TangentGeom::Circle { .. } return WireKind::Circle;
| crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. } }
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => { if super::ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_some() {
if super::circle_gpu::extract_circle_instance(wire, 0.0).is_some() { return WireKind::Ellipse;
return WireKind::Circle;
}
}
crate::scene::model::wire_model::TangentGeom::PlanarEllipse { .. } => {
if super::ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_some() {
return WireKind::Ellipse;
}
}
_ => {}
} }
} }
if !wire.points.is_empty() { if !wire.points.is_empty() {
@@ -209,27 +200,18 @@ pub fn partition_wires<'a>(
continue; continue;
} }
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map); let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
if wire.tangent_geoms.len() == 1 if !wire.tangent_geoms.is_empty()
&& wire.fill_tris.is_empty() && wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty() && wire.pick_tris.is_empty()
&& wire.text_verts.is_empty() && wire.text_verts.is_empty()
{ {
match &wire.tangent_geoms[0] { if let Some(insts) = super::circle_gpu::extract_circle_instances(wire, depth) {
crate::scene::model::wire_model::TangentGeom::Circle { .. } circle_instances.extend(insts);
| crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. } continue;
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => { }
if let Some(inst) = super::circle_gpu::extract_circle_instance(wire, depth) { if let Some(insts) = super::ellipse_gpu::extract_ellipse_instances(wire, depth) {
circle_instances.push(inst); ellipse_instances.extend(insts);
continue; continue;
}
}
crate::scene::model::wire_model::TangentGeom::PlanarEllipse { .. } => {
if let Some(inst) = super::ellipse_gpu::extract_ellipse_instance(wire, depth) {
ellipse_instances.push(inst);
continue;
}
}
_ => {}
} }
} }
if !wire.points.is_empty() { if !wire.points.is_empty() {