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
// 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.
if lw.tangent_geoms.len() == 1
if !lw.tangent_geoms.is_empty()
&& lw.fill_tris.is_empty()
&& lw.pick_tris.is_empty()
&& lw.text_verts.is_empty()
{
if let Some(tangent) = transform_tangent(&lw.tangent_geoms[0], accum_xform) {
if matches!(
tangent,
TangentGeom::Circle { .. }
| TangentGeom::PlanarCircle { .. }
| TangentGeom::Arc { .. }
| TangentGeom::PlanarEllipse { .. }
) {
let transformed_tangents: Option<Vec<TangentGeom>> = lw
.tangent_geoms
.iter()
.map(|tg| transform_tangent(tg, accum_xform))
.collect();
if let Some(tangents) = transformed_tangents {
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 color = if lw.canvas_color {
ctx.bg_color
@@ -2421,7 +2428,7 @@ fn emit_wire(
line_weight_px: final_lw_px,
aci: final_aci,
snap_pts,
tangent_geoms: vec![tangent],
tangent_geoms: tangents,
key_vertices,
aabb,
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"
);
}
#[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)
}
/// Helper to extract an analytical circle or circular arc instance from a `WireModel`.
pub fn extract_circle_instance(
/// Helper to extract an analytical circle or circular arc instance from a single `TangentGeom`.
pub fn extract_circle_instance_from_geom(
geom: &crate::scene::model::wire_model::TangentGeom,
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> Option<CircleInstance> {
// Only thin 2D planar circles or circular arcs have exactly 1 PlanarCircle / Arc tangent geometry
// 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] {
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match *geom {
crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => (
[center[0] as f64, center[1] as f64, center[2] as f64],
[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]) {
let hx = x 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)
}
/// Helper to extract an analytical ellipse instance from a `WireModel`.
pub fn extract_ellipse_instance(
/// Helper to extract an analytical ellipse instance from a single `TangentGeom`.
pub fn extract_ellipse_instance_from_geom(
geom: &crate::scene::model::wire_model::TangentGeom,
wire: &crate::scene::WireModel,
draw_depth: f32,
) -> 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 {
center,
major_axis,
@@ -219,7 +212,7 @@ pub fn extract_ellipse_instance(
minor_axis_ratio,
start_param,
end_param,
} = wire.tangent_geoms[0] else {
} = *geom else {
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]) {
let hx = x as f32;
let lx = (x - hx as f64) as f32;
+30 -22
View File
@@ -2421,8 +2421,8 @@ impl Pipeline {
continue;
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(inst) = circle_gpu::extract_circle_instance(wire, depth) {
instances.push(inst);
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
instances.extend(insts);
}
}
self.upload_circles_from_instances(device, queue, &instances)
@@ -2461,8 +2461,8 @@ impl Pipeline {
continue;
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) {
instances.push(inst);
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
instances.extend(insts);
}
}
self.upload_ellipses_from_instances(device, queue, &instances)
@@ -2656,18 +2656,22 @@ impl Pipeline {
let mut selected_circles: Vec<CircleInstance> = Vec::new();
for &wire in &selected_wires {
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(tint) = selected_tint {
inst.color = tint;
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
for mut inst in insts {
if let Some(tint) = selected_tint {
inst.color = tint;
}
selected_circles.push(inst);
}
selected_circles.push(inst);
}
}
for &wire in &hover_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = circle_gpu::extract_circle_instance(wire, depth) {
inst.color = WireModel::HOVER;
selected_circles.push(inst);
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
for mut inst in insts {
inst.color = WireModel::HOVER;
selected_circles.push(inst);
}
}
}
self.gpu_selected_circles = if selected_circles.is_empty() {
@@ -2684,18 +2688,22 @@ impl Pipeline {
let mut selected_ellipses: Vec<EllipseInstance> = Vec::new();
for &wire in &selected_wires {
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(tint) = selected_tint {
inst.color = tint;
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
for mut inst in insts {
if let Some(tint) = selected_tint {
inst.color = tint;
}
selected_ellipses.push(inst);
}
selected_ellipses.push(inst);
}
}
for &wire in &hover_wires {
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(mut inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) {
inst.color = WireModel::HOVER;
selected_ellipses.push(inst);
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
for mut inst in insts {
inst.color = WireModel::HOVER;
selected_ellipses.push(inst);
}
}
}
self.gpu_selected_ellipses = if selected_ellipses.is_empty() {
@@ -2714,8 +2722,8 @@ impl Pipeline {
.copied()
.filter(|wire| {
wire.render_instance.is_none()
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none()
&& circle_gpu::extract_circle_instances(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
})
.collect();
let hover_regular: Vec<&WireModel> = hover_wires
@@ -2723,8 +2731,8 @@ impl Pipeline {
.copied()
.filter(|wire| {
wire.render_instance.is_none()
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none()
&& circle_gpu::extract_circle_instances(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
})
.collect();
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() {
return WireKind::Block;
}
if wire.tangent_geoms.len() == 1
if !wire.tangent_geoms.is_empty()
&& wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty()
&& wire.text_verts.is_empty()
{
match &wire.tangent_geoms[0] {
crate::scene::model::wire_model::TangentGeom::Circle { .. }
| crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. }
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => {
if super::circle_gpu::extract_circle_instance(wire, 0.0).is_some() {
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 super::circle_gpu::extract_circle_instances(wire, 0.0).is_some() {
return WireKind::Circle;
}
if super::ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_some() {
return WireKind::Ellipse;
}
}
if !wire.points.is_empty() {
@@ -209,27 +200,18 @@ pub fn partition_wires<'a>(
continue;
}
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.pick_tris.is_empty()
&& wire.text_verts.is_empty()
{
match &wire.tangent_geoms[0] {
crate::scene::model::wire_model::TangentGeom::Circle { .. }
| crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. }
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => {
if let Some(inst) = super::circle_gpu::extract_circle_instance(wire, depth) {
circle_instances.push(inst);
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 let Some(insts) = super::circle_gpu::extract_circle_instances(wire, depth) {
circle_instances.extend(insts);
continue;
}
if let Some(insts) = super::ellipse_gpu::extract_ellipse_instances(wire, depth) {
ellipse_instances.extend(insts);
continue;
}
}
if !wire.points.is_empty() {