diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 9416faae6..f441228e6 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -1108,7 +1108,7 @@ pub fn expand_insert( ); let mut first = first_batches.finalize(&name, selected, bg_color); for wire in &mut first { - if wire.render_instance.is_none() { + if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() { wire.render_instance = Some( crate::scene::model::instance_model::RenderInstance { source_id: crate::scene::model::instance_model::next_source_id(), @@ -1149,7 +1149,7 @@ pub fn expand_insert( if let Some(guard) = prototype_guard.as_mut() { let translation = transform_translation(&xform); for wire in &mut wires { - if wire.render_instance.is_none() { + if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() { wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance { source_id: crate::scene::model::instance_model::next_source_id(), translation, @@ -1314,6 +1314,20 @@ fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 { w.max(h) / world_per_pixel } +fn is_standalone_analytical_curve(wire: &WireModel) -> bool { + wire.tangent_geoms.len() == 1 + && wire.fill_tris.is_empty() + && wire.pick_tris.is_empty() + && wire.text_verts.is_empty() + && matches!( + wire.tangent_geoms[0], + TangentGeom::Circle { .. } + | TangentGeom::PlanarCircle { .. } + | TangentGeom::Arc { .. } + | TangentGeom::PlanarEllipse { .. } + ) +} + struct ExpandCtx<'a> { cache: &'a BlockCache, ins_color: [f32; 4], @@ -1931,12 +1945,14 @@ fn expand_defn( .collect(); crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly); for wire in &mut wires { - wire.render_instance = Some( - crate::scene::model::instance_model::RenderInstance { - source_id: crate::scene::model::instance_model::next_source_id(), - translation, - }, - ); + if !is_standalone_analytical_curve(wire) { + wire.render_instance = Some( + crate::scene::model::instance_model::RenderInstance { + source_id: crate::scene::model::instance_model::next_source_id(), + translation, + }, + ); + } } out.extra_wires.extend(wires.iter().cloned()); first = Some((wires, translation)); @@ -1984,7 +2000,7 @@ fn expand_defn( ); let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); for wire in &mut wires { - if wire.render_instance.is_none() { + if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() { wire.render_instance = Some( crate::scene::model::instance_model::RenderInstance { source_id: crate::scene::model::instance_model::next_source_id(), @@ -2297,6 +2313,127 @@ fn emit_wire( lw.line_weight_px }; + // Analytical GPU circle / arc / ellipse fast path: + // If this LocalWire is a pure analytical curve (single tangent geometry, no fills/text/pick), + // 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 + && 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 contrast_bg = lw.contrast_bg.unwrap_or(ctx.bg_color); + let color = if lw.canvas_color { + ctx.bg_color + } else if lw.preserve_color { + final_color + } else { + crate::scene::view::render::adapt_to_bg(final_color, contrast_bg) + }; + let bg_adapt: crate::scene::model::wire_model::BgAdapt = + (lw.canvas_color || !lw.preserve_color).then(|| { + Box::new(crate::scene::model::wire_model::BgAdaptInputs { + raw_color: final_color, + text_raw_colors: Vec::new(), + contrast_bg: lw.contrast_bg, + canvas_color: lw.canvas_color, + preserve_color: lw.preserve_color, + }) + }); + + let mut points = Vec::with_capacity(lw.points.len()); + let mut points_low = Vec::with_capacity(lw.points.len()); + let mut min_x = f32::INFINITY; + let mut min_y = f32::INFINITY; + let mut max_x = f32::NEG_INFINITY; + let mut max_y = f32::NEG_INFINITY; + for (idx, p) in lw.points.iter().enumerate() { + let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]); + let point = accum_xform.apply(Vector3::new( + p[0] as f64 + pl[0] as f64, + p[1] as f64 + pl[1] as f64, + p[2] as f64 + pl[2] as f64, + )); + let (hx, lx) = WireModel::split_ds(point.x); + let (hy, ly) = WireModel::split_ds(point.y); + let (hz, lz) = WireModel::split_ds(point.z); + if hx < min_x { min_x = hx; } + if hy < min_y { min_y = hy; } + if hx > max_x { max_x = hx; } + if hy > max_y { max_y = hy; } + points.push([hx, hy, hz]); + points_low.push([lx, ly, lz]); + } + let aabb = if min_x.is_infinite() { + WireModel::UNBOUNDED_AABB + } else { + [min_x, min_y, max_x, max_y] + }; + let mut snap_pts = Vec::with_capacity(lw.snap_pts.len()); + for (p, hint) in &lw.snap_pts { + let v = accum_xform.apply(Vector3::new(p.x, p.y, p.z)); + snap_pts.push((glam::DVec3::new(v.x, v.y, v.z), *hint)); + } + let mut key_vertices = Vec::with_capacity(lw.key_vertices.len()); + for p in &lw.key_vertices { + let v = accum_xform.apply(Vector3::new(p[0], p[1], p[2])); + key_vertices.push([v.x, v.y, v.z]); + } + let local_depth = if d_range != (0.0, 1.0) { + Some(d_range.0) + } else { + None + }; + + let wire = WireModel { + bg_adapt, + point_marker: lw.point_marker, + taper_widths: Vec::new(), + pattern_stations: Vec::new(), + world_width: 0.0, + depth_override: local_depth, + display_visible: !lw.hide_unselected || ctx.selected, + plot_visible: lw.plot_visible, + fill_is_3d: false, + fill_is_2d_solid: false, + render_instance: None, + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), + name: String::new(), + points, + points_low, + color, + selected: ctx.selected, + pattern_length: final_pat_len, + pattern: final_pat, + line_weight_px: final_lw_px, + aci: final_aci, + snap_pts, + tangent_geoms: vec![tangent], + key_vertices, + aabb, + plinegen: lw.plinegen, + fill_tris: Vec::new(), + fill_tris_low: Vec::new(), + }; + out.extra_wires.push(wire); + return; + } + } + } + let station_values = pattern_station_values(&lw.pattern_stations, lw.points.len()); let station_map = decode_pattern_station_map(&lw.pattern_stations, lw.points.len()); let transformed_stations = station_values.and_then(|(values, _)| { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b1aea6480..2d06cabcb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -11284,4 +11284,45 @@ mod layout_cache_tests { | crate::scene::model::wire_model::TangentGeom::Circle { .. } )); } + + #[test] + fn block_circles_and_arcs_extract_as_analytical_gpu_instances() { + let mut s = Scene::new(); + // Create initial entities + let mut circle = acadrust::entities::Circle::default(); + circle.radius = 50.0; + let c_h = s.add_entity(EntityType::Circle(circle)); + + let mut arc = acadrust::entities::Arc::default(); + arc.radius = 25.0; + arc.start_angle = 0.0; + arc.end_angle = 3.14159; + let a_h = s.add_entity(EntityType::Arc(arc)); + + let line = acadrust::entities::Line::from_points( + acadrust::types::Vector3::new(0.0, 0.0, 0.0), + acadrust::types::Vector3::new(100.0, 100.0, 0.0), + ); + let l_h = s.add_entity(EntityType::Line(line)); + + // Create block and place insert at (200, 300, 0) + let xform = acadrust::types::Transform::from_translation(acadrust::types::Vector3::new(200.0, 300.0, 0.0)); + let _ = s.create_block_from_entities( + &[c_h, a_h, l_h], + "TEST_ANALYTICAL_BLOCK", + &acadrust::types::Transform::identity(), + &xform, + ).unwrap(); + + 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); + + // Both the circle and the arc inside the block must be extracted as analytical GPU circle instances! + assert!( + partitioned.circle_instances.len() >= 2, + "Expected at least 2 analytical circle instances from block, got {}", + partitioned.circle_instances.len() + ); + } } diff --git a/src/scene/pick/xclip.rs b/src/scene/pick/xclip.rs index 26f0b2982..0378dc7a7 100644 --- a/src/scene/pick/xclip.rs +++ b/src/scene/pick/xclip.rs @@ -197,6 +197,9 @@ pub fn clip_wires(wires: &mut Vec, poly: &[[f64; 2]]) { } else { w.pattern_stations.clear(); } + if !w.tangent_geoms.is_empty() { + w.tangent_geoms.clear(); + } } if !w.fill_tris.is_empty() { let local: Vec<[f32; 3]> = (0..w.fill_tris.len()) diff --git a/src/scene/pipeline/circle_gpu.rs b/src/scene/pipeline/circle_gpu.rs index 8c32369a0..a84929641 100644 --- a/src/scene/pipeline/circle_gpu.rs +++ b/src/scene/pipeline/circle_gpu.rs @@ -215,6 +215,14 @@ pub fn extract_circle_instance( 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 } => ( + [center[0] as f64, center[1] as f64, center[2] as f64], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + radius as f64, + 0.0f32, + std::f32::consts::TAU, + ), crate::scene::model::wire_model::TangentGeom::PlanarCircle { center, axis_x, diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 2c24e09bd..b7c6c89aa 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -150,7 +150,8 @@ pub fn classify_wire(wire: &WireModel) -> WireKind { && wire.text_verts.is_empty() { match &wire.tangent_geoms[0] { - crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. } + 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; @@ -214,7 +215,8 @@ pub fn partition_wires<'a>( && wire.text_verts.is_empty() { match &wire.tangent_geoms[0] { - crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. } + 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);