From 75ec08dc5589bf57dcae6b5fdbdb8daf507025fc Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Mon, 7 Sep 2026 14:59:50 +0100 Subject: [PATCH] feat(render): GPU analytical curve extraction for multi-arc polylines and blocks --- src/scene/cache/block_cache.rs | 27 +++++++++------ src/scene/mod.rs | 40 ++++++++++++++++++++++ src/scene/pipeline/circle_gpu.rs | 57 ++++++++++++++++++++++++------- src/scene/pipeline/ellipse_gpu.rs | 52 ++++++++++++++++++++++------ src/scene/pipeline/mod.rs | 52 ++++++++++++++++------------ src/scene/pipeline/wire_arena.rs | 46 ++++++++----------------- 6 files changed, 186 insertions(+), 88 deletions(-) diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index f441228e6..307a9a526 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -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> = 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, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 2d90e9655..71d6e031d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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" + ); + } } diff --git a/src/scene/pipeline/circle_gpu.rs b/src/scene/pipeline/circle_gpu.rs index a84929641..92d7332fb 100644 --- a/src/scene/pipeline/circle_gpu.rs +++ b/src/scene/pipeline/circle_gpu.rs @@ -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 { - // 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> { + 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 { + 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; diff --git a/src/scene/pipeline/ellipse_gpu.rs b/src/scene/pipeline/ellipse_gpu.rs index 1c6b0765c..1291be8f5 100644 --- a/src/scene/pipeline/ellipse_gpu.rs +++ b/src/scene/pipeline/ellipse_gpu.rs @@ -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 { - 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> { + 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 { + 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; diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index abd1e9d5b..9e27c2a00 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -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 = 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 = 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 diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index b7c6c89aa..d4e927da4 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -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() {