forked from 1b-agent/opencadstudio-upstream
Merge branch 'main' into feature/parametric-constraint-system
# Conflicts: # src/app/automation.rs # src/app/mod.rs # src/ui/ribbon/widgets.rs
This commit is contained in:
commit
e2d39b049d
114 files changed
+13641
-1685
No files matched your search
+84
-10
@@ -84,6 +84,38 @@ fn entity_curves_on_plane(
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_boundary_source_on_plane(
|
||||
entity: &EntityType,
|
||||
plane: WorkingPlane,
|
||||
tolerance: f64,
|
||||
) -> Option<BoundarySource> {
|
||||
let curves = entity_curves_on_plane(entity, plane, tolerance);
|
||||
|
||||
if curves.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let segments: Vec<Line> = curves
|
||||
.iter()
|
||||
.flat_map(|curve| {
|
||||
curve
|
||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||
.windows(2)
|
||||
.filter_map(|pair| {
|
||||
let start = pair[0];
|
||||
let end = pair[1];
|
||||
|
||||
(start.iter().chain(&end).all(|value| value.is_finite())
|
||||
&& start != end)
|
||||
.then_some(Line { start, end })
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
|
||||
}
|
||||
|
||||
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> {
|
||||
let mut ring = Vec::new();
|
||||
for edge in &path.edges {
|
||||
@@ -731,10 +763,22 @@ impl Scene {
|
||||
.boundary_handles
|
||||
.iter()
|
||||
.filter_map(|source| {
|
||||
all_sources
|
||||
.get(source)
|
||||
.cloned()
|
||||
.map(|geometry| (*source, geometry))
|
||||
// Associative boundaries are explicit document references.
|
||||
// During a grip drag their display wire can be preview-hidden,
|
||||
// so prefer the live entity geometry over the resident wire cache.
|
||||
let geometry = self
|
||||
.document
|
||||
.get_entity(*source)
|
||||
.and_then(|entity| {
|
||||
entity_boundary_source_on_plane(
|
||||
entity,
|
||||
plane,
|
||||
WELD_TOLERANCE,
|
||||
)
|
||||
})
|
||||
.or_else(|| all_sources.get(source).cloned());
|
||||
|
||||
geometry.map(|geometry| (*source, geometry))
|
||||
})
|
||||
.collect();
|
||||
let segments: Vec<_> = sources
|
||||
@@ -817,17 +861,47 @@ impl Scene {
|
||||
.extend(segments);
|
||||
}
|
||||
for (&handle, source) in &mut sources {
|
||||
source.curves = self
|
||||
let curves = self
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|entity| entity_curves_on_plane(entity, plane, tolerance))
|
||||
.unwrap_or_default();
|
||||
source.curves.retain(|curve| {
|
||||
source.segments.iter().any(|segment| {
|
||||
distance_to(curve, segment.start).max(distance_to(curve, segment.end))
|
||||
<= tolerance * 4.0
|
||||
|
||||
// Associative hatch regeneration must use the entity's live geometry,
|
||||
// not a potentially one-frame-old display wire. Grip edits and STRETCH
|
||||
// mutate the document before the resident wire cache catches up.
|
||||
let live_segments: Vec<Line> = curves
|
||||
.iter()
|
||||
.flat_map(|curve| {
|
||||
curve
|
||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||
.windows(2)
|
||||
.filter_map(|pair| {
|
||||
let start = pair[0];
|
||||
let end = pair[1];
|
||||
|
||||
(start.iter().chain(&end).all(|value| value.is_finite())
|
||||
&& start != end)
|
||||
.then_some(Line { start, end })
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
});
|
||||
.collect();
|
||||
|
||||
if !live_segments.is_empty() {
|
||||
source.segments = live_segments;
|
||||
source.curves = curves;
|
||||
} else {
|
||||
// Fallback for entity types that do not expose usable native curves:
|
||||
// retain the existing display-wire geometry.
|
||||
source.curves = curves;
|
||||
source.curves.retain(|curve| {
|
||||
source.segments.iter().any(|segment| {
|
||||
distance_to(curve, segment.start).max(distance_to(curve, segment.end))
|
||||
<= tolerance * 4.0
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
Vendored
+153
-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,134 @@ 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.is_empty()
|
||||
&& lw.fill_tris.is_empty()
|
||||
&& lw.pick_tris.is_empty()
|
||||
&& lw.text_verts.is_empty()
|
||||
{
|
||||
let transformed_tangents: Option<Vec<TangentGeom>> = 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
|
||||
} 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: tangents,
|
||||
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, _)| {
|
||||
|
||||
@@ -21,3 +21,54 @@ pub fn solid_to_sat(body: &Body) -> Option<SatDocument> {
|
||||
let binary = SabWriter::write(&document);
|
||||
valid(&SabReader::read(&binary).ok()?).then_some(document)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cadkernel::geom2d::{Arc, Curve};
|
||||
use cadkernel::space::Plane;
|
||||
|
||||
fn circle_section(z: f64, radius: f64) -> (Plane, Vec<Curve>) {
|
||||
let curves = (0..4)
|
||||
.map(|part| {
|
||||
let start = std::f64::consts::FRAC_PI_2 * part as f64;
|
||||
Curve::Arc(Arc {
|
||||
centre: [0.0, 0.0],
|
||||
radius,
|
||||
start_angle: start,
|
||||
end_angle: start + std::f64::consts::FRAC_PI_2,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
Plane::from_axes([0.0, 0.0, z], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
curves,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_curved_loft_round_trips_through_text_and_binary_acis() {
|
||||
let body = cadkernel::brep::loft(&[
|
||||
circle_section(0.0, 5.0),
|
||||
circle_section(10.0, 2.0),
|
||||
])
|
||||
.unwrap();
|
||||
assert!(body.validate().is_empty());
|
||||
|
||||
let mut document = SatDocument::new();
|
||||
cadkernel::acis::append(&body, &mut document).unwrap();
|
||||
let text = document.to_sat_string();
|
||||
let parsed = SatDocument::parse(&text).unwrap();
|
||||
let (restored, text_loss) = cadkernel::acis::lift(&parsed);
|
||||
assert!(text_loss.is_empty(), "{text_loss:?}");
|
||||
assert_eq!(restored.len(), 1);
|
||||
assert!(restored[0].validate().is_empty());
|
||||
|
||||
let binary = SabWriter::write(&parsed);
|
||||
let parsed_binary = SabReader::read(&binary).unwrap();
|
||||
let (restored, binary_loss) = cadkernel::acis::lift(&parsed_binary);
|
||||
assert!(binary_loss.is_empty(), "{binary_loss:?}");
|
||||
assert_eq!(restored.len(), 1);
|
||||
assert!(restored[0].validate().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,11 @@ pub fn tessellate_sat(
|
||||
color,
|
||||
None,
|
||||
));
|
||||
if let [single] = bodies.as_slice() {
|
||||
if let Some(properties) = brep::analytic_mass_properties(&single.0) {
|
||||
set.apply_mass_properties(properties);
|
||||
}
|
||||
}
|
||||
set.curved_gens = curved_gens;
|
||||
for point in edges {
|
||||
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
|
||||
|
||||
@@ -159,6 +159,10 @@ pub fn kernel_body(solid: &Solid3D) -> Option<KernelBody> {
|
||||
kernel_acis_body(&solid.acis_data)
|
||||
}
|
||||
|
||||
pub fn kernel_region_body(region: &Region) -> Option<KernelBody> {
|
||||
kernel_acis_body(®ion.acis_data)
|
||||
}
|
||||
|
||||
pub fn kernel_surface_body(surface: &Surface) -> Option<KernelBody> {
|
||||
kernel_acis_body(&surface.acis_data)
|
||||
}
|
||||
|
||||
+281
-36
@@ -19,7 +19,7 @@ use acadrust::{CadDocument, EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::scene::convert::acad_to_render::{convert, RenderObject};
|
||||
use crate::scene::model::wire_model::{SnapHint, WireModel};
|
||||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
|
||||
/// Split an f64 offset-relative coordinate into the double-single (high, low)
|
||||
/// f32 pair the renderer consumes. `high + low ≈ value` to ~f64 precision; the
|
||||
@@ -202,6 +202,176 @@ pub(crate) fn points_to_ds(
|
||||
(high, low)
|
||||
}
|
||||
|
||||
fn split_mixed_polyline(
|
||||
tangent_geoms: &[TangentGeom],
|
||||
key_vertices: &[[f64; 3]],
|
||||
name: &str,
|
||||
color: [f32; 4],
|
||||
selected: bool,
|
||||
pattern_length: f32,
|
||||
pattern: [f32; 8],
|
||||
line_weight_px: f32,
|
||||
snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
||||
point_marker: Option<crate::scene::model::wire_model::PointMarker>,
|
||||
plinegen: bool,
|
||||
) -> Vec<WireModel> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Emit each arc segment as an analytical wire (CircleGpu target)
|
||||
for tg in tangent_geoms {
|
||||
if let TangentGeom::Arc {
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
} = *tg
|
||||
{
|
||||
let mut arc_pts = Vec::with_capacity(17);
|
||||
let n = 16;
|
||||
let sweep = if end_angle >= start_angle {
|
||||
end_angle - start_angle
|
||||
} else {
|
||||
end_angle - start_angle + std::f64::consts::TAU
|
||||
};
|
||||
for s in 0..=n {
|
||||
let frac = s as f64 / n as f64;
|
||||
let ang = start_angle + frac * sweep;
|
||||
let p = [
|
||||
center[0] + radius * (ang.cos() * axis_x[0] + ang.sin() * axis_y[0]),
|
||||
center[1] + radius * (ang.cos() * axis_x[1] + ang.sin() * axis_y[1]),
|
||||
center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]),
|
||||
];
|
||||
arc_pts.push(p);
|
||||
}
|
||||
let (points, points_low) = points_to_ds(arc_pts);
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
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: name.to_string(),
|
||||
points,
|
||||
points_low,
|
||||
color,
|
||||
selected,
|
||||
pattern_length,
|
||||
pattern,
|
||||
line_weight_px,
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: vec![tg.clone()],
|
||||
aci: 0,
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collect straight lines into a line wire
|
||||
let mut straight_pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut straight_tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut last_end: Option<[f64; 3]> = None;
|
||||
|
||||
for (i, tg) in tangent_geoms.iter().enumerate() {
|
||||
if let TangentGeom::Line { p1, p2 } = tg {
|
||||
straight_tangents.push(tg.clone());
|
||||
let (p_start, p_end) = if i + 1 < key_vertices.len() {
|
||||
(key_vertices[i], key_vertices[i + 1])
|
||||
} else {
|
||||
(
|
||||
[p1[0] as f64, p1[1] as f64, p1[2] as f64],
|
||||
[p2[0] as f64, p2[1] as f64, p2[2] as f64],
|
||||
)
|
||||
};
|
||||
if !plinegen {
|
||||
if !straight_pts.is_empty() {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
}
|
||||
straight_pts.push(p_start);
|
||||
straight_pts.push(p_end);
|
||||
} else {
|
||||
if let Some(prev) = last_end {
|
||||
if (prev[0] - p_start[0]).abs() < 1e-7
|
||||
&& (prev[1] - p_start[1]).abs() < 1e-7
|
||||
&& (prev[2] - p_start[2]).abs() < 1e-7
|
||||
{
|
||||
straight_pts.push(p_end);
|
||||
} else {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_pts.push(p_start);
|
||||
straight_pts.push(p_end);
|
||||
}
|
||||
} else {
|
||||
straight_pts.push(p_start);
|
||||
straight_pts.push(p_end);
|
||||
}
|
||||
last_end = Some(p_end);
|
||||
}
|
||||
} else {
|
||||
last_end = None;
|
||||
}
|
||||
}
|
||||
|
||||
let (line_pts, line_pts_low) = points_to_ds(straight_pts);
|
||||
if !line_pts.is_empty() {
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker,
|
||||
taper_widths: Vec::new(),
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
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: name.to_string(),
|
||||
points: line_pts,
|
||||
points_low: line_pts_low,
|
||||
color,
|
||||
selected,
|
||||
pattern_length,
|
||||
pattern,
|
||||
line_weight_px,
|
||||
snap_pts,
|
||||
tangent_geoms: straight_tangents,
|
||||
aci: 0,
|
||||
key_vertices: key_vertices.to_vec(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
} else if let Some(first_arc) = out.first_mut() {
|
||||
first_arc.snap_pts = snap_pts;
|
||||
first_arc.key_vertices = key_vertices.to_vec();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn point_cloud_wires(
|
||||
document: &CadDocument,
|
||||
handle: Handle,
|
||||
@@ -707,9 +877,11 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
// ── Try the kernel path first ───────────────────────────────────────────
|
||||
// Zoom-adaptive circles scale their segment count from the current zoom level and radius.
|
||||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
||||
let te = crate::entities::circle::relative_render(entity, document, world_per_pixel)
|
||||
.or_else(|| crate::entities::point::relative_render(entity, document, world_per_pixel))
|
||||
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
||||
.or_else(|| match entity {
|
||||
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
|
||||
@@ -1567,40 +1739,71 @@ pub fn tessellate(
|
||||
};
|
||||
let point_marker =
|
||||
crate::entities::point::relative_marker_spec(entity, document);
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker,
|
||||
taper_widths: Vec::new(),
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: polyline_band_width(entity, document.header.fill_mode),
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris,
|
||||
pick_tris_low,
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: local_pts,
|
||||
points_low: local_pts_low,
|
||||
color: edge_color,
|
||||
selected,
|
||||
pattern_length: edge_pattern_length,
|
||||
pattern: edge_pattern,
|
||||
line_weight_px,
|
||||
snap_pts: snap,
|
||||
tangent_geoms: tangents,
|
||||
aci: 0,
|
||||
key_vertices: keys,
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
|
||||
let has_arc = te
|
||||
.tangent_geoms
|
||||
.iter()
|
||||
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
||||
let has_line = te
|
||||
.tangent_geoms
|
||||
.iter()
|
||||
.any(|tg| matches!(tg, TangentGeom::Line { .. }));
|
||||
let is_thin_polyline = !is_thick_extrusion
|
||||
&& polyline_band_width(entity, document.header.fill_mode) <= 1e-9
|
||||
&& fill_tris.is_empty()
|
||||
&& pick_tris.is_empty()
|
||||
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
||||
|
||||
if has_arc && has_line && is_thin_polyline {
|
||||
out.extend(split_mixed_polyline(
|
||||
&te.tangent_geoms,
|
||||
&keys,
|
||||
&name,
|
||||
edge_color,
|
||||
selected,
|
||||
edge_pattern_length,
|
||||
edge_pattern,
|
||||
line_weight_px,
|
||||
snap,
|
||||
point_marker,
|
||||
true,
|
||||
));
|
||||
} else {
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker,
|
||||
taper_widths: Vec::new(),
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: polyline_band_width(entity, document.header.fill_mode),
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris,
|
||||
pick_tris_low,
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: local_pts,
|
||||
points_low: local_pts_low,
|
||||
color: edge_color,
|
||||
selected,
|
||||
pattern_length: edge_pattern_length,
|
||||
pattern: edge_pattern,
|
||||
line_weight_px,
|
||||
snap_pts: snap,
|
||||
tangent_geoms: tangents,
|
||||
aci: 0,
|
||||
key_vertices: keys,
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !fill_tris.is_empty() {
|
||||
@@ -1756,6 +1959,48 @@ pub fn tessellate(
|
||||
// A wide polyline with PLINEGEN=0 arrives here: same shader-band
|
||||
// treatment as the Contour arm, restarting the dash per segment.
|
||||
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
||||
let is_thick_extrusion = matches!(
|
||||
entity,
|
||||
EntityType::LwPolyline(p) if p.thickness.abs() > 1e-10
|
||||
) || matches!(
|
||||
entity,
|
||||
EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10
|
||||
);
|
||||
let is_thin_polyline = !is_thick_extrusion
|
||||
&& polyline_band_width(entity, document.header.fill_mode) <= 1e-9
|
||||
&& pick_tris.is_empty()
|
||||
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
|
||||
let has_arc = te
|
||||
.tangent_geoms
|
||||
.iter()
|
||||
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
|
||||
let has_line = te
|
||||
.tangent_geoms
|
||||
.iter()
|
||||
.any(|tg| matches!(tg, TangentGeom::Line { .. }));
|
||||
|
||||
if has_arc && has_line && is_thin_polyline {
|
||||
let edge_color = if is_thick_extrusion {
|
||||
[0.0, 0.0, 0.0, 1.0]
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let point_marker =
|
||||
crate::entities::point::relative_marker_spec(entity, document);
|
||||
return split_mixed_polyline(
|
||||
&te.tangent_geoms,
|
||||
&key_vertices,
|
||||
&name,
|
||||
edge_color,
|
||||
selected,
|
||||
pattern_length,
|
||||
pattern,
|
||||
line_weight_px,
|
||||
snap_pts,
|
||||
point_marker,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return vec![WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker: None,
|
||||
|
||||
+5
-2
@@ -596,12 +596,15 @@ impl Scene {
|
||||
.filter_map(|&handle| {
|
||||
let from_history = self
|
||||
.document
|
||||
.solid_history_operation(handle)
|
||||
.and_then(|operation| cadkernel::acis::rebuild_body(operation).ok());
|
||||
.solid_history_operations(handle)
|
||||
.and_then(|operations| cadkernel::acis::rebuild_history(&operations).ok());
|
||||
let body = from_history.or_else(|| match self.document.get_entity(handle) {
|
||||
Some(EntityType::Solid3D(solid)) => {
|
||||
crate::scene::convert::solid3d_tess::kernel_body(solid)
|
||||
}
|
||||
Some(EntityType::Region(region)) => {
|
||||
crate::scene::convert::solid3d_tess::kernel_region_body(region)
|
||||
}
|
||||
Some(EntityType::Surface(surface)) => {
|
||||
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||
}
|
||||
|
||||
+888
-101
File diff suppressed because it is too large.
Load diff
@@ -55,6 +55,44 @@ pub struct MeshMetrics {
|
||||
pub surface_area: f64,
|
||||
pub volume: f64,
|
||||
pub centroid: [f64; 3],
|
||||
pub moment_of_inertia: [f64; 3],
|
||||
pub principal_directions: [f64; 9],
|
||||
pub principal_moments: [f64; 3],
|
||||
pub product_of_inertia: [f64; 3],
|
||||
pub radii_of_gyration: [f64; 3],
|
||||
}
|
||||
|
||||
impl MeshMetrics {
|
||||
fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
|
||||
self.volume = properties.volume;
|
||||
self.centroid = properties.centroid;
|
||||
self.moment_of_inertia = properties.moment_of_inertia;
|
||||
self.principal_directions = properties.principal_directions;
|
||||
self.principal_moments = properties.principal_moments;
|
||||
self.product_of_inertia = properties.product_of_inertia;
|
||||
self.radii_of_gyration = properties.radii_of_gyration;
|
||||
}
|
||||
|
||||
pub fn translate(&mut self, delta: [f64; 3]) {
|
||||
if self.volume > 1e-18 {
|
||||
self.apply_mass_properties(
|
||||
cadkernel::brep::MassProperties {
|
||||
volume: self.volume,
|
||||
centroid: self.centroid,
|
||||
moment_of_inertia: self.moment_of_inertia,
|
||||
principal_directions: self.principal_directions,
|
||||
principal_moments: self.principal_moments,
|
||||
product_of_inertia: self.product_of_inertia,
|
||||
radii_of_gyration: self.radii_of_gyration,
|
||||
}
|
||||
.translated(delta),
|
||||
);
|
||||
} else {
|
||||
for axis in 0..3 {
|
||||
self.centroid[axis] += delta[axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -146,74 +184,52 @@ fn compute_mesh_metrics(lods: &[MeshModel]) -> MeshMetrics {
|
||||
let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
|
||||
return MeshMetrics::default();
|
||||
};
|
||||
let point = |index: u32| {
|
||||
let index = index as usize;
|
||||
let high = mesh.verts.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
[
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
high[2] as f64 + low[2] as f64,
|
||||
]
|
||||
let kernel_mesh = cadkernel::brep::Mesh {
|
||||
positions: mesh
|
||||
.verts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, high)| {
|
||||
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
[
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
high[2] as f64 + low[2] as f64,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
normals: Vec::new(),
|
||||
triangles: mesh
|
||||
.indices
|
||||
.chunks_exact(3)
|
||||
.map(|triangle| {
|
||||
[
|
||||
triangle[0] as usize,
|
||||
triangle[1] as usize,
|
||||
triangle[2] as usize,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let mut area = 0.0;
|
||||
let mut area_centroid_numerator = [0.0; 3];
|
||||
let mut signed_volume = 0.0;
|
||||
let mut centroid_numerator = [0.0; 3];
|
||||
for triangle in mesh.indices.chunks_exact(3) {
|
||||
let a = point(triangle[0]);
|
||||
let b = point(triangle[1]);
|
||||
let c = point(triangle[2]);
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let cross = [
|
||||
ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0],
|
||||
];
|
||||
let triangle_area = 0.5
|
||||
* (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2])
|
||||
.sqrt();
|
||||
area += triangle_area;
|
||||
for axis in 0..3 {
|
||||
area_centroid_numerator[axis] +=
|
||||
triangle_area * (a[axis] + b[axis] + c[axis]) / 3.0;
|
||||
}
|
||||
let tetra = (
|
||||
a[0] * (b[1] * c[2] - b[2] * c[1])
|
||||
- a[1] * (b[0] * c[2] - b[2] * c[0])
|
||||
+ a[2] * (b[0] * c[1] - b[1] * c[0])
|
||||
) / 6.0;
|
||||
signed_volume += tetra;
|
||||
for axis in 0..3 {
|
||||
centroid_numerator[axis] += tetra * (a[axis] + b[axis] + c[axis]) / 4.0;
|
||||
}
|
||||
}
|
||||
let centroid = if signed_volume.abs() > 1e-18 {
|
||||
[
|
||||
centroid_numerator[0] / signed_volume,
|
||||
centroid_numerator[1] / signed_volume,
|
||||
centroid_numerator[2] / signed_volume,
|
||||
]
|
||||
} else if area > 1e-18 {
|
||||
[
|
||||
area_centroid_numerator[0] / area,
|
||||
area_centroid_numerator[1] / area,
|
||||
area_centroid_numerator[2] / area,
|
||||
]
|
||||
} else {
|
||||
[0.0; 3]
|
||||
};
|
||||
MeshMetrics {
|
||||
let surface = kernel_mesh.surface_properties();
|
||||
let mut metrics = MeshMetrics {
|
||||
vertices: mesh.verts.len(),
|
||||
triangles: mesh.indices.len() / 3,
|
||||
surface_area: area,
|
||||
volume: signed_volume.abs(),
|
||||
centroid,
|
||||
surface_area: surface.map_or(0.0, |value| value.0),
|
||||
centroid: surface.map_or([0.0; 3], |value| value.1),
|
||||
..MeshMetrics::default()
|
||||
};
|
||||
if let Some(properties) = kernel_mesh.inertial_properties() {
|
||||
metrics.apply_mass_properties(properties);
|
||||
}
|
||||
metrics
|
||||
}
|
||||
|
||||
impl MeshLodSet {
|
||||
pub fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
|
||||
self.metrics.apply_mass_properties(properties);
|
||||
}
|
||||
|
||||
/// Build a set from its LODs, computing the 3D AABB.
|
||||
pub fn from_lods(lods: Vec<MeshModel>) -> Self {
|
||||
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
||||
use acadrust::objects::{
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
|
||||
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer,
|
||||
SolidHistoryCone, SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft,
|
||||
SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
|
||||
SolidHistoryTorus,
|
||||
};
|
||||
use acadrust::EntityType;
|
||||
use cadkernel::brep::Body;
|
||||
use cadkernel::brep::{Body, Surface};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
@@ -32,6 +33,9 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000;
|
||||
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
|
||||
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
|
||||
pub const GRIP_REVOLVE_AXIS: usize = 10_013;
|
||||
pub const GRIP_FILLET_RADIUS: usize = 10_014;
|
||||
pub const GRIP_CHAMFER_DISTANCE1: usize = 10_015;
|
||||
pub const GRIP_CHAMFER_DISTANCE2: usize = 10_016;
|
||||
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
|
||||
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||
@@ -197,7 +201,7 @@ pub fn has_specialized_primitive_properties(
|
||||
handle: acadrust::Handle,
|
||||
) -> bool {
|
||||
matches!(
|
||||
document.solid_history_operation(handle),
|
||||
primitive_property_operation(document, handle).as_ref(),
|
||||
Some(
|
||||
SolidHistoryOperation::Box(_)
|
||||
| SolidHistoryOperation::Wedge(_)
|
||||
@@ -214,6 +218,32 @@ pub fn has_specialized_primitive_properties(
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the primitive that owns the public Geometry rows. Later edge
|
||||
/// operations refine that primitive without replacing its editable type,
|
||||
/// position, or dimensions.
|
||||
pub fn primitive_property_operation(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> Option<SolidHistoryOperation> {
|
||||
document
|
||||
.solid_history_operations(handle)
|
||||
.and_then(|operations| operations.into_iter().next())
|
||||
.or_else(|| document.solid_history_operation(handle).cloned())
|
||||
}
|
||||
|
||||
/// Solid history results whose public Properties palette is fully described by
|
||||
/// the common entity rows plus [`primitive_properties`]. A generic B-rep has
|
||||
/// no stable primitive dimensions to expose, but it still uses the compact
|
||||
/// Solid History palette rather than the internal ACIS/cache diagnostics.
|
||||
pub fn has_compact_solid_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> bool {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| matches!(entity, acadrust::EntityType::Solid3D(_)))
|
||||
}
|
||||
|
||||
pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3> {
|
||||
match operation {
|
||||
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => world_point(
|
||||
@@ -975,6 +1005,77 @@ fn cone_properties(
|
||||
]
|
||||
}
|
||||
|
||||
fn brep_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> Vec<PropSection> {
|
||||
let (record_history, object_show_history, show_history_mode) =
|
||||
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||
let (show_history, show_history_editable) =
|
||||
displayed_history_state(object_show_history, show_history_mode);
|
||||
let mut geometry = vec![Property {
|
||||
label: t!("Solid type").into_owned(),
|
||||
field: "solid_history_type",
|
||||
value: PropValue::ReadOnly(t!("3D Solid").into_owned()),
|
||||
}];
|
||||
if let Some(position) = document
|
||||
.solid_history_operation(handle)
|
||||
.and_then(|operation| match operation {
|
||||
SolidHistoryOperation::Brep(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
geometry.extend([
|
||||
crate::entities::common::edit_prop(
|
||||
t!("Position X").as_ref(),
|
||||
PROP_POSITION_X,
|
||||
position.x,
|
||||
),
|
||||
crate::entities::common::edit_prop(
|
||||
t!("Position Y").as_ref(),
|
||||
PROP_POSITION_Y,
|
||||
position.y,
|
||||
),
|
||||
crate::entities::common::edit_prop(
|
||||
t!("Position Z").as_ref(),
|
||||
PROP_POSITION_Z,
|
||||
position.z,
|
||||
),
|
||||
]);
|
||||
}
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: geometry,
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Solid History").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("History").into_owned(),
|
||||
field: PROP_HISTORY,
|
||||
value: PropValue::Choice {
|
||||
selected: if record_history { "Record" } else { "None" }.to_string(),
|
||||
options: vec!["None".to_string(), "Record".to_string()],
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Show History").into_owned(),
|
||||
field: PROP_SHOW_HISTORY,
|
||||
value: if show_history_editable {
|
||||
PropValue::Choice {
|
||||
selected: if show_history { "Yes" } else { "No" }.to_string(),
|
||||
options: vec!["No".to_string(), "Yes".to_string()],
|
||||
}
|
||||
} else {
|
||||
PropValue::ReadOnly(if show_history { "Yes" } else { "No" }.to_string())
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn cylinder_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -1390,10 +1491,10 @@ pub fn primitive_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> Vec<PropSection> {
|
||||
let Some(operation) = document.solid_history_operation(handle) else {
|
||||
return Vec::new();
|
||||
let Some(operation) = primitive_property_operation(document, handle) else {
|
||||
return brep_properties(document, handle);
|
||||
};
|
||||
match operation {
|
||||
match &operation {
|
||||
SolidHistoryOperation::Box(value) => {
|
||||
rectangular_properties(document, handle, value, "Box")
|
||||
}
|
||||
@@ -1411,7 +1512,8 @@ pub fn primitive_properties(
|
||||
extrusion_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value),
|
||||
_ => Vec::new(),
|
||||
SolidHistoryOperation::Brep(_) => brep_properties(document, handle),
|
||||
_ => brep_properties(document, handle),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2137,11 +2239,50 @@ fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_brep_position_property(
|
||||
value: &mut SolidHistoryBrep,
|
||||
field: &str,
|
||||
text: &str,
|
||||
) -> Option<bool> {
|
||||
let axis = match field {
|
||||
PROP_POSITION_X => 0,
|
||||
PROP_POSITION_Y => 1,
|
||||
PROP_POSITION_Z => 2,
|
||||
_ => return None,
|
||||
};
|
||||
let Some(target) = crate::entities::common::parse_length(text) else {
|
||||
return Some(false);
|
||||
};
|
||||
if !target.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
let Some(current) = matrix(value.base.transform) else {
|
||||
return Some(false);
|
||||
};
|
||||
let position = current.transform_point3(glam::DVec3::ZERO);
|
||||
let mut next = position;
|
||||
next[axis] = target;
|
||||
if next == position {
|
||||
return Some(true);
|
||||
}
|
||||
let updated = glam::DMat4::from_translation(next - position) * current;
|
||||
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
value.base.transform = updated.to_cols_array();
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn apply_primitive_property(
|
||||
operation: &mut SolidHistoryOperation,
|
||||
field: &str,
|
||||
value: &str,
|
||||
) -> bool {
|
||||
if let SolidHistoryOperation::Brep(brep_value) = operation {
|
||||
if let Some(applied) = apply_brep_position_property(brep_value, field, value) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Box(rectangular_value)
|
||||
| SolidHistoryOperation::Wedge(rectangular_value) = operation
|
||||
{
|
||||
@@ -3037,6 +3178,149 @@ fn grip(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fillet_radius_grip(body: &Body, radius: f64) -> Option<GripDef> {
|
||||
if !radius.is_finite() || radius <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
for (face_key, face) in body.faces.iter() {
|
||||
let Some(Surface::Cylinder(cylinder)) = body.surfaces.get(face.surface) else {
|
||||
continue;
|
||||
};
|
||||
if (cylinder.radius.abs() - radius).abs() > radius.max(1.0) * 1.0e-7 {
|
||||
continue;
|
||||
}
|
||||
let axis = glam::DVec3::from_array(cylinder.base.normal()?).try_normalize()?;
|
||||
let origin = glam::DVec3::from_array(cylinder.base.origin);
|
||||
let points = body
|
||||
.face_coedges(face_key)
|
||||
.into_iter()
|
||||
.filter_map(|coedge| body.coedges.get(coedge))
|
||||
.filter_map(|coedge| body.edges.get(coedge.edge))
|
||||
.flat_map(|edge| [edge.start, edge.end])
|
||||
.filter_map(|vertex| body.vertices.get(vertex))
|
||||
.map(|vertex| glam::DVec3::from_array(vertex.point))
|
||||
.collect::<Vec<_>>();
|
||||
let minimum = points
|
||||
.iter()
|
||||
.map(|point| (*point - origin).dot(axis))
|
||||
.reduce(f64::min)?;
|
||||
let maximum = points
|
||||
.iter()
|
||||
.map(|point| (*point - origin).dot(axis))
|
||||
.reduce(f64::max)?;
|
||||
let center = origin + axis * ((minimum + maximum) * 0.5);
|
||||
let radial = points
|
||||
.iter()
|
||||
.map(|point| *point - center - axis * (*point - center).dot(axis))
|
||||
.find_map(|radial| radial.try_normalize())?;
|
||||
return Some(grip(
|
||||
GRIP_FILLET_RADIUS,
|
||||
center + radial * radius,
|
||||
GripShape::Square,
|
||||
Some(radial),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn chamfer_distance_grips(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
value: &SolidHistoryChamfer,
|
||||
) -> Vec<GripDef> {
|
||||
let Some(operations) = document.solid_history_operations(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(chamfer_index) = operations
|
||||
.iter()
|
||||
.rposition(|operation| matches!(operation, SolidHistoryOperation::Chamfer(_)))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(source) = cadkernel::acis::rebuild_history(&operations[..chamfer_index]) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let edges = source.edge_keys().collect::<Vec<_>>();
|
||||
let faces = source.face_keys().collect::<Vec<_>>();
|
||||
let Some(edge) = value
|
||||
.edges
|
||||
.first()
|
||||
.and_then(|ordinal| usize::try_from(*ordinal).ok())
|
||||
.and_then(|ordinal| edges.get(ordinal).copied())
|
||||
.and_then(|edge| source.edges.get(edge))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(base_face) = usize::try_from(value.base_face)
|
||||
.ok()
|
||||
.and_then(|ordinal| faces.get(ordinal).copied())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let adjacent = edge
|
||||
.coedges
|
||||
.iter()
|
||||
.filter_map(|coedge| source.coedges.get(*coedge))
|
||||
.filter_map(|coedge| source.loops.get(coedge.owner))
|
||||
.map(|edge_loop| edge_loop.owner)
|
||||
.collect::<Vec<_>>();
|
||||
let Some(other_face) = adjacent.iter().copied().find(|face| *face != base_face) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let outward = |face_key| {
|
||||
let face = source.faces.get(face_key)?;
|
||||
let Surface::Plane(plane) = source.surfaces.get(face.surface)? else {
|
||||
return None;
|
||||
};
|
||||
let normal = glam::DVec3::from_array(plane.normal()?).try_normalize()?;
|
||||
Some(if face.forward { normal } else { -normal })
|
||||
};
|
||||
let Some(base_normal) = outward(base_face) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(other_normal) = outward(other_face) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let dot = base_normal.dot(other_normal);
|
||||
let Some(base_inward) = (-(other_normal - base_normal * dot)).try_normalize() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(other_inward) = (-(base_normal - other_normal * dot)).try_normalize() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(start) = source.vertices.get(edge.start) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(end) = source.vertices.get(edge.end) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let midpoint =
|
||||
(glam::DVec3::from_array(start.point) + glam::DVec3::from_array(end.point)) * 0.5;
|
||||
[
|
||||
(
|
||||
GRIP_CHAMFER_DISTANCE1,
|
||||
value.base_distance,
|
||||
base_inward,
|
||||
),
|
||||
(
|
||||
GRIP_CHAMFER_DISTANCE2,
|
||||
value.other_distance,
|
||||
other_inward,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, distance, _)| distance.is_finite() && *distance > 0.0)
|
||||
.map(|(id, distance, axis)| {
|
||||
grip(
|
||||
id,
|
||||
midpoint + axis * distance,
|
||||
GripShape::Square,
|
||||
Some(axis),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn primitive_grips(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -3044,6 +3328,21 @@ pub fn primitive_grips(
|
||||
let Some(operation) = document.solid_history_operation(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if let SolidHistoryOperation::Fillet(value) = operation {
|
||||
let Some(radius) = value.radii.first().copied() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(EntityType::Solid3D(solid)) = document.get_entity(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
return crate::scene::convert::solid3d_tess::kernel_body(solid)
|
||||
.and_then(|body| fillet_radius_grip(&body, radius))
|
||||
.into_iter()
|
||||
.collect();
|
||||
}
|
||||
if let SolidHistoryOperation::Chamfer(value) = operation {
|
||||
return chamfer_distance_grips(document, handle, value);
|
||||
}
|
||||
let mut grips = Vec::new();
|
||||
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
||||
if let Some(world) = world_point(transform, point) {
|
||||
@@ -3664,3 +3963,32 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
||||
..SolidHistoryBrep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fillet_op(edges: Vec<i32>, radius: f64) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Fillet(SolidHistoryFillet {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
method: 0,
|
||||
edges,
|
||||
radii: vec![radius],
|
||||
..SolidHistoryFillet::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chamfer_op(
|
||||
edges: Vec<i32>,
|
||||
base_face: i32,
|
||||
base_distance: f64,
|
||||
other_distance: f64,
|
||||
) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Chamfer(SolidHistoryChamfer {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
method: 0,
|
||||
base_distance,
|
||||
other_distance,
|
||||
edges,
|
||||
base_face,
|
||||
..SolidHistoryChamfer::default()
|
||||
})
|
||||
}
|
||||
@@ -365,13 +365,18 @@ pub enum Bool {
|
||||
/// missing, and passing that on unchanged is the point: a half-done boolean
|
||||
/// looks finished.
|
||||
pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> {
|
||||
boolean_result(op, a, b).ok()
|
||||
}
|
||||
|
||||
/// Combine two solids while retaining the kernel's exact refusal reason.
|
||||
pub fn boolean_result(op: Bool, a: &Body, b: &Body) -> Result<Body, brep::Snag> {
|
||||
let how = match op {
|
||||
Bool::Union => brep::Operation::Union,
|
||||
Bool::Subtract => brep::Operation::Difference,
|
||||
Bool::Intersect => brep::Operation::Intersection,
|
||||
};
|
||||
let tolerance = brep::operation_tolerance(&[a, b]);
|
||||
brep::combine(a.clone(), b.clone(), how, tolerance).ok()
|
||||
brep::combine(a.clone(), b.clone(), how, tolerance)
|
||||
}
|
||||
|
||||
// ── Tessellation ────────────────────────────────────────────────────────────
|
||||
@@ -470,7 +475,11 @@ pub fn display_from_solid(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some((mesh_from_tessellation(tessellation, color)?, wires, center))
|
||||
let mut mesh = mesh_from_tessellation(tessellation, color)?;
|
||||
if let Some(properties) = cadkernel::brep::analytic_mass_properties(body) {
|
||||
mesh.apply_mass_properties(properties);
|
||||
}
|
||||
Some((mesh, wires, center))
|
||||
}
|
||||
|
||||
/// The middle of a body, for a caller needing a point to turn or scale about.
|
||||
|
||||
+202
-11
@@ -807,11 +807,39 @@ impl Scene {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn append_solid_history(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
operation: acadrust::objects::SolidHistoryOperation,
|
||||
) -> bool {
|
||||
let previous = self
|
||||
.document
|
||||
.solid_history_graph(handle)
|
||||
.map(|graph| graph.nodes)
|
||||
.unwrap_or_default();
|
||||
self.record_solid_history_before(handle);
|
||||
let Some(graph) = self.document.append_solid_history(handle, operation) else {
|
||||
return false;
|
||||
};
|
||||
for node in graph.nodes {
|
||||
if !previous.contains(&node) {
|
||||
self.record_undo_object_before(node, None);
|
||||
}
|
||||
}
|
||||
self.sync_solid_reference_point(handle);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) {
|
||||
let reference = self
|
||||
.document
|
||||
.solid_history_operation(handle)
|
||||
.and_then(crate::scene::model::solid_history::reference_point);
|
||||
.solid_history_operations(handle)
|
||||
.and_then(|operations| {
|
||||
operations
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(crate::scene::model::solid_history::reference_point)
|
||||
});
|
||||
let Some(reference) = reference else {
|
||||
return;
|
||||
};
|
||||
@@ -860,8 +888,31 @@ impl Scene {
|
||||
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
|
||||
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
|
||||
}
|
||||
(EntityType::Surface(_), SolidHistoryOperation::Loft(_))
|
||||
| (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(),
|
||||
(EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => {
|
||||
cadkernel::acis::rebuild_body(operation).ok()
|
||||
}
|
||||
(EntityType::Solid3D(_), _) => {
|
||||
let mut operations = self.document.solid_history_operations(handle)?;
|
||||
let replacement_id = operation.base().map(|base| {
|
||||
if base.eval.node_id > 0 {
|
||||
base.eval.node_id
|
||||
} else {
|
||||
base.step_id
|
||||
}
|
||||
})?;
|
||||
let target = operations.iter_mut().find(|candidate| {
|
||||
candidate.base().is_some_and(|base| {
|
||||
let node_id = if base.eval.node_id > 0 {
|
||||
base.eval.node_id
|
||||
} else {
|
||||
base.step_id
|
||||
};
|
||||
node_id == replacement_id
|
||||
})
|
||||
})?;
|
||||
*target = operation.clone();
|
||||
cadkernel::acis::rebuild_history(&operations).ok()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -914,7 +965,7 @@ impl Scene {
|
||||
self.record_solid_history_before(handle);
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.update_solid_history_step(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
@@ -1051,16 +1102,71 @@ impl Scene {
|
||||
value: &str,
|
||||
) -> bool {
|
||||
self.record_solid_history_before(handle);
|
||||
let mut created = false;
|
||||
if self.document.solid_history_graph(handle).is_none() {
|
||||
let create = match field {
|
||||
crate::scene::model::solid_history::PROP_HISTORY => {
|
||||
if value.eq_ignore_ascii_case("Record") {
|
||||
true
|
||||
} else if value.eq_ignore_ascii_case("None") {
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
crate::scene::model::solid_history::PROP_SHOW_HISTORY
|
||||
if self.document.header.show_solid_history.clamp(0, 2) == 1 =>
|
||||
{
|
||||
if value.eq_ignore_ascii_case("Yes") {
|
||||
true
|
||||
} else if value.eq_ignore_ascii_case("No") {
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
if create {
|
||||
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_))) {
|
||||
return false;
|
||||
}
|
||||
self.restore_solid_models(&[handle]);
|
||||
let Some(body) = self.solid_models.get(&handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity_arc(handle);
|
||||
self.record_undo_before(handle, before);
|
||||
}
|
||||
if !self.create_solid_history(
|
||||
handle,
|
||||
crate::scene::model::solid_history::brep_op(&body),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// A solid without a graph was displayed as History=None.
|
||||
// Preserve that object state before applying the requested
|
||||
// choice, independent of the drawing-wide SOLIDHIST setting.
|
||||
crate::scene::model::solid_history::apply_history_choice(
|
||||
&mut self.document,
|
||||
handle,
|
||||
crate::scene::model::solid_history::PROP_HISTORY,
|
||||
"None",
|
||||
);
|
||||
created = true;
|
||||
}
|
||||
}
|
||||
let applied = crate::scene::model::solid_history::apply_history_choice(
|
||||
&mut self.document,
|
||||
handle,
|
||||
field,
|
||||
value,
|
||||
);
|
||||
if applied {
|
||||
if created || applied {
|
||||
self.bump_entities(&[(handle, ChangeKind::Modified)]);
|
||||
}
|
||||
applied
|
||||
created || applied
|
||||
}
|
||||
|
||||
fn apply_solid_history_grip(
|
||||
@@ -1072,6 +1178,79 @@ impl Scene {
|
||||
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
if grip_id == crate::scene::model::solid_history::GRIP_FILLET_RADIUS {
|
||||
let acadrust::objects::SolidHistoryOperation::Fillet(value) = &mut operation else {
|
||||
return false;
|
||||
};
|
||||
let Some(radius) = value.radii.first().copied() else {
|
||||
return false;
|
||||
};
|
||||
self.restore_solid_models(&[handle]);
|
||||
let Some(definition) = self
|
||||
.solid_models
|
||||
.get(&handle)
|
||||
.and_then(|body| {
|
||||
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
|
||||
})
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(axis) = definition.axis else {
|
||||
return false;
|
||||
};
|
||||
let change = match apply {
|
||||
GripApply::Absolute(world) => (world - definition.world).dot(axis),
|
||||
GripApply::Translate(delta) => delta.dot(axis),
|
||||
};
|
||||
let radius = radius + change;
|
||||
if !radius.is_finite() || radius <= 1.0e-6 {
|
||||
return false;
|
||||
}
|
||||
value.radii.clear();
|
||||
value.radii.push(radius);
|
||||
return self.preview_solid_history(handle, operation);
|
||||
}
|
||||
if matches!(
|
||||
grip_id,
|
||||
crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
|
||||
| crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2
|
||||
) {
|
||||
let definitions = {
|
||||
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &operation else {
|
||||
return false;
|
||||
};
|
||||
crate::scene::model::solid_history::chamfer_distance_grips(
|
||||
&self.document,
|
||||
handle,
|
||||
value,
|
||||
)
|
||||
};
|
||||
let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(axis) = definition.axis else {
|
||||
return false;
|
||||
};
|
||||
let change = match apply {
|
||||
GripApply::Absolute(world) => (world - definition.world).dot(axis),
|
||||
GripApply::Translate(delta) => delta.dot(axis),
|
||||
};
|
||||
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else {
|
||||
return false;
|
||||
};
|
||||
let distance = if grip_id
|
||||
== crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
|
||||
{
|
||||
&mut value.base_distance
|
||||
} else {
|
||||
&mut value.other_distance
|
||||
};
|
||||
*distance += change;
|
||||
if !distance.is_finite() || *distance <= 1.0e-6 {
|
||||
return false;
|
||||
}
|
||||
return self.preview_solid_history(handle, operation);
|
||||
}
|
||||
if !crate::scene::model::solid_history::apply_primitive_grip(
|
||||
&mut operation,
|
||||
grip_id,
|
||||
@@ -1088,7 +1267,12 @@ impl Scene {
|
||||
field: &str,
|
||||
value: &str,
|
||||
) -> bool {
|
||||
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||
let Some(mut operation) =
|
||||
crate::scene::model::solid_history::primitive_property_operation(
|
||||
&self.document,
|
||||
handle,
|
||||
)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !crate::scene::model::solid_history::apply_primitive_property(
|
||||
@@ -1185,9 +1369,7 @@ impl Scene {
|
||||
generator.source = source;
|
||||
}
|
||||
}
|
||||
set.metrics.centroid[0] += delta[0];
|
||||
set.metrics.centroid[1] += delta[1];
|
||||
set.metrics.centroid[2] += delta[2];
|
||||
set.metrics.translate(delta);
|
||||
set.recompute_aabb();
|
||||
}
|
||||
|
||||
@@ -1337,5 +1519,14 @@ impl Scene {
|
||||
self.images.insert(handle, model);
|
||||
}
|
||||
}
|
||||
// A grip edit can move geometry used as the boundary of an associative hatch.
|
||||
// The source entity has already been mutated above, so rebuild any dependent
|
||||
// hatch from the live document geometry immediately.
|
||||
let source_change = [(handle, ChangeKind::Modified)];
|
||||
let hatch_changes = self.refresh_associative_hatches(&source_change);
|
||||
|
||||
if !hatch_changes.is_empty() {
|
||||
self.bump_entities(&hatch_changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
+287
-87
@@ -1615,10 +1615,10 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
||||
);
|
||||
}
|
||||
|
||||
// Q: lazy projection — accumulate screen points without allocating per-wire Vec.
|
||||
wires
|
||||
.iter()
|
||||
.filter_map(|wire| {
|
||||
if crossing {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::default();
|
||||
for wire in wires.iter() {
|
||||
// Fallback: when wire has no line geometry (e.g. greek text emits
|
||||
// only fill_tris) treat the AABB rectangle as the hit-test shape
|
||||
// so low-LOD text stays selectable. See #19.
|
||||
@@ -1639,7 +1639,7 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
||||
];
|
||||
&aabb_pts
|
||||
} else {
|
||||
return None;
|
||||
continue;
|
||||
};
|
||||
|
||||
// Low residual parallel to `pts` (empty for the AABB fallback,
|
||||
@@ -1650,6 +1650,85 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
||||
&[]
|
||||
};
|
||||
let mut hit = false;
|
||||
let mut prev: Option<Point> = None;
|
||||
|
||||
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||
if px.is_nan() {
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let world = if !wire.points.is_empty() {
|
||||
wire_point_world(wire, i, view_rot, eye)
|
||||
} else {
|
||||
wp64([px, py, pz], low, i)
|
||||
};
|
||||
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||
if inside(sp) {
|
||||
hit = true;
|
||||
}
|
||||
if let Some(p0) = prev {
|
||||
if !hit {
|
||||
hit = segments_intersect(p0, sp, box_tl, box_tr)
|
||||
|| segments_intersect(p0, sp, box_tr, box_br)
|
||||
|| segments_intersect(p0, sp, box_br, box_bl)
|
||||
|| segments_intersect(p0, sp, box_bl, box_tl);
|
||||
}
|
||||
}
|
||||
prev = Some(sp);
|
||||
}
|
||||
|
||||
let mut glyph_crosses = false;
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||
continue;
|
||||
};
|
||||
if [0usize, 3].into_iter().any(|offset| {
|
||||
triangle_crosses_box(
|
||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||
box_corners,
|
||||
)
|
||||
}) {
|
||||
glyph_crosses = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
}
|
||||
}
|
||||
out
|
||||
} else {
|
||||
let mut qualified = Vec::new();
|
||||
let mut disqualified = HashSet::default();
|
||||
let mut seen = HashSet::default();
|
||||
|
||||
for wire in wires.iter() {
|
||||
let aabb_pts: Vec<[f32; 3]>;
|
||||
let empty_pts: [[f32; 3]; 0] = [];
|
||||
let pts: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||
&wire.points
|
||||
} else if !wire.text_verts.is_empty() {
|
||||
&empty_pts
|
||||
} else if wire.aabb != WireModel::UNBOUNDED_AABB {
|
||||
let [ax, ay, bx, by] = wire.aabb;
|
||||
aabb_pts = vec![
|
||||
[ax, ay, 0.0],
|
||||
[bx, ay, 0.0],
|
||||
[bx, by, 0.0],
|
||||
[ax, by, 0.0],
|
||||
[ax, ay, 0.0],
|
||||
];
|
||||
&aabb_pts
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||
&wire.points_low
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let mut all_inside = true;
|
||||
let mut prev: Option<Point> = None;
|
||||
|
||||
@@ -1664,61 +1743,42 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
||||
wp64([px, py, pz], low, i)
|
||||
};
|
||||
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||
if crossing {
|
||||
if inside(sp) {
|
||||
hit = true;
|
||||
}
|
||||
if let Some(p0) = prev {
|
||||
if !hit {
|
||||
hit = segments_intersect(p0, sp, box_tl, box_tr)
|
||||
|| segments_intersect(p0, sp, box_tr, box_br)
|
||||
|| segments_intersect(p0, sp, box_br, box_bl)
|
||||
|| segments_intersect(p0, sp, box_bl, box_tl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !inside(sp) {
|
||||
all_inside = false;
|
||||
}
|
||||
if !inside(sp) {
|
||||
all_inside = false;
|
||||
}
|
||||
prev = Some(sp);
|
||||
}
|
||||
|
||||
let glyphs_present = !wire.text_verts.is_empty();
|
||||
let mut glyph_crosses = false;
|
||||
let mut glyphs_inside = true;
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||
continue;
|
||||
};
|
||||
if crossing {
|
||||
if [0usize, 3].into_iter().any(|offset| {
|
||||
triangle_crosses_box(
|
||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||
box_corners,
|
||||
)
|
||||
}) {
|
||||
glyph_crosses = true;
|
||||
break;
|
||||
}
|
||||
} else if !screen.iter().copied().all(inside) {
|
||||
if !screen.iter().copied().all(inside) {
|
||||
glyphs_inside = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = if crossing {
|
||||
hit || glyph_crosses
|
||||
} else {
|
||||
all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
|
||||
};
|
||||
if result {
|
||||
Some(wire.name.as_str())
|
||||
} else {
|
||||
None
|
||||
let has_geom = prev.is_some() || glyphs_present;
|
||||
if !has_geom {
|
||||
continue;
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
let name = wire.name.as_str();
|
||||
if all_inside && glyphs_inside {
|
||||
if seen.insert(name) {
|
||||
qualified.push(name);
|
||||
}
|
||||
} else {
|
||||
disqualified.insert(name);
|
||||
}
|
||||
}
|
||||
|
||||
qualified.retain(|name| !disqualified.contains(name));
|
||||
qualified
|
||||
}
|
||||
}
|
||||
|
||||
// ── Polygon / lasso selection ─────────────────────────────────────────────
|
||||
@@ -1743,10 +1803,10 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
return indexed_polygon_crossing_hits(wires, poly, view_rot, eye, bounds);
|
||||
}
|
||||
|
||||
// Q: lazy projection — no Vec allocation per wire.
|
||||
wires
|
||||
.iter()
|
||||
.filter_map(|wire| {
|
||||
if crossing {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::default();
|
||||
for wire in wires.iter() {
|
||||
// Same AABB fallback as `box_hit`: when a wire has no line
|
||||
// geometry (e.g. greek-LOD text emits only fill_tris) treat the
|
||||
// AABB rectangle as the hit-test shape so low-LOD text stays
|
||||
@@ -1768,7 +1828,7 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
];
|
||||
&aabb_pts
|
||||
} else {
|
||||
return None;
|
||||
continue;
|
||||
};
|
||||
|
||||
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||
@@ -1777,6 +1837,88 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
&[]
|
||||
};
|
||||
let mut hit = false;
|
||||
let mut prev: Option<Point> = None;
|
||||
|
||||
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||
if px.is_nan() {
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let world = if !wire.points.is_empty() {
|
||||
wire_point_world(wire, i, view_rot, eye)
|
||||
} else {
|
||||
wp64([px, py, pz], low, i)
|
||||
};
|
||||
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||
if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height {
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
if point_in_polygon(sp, poly) {
|
||||
hit = true;
|
||||
}
|
||||
if !hit {
|
||||
if let Some(p0) = prev {
|
||||
if segment_crosses_polygon(p0, sp, poly) {
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = Some(sp);
|
||||
}
|
||||
|
||||
let mut glyph_crosses = false;
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||
continue;
|
||||
};
|
||||
if [0usize, 3].into_iter().any(|offset| {
|
||||
triangle_crosses_polygon(
|
||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||
poly,
|
||||
)
|
||||
}) {
|
||||
glyph_crosses = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
}
|
||||
}
|
||||
out
|
||||
} else {
|
||||
let mut qualified = Vec::new();
|
||||
let mut disqualified = HashSet::default();
|
||||
let mut seen = HashSet::default();
|
||||
|
||||
for wire in wires.iter() {
|
||||
let aabb_pts: Vec<[f32; 3]>;
|
||||
let empty_pts: [[f32; 3]; 0] = [];
|
||||
let pts: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||
&wire.points
|
||||
} else if !wire.text_verts.is_empty() {
|
||||
&empty_pts
|
||||
} else if wire.aabb != WireModel::UNBOUNDED_AABB {
|
||||
let [ax, ay, bx, by] = wire.aabb;
|
||||
aabb_pts = vec![
|
||||
[ax, ay, 0.0],
|
||||
[bx, ay, 0.0],
|
||||
[bx, by, 0.0],
|
||||
[ax, by, 0.0],
|
||||
[ax, ay, 0.0],
|
||||
];
|
||||
&aabb_pts
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||
&wire.points_low
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let mut all_inside = true;
|
||||
let mut prev: Option<Point> = None;
|
||||
|
||||
@@ -1798,43 +1940,19 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
if crossing {
|
||||
if point_in_polygon(sp, poly) {
|
||||
hit = true;
|
||||
}
|
||||
if !hit {
|
||||
if let Some(p0) = prev {
|
||||
if segment_crosses_polygon(p0, sp, poly) {
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !point_in_polygon(sp, poly) {
|
||||
all_inside = false;
|
||||
}
|
||||
if !point_in_polygon(sp, poly) {
|
||||
all_inside = false;
|
||||
}
|
||||
prev = Some(sp);
|
||||
}
|
||||
|
||||
let glyphs_present = !wire.text_verts.is_empty();
|
||||
let mut glyph_crosses = false;
|
||||
let mut glyphs_inside = true;
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||
continue;
|
||||
};
|
||||
if crossing {
|
||||
if [0usize, 3].into_iter().any(|offset| {
|
||||
triangle_crosses_polygon(
|
||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||
poly,
|
||||
)
|
||||
}) {
|
||||
glyph_crosses = true;
|
||||
break;
|
||||
}
|
||||
} else if !screen.iter().copied().all(|point| {
|
||||
if !screen.iter().copied().all(|point| {
|
||||
point.x >= 0.0
|
||||
&& point.x <= bounds.width
|
||||
&& point.y >= 0.0
|
||||
@@ -1846,18 +1964,24 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
let result = if crossing {
|
||||
hit || glyph_crosses
|
||||
} else {
|
||||
all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
|
||||
};
|
||||
if result {
|
||||
Some(wire.name.as_str())
|
||||
} else {
|
||||
None
|
||||
let has_geom = prev.is_some() || glyphs_present;
|
||||
if !has_geom {
|
||||
continue;
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
let name = wire.name.as_str();
|
||||
if all_inside && glyphs_inside {
|
||||
if seen.insert(name) {
|
||||
qualified.push(name);
|
||||
}
|
||||
} else {
|
||||
disqualified.insert(name);
|
||||
}
|
||||
}
|
||||
|
||||
qualified.retain(|name| !disqualified.contains(name));
|
||||
qualified
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
@@ -2511,4 +2635,80 @@ mod aabb_reject_tests {
|
||||
vec!["479"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_selection_multi_wire_entity_behavior() {
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
};
|
||||
// In identity ortho view:
|
||||
// world (0.0, 0.0) -> screen (100.0, 100.0)
|
||||
// world (0.1, 0.1) -> screen (110.0, 90.0)
|
||||
// world (0.8, 0.8) -> screen (180.0, 20.0)
|
||||
let wire_arc = wire(
|
||||
"4F",
|
||||
vec![[0.0, 0.0, 0.0], [0.1, 0.1, 0.0]],
|
||||
[0.0, 0.0, 0.1, 0.1],
|
||||
);
|
||||
let wire_lines = wire(
|
||||
"4F",
|
||||
vec![[0.1, 0.1, 0.0], [0.8, 0.8, 0.0]],
|
||||
[0.1, 0.1, 0.8, 0.8],
|
||||
);
|
||||
let wires = [wire_arc, wire_lines];
|
||||
|
||||
// Box enclosing ONLY wire_arc (screen 95..115, 85..105):
|
||||
let box_a = Point::new(95.0, 85.0);
|
||||
let box_b = Point::new(115.0, 105.0);
|
||||
let poly_small = [
|
||||
Point::new(95.0, 85.0),
|
||||
Point::new(115.0, 85.0),
|
||||
Point::new(115.0, 105.0),
|
||||
Point::new(95.0, 105.0),
|
||||
];
|
||||
|
||||
// Crossing mode selects "4F" because wire_arc is inside:
|
||||
assert_eq!(
|
||||
box_hit(box_a, box_b, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
vec!["4F"]
|
||||
);
|
||||
assert_eq!(
|
||||
poly_hit(&poly_small, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
vec!["4F"]
|
||||
);
|
||||
|
||||
// Window mode MUST NOT select "4F" because wire_lines has a vertex at (0.8, 0.8) -> (180.0, 20.0) outside:
|
||||
assert_eq!(
|
||||
box_hit(box_a, box_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
Vec::<&str>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
poly_hit(&poly_small, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
Vec::<&str>::new()
|
||||
);
|
||||
|
||||
// Big box enclosing BOTH wire_arc and wire_lines (screen 50..190, 10..110):
|
||||
let big_a = Point::new(50.0, 10.0);
|
||||
let big_b = Point::new(190.0, 110.0);
|
||||
let poly_big = [
|
||||
Point::new(50.0, 10.0),
|
||||
Point::new(190.0, 10.0),
|
||||
Point::new(190.0, 110.0),
|
||||
Point::new(50.0, 110.0),
|
||||
];
|
||||
|
||||
// Window mode now selects "4F" once (deduplicated):
|
||||
assert_eq!(
|
||||
box_hit(big_a, big_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
vec!["4F"]
|
||||
);
|
||||
assert_eq!(
|
||||
poly_hit(&poly_big, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||
vec!["4F"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
//! GPU-instanced analytical circle buffer & pipeline layout.
|
||||
//!
|
||||
//! Renders 2D/planar circles as analytical screen/plane-aligned quads with
|
||||
//! screen-space derivative anti-aliasing. Each circle is a single instance
|
||||
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom
|
||||
//! and providing infinite visual smoothness.
|
||||
|
||||
use iced::wgpu;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct CircleInstance {
|
||||
pub center_high: [f32; 3],
|
||||
pub _pad0: f32,
|
||||
pub center_low: [f32; 3],
|
||||
pub radius: f32,
|
||||
pub axis_x: [f32; 3],
|
||||
pub start_angle: f32,
|
||||
pub axis_y: [f32; 3],
|
||||
pub end_angle: f32,
|
||||
pub color: [f32; 4],
|
||||
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||
pub params: [f32; 4],
|
||||
pub pat0: [f32; 4],
|
||||
pub pat1: [f32; 4],
|
||||
}
|
||||
|
||||
impl CircleInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, center_high) as u64,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, center_low) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, axis_x) as u64,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, axis_y) as u64,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, color) as u64,
|
||||
shader_location: 4,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, params) as u64,
|
||||
shader_location: 5,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, pat0) as u64,
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, pat1) as u64,
|
||||
shader_location: 7,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<CircleInstance>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CircleGpu {
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
pub instance_count: u32,
|
||||
}
|
||||
|
||||
impl CircleGpu {
|
||||
pub fn from_instances(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
instances: &[CircleInstance],
|
||||
) -> Self {
|
||||
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
label,
|
||||
instances,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: instances.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the base and xray circle pipelines.
|
||||
pub fn create_pipelines(
|
||||
device: &wgpu::Device,
|
||||
frame_bgl: &wgpu::BindGroupLayout,
|
||||
color_format: wgpu::TextureFormat,
|
||||
sample_count: u32,
|
||||
content_stencil: &wgpu::StencilState,
|
||||
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("circle.wgsl"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/circle.wgsl").into()),
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("circle.pipeline.layout"),
|
||||
bind_group_layouts: &[Some(frame_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let circle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("circle.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[CircleInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let circle_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("circle_xray.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[CircleInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
(circle_pipeline, circle_xray_pipeline)
|
||||
}
|
||||
|
||||
/// 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<CircleInstance> {
|
||||
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],
|
||||
[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,
|
||||
axis_y,
|
||||
radius,
|
||||
} => (center, axis_x, axis_y, radius, 0.0f32, std::f32::consts::TAU),
|
||||
crate::scene::model::wire_model::TangentGeom::Arc {
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
} => (
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
start_angle as f32,
|
||||
end_angle as f32,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() || radius > 1e6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||
let hw = wire.line_weight_px * 0.5;
|
||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||
|
||||
Some(CircleInstance {
|
||||
center_high: ch,
|
||||
_pad0: 0.0,
|
||||
center_low: cl,
|
||||
radius: radius as f32,
|
||||
axis_x: [axis_x[0] as f32, axis_x[1] as f32, axis_x[2] as f32],
|
||||
start_angle,
|
||||
axis_y: [axis_y[0] as f32, axis_y[1] as f32, axis_y[2] as f32],
|
||||
end_angle,
|
||||
color: wire.color,
|
||||
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||
pat0,
|
||||
pat1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<Vec<CircleInstance>> {
|
||||
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<CircleInstance> {
|
||||
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;
|
||||
let hy = y as f32;
|
||||
let ly = (y - hy as f64) as f32;
|
||||
let hz = z as f32;
|
||||
let lz = (z - hz as f64) as f32;
|
||||
([hx, hy, hz], [lx, ly, lz])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn circle_instance_size_matches_stride() {
|
||||
assert_eq!(std::mem::size_of::<CircleInstance>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_shader_validates_with_naga() {
|
||||
let source = include_str!("../../shaders/circle.wgsl");
|
||||
let module = naga::front::wgsl::parse_str(source).expect("circle.wgsl parses cleanly");
|
||||
let mut validator = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
);
|
||||
validator
|
||||
.validate(&module)
|
||||
.expect("circle.wgsl validates cleanly with naga");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_planar_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [100.0, 200.0, 300.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 50.0,
|
||||
});
|
||||
wire.line_weight_px = 2.0;
|
||||
wire.color = [1.0, 0.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_circle_instance(&wire, 1.5).expect("should extract planar circle");
|
||||
assert_eq!(inst.radius, 50.0);
|
||||
assert_eq!(inst.start_angle, 0.0);
|
||||
assert_eq!(inst.end_angle, std::f32::consts::TAU);
|
||||
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||
assert_eq!(inst.color, [1.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.0); // half lineweight
|
||||
assert_eq!(inst.params[2], 1.5); // draw depth
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_arc_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||
center: [10.0, 20.0, 30.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 25.0,
|
||||
start_angle: 0.5,
|
||||
end_angle: 2.5,
|
||||
});
|
||||
wire.line_weight_px = 3.0;
|
||||
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_circle_instance(&wire, 0.5).expect("should extract circular arc");
|
||||
assert_eq!(inst.radius, 25.0);
|
||||
assert_eq!(inst.start_angle, 0.5);
|
||||
assert_eq!(inst.end_angle, 2.5);
|
||||
assert_eq!(inst.center_high, [10.0, 20.0, 30.0]);
|
||||
assert_eq!(inst.color, [0.0, 1.0, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_rejects_non_planar_or_complex() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
});
|
||||
// 1. With fill_tris (e.g. 3D solid or hatch)
|
||||
wire.fill_tris = vec![[0.0; 3]];
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
|
||||
// 2. Clear fill_tris, add render_instance (block instance)
|
||||
wire.fill_tris.clear();
|
||||
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
|
||||
source_id: 1,
|
||||
translation: [0.0; 3],
|
||||
});
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
|
||||
// 3. Invalid radius
|
||||
wire.render_instance = None;
|
||||
wire.tangent_geoms[0] = crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: -5.0,
|
||||
};
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
//! GPU-instanced analytical ellipse buffer & pipeline layout.
|
||||
//!
|
||||
//! Renders 2D/planar ellipses and elliptical arcs as analytical screen/plane-aligned quads
|
||||
//! with sub-pixel screen-space derivative anti-aliasing. Each ellipse is a single instance
|
||||
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom and providing
|
||||
//! mathematically exact curvature at any magnification.
|
||||
|
||||
use iced::wgpu;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct EllipseInstance {
|
||||
pub center_high: [f32; 3],
|
||||
pub _pad0: f32,
|
||||
pub center_low: [f32; 3],
|
||||
pub minor_axis_ratio: f32,
|
||||
pub major_axis: [f32; 3],
|
||||
pub start_param: f32,
|
||||
pub normal: [f32; 3],
|
||||
pub end_param: f32,
|
||||
pub color: [f32; 4],
|
||||
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||
pub params: [f32; 4],
|
||||
pub pat0: [f32; 4],
|
||||
pub pat1: [f32; 4],
|
||||
}
|
||||
|
||||
impl EllipseInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, center_high) as u64,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, center_low) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, major_axis) as u64,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, normal) as u64,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, color) as u64,
|
||||
shader_location: 4,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, params) as u64,
|
||||
shader_location: 5,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, pat0) as u64,
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, pat1) as u64,
|
||||
shader_location: 7,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<EllipseInstance>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EllipseGpu {
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
pub instance_count: u32,
|
||||
}
|
||||
|
||||
impl EllipseGpu {
|
||||
pub fn from_instances(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
instances: &[EllipseInstance],
|
||||
) -> Self {
|
||||
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
label,
|
||||
instances,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: instances.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the base and xray ellipse pipelines.
|
||||
pub fn create_pipelines(
|
||||
device: &wgpu::Device,
|
||||
frame_bgl: &wgpu::BindGroupLayout,
|
||||
color_format: wgpu::TextureFormat,
|
||||
sample_count: u32,
|
||||
content_stencil: &wgpu::StencilState,
|
||||
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("ellipse.wgsl"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/ellipse.wgsl").into()),
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("ellipse.pipeline.layout"),
|
||||
bind_group_layouts: &[Some(frame_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let ellipse_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("ellipse.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[EllipseInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let ellipse_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("ellipse_xray.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[EllipseInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
(ellipse_pipeline, ellipse_xray_pipeline)
|
||||
}
|
||||
|
||||
/// 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<EllipseInstance> {
|
||||
let crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center,
|
||||
major_axis,
|
||||
normal,
|
||||
minor_axis_ratio,
|
||||
start_param,
|
||||
end_param,
|
||||
} = *geom else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2];
|
||||
let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
|
||||
if major_len_sq <= 1e-12 || !major_len_sq.is_finite() || major_len_sq > 1e12
|
||||
|| norm_len_sq <= 1e-12 || !norm_len_sq.is_finite()
|
||||
|| minor_axis_ratio <= 1e-6 || !minor_axis_ratio.is_finite()
|
||||
|| !start_param.is_finite() || !end_param.is_finite()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||
let hw = wire.line_weight_px * 0.5;
|
||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||
|
||||
Some(EllipseInstance {
|
||||
center_high: ch,
|
||||
_pad0: 0.0,
|
||||
center_low: cl,
|
||||
minor_axis_ratio: minor_axis_ratio.min(1.0) as f32,
|
||||
major_axis: [major_axis[0] as f32, major_axis[1] as f32, major_axis[2] as f32],
|
||||
start_param: start_param as f32,
|
||||
normal: [normal[0] as f32, normal[1] as f32, normal[2] as f32],
|
||||
end_param: end_param as f32,
|
||||
color: wire.color,
|
||||
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||
pat0,
|
||||
pat1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper to extract all analytical ellipse instances from a `WireModel`.
|
||||
pub fn extract_ellipse_instances(
|
||||
wire: &crate::scene::WireModel,
|
||||
draw_depth: f32,
|
||||
) -> Option<Vec<EllipseInstance>> {
|
||||
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<EllipseInstance> {
|
||||
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;
|
||||
let hy = y as f32;
|
||||
let ly = (y - hy as f64) as f32;
|
||||
let hz = z as f32;
|
||||
let lz = (z - hz as f64) as f32;
|
||||
([hx, hy, hz], [lx, ly, lz])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ellipse_instance_size_matches_stride() {
|
||||
assert_eq!(std::mem::size_of::<EllipseInstance>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_shader_validates_with_naga() {
|
||||
let source = include_str!("../../shaders/ellipse.wgsl");
|
||||
let module = naga::front::wgsl::parse_str(source).expect("ellipse.wgsl parses cleanly");
|
||||
let mut validator = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
);
|
||||
validator
|
||||
.validate(&module)
|
||||
.expect("ellipse.wgsl validates cleanly with naga");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_ellipse_instance_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center: [100.0, 200.0, 300.0],
|
||||
major_axis: [50.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.5,
|
||||
start_param: 0.0,
|
||||
end_param: std::f64::consts::TAU,
|
||||
});
|
||||
wire.line_weight_px = 2.0;
|
||||
wire.color = [1.0, 0.5, 0.0, 1.0];
|
||||
|
||||
let inst = extract_ellipse_instance(&wire, 1.0).expect("should extract ellipse");
|
||||
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||
assert_eq!(inst.major_axis, [50.0, 0.0, 0.0]);
|
||||
assert_eq!(inst.minor_axis_ratio, 0.5);
|
||||
assert_eq!(inst.start_param, 0.0);
|
||||
assert_eq!(inst.end_param, std::f32::consts::TAU);
|
||||
assert_eq!(inst.color, [1.0, 0.5, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.0);
|
||||
assert_eq!(inst.params[2], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_elliptical_arc_instance_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
major_axis: [10.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.8,
|
||||
start_param: 0.5,
|
||||
end_param: 2.5,
|
||||
});
|
||||
wire.line_weight_px = 1.0;
|
||||
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_ellipse_instance(&wire, 0.5).expect("should extract elliptical arc");
|
||||
assert_eq!(inst.minor_axis_ratio, 0.8);
|
||||
assert_eq!(inst.start_param, 0.5);
|
||||
assert_eq!(inst.end_param, 2.5);
|
||||
assert_eq!(inst.params[0], 0.5); // half-width
|
||||
assert_eq!(inst.params[2], 0.5); // draw-depth
|
||||
}
|
||||
|
||||
fn rust_closest_point_ellipse(p_in: [f32; 2], ab_in: [f32; 2]) -> [f32; 2] {
|
||||
let px = p_in[0].abs();
|
||||
let py = p_in[1].abs();
|
||||
let a = ab_in[0];
|
||||
let b = ab_in[1];
|
||||
if (a - b).abs() < 1e-6 {
|
||||
let len = (px * px + py * py).sqrt();
|
||||
let q = if len > 1e-6 {
|
||||
[px / len * a, py / len * a]
|
||||
} else {
|
||||
[a, 0.0]
|
||||
};
|
||||
return [
|
||||
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||
];
|
||||
}
|
||||
|
||||
// Initial estimate from eccentric anomaly of radial direction
|
||||
let mut t = (py * a).atan2(px * b);
|
||||
let k = a * a - b * b;
|
||||
|
||||
// 3 iterations of Newton-Raphson
|
||||
for _ in 0..3 {
|
||||
let (st, ct) = t.sin_cos();
|
||||
let f = k * st * ct - px * a * st + py * b * ct;
|
||||
let f_prime = k * (ct * ct - st * st) - px * a * ct - py * b * st;
|
||||
if f_prime.abs() > 1e-7 {
|
||||
t -= f / f_prime;
|
||||
}
|
||||
}
|
||||
t = t.clamp(0.0, std::f32::consts::FRAC_PI_2);
|
||||
let q = [a * t.cos(), b * t.sin()];
|
||||
[
|
||||
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_point_ellipse_accuracy() {
|
||||
let a = 100.0f32;
|
||||
let b = 40.0f32;
|
||||
// Test points directly on the ellipse: closest point should be itself!
|
||||
for i in 0..36 {
|
||||
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||
let px = a * angle.cos();
|
||||
let py = b * angle.sin();
|
||||
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||
let dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||
assert!(
|
||||
dist < 0.2,
|
||||
"Point on ellipse at angle {angle} failed: px={px}, py={py}, q={:?}, dist={dist}",
|
||||
q
|
||||
);
|
||||
// Verify q is on the ellipse (x/a)^2 + (y/b)^2 == 1
|
||||
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||
assert!((eq - 1.0).abs() < 0.01, "eq={eq} for angle {angle}");
|
||||
}
|
||||
|
||||
// Test points offset from ellipse (inside and outside)
|
||||
for i in 0..36 {
|
||||
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||
for offset in [-5.0f32, -1.0, 1.0, 5.0] {
|
||||
let norm = [angle.cos() / a, angle.sin() / b];
|
||||
let norm_len = (norm[0] * norm[0] + norm[1] * norm[1]).sqrt();
|
||||
let un = [norm[0] / norm_len, norm[1] / norm_len];
|
||||
let px = a * angle.cos() + offset * un[0];
|
||||
let py = b * angle.sin() + offset * un[1];
|
||||
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||
assert!(
|
||||
(eq - 1.0).abs() < 0.05,
|
||||
"Offset point at angle {angle}, offset {offset}: q={:?}, eq={eq}",
|
||||
q
|
||||
);
|
||||
let measured_dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||
assert!(
|
||||
(measured_dist - offset.abs()).abs() < 0.2,
|
||||
"Distance error at angle {angle}, offset {offset}: measured={measured_dist}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,3 +408,303 @@ fn late_device_errors_invalidate_uploaded_content_and_targets() {
|
||||
"the same error must not invalidate twice"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn test_selected_circle_arc_ellipse_highlight_overlay() {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||
.expect("GPU adapter");
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("GPU device");
|
||||
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
|
||||
let depth = rustc_hash::FxHashMap::default();
|
||||
|
||||
let mut circle_wire = WireModel::default();
|
||||
circle_wire.name = "circle_wire".into();
|
||||
circle_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
});
|
||||
|
||||
let handle = acadrust::Handle::new(100);
|
||||
let mut selected_handles = rustc_hash::FxHashSet::default();
|
||||
selected_handles.insert(handle);
|
||||
let hover_handles = rustc_hash::FxHashSet::default();
|
||||
|
||||
let mut index = rustc_hash::FxHashMap::default();
|
||||
index.insert(handle.value(), vec![0]);
|
||||
pipeline.wire_handle_index = std::sync::Arc::new(index);
|
||||
|
||||
let wires = vec![circle_wire];
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&selected_handles,
|
||||
&hover_handles,
|
||||
&[],
|
||||
&depth,
|
||||
Some([0.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
|
||||
assert_eq!(pipeline.gpu_selected_circles.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1);
|
||||
assert!(pipeline.gpu_selected_wires.is_empty());
|
||||
|
||||
// Verify classic highlight mode (SELECTIONEFFECT 0 / selected_tint: None)
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&selected_handles,
|
||||
&hover_handles,
|
||||
&[],
|
||||
&depth,
|
||||
None,
|
||||
);
|
||||
assert_eq!(pipeline.gpu_selected_circles.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1);
|
||||
|
||||
// Verify hover highlight
|
||||
let mut circle_hover = rustc_hash::FxHashSet::default();
|
||||
circle_hover.insert(handle);
|
||||
let empty_selected = rustc_hash::FxHashSet::default();
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&empty_selected,
|
||||
&circle_hover,
|
||||
&[],
|
||||
&depth,
|
||||
None,
|
||||
);
|
||||
assert_eq!(pipeline.gpu_selected_circles.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1);
|
||||
|
||||
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("test_target"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 512,
|
||||
height: 512,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("test_encoder"),
|
||||
});
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
queue.submit(Some(encoder.finish()));
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn test_selected_ellipse_overlay() {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||
.expect("GPU adapter");
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("GPU device");
|
||||
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
|
||||
let depth = rustc_hash::FxHashMap::default();
|
||||
|
||||
let mut ellipse_wire = WireModel::default();
|
||||
ellipse_wire.name = "ellipse_wire".into();
|
||||
ellipse_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
major_axis: [10.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.5,
|
||||
start_param: 0.0,
|
||||
end_param: std::f64::consts::TAU,
|
||||
});
|
||||
|
||||
let handle = acadrust::Handle::new(200);
|
||||
let mut selected_handles = rustc_hash::FxHashSet::default();
|
||||
selected_handles.insert(handle);
|
||||
let hover_handles = rustc_hash::FxHashSet::default();
|
||||
|
||||
let mut index = rustc_hash::FxHashMap::default();
|
||||
index.insert(handle.value(), vec![0]);
|
||||
pipeline.wire_handle_index = std::sync::Arc::new(index);
|
||||
|
||||
let wires = vec![ellipse_wire];
|
||||
// Standard tint
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&selected_handles,
|
||||
&hover_handles,
|
||||
&[],
|
||||
&depth,
|
||||
Some([0.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
|
||||
assert_eq!(pipeline.gpu_selected_ellipses.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1);
|
||||
assert!(pipeline.gpu_selected_wires.is_empty());
|
||||
|
||||
// Classic highlight mode (SELECTIONEFFECT 0 / selected_tint: None)
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&selected_handles,
|
||||
&hover_handles,
|
||||
&[],
|
||||
&depth,
|
||||
None,
|
||||
);
|
||||
assert_eq!(pipeline.gpu_selected_ellipses.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1);
|
||||
|
||||
// Hover highlight
|
||||
let mut ellipse_hover = rustc_hash::FxHashSet::default();
|
||||
ellipse_hover.insert(handle);
|
||||
let empty_selected = rustc_hash::FxHashSet::default();
|
||||
pipeline.upload_selected_wires(
|
||||
&device,
|
||||
&queue,
|
||||
&wires,
|
||||
&empty_selected,
|
||||
&ellipse_hover,
|
||||
&[],
|
||||
&depth,
|
||||
None,
|
||||
);
|
||||
assert_eq!(pipeline.gpu_selected_ellipses.len(), 1);
|
||||
assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1);
|
||||
|
||||
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("test_target_ellipse"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 512,
|
||||
height: 512,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("test_encoder_ellipse"),
|
||||
});
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
queue.submit(Some(encoder.finish()));
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn test_pline_arc_switch_preview_and_render() {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||
.expect("GPU adapter");
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("GPU device");
|
||||
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
|
||||
let depth = rustc_hash::FxHashMap::default();
|
||||
use crate::command::CadCommand;
|
||||
|
||||
let mut cmd = crate::modules::draw::draw::polyline::PlineCommand::new();
|
||||
cmd.on_point(glam::DVec3::new(0.0, 0.0, 0.0));
|
||||
let _res = cmd.on_point(glam::DVec3::new(10.0, 0.0, 0.0));
|
||||
cmd.set_live_handle(acadrust::Handle::new(1));
|
||||
cmd.on_text_input("A");
|
||||
|
||||
let test_points = [
|
||||
glam::DVec3::new(10.0, 0.0, 0.0),
|
||||
glam::DVec3::new(10.0000001, 0.0, 0.0),
|
||||
glam::DVec3::new(10.0, 0.0000001, 0.0),
|
||||
glam::DVec3::new(10.0, -0.0000001, 0.0),
|
||||
glam::DVec3::new(9.9999999, 0.0, 0.0),
|
||||
glam::DVec3::new(15.0, 5.0, 0.0),
|
||||
glam::DVec3::new(10.0, 10.0, 0.0),
|
||||
glam::DVec3::new(5.0, 5.0, 0.0),
|
||||
glam::DVec3::new(0.0, 0.0, 0.0),
|
||||
];
|
||||
|
||||
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("test_target"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 512,
|
||||
height: 512,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
for pt in test_points {
|
||||
let wire = cmd.on_mouse_move(pt);
|
||||
if let Some(w) = wire {
|
||||
pipeline.upload_preview_wires(&device, &queue, &[w], &depth);
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("test_encoder"),
|
||||
});
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
queue.submit(Some(encoder.finish()));
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
+372
-41
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod gpu_tests;
|
||||
mod device_capabilities;
|
||||
pub mod circle_gpu;
|
||||
pub mod ellipse_gpu;
|
||||
pub mod face3d_gpu;
|
||||
pub mod gpu_budget;
|
||||
pub mod gpu_upload;
|
||||
@@ -20,6 +22,8 @@ pub mod wire_gpu;
|
||||
use iced::wgpu;
|
||||
use iced::{Rectangle, Size};
|
||||
|
||||
pub use circle_gpu::{CircleGpu, CircleInstance};
|
||||
pub use ellipse_gpu::{EllipseGpu, EllipseInstance};
|
||||
pub use face3d_gpu::Face3DGpu;
|
||||
pub use wipeout_gpu::WipeoutGpu;
|
||||
pub use image_gpu::ImageGpu;
|
||||
@@ -79,6 +83,8 @@ pub struct Pipeline {
|
||||
shadow_pipeline: wgpu::RenderPipeline,
|
||||
shadow_plain_pipeline: wgpu::RenderPipeline,
|
||||
wire_pipeline: wgpu::RenderPipeline,
|
||||
circle_pipeline: wgpu::RenderPipeline,
|
||||
ellipse_pipeline: wgpu::RenderPipeline,
|
||||
block_wire_pipeline: wgpu::RenderPipeline,
|
||||
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
|
||||
clip_mask_pipeline: wgpu::RenderPipeline,
|
||||
@@ -89,6 +95,8 @@ pub struct Pipeline {
|
||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
circle_xray_pipeline: wgpu::RenderPipeline,
|
||||
ellipse_xray_pipeline: wgpu::RenderPipeline,
|
||||
block_wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). `Some` on any storage-capable device; `None` in packed
|
||||
@@ -225,6 +233,8 @@ pub struct Pipeline {
|
||||
/// replace this thin draw-range list on camera changes without touching the
|
||||
/// shared resident buffer.
|
||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||
pub(crate) gpu_circles: std::sync::Arc<Vec<CircleGpu>>,
|
||||
pub(crate) gpu_ellipses: std::sync::Arc<Vec<EllipseGpu>>,
|
||||
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
/// Persistent per-entity wire instance arena (capability-selected format).
|
||||
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
||||
@@ -241,6 +251,11 @@ pub struct Pipeline {
|
||||
pub(crate) wire_arena_fallback_handles: rustc_hash::FxHashSet<acadrust::Handle>,
|
||||
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
|
||||
pub(crate) wire_arena_id: u64,
|
||||
/// Handles that contributed to this slot's retained analytical uploads.
|
||||
pub(crate) partition_contributors: rustc_hash::FxHashSet<acadrust::Handle>,
|
||||
/// The draw-depth generation those uploads baked. A full depth rebuild
|
||||
/// reassigns every label, so they stop being reusable when it moves.
|
||||
pub(crate) partition_depth_generation: u64,
|
||||
/// Last content/camera/viewport tuple used to derive visible instance
|
||||
/// ranges from the resident arena.
|
||||
pub(crate) wire_cull_key: (u64, u64, u32, u32),
|
||||
@@ -256,11 +271,15 @@ pub struct Pipeline {
|
||||
clip_boundary: Option<(wgpu::Buffer, u32)>,
|
||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||
gpu_selected_wires: Vec<WireGpu>,
|
||||
gpu_selected_circles: Vec<CircleGpu>,
|
||||
gpu_selected_ellipses: Vec<EllipseGpu>,
|
||||
gpu_selected_block_wires: Vec<BlockWireGpu>,
|
||||
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
|
||||
/// frame they are present (small), drawn on top of the base wire pass — so
|
||||
/// a live drag never re-uploads the resident base buffer.
|
||||
gpu_preview_wires: Vec<WireGpu>,
|
||||
gpu_preview_circles: Vec<CircleGpu>,
|
||||
gpu_preview_ellipses: Vec<EllipseGpu>,
|
||||
/// Wipeout masks — solid fills rendered after wires in a separate pass via
|
||||
/// the legacy per-primitive `WipeoutGpu` renderer.
|
||||
gpu_wipeouts: Vec<WipeoutGpu>,
|
||||
@@ -2083,6 +2102,22 @@ impl Pipeline {
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let (circle_pipeline, circle_xray_pipeline) = circle_gpu::create_pipelines(
|
||||
device,
|
||||
&frame_bgl,
|
||||
format,
|
||||
MSAA_SAMPLES,
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let (ellipse_pipeline, ellipse_xray_pipeline) = ellipse_gpu::create_pipelines(
|
||||
device,
|
||||
&frame_bgl,
|
||||
format,
|
||||
MSAA_SAMPLES,
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let viewcube = ViewCubePipeline::new(device, queue, format);
|
||||
|
||||
let init_size = Size::new(1, 1);
|
||||
@@ -2223,6 +2258,10 @@ impl Pipeline {
|
||||
wire_black_pipeline,
|
||||
block_wire_black_pipeline,
|
||||
wire_xray_pipeline,
|
||||
circle_pipeline,
|
||||
circle_xray_pipeline,
|
||||
ellipse_pipeline,
|
||||
ellipse_xray_pipeline,
|
||||
block_wire_xray_pipeline,
|
||||
wire_const_bgl,
|
||||
block_wire_const_bgl,
|
||||
@@ -2295,6 +2334,8 @@ impl Pipeline {
|
||||
blit_uniform_buffer,
|
||||
surface_format: format,
|
||||
gpu_wires: std::sync::Arc::new(vec![]),
|
||||
gpu_circles: std::sync::Arc::new(vec![]),
|
||||
gpu_ellipses: std::sync::Arc::new(vec![]),
|
||||
gpu_block_wires: std::sync::Arc::new(vec![]),
|
||||
wire_arena: None,
|
||||
wire_arena_mesh: None,
|
||||
@@ -2302,14 +2343,20 @@ impl Pipeline {
|
||||
wire_arena_fallback_kind: None,
|
||||
wire_arena_fallback_handles: rustc_hash::FxHashSet::default(),
|
||||
wire_arena_id: u64::MAX,
|
||||
partition_contributors: rustc_hash::FxHashSet::default(),
|
||||
partition_depth_generation: u64::MAX,
|
||||
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
|
||||
hatch_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
|
||||
wipeout_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
|
||||
silhouette_key: (usize::MAX, u64::MAX, [u32::MAX; 3], false),
|
||||
clip_boundary: None,
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_selected_circles: vec![],
|
||||
gpu_selected_ellipses: vec![],
|
||||
gpu_selected_block_wires: vec![],
|
||||
gpu_preview_wires: vec![],
|
||||
gpu_preview_circles: vec![],
|
||||
gpu_preview_ellipses: vec![],
|
||||
gpu_wipeouts: vec![],
|
||||
wipeout_skip_flags: vec![],
|
||||
gpu_images: vec![],
|
||||
@@ -2371,6 +2418,86 @@ impl Pipeline {
|
||||
)
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical circles from a wire slice.
|
||||
pub fn upload_circles(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) -> Vec<CircleGpu> {
|
||||
let mut instances: Vec<CircleInstance> = Vec::new();
|
||||
for wire in wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
|
||||
instances.extend(insts);
|
||||
}
|
||||
}
|
||||
self.upload_circles_from_instances(device, queue, &instances)
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical circles from pre-extracted instances.
|
||||
pub fn upload_circles_from_instances(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
instances: &[CircleInstance],
|
||||
) -> Vec<CircleGpu> {
|
||||
if instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![CircleGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.circles",
|
||||
instances,
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical ellipses from a wire slice.
|
||||
pub fn upload_ellipses(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) -> Vec<EllipseGpu> {
|
||||
let mut instances: Vec<EllipseInstance> = Vec::new();
|
||||
for wire in wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||
instances.extend(insts);
|
||||
}
|
||||
}
|
||||
self.upload_ellipses_from_instances(device, queue, &instances)
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical ellipses from pre-extracted instances.
|
||||
pub fn upload_ellipses_from_instances(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
instances: &[EllipseInstance],
|
||||
) -> Vec<EllipseGpu> {
|
||||
if instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![EllipseGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.ellipses",
|
||||
instances,
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Build resident batches and a handle index shared across viewport slots.
|
||||
pub fn build_wire_buffers(
|
||||
&mut self,
|
||||
@@ -2383,6 +2510,8 @@ impl Pipeline {
|
||||
std::sync::Arc<Vec<WireGpu>>,
|
||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||
std::sync::Arc<Vec<CircleGpu>>,
|
||||
std::sync::Arc<Vec<EllipseGpu>>,
|
||||
) {
|
||||
// Batch the wire pass: instead of one GPU buffer + one draw call per
|
||||
// WireModel (tens of thousands on a large drawing), merge maximal runs
|
||||
@@ -2392,40 +2521,42 @@ impl Pipeline {
|
||||
// wire order — already sorted by draw order — is preserved; depth bias
|
||||
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
||||
// grouping keys because the draw loop sets one scissor per batch and
|
||||
// skips whole mesh-edge batches in shaded modes.
|
||||
let is_mesh_edge = |w: &WireModel| !w.points.is_empty() && w.fill_is_3d;
|
||||
let mut batches: Vec<WireGpu> = Vec::new();
|
||||
let mut block_wires: Vec<&WireModel> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < wires.len() {
|
||||
if !wires[i].display_visible {
|
||||
i += 1;
|
||||
continue;
|
||||
let kind = wire_arena::classify_wire(&wires[i]);
|
||||
match kind {
|
||||
wire_arena::WireKind::Invisible
|
||||
| wire_arena::WireKind::Circle
|
||||
| wire_arena::WireKind::Ellipse
|
||||
| wire_arena::WireKind::Empty => {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
wire_arena::WireKind::Block => {
|
||||
block_wires.push(&wires[i]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
wire_arena::WireKind::Regular | wire_arena::WireKind::MeshEdge => {
|
||||
let mesh_edge = kind == wire_arena::WireKind::MeshEdge;
|
||||
let mut j = i + 1;
|
||||
while j < wires.len() && wire_arena::classify_wire(&wires[j]) == kind {
|
||||
j += 1;
|
||||
}
|
||||
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
|
||||
batches.extend(WireGpu::from_run_refs(
|
||||
device,
|
||||
queue,
|
||||
&refs,
|
||||
depth_map,
|
||||
mesh_edge,
|
||||
self.wire_const_bgl.as_ref(),
|
||||
));
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
if wires[i].render_instance.is_some() {
|
||||
block_wires.push(&wires[i]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mesh_edge = is_mesh_edge(&wires[i]);
|
||||
let mut j = i + 1;
|
||||
while j < wires.len()
|
||||
&& wires[j].display_visible
|
||||
&& wires[j].render_instance.is_none()
|
||||
&& is_mesh_edge(&wires[j]) == mesh_edge
|
||||
{
|
||||
j += 1;
|
||||
}
|
||||
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
|
||||
batches.extend(WireGpu::from_run_refs(
|
||||
device,
|
||||
queue,
|
||||
&refs,
|
||||
depth_map,
|
||||
mesh_edge,
|
||||
self.wire_const_bgl.as_ref(),
|
||||
));
|
||||
i = j;
|
||||
}
|
||||
let block_batches = BlockWireGpu::from_wires(
|
||||
device,
|
||||
@@ -2462,10 +2593,14 @@ impl Pipeline {
|
||||
wires.len(),
|
||||
);
|
||||
}
|
||||
let circle_batches = self.upload_circles(device, queue, wires, depth_map);
|
||||
let ellipse_batches = self.upload_ellipses(device, queue, wires, depth_map);
|
||||
(
|
||||
std::sync::Arc::new(batches),
|
||||
std::sync::Arc::new(block_batches),
|
||||
std::sync::Arc::new(index),
|
||||
std::sync::Arc::new(circle_batches),
|
||||
std::sync::Arc::new(ellipse_batches),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2492,6 +2627,8 @@ impl Pipeline {
|
||||
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
||||
self.gpu_selected_wires = vec![];
|
||||
self.gpu_selected_block_wires = vec![];
|
||||
self.gpu_selected_circles = vec![];
|
||||
self.gpu_selected_ellipses = vec![];
|
||||
return;
|
||||
}
|
||||
// Gather borrowed highlighted wires via the prebuilt index —
|
||||
@@ -2504,6 +2641,9 @@ impl Pipeline {
|
||||
slots.sort_unstable();
|
||||
for &i in &slots {
|
||||
if let Some(w) = wires.get(i as usize) {
|
||||
if !w.display_visible {
|
||||
continue;
|
||||
}
|
||||
selected_wires.push(w);
|
||||
}
|
||||
}
|
||||
@@ -2515,27 +2655,103 @@ impl Pipeline {
|
||||
slots.sort_unstable();
|
||||
for &i in &slots {
|
||||
if let Some(w) = wires.get(i as usize) {
|
||||
if !w.display_visible {
|
||||
continue;
|
||||
}
|
||||
hover_wires.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for wire in annotation_context_wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
if wire.selected {
|
||||
selected_wires.push(wire);
|
||||
} else {
|
||||
hover_wires.push(wire);
|
||||
}
|
||||
}
|
||||
let mut selected_circles: Vec<CircleInstance> = Vec::new();
|
||||
let circle_sel_tint = selected_tint.unwrap_or(WireModel::SELECTED);
|
||||
for &wire in &selected_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
|
||||
for mut inst in insts {
|
||||
inst.color = circle_sel_tint;
|
||||
selected_circles.push(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
for &wire in &hover_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
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() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![CircleGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.selected_circles",
|
||||
&selected_circles,
|
||||
)]
|
||||
};
|
||||
|
||||
let mut selected_ellipses: Vec<EllipseInstance> = Vec::new();
|
||||
let ellipse_sel_tint = selected_tint.unwrap_or(WireModel::SELECTED);
|
||||
for &wire in &selected_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||
for mut inst in insts {
|
||||
inst.color = ellipse_sel_tint;
|
||||
selected_ellipses.push(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
for &wire in &hover_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
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() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![EllipseGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.selected_ellipses",
|
||||
&selected_ellipses,
|
||||
)]
|
||||
};
|
||||
|
||||
let selected_regular: Vec<&WireModel> = selected_wires
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|wire| wire.render_instance.is_none())
|
||||
.filter(|wire| {
|
||||
wire.render_instance.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
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|wire| wire.render_instance.is_none())
|
||||
.filter(|wire| {
|
||||
wire.render_instance.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
|
||||
.iter()
|
||||
@@ -2742,11 +2958,47 @@ impl Pipeline {
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) {
|
||||
self.gpu_preview_wires = if wires.is_empty() {
|
||||
vec![]
|
||||
if wires.is_empty() {
|
||||
self.gpu_preview_wires.clear();
|
||||
self.gpu_preview_circles.clear();
|
||||
self.gpu_preview_ellipses.clear();
|
||||
} else {
|
||||
WireGpu::from_run(device, queue, wires, depth_map, false, self.wire_const_bgl.as_ref())
|
||||
};
|
||||
let partitioned = wire_arena::partition_wires(wires, depth_map);
|
||||
let mut non_analytical = partitioned.regular;
|
||||
non_analytical.extend(partitioned.mesh);
|
||||
self.gpu_preview_wires = if non_analytical.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
WireGpu::from_run_refs(
|
||||
device,
|
||||
queue,
|
||||
&non_analytical,
|
||||
depth_map,
|
||||
false,
|
||||
self.wire_const_bgl.as_ref(),
|
||||
)
|
||||
};
|
||||
self.gpu_preview_circles = if partitioned.circle_instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![CircleGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"preview.circles",
|
||||
&partitioned.circle_instances,
|
||||
)]
|
||||
};
|
||||
self.gpu_preview_ellipses = if partitioned.ellipse_instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![EllipseGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"preview.ellipses",
|
||||
&partitioned.ellipse_instances,
|
||||
)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload the live grip-drag / command-preview SDF glyph quads. Re-uploaded
|
||||
@@ -4373,6 +4625,26 @@ impl Pipeline {
|
||||
}
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
// Analytical GPU circles (instanced screen-space quads)
|
||||
if self.gpu_circles.iter().any(|cg| cg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.circle_pipeline);
|
||||
for cg in self.gpu_circles.iter() {
|
||||
if cg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..cg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Analytical GPU ellipses (instanced screen-space quads)
|
||||
if self.gpu_ellipses.iter().any(|eg| eg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.ellipse_pipeline);
|
||||
for eg in self.gpu_ellipses.iter() {
|
||||
if eg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..eg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Live overlay wires (command preview / interim / grip drag) always
|
||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||
// keeps them visible through any occluding geometry — a 3D solid, or
|
||||
@@ -4393,6 +4665,24 @@ impl Pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.gpu_preview_circles.iter().any(|cg| cg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.circle_xray_pipeline);
|
||||
for cg in &self.gpu_preview_circles {
|
||||
if cg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..cg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.gpu_preview_ellipses.iter().any(|eg| eg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.ellipse_xray_pipeline);
|
||||
for eg in &self.gpu_preview_ellipses {
|
||||
if eg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..eg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 5c: SDF text quads (drawn over wires) ────────────────────
|
||||
@@ -4503,6 +4793,8 @@ impl Pipeline {
|
||||
|| !self.block_text_highlight_gpu.is_empty();
|
||||
if !self.gpu_selected_wires.is_empty()
|
||||
|| !self.gpu_selected_block_wires.is_empty()
|
||||
|| !self.gpu_selected_circles.is_empty()
|
||||
|| !self.gpu_selected_ellipses.is_empty()
|
||||
|| have_text_highlight
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
@@ -4563,6 +4855,34 @@ impl Pipeline {
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
}
|
||||
if !self.gpu_selected_circles.is_empty() {
|
||||
pass.set_pipeline(if hidden_line {
|
||||
&self.circle_pipeline
|
||||
} else {
|
||||
&self.circle_xray_pipeline
|
||||
});
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for cg in &self.gpu_selected_circles {
|
||||
if cg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..cg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.gpu_selected_ellipses.is_empty() {
|
||||
pass.set_pipeline(if hidden_line {
|
||||
&self.ellipse_pipeline
|
||||
} else {
|
||||
&self.ellipse_xray_pipeline
|
||||
});
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for eg in &self.gpu_selected_ellipses {
|
||||
if eg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..eg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(atlas) = &self.text_atlas_gpu {
|
||||
if !self.text_highlight_gpu.is_empty() {
|
||||
pass.set_pipeline(&self.text_highlight_pipeline);
|
||||
@@ -4648,12 +4968,7 @@ impl Pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// Forget every cache key that describes this slot's uploaded content.
|
||||
///
|
||||
/// Shared by slot reuse and by [`Self::release_heavy_resources`] so the two
|
||||
/// cannot disagree about what "this slot holds nothing you can trust"
|
||||
/// means. Keys only — the buffers themselves are dropped by the caller that
|
||||
/// wants the memory back.
|
||||
/// Reset every cache key that describes this slot's uploaded content.
|
||||
pub(crate) fn forget_cached_keys(&mut self) {
|
||||
self.cached_epoch = (u64::MAX, u64::MAX, u64::MAX);
|
||||
self.cached_wire_id = u64::MAX;
|
||||
@@ -4676,6 +4991,9 @@ impl Pipeline {
|
||||
self.silhouette_key = (usize::MAX, u64::MAX, [u32::MAX; 3], false);
|
||||
self.silhouette_source_key = (usize::MAX, usize::MAX, u64::MAX);
|
||||
self.render_sig = u64::MAX;
|
||||
// The retained-upload record is part of the slot's cached state.
|
||||
self.partition_contributors.clear();
|
||||
self.partition_depth_generation = u64::MAX;
|
||||
}
|
||||
|
||||
/// Catch errors delivered after the previous upload/submit completed.
|
||||
@@ -4689,6 +5007,8 @@ impl Pipeline {
|
||||
self.wire_arena = None;
|
||||
self.wire_arena_mesh = None;
|
||||
self.wire_arena_id = u64::MAX;
|
||||
self.partition_contributors.clear();
|
||||
self.partition_depth_generation = u64::MAX;
|
||||
self.alloc_size = Size::new(0, 0);
|
||||
self.shadow_full = None;
|
||||
self.background_source_id = usize::MAX;
|
||||
@@ -4711,6 +5031,8 @@ impl Pipeline {
|
||||
|
||||
self.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||
self.wire_handle_index = std::sync::Arc::new(rustc_hash::FxHashMap::default());
|
||||
self.wire_arena = None;
|
||||
self.wire_arena_mesh = None;
|
||||
@@ -4718,10 +5040,16 @@ impl Pipeline {
|
||||
self.wire_arena_fallback_kind = None;
|
||||
self.wire_arena_fallback_handles.clear();
|
||||
self.wire_arena_id = u64::MAX;
|
||||
self.partition_contributors.clear();
|
||||
self.partition_depth_generation = u64::MAX;
|
||||
|
||||
self.gpu_selected_wires.clear();
|
||||
self.gpu_selected_block_wires.clear();
|
||||
self.gpu_selected_circles.clear();
|
||||
self.gpu_selected_ellipses.clear();
|
||||
self.gpu_preview_wires.clear();
|
||||
self.gpu_preview_circles.clear();
|
||||
self.gpu_preview_ellipses.clear();
|
||||
self.hatch_gpu.clear();
|
||||
self.gpu_wipeouts.clear();
|
||||
self.wipeout_skip_flags.clear();
|
||||
@@ -4990,6 +5318,7 @@ impl MultiPipeline {
|
||||
self.wire_buffer_cache.retain(|_, entry| {
|
||||
std::sync::Arc::strong_count(&entry.0) > 1
|
||||
|| std::sync::Arc::strong_count(&entry.1) > 1
|
||||
|| std::sync::Arc::strong_count(&entry.3) > 1
|
||||
});
|
||||
self.block_geometry.retain(|_, chunks| {
|
||||
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
||||
@@ -5140,6 +5469,8 @@ pub struct MultiPipeline {
|
||||
std::sync::Arc<Vec<WireGpu>>,
|
||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||
std::sync::Arc<Vec<CircleGpu>>,
|
||||
std::sync::Arc<Vec<EllipseGpu>>,
|
||||
),
|
||||
>,
|
||||
}
|
||||
|
||||
@@ -120,15 +120,152 @@ fn handle_of(w: &WireModel) -> Option<Handle> {
|
||||
crate::scene::Scene::handle_from_wire_name(&w.name)
|
||||
}
|
||||
|
||||
/// Classification of a wire model for rendering pipelines.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WireKind {
|
||||
Invisible,
|
||||
Block,
|
||||
Circle,
|
||||
Ellipse,
|
||||
Regular,
|
||||
MeshEdge,
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// Fast, branch-predicted classification of a wire model.
|
||||
///
|
||||
/// Bypasses analytical circle/ellipse extraction for the 95%+ of wires that have
|
||||
/// no tangent geometry or multiple tangent primitives, reducing overhead to ~2 ns.
|
||||
#[inline]
|
||||
pub fn classify_wire(wire: &WireModel) -> WireKind {
|
||||
if !wire.display_visible {
|
||||
return WireKind::Invisible;
|
||||
}
|
||||
if wire.render_instance.is_some() {
|
||||
return WireKind::Block;
|
||||
}
|
||||
if !wire.tangent_geoms.is_empty()
|
||||
&& wire.fill_tris.is_empty()
|
||||
&& wire.pick_tris.is_empty()
|
||||
&& wire.text_verts.is_empty()
|
||||
{
|
||||
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() {
|
||||
if wire.fill_is_3d {
|
||||
WireKind::MeshEdge
|
||||
} else {
|
||||
WireKind::Regular
|
||||
}
|
||||
} else {
|
||||
WireKind::Empty
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a single-pass partitioning of viewport wires.
|
||||
pub struct PartitionedWires<'a> {
|
||||
pub regular: Vec<&'a WireModel>,
|
||||
pub mesh: Vec<&'a WireModel>,
|
||||
pub instanced: Vec<&'a WireModel>,
|
||||
pub circle_instances: Vec<super::circle_gpu::CircleInstance>,
|
||||
pub ellipse_instances: Vec<super::ellipse_gpu::EllipseInstance>,
|
||||
/// Handles that contributed to the retained analytical uploads.
|
||||
pub contributors: rustc_hash::FxHashSet<Handle>,
|
||||
}
|
||||
|
||||
/// Whether this wire contributes to an upload retained across arena patches.
|
||||
pub fn feeds_analytical_uploads(wire: &WireModel) -> bool {
|
||||
matches!(
|
||||
classify_wire(wire),
|
||||
WireKind::Block | WireKind::Circle | WireKind::Ellipse
|
||||
)
|
||||
}
|
||||
|
||||
/// Single-pass classification and extraction of all viewport wire categories.
|
||||
///
|
||||
/// Traverses the wire slice exactly once, eliminating redundant passes and duplicate
|
||||
/// analytical extractions across split_wires, upload_block_wires, upload_circles,
|
||||
/// and upload_ellipses.
|
||||
pub fn partition_wires<'a>(
|
||||
wires: &'a [WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) -> PartitionedWires<'a> {
|
||||
let mut regular = Vec::new();
|
||||
let mut mesh = Vec::new();
|
||||
let mut instanced = Vec::new();
|
||||
let mut circle_instances = Vec::new();
|
||||
let mut ellipse_instances = Vec::new();
|
||||
let mut contributors: rustc_hash::FxHashSet<Handle> = rustc_hash::FxHashSet::default();
|
||||
let note = |wire: &WireModel, set: &mut rustc_hash::FxHashSet<Handle>| {
|
||||
if let Some(handle) = handle_of(wire) {
|
||||
set.insert(handle);
|
||||
}
|
||||
};
|
||||
|
||||
for wire in wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
if wire.render_instance.is_some() {
|
||||
note(wire, &mut contributors);
|
||||
instanced.push(wire);
|
||||
continue;
|
||||
}
|
||||
// Resolve draw depth only for wires eligible for analytical extraction.
|
||||
if !wire.tangent_geoms.is_empty()
|
||||
&& wire.fill_tris.is_empty()
|
||||
&& wire.pick_tris.is_empty()
|
||||
&& wire.text_verts.is_empty()
|
||||
{
|
||||
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(insts) = super::circle_gpu::extract_circle_instances(wire, depth) {
|
||||
note(wire, &mut contributors);
|
||||
circle_instances.extend(insts);
|
||||
continue;
|
||||
}
|
||||
if let Some(insts) = super::ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||
note(wire, &mut contributors);
|
||||
ellipse_instances.extend(insts);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !wire.points.is_empty() {
|
||||
if wire.fill_is_3d {
|
||||
mesh.push(wire);
|
||||
} else {
|
||||
regular.push(wire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PartitionedWires {
|
||||
regular,
|
||||
mesh,
|
||||
instanced,
|
||||
circle_instances,
|
||||
ellipse_instances,
|
||||
contributors,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the same regular/mesh partition for full uploads and changed runs.
|
||||
/// Instanced blocks are uploaded separately by `BlockWireGpu`.
|
||||
pub fn split_wires(wires: &[WireModel]) -> (Vec<&WireModel>, Vec<&WireModel>) {
|
||||
wires
|
||||
.iter()
|
||||
.filter(|wire| {
|
||||
wire.display_visible && !wire.points.is_empty() && wire.render_instance.is_none()
|
||||
})
|
||||
.partition(|wire| !wire.fill_is_3d)
|
||||
let mut regular = Vec::new();
|
||||
let mut mesh = Vec::new();
|
||||
for wire in wires {
|
||||
match classify_wire(wire) {
|
||||
WireKind::Regular => regular.push(wire),
|
||||
WireKind::MeshEdge => mesh.push(wire),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(regular, mesh)
|
||||
}
|
||||
|
||||
/// True when appending a new entity at the tail could change the image, so the
|
||||
@@ -1528,4 +1665,48 @@ mod tests {
|
||||
assert!(blank_const().pattern_length < 0.0);
|
||||
assert!(blank_packed_instance().pattern_length < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_wires_partitions_preview_analytical_curves() {
|
||||
use crate::scene::model::wire_model::TangentGeom;
|
||||
let mut circle_preview = WireModel::default();
|
||||
circle_preview.tangent_geoms.push(TangentGeom::PlanarCircle {
|
||||
center: [1.0, 2.0, 3.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
});
|
||||
|
||||
let mut arc_preview = WireModel::default();
|
||||
arc_preview.tangent_geoms.push(TangentGeom::Arc {
|
||||
center: [4.0, 5.0, 6.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 8.0,
|
||||
start_angle: 0.1,
|
||||
end_angle: 2.0,
|
||||
});
|
||||
|
||||
let mut ellipse_preview = WireModel::default();
|
||||
ellipse_preview.tangent_geoms.push(TangentGeom::PlanarEllipse {
|
||||
center: [7.0, 8.0, 9.0],
|
||||
major_axis: [5.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.6,
|
||||
start_param: 0.0,
|
||||
end_param: std::f64::consts::TAU,
|
||||
});
|
||||
|
||||
let mut regular_line = WireModel::default();
|
||||
regular_line.points.push([0.0, 0.0, 0.0]);
|
||||
regular_line.points.push([1.0, 1.0, 1.0]);
|
||||
|
||||
let wires = vec![circle_preview, arc_preview, ellipse_preview, regular_line];
|
||||
let depth_map = rustc_hash::FxHashMap::default();
|
||||
let partitioned = partition_wires(&wires, &depth_map);
|
||||
|
||||
assert_eq!(partitioned.circle_instances.len(), 2);
|
||||
assert_eq!(partitioned.ellipse_instances.len(), 1);
|
||||
assert_eq!(partitioned.regular.len(), 1);
|
||||
}
|
||||
}
|
||||
+114
-11
@@ -360,19 +360,62 @@ impl Scene {
|
||||
self.selected.len()
|
||||
}
|
||||
|
||||
/// Sorted entity types in the current layout, cached until geometry or layout changes.
|
||||
/// Entity type names in the current layout for the selection-filter menu.
|
||||
/// Pure additions are folded into the cached set; other changes rebuild it.
|
||||
pub fn entity_type_names_in_layout(&self) -> std::sync::Arc<Vec<String>> {
|
||||
use crate::entities::traits::entity_type_name;
|
||||
let block = self.current_layout_block_handle();
|
||||
let mut cached_epoch = None;
|
||||
{
|
||||
let cache = self.layout_type_names_cache.borrow();
|
||||
if let Some((epoch, cached_block, names)) = cache.as_ref() {
|
||||
if *epoch == self.geometry_epoch && *cached_block == block {
|
||||
return std::sync::Arc::clone(names);
|
||||
if let Some((epoch, cached_block, _, names)) = cache.as_ref() {
|
||||
if *cached_block == block {
|
||||
if *epoch == self.geometry_epoch {
|
||||
return std::sync::Arc::clone(names);
|
||||
}
|
||||
cached_epoch = Some(*epoch);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut names: std::collections::BTreeSet<&str> =
|
||||
|
||||
// Incremental: fold the changes since the cached epoch into the set.
|
||||
if let Some(since) = cached_epoch {
|
||||
if let Some(deltas) = self.replay_since(since) {
|
||||
if deltas
|
||||
.iter()
|
||||
.all(|(_, kind)| *kind == ChangeKind::Added)
|
||||
{
|
||||
let mut cache = self.layout_type_names_cache.borrow_mut();
|
||||
if let Some((epoch, _, present, names)) = cache.as_mut() {
|
||||
let mut added = false;
|
||||
for (handle, _) in &deltas {
|
||||
if let Some(entity) = self.document.get_entity(*handle) {
|
||||
if entity.common().owner_handle == block {
|
||||
// `contains` first: the common case is a
|
||||
// type already present, and that path must
|
||||
// not allocate.
|
||||
let name = entity_type_name(entity);
|
||||
if !present.contains(name) {
|
||||
present.insert(name.to_string());
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only rebuild the list when the set actually moved;
|
||||
// otherwise the existing `Arc` is still the answer.
|
||||
if added {
|
||||
*names =
|
||||
std::sync::Arc::new(present.iter().cloned().collect());
|
||||
}
|
||||
*epoch = self.geometry_epoch;
|
||||
return std::sync::Arc::clone(names);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut present: std::collections::BTreeSet<String> =
|
||||
std::collections::BTreeSet::new();
|
||||
if let Some(record) = self
|
||||
.document
|
||||
@@ -382,15 +425,21 @@ impl Scene {
|
||||
{
|
||||
for &handle in &record.entity_handles {
|
||||
if let Some(entity) = self.document.get_entity(handle) {
|
||||
names.insert(entity_type_name(entity));
|
||||
let name = entity_type_name(entity);
|
||||
if !present.contains(name) {
|
||||
present.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let names: std::sync::Arc<Vec<String>> = std::sync::Arc::new(
|
||||
names.into_iter().map(str::to_string).collect(),
|
||||
);
|
||||
*self.layout_type_names_cache.borrow_mut() =
|
||||
Some((self.geometry_epoch, block, std::sync::Arc::clone(&names)));
|
||||
let names: std::sync::Arc<Vec<String>> =
|
||||
std::sync::Arc::new(present.iter().cloned().collect());
|
||||
*self.layout_type_names_cache.borrow_mut() = Some((
|
||||
self.geometry_epoch,
|
||||
block,
|
||||
present,
|
||||
std::sync::Arc::clone(&names),
|
||||
));
|
||||
names
|
||||
}
|
||||
|
||||
@@ -954,6 +1003,60 @@ impl Scene {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adding_a_type_already_present_reuses_the_list() {
|
||||
use acadrust::entities::{Circle, EntityType, Line};
|
||||
use acadrust::types::Vector3;
|
||||
use std::sync::Arc;
|
||||
|
||||
let line = || {
|
||||
EntityType::Line(Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
))
|
||||
};
|
||||
let mut scene = Scene::new();
|
||||
scene.add_entity(line());
|
||||
let first = scene.entity_type_names_in_layout();
|
||||
|
||||
scene.add_entity(line());
|
||||
let second = scene.entity_type_names_in_layout();
|
||||
assert!(
|
||||
Arc::ptr_eq(&first, &second),
|
||||
"a second line changes no type name, so the list must be reused",
|
||||
);
|
||||
|
||||
scene.add_entity(EntityType::Circle(Circle::new()));
|
||||
let third = scene.entity_type_names_in_layout();
|
||||
assert_eq!(third.as_slice(), ["Circle", "Line"], "a new type must appear");
|
||||
|
||||
// The incremental answer has to be the answer a full walk gives.
|
||||
scene.layout_type_names_cache.borrow_mut().take();
|
||||
assert_eq!(
|
||||
scene.entity_type_names_in_layout().as_slice(),
|
||||
third.as_slice(),
|
||||
"folding edits in must match rebuilding from scratch",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_an_entity_type_rebuilds_the_type_names() {
|
||||
use acadrust::entities::{Circle, EntityType, Line};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let handle = scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
)));
|
||||
assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Line"]);
|
||||
|
||||
let mut circle = Circle::new();
|
||||
circle.common.handle = handle;
|
||||
assert!(scene.update_entity(EntityType::Circle(circle)));
|
||||
assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Circle"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_type_cache_reuses_and_invalidates_on_edits_undo_and_layout() {
|
||||
use acadrust::entities::{Circle, EntityType, Line};
|
||||
|
||||
@@ -391,8 +391,13 @@ impl GlyphAtlas {
|
||||
|
||||
let (fw, fh) = (self.width as f32, self.height as f32);
|
||||
Some(AtlasEntry {
|
||||
uv_min: [ox as f32 / fw, oy as f32 / fh],
|
||||
uv_max: [(ox + tile.w) as f32 / fw, (oy + tile.h) as f32 / fh],
|
||||
// Sample the centers of the edge texels rather than the tile boundaries.
|
||||
// This prevents bilinear filtering from bleeding into neighbouring tiles.
|
||||
uv_min: [(ox as f32 + 0.5) / fw, (oy as f32 + 0.5) / fh],
|
||||
uv_max: [
|
||||
((ox + tile.w) as f32 - 0.5) / fw,
|
||||
((oy + tile.h) as f32 - 0.5) / fh,
|
||||
],
|
||||
plane_min: tile.plane_min,
|
||||
plane_max: tile.plane_max,
|
||||
advance: tile.advance,
|
||||
|
||||
+90
-16
@@ -149,6 +149,9 @@ pub struct ViewportData {
|
||||
/// by the wire / face3d pipelines as a clip-z bias. WireModels carry no
|
||||
/// depth field (84 construction sites); the bias is looked up by handle
|
||||
/// at GPU-upload time from this map instead.
|
||||
/// See `Scene::draw_depth_generation`. Cached uploads that bake a depth are
|
||||
/// only reusable while this is unchanged.
|
||||
pub(in crate::scene) draw_depth_generation: u64,
|
||||
pub(in crate::scene) draw_depths:
|
||||
std::sync::Weak<rustc_hash::FxHashMap<u64, [f32; 2]>>,
|
||||
pub(in crate::scene) hatches: Arc<Vec<HatchModel>>,
|
||||
@@ -839,20 +842,83 @@ impl shader::Primitive for Primitive {
|
||||
gpus.extend(arena.wire_gpus());
|
||||
}
|
||||
inner.gpu_wires = std::sync::Arc::new(gpus);
|
||||
// Blocks keep their own cached geometry and placements.
|
||||
let instanced: Vec<&crate::scene::WireModel> = vp_wires
|
||||
.iter()
|
||||
.filter(|w| w.render_instance.is_some())
|
||||
.collect();
|
||||
inner.gpu_block_wires = std::sync::Arc::new(
|
||||
inner.upload_block_wires(
|
||||
device,
|
||||
queue,
|
||||
&instanced,
|
||||
&draw_depths,
|
||||
&mut pipeline.block_geometry,
|
||||
),
|
||||
);
|
||||
// Retain analytical uploads when a patch changes neither
|
||||
// their contributors nor their baked draw depths.
|
||||
let analytical_untouched = _patched
|
||||
&& inner.partition_depth_generation == vp.draw_depth_generation
|
||||
&& patch.is_some_and(|patch| {
|
||||
patch.changes.iter().all(|(handle, _)| {
|
||||
!inner.partition_contributors.contains(handle)
|
||||
&& !patch.runs.get(handle).is_some_and(|run| {
|
||||
run.iter()
|
||||
.any(wire_arena::feeds_analytical_uploads)
|
||||
})
|
||||
})
|
||||
});
|
||||
let t_part = _perf.then(iced::time::Instant::now);
|
||||
let partitioned = (!analytical_untouched)
|
||||
.then(|| wire_arena::partition_wires(&vp_wires, &draw_depths));
|
||||
let part_ms = t_part
|
||||
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t_blk = _perf.then(iced::time::Instant::now);
|
||||
let mut blk_ms = 0.0f64;
|
||||
let mut curve_ms = 0.0f64;
|
||||
// `None` means the three uploads this slot holds are
|
||||
// still the answer, so they are left alone.
|
||||
if let Some(partitioned) = &partitioned {
|
||||
inner.gpu_block_wires = std::sync::Arc::new(
|
||||
inner.upload_block_wires(
|
||||
device,
|
||||
queue,
|
||||
&partitioned.instanced,
|
||||
&draw_depths,
|
||||
&mut pipeline.block_geometry,
|
||||
),
|
||||
);
|
||||
blk_ms = t_blk
|
||||
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t_curve = _perf.then(iced::time::Instant::now);
|
||||
inner.gpu_circles = std::sync::Arc::new(
|
||||
inner.upload_circles_from_instances(
|
||||
device,
|
||||
queue,
|
||||
&partitioned.circle_instances,
|
||||
),
|
||||
);
|
||||
inner.gpu_ellipses = std::sync::Arc::new(
|
||||
inner.upload_ellipses_from_instances(
|
||||
device,
|
||||
queue,
|
||||
&partitioned.ellipse_instances,
|
||||
),
|
||||
);
|
||||
inner
|
||||
.partition_contributors
|
||||
.clone_from(&partitioned.contributors);
|
||||
inner.partition_depth_generation = vp.draw_depth_generation;
|
||||
curve_ms = t_curve
|
||||
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
if _perf {
|
||||
// Report counts only when partitioning ran.
|
||||
match &partitioned {
|
||||
Some(partitioned) => crate::perf_record!(
|
||||
"[perf] arena-post partition={part_ms:.1}ms \
|
||||
blocks={blk_ms:.1}ms curves={curve_ms:.1}ms skipped=false wires={} instanced={} \
|
||||
circles={} ellipses={}",
|
||||
vp_wires.len(),
|
||||
partitioned.instanced.len(),
|
||||
partitioned.circle_instances.len(),
|
||||
partitioned.ellipse_instances.len(),
|
||||
),
|
||||
None => crate::perf_record!(
|
||||
"[perf] arena-post skipped=true wires={} \
|
||||
retained_contributors={}",
|
||||
vp_wires.len(),
|
||||
inner.partition_contributors.len(),
|
||||
),
|
||||
}
|
||||
}
|
||||
if _patched {
|
||||
wire_arena::patch_handle_index(
|
||||
&mut inner.wire_handle_index,
|
||||
@@ -911,10 +977,14 @@ impl shader::Primitive for Primitive {
|
||||
// Other panes retain their own references to shared geometry.
|
||||
inner.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||
let held_before = pipeline.wire_buffer_cache.len();
|
||||
pipeline.wire_buffer_cache.retain(|_, (w, b, _)| {
|
||||
pipeline.wire_buffer_cache.retain(|_, (w, b, _, c, e)| {
|
||||
std::sync::Arc::strong_count(w) > 1
|
||||
|| std::sync::Arc::strong_count(b) > 1
|
||||
|| std::sync::Arc::strong_count(c) > 1
|
||||
|| std::sync::Arc::strong_count(e) > 1
|
||||
});
|
||||
if _perf {
|
||||
crate::perf_record!(
|
||||
@@ -959,6 +1029,8 @@ impl shader::Primitive for Primitive {
|
||||
inner.gpu_wires = built.0;
|
||||
inner.gpu_block_wires = built.1;
|
||||
inner.wire_handle_index = built.2;
|
||||
inner.gpu_circles = built.3;
|
||||
inner.gpu_ellipses = built.4;
|
||||
} // end !arena_served
|
||||
inner.cached_wire_id = vp.wire_content_id;
|
||||
if _perf {
|
||||
@@ -1199,8 +1271,9 @@ impl shader::Primitive for Primitive {
|
||||
let prepare_ms = nav_prepare_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if let Some(sample) = self.nav_perf {
|
||||
crate::perf::record(format_args!(
|
||||
"[perf] nav-prepare op={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}",
|
||||
"[perf] nav-prepare op={} cause={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}",
|
||||
sample.op.label(),
|
||||
sample.cause,
|
||||
sample.space,
|
||||
sample.mode,
|
||||
sample.input_ms,
|
||||
@@ -4221,6 +4294,7 @@ impl Scene {
|
||||
face3d_wires,
|
||||
text_verts,
|
||||
preview_text_verts,
|
||||
draw_depth_generation: self.draw_depth_generation(),
|
||||
draw_depths: Arc::downgrade(&draw_depths),
|
||||
hatches,
|
||||
wipeout_hatches,
|
||||
|
||||
Reference in new issue
Block a user