forked from 1b-agent/opencadstudio-upstream
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:
1 parent
690971c44e
commit
73cd759ca4
5 files changed
+202
-11
No files matched your search
Vendored
+146
-9
@@ -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, _)| {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user