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, _)| {