Extend GPU analytical circle and arc rendering to block inserts

- Emit transformed analytical curves (circles, arcs, ellipses) in block
  expansion as standalone wires rather than merging them into batched line
  strips.
- Keep render_instance unset on analytical curves in block prototypes so
  they route directly to CircleGpu / EllipseGpu instead of BlockWireGpu.
- Support TangentGeom::Circle in extract_circle_instance and wire partitioning.
- Drop analytical tangents on wires clipped by xclip so clipped geometry
  falls back correctly to line segments.
- Add unit test verifying block circles and arcs extract as analytical GPU
  instances.
This commit is contained in:
Karim Jerbi committed 2026-09-07 14:43:38 +01:00
1 parent 690971c44e
commit 73cd759ca4
5 files changed
+202 -11

No files matched your search

+146 -9
View File
@@ -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, _)| {
+41
View File
@@ -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()
);
}
}
+3
View File
@@ -197,6 +197,9 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, 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())
+8
View File
@@ -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,
+4 -2
View File
@@ -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);