forked from 1b-agent/opencadstudio-upstream
Merge branch 'main' into feature/parametric-constraint-system
Brings in 54 commits from origin/main: a selection-performance overhaul, surface thickening, GPU-native wide/tapered stroke rendering, continued ocs_doc_api evolution, and assorted fixes/polish (block double-click editing, DWG hatch/database repair, clipboard, collapsible Properties sections). Six real conflicts, all where this branch had independently re-applied a fix upstream later revised or already carried natively: - src/app/automation.rs, src/app/command_driver.rs: combined both sides — kept this branch's sketch-constraint/named-parameter loading on "new"/"open", adopted main's Scene::clear()/material_base_dir and its newer rebuild_derived_caches() over the older bump_geometry(). Scene::clear() (src/scene/entity.rs) now also resets sketch_constraints/named_parameters, so every caller gets this for free instead of each call site repeating it. - src/io/mod.rs, src/scene/render_graph.rs: adopted main's design outright (commit973af87d, authored by Hakan directly) — a large MINSERT is no longer purged as "corrupt" at load, only its rendering is bounded, preserving the source data. This branch's own earlier design (reject at load) predates that revision. - src/scene/pipeline/mesh_gpu.rs: adopted main's valid_rgba_size() guard (a real robustness improvement over this branch's version). - src/snap.rs: replaced wholesale with main's version — this branch's copy was an explicit verbatim snapshot from an earlier upstream PR (commit4c264b8c) with zero constraint-system-specific content, and main has since evolved it considerably further. Also removed two now-dead duplicates the merge surfaced without a conflict marker (both branches had independently fixed the same things): the OPTIONS/OP command dispatch/autocomplete entries, and automation.rs's open_finalizes_and_purges_like_the_ui_open_path test (kept main's version, which covers strictly more: solid_models/ material_base_dir reset on "new"). Full workspace suite passes (812 lib tests + this branch's own 24 constraint/persistence integration tests) except the one pre-existing, unrelated font-test failure already present on main. Zero clippy findings in the hand-written merge resolution itself; the ~1,171 warnings clippy reports are pre-existing in the 54 absorbed commits, not introduced here. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
commit
d675244fb8
89 files changed
+5700
-1101
No files matched your search
+117
-16
@@ -13,7 +13,7 @@ use iced::{Point, Rectangle};
|
||||
use crate::scene::model::hatch_model::HatchModel;
|
||||
use crate::scene::model::mesh_model::MeshModel;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::scene::pick::interaction_index::WireSource;
|
||||
use crate::scene::pick::interaction_index::{SegmentRef, WireSource};
|
||||
|
||||
/// Pick radius for one wire, in screen pixels.
|
||||
///
|
||||
@@ -1247,13 +1247,14 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
|| (0..4).any(|edge| segments_intersect(a, b, corners[edge], corners[(edge + 1) % 4]))
|
||||
};
|
||||
|
||||
for segment in wires.segments().unwrap_or_default() {
|
||||
let Some(wire) = wires.source_wire(segment.wire) else {
|
||||
continue;
|
||||
};
|
||||
// One projection pair per segment, reading nothing but the wire, the view
|
||||
// and the box. Hoisted into a closure so the sequential and the parallel
|
||||
// path below run identical code.
|
||||
let hit_name = |segment: &SegmentRef| -> Option<&'a str> {
|
||||
let wire = wires.source_wire(segment.wire)?;
|
||||
let start = segment.start as usize;
|
||||
if start + 1 >= wire.points.len() {
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
let a = world_to_screen(
|
||||
wire_point_world(wire, start, view_rot, eye),
|
||||
@@ -1267,8 +1268,30 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
if segment_hits(a, b) && seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
segment_hits(a, b).then_some(wire.name.as_str())
|
||||
};
|
||||
|
||||
let mut wire_hit: Vec<bool> = Vec::new();
|
||||
fn mark(wire_hit: &mut Vec<bool>, wire: u32) {
|
||||
let index = wire as usize;
|
||||
if index >= wire_hit.len() {
|
||||
wire_hit.resize(index + 1, false);
|
||||
}
|
||||
wire_hit[index] = true;
|
||||
}
|
||||
fn already(wire_hit: &[bool], wire: u32) -> bool {
|
||||
wire_hit.get(wire as usize).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
for segment in wires.segments().unwrap_or_default() {
|
||||
if already(&wire_hit, segment.wire) {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = hit_name(segment) {
|
||||
mark(&mut wire_hit, segment.wire);
|
||||
if seen.insert(name) {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
|
||||
@@ -1287,7 +1310,7 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
let Some(wire) = wires.source_wire(triangle.wire) else {
|
||||
continue;
|
||||
};
|
||||
if seen.contains(wire.name.as_str()) {
|
||||
if already(&wire_hit, triangle.wire) {
|
||||
continue;
|
||||
}
|
||||
if projected_wire_triangle(
|
||||
@@ -1299,9 +1322,11 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
bounds,
|
||||
)
|
||||
.is_some_and(|triangle| triangle_crosses_box(triangle, corners))
|
||||
&& seen.insert(wire.name.as_str())
|
||||
{
|
||||
out.push(wire.name.as_str());
|
||||
mark(&mut wire_hit, triangle.wire);
|
||||
if seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1309,7 +1334,7 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
let Some(wire) = wires.source_wire(glyph.wire) else {
|
||||
continue;
|
||||
};
|
||||
if seen.contains(wire.name.as_str()) {
|
||||
if already(&wire_hit, glyph.wire) {
|
||||
continue;
|
||||
}
|
||||
let Some(screen) =
|
||||
@@ -1330,11 +1355,11 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
|
||||
// Degenerate point-only wires have no indexed segment or surface primitive.
|
||||
for wire in wires.iter() {
|
||||
if seen.contains(wire.name.as_str())
|
||||
|| wire.points.len() >= 2
|
||||
if wire.points.len() >= 2
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| !wire.pick_tris.is_empty()
|
||||
|| !wire.text_verts.is_empty()
|
||||
|| seen.contains(wire.name.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1540,11 +1565,11 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
}
|
||||
}
|
||||
for wire in wires.iter() {
|
||||
if seen.contains(wire.name.as_str())
|
||||
|| wire.points.len() >= 2
|
||||
if wire.points.len() >= 2
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| !wire.pick_tris.is_empty()
|
||||
|| !wire.text_verts.is_empty()
|
||||
|| seen.contains(wire.name.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2712,3 +2737,79 @@ mod aabb_reject_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parallel_selection_tests {
|
||||
use super::*;
|
||||
use crate::scene::pick::interaction_index::{InteractionCandidates, InteractionIndex};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn many_wires_with_spans(count: usize, spans: usize) -> Vec<WireModel> {
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let t = i as f32 / count as f32;
|
||||
let x = -0.95 + 1.9 * t;
|
||||
let y = -0.95 + 1.9 * t;
|
||||
let points: Vec<[f32; 3]> = (0..=spans)
|
||||
.map(|s| {
|
||||
let f = s as f32 / spans as f32;
|
||||
[x + 0.01 * f, y + 0.01 * f, 0.0]
|
||||
})
|
||||
.collect();
|
||||
let mut w = WireModel::solid(
|
||||
(i as u64 + 1).to_string(),
|
||||
points,
|
||||
[1.0; 4],
|
||||
false,
|
||||
);
|
||||
w.aabb = [x, y, x + 0.01, y + 0.01];
|
||||
w
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn selection_scaling_numbers() {
|
||||
use std::time::Instant;
|
||||
let wires = many_wires_with_spans(186_468, 24);
|
||||
let index = InteractionIndex::build(&wires);
|
||||
let arc = Arc::new(wires);
|
||||
let aabb = [-2.0, -2.0, 2.0, 2.0];
|
||||
|
||||
let t = Instant::now();
|
||||
let full = index.query_xy(Arc::clone(&arc), aabb);
|
||||
let full_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let t = Instant::now();
|
||||
let area = index.query_xy_area(Arc::clone(&arc), aabb);
|
||||
let area_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
};
|
||||
let run = |candidates: &InteractionCandidates| {
|
||||
let t = Instant::now();
|
||||
let hits = box_hit(
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(200.0, 200.0),
|
||||
true,
|
||||
candidates,
|
||||
Mat4::IDENTITY,
|
||||
glam::DVec3::ZERO,
|
||||
bounds,
|
||||
);
|
||||
(t.elapsed().as_secs_f64() * 1000.0, hits.len())
|
||||
};
|
||||
let (scan_ms, scan_n) = run(&area);
|
||||
|
||||
println!(
|
||||
"candidates: full={full_ms:.1}ms ({} wires) area={area_ms:.1}ms ({} wires)",
|
||||
full.len(),
|
||||
area.len(),
|
||||
);
|
||||
println!("crossing scan: {scan_ms:.1}ms ({scan_n} hits)");
|
||||
}
|
||||
}
|
||||
@@ -458,6 +458,62 @@ impl SpatialGrid {
|
||||
}
|
||||
}
|
||||
|
||||
fn query_unordered<T: Copy>(&self, entries: &[Entry3<T>], query: [f64; 4]) -> Vec<T> {
|
||||
let mut indices = Vec::new();
|
||||
self.oversized.query(entries, query, &mut indices);
|
||||
let mut out: Vec<T> = indices
|
||||
.into_iter()
|
||||
.filter_map(|idx| {
|
||||
let entry = entries.get(idx as usize)?;
|
||||
aabb_overlaps(entry_aabb2(entry), query).then_some(entry.value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let grid_max_x = self.min[0] + self.cols as f64 * self.cell;
|
||||
let grid_max_y = self.min[1] + self.rows as f64 * self.cell;
|
||||
if query[2] < self.min[0]
|
||||
|| query[3] < self.min[1]
|
||||
|| query[0] > grid_max_x
|
||||
|| query[1] > grid_max_y
|
||||
{
|
||||
return out;
|
||||
}
|
||||
let col = |x: f64| {
|
||||
(((x - self.min[0]) / self.cell).floor()).clamp(0.0, (self.cols - 1) as f64) as u32
|
||||
};
|
||||
let row = |y: f64| {
|
||||
(((y - self.min[1]) / self.cell).floor()).clamp(0.0, (self.rows - 1) as f64) as u32
|
||||
};
|
||||
let (row_lo, row_hi) = (row(query[1]), row(query[3]));
|
||||
let (col_lo, col_hi) = (col(query[0]), col(query[2]));
|
||||
|
||||
// One growth instead of a chain of reallocations: a box over the whole
|
||||
// drawing lands nearly every entry here.
|
||||
let spanned: usize = (row_lo..=row_hi)
|
||||
.map(|r| {
|
||||
let base = r as usize * self.cols as usize;
|
||||
let start = self.cell_offsets[base + col_lo as usize] as usize;
|
||||
let end = self.cell_offsets[base + col_hi as usize + 1] as usize;
|
||||
end.saturating_sub(start)
|
||||
})
|
||||
.sum();
|
||||
out.reserve(spanned);
|
||||
|
||||
for r in row_lo..=row_hi {
|
||||
let base = r as usize * self.cols as usize;
|
||||
for c in col_lo..=col_hi {
|
||||
let cell_idx = base + c as usize;
|
||||
let start = self.cell_offsets[cell_idx] as usize;
|
||||
let end = self.cell_offsets[cell_idx + 1] as usize;
|
||||
out.extend(self.cell_entries[start..end].iter().filter_map(|&idx| {
|
||||
let entry = entries.get(idx as usize)?;
|
||||
aabb_overlaps(entry_aabb2(entry), query).then_some(entry.value)
|
||||
}));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn query<T: Copy + Ord>(&self, entries: &[Entry3<T>], query: [f64; 4]) -> Vec<T> {
|
||||
let mut entry_indices = Vec::new();
|
||||
self.oversized.query(entries, query, &mut entry_indices);
|
||||
@@ -520,6 +576,11 @@ impl<T: Copy + Ord + Sync> SpatialSet<T> {
|
||||
self.xy.query(&self.entries, aabb)
|
||||
}
|
||||
|
||||
/// See [`SpatialGrid::query_unordered`] — same set, no sort, duplicates in.
|
||||
fn query_xy_unordered(&self, aabb: [f64; 4]) -> Vec<T> {
|
||||
self.xy.query_unordered(&self.entries, aabb)
|
||||
}
|
||||
|
||||
fn prepare_screen(&self) {
|
||||
self.xyz
|
||||
.get_or_init(|| SpatialBvh3::build(&self.entries));
|
||||
@@ -839,17 +900,17 @@ impl InteractionIndex {
|
||||
let (
|
||||
((segment_entries, snap_point_entries), (key_vertex_entries, key_segment_entries)),
|
||||
((fill_triangle_entries, pick_triangle_entries), glyph_entries),
|
||||
) = rayon::join(
|
||||
) = crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| flatten_entry_parts(segment_parts),
|
||||
|| flatten_entry_parts(snap_point_parts),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| flatten_entry_parts(key_vertex_parts),
|
||||
|| flatten_entry_parts(key_segment_parts),
|
||||
)
|
||||
@@ -857,7 +918,7 @@ impl InteractionIndex {
|
||||
)
|
||||
},
|
||||
|| {
|
||||
let (fill, pick) = rayon::join(
|
||||
let (fill, pick) = crate::par::join(
|
||||
|| flatten_entry_parts(fill_triangle_parts),
|
||||
|| flatten_entry_parts(pick_triangle_parts),
|
||||
);
|
||||
@@ -891,17 +952,17 @@ impl InteractionIndex {
|
||||
let (
|
||||
((wires, segments), (snap_points, key_vertices)),
|
||||
((key_segments, fill_triangles), (pick_triangles, glyphs)),
|
||||
) = rayon::join(
|
||||
) = crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| SpatialSet::build(wire_entries),
|
||||
|| SpatialSet::build(segment_entries),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| SpatialSet::build(snap_point_entries),
|
||||
|| SpatialSet::build(key_vertex_entries),
|
||||
)
|
||||
@@ -909,15 +970,15 @@ impl InteractionIndex {
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| SpatialSet::build(key_segment_entries),
|
||||
|| SpatialSet::build(fill_triangle_entries),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| SpatialSet::build(pick_triangle_entries),
|
||||
|| SpatialSet::build(glyph_entries),
|
||||
)
|
||||
@@ -979,12 +1040,12 @@ impl InteractionIndex {
|
||||
/// thread. Perspective/orbit hover must never pay this one-time cost.
|
||||
pub fn prepare_screen(&self) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
|| rayon::join(|| self.wires.prepare_screen(), || self.segments.prepare_screen()),
|
||||
crate::par::join(
|
||||
|| crate::par::join(|| self.wires.prepare_screen(), || self.segments.prepare_screen()),
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| self.snap_points.prepare_screen(),
|
||||
|| self.key_vertices.prepare_screen(),
|
||||
)
|
||||
@@ -992,15 +1053,15 @@ impl InteractionIndex {
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| self.key_segments.prepare_screen(),
|
||||
|| self.fill_triangles.prepare_screen(),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
crate::par::join(
|
||||
|| self.pick_triangles.prepare_screen(),
|
||||
|| self.glyphs.prepare_screen(),
|
||||
)
|
||||
@@ -1136,6 +1197,52 @@ impl InteractionIndex {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn query_remapped_xy_area(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
slots: &rustc_hash::FxHashMap<(u64, u32), u32>,
|
||||
aabb: [f64; 4],
|
||||
) -> InteractionCandidates {
|
||||
InteractionCandidates {
|
||||
wires,
|
||||
wire_indices: Some(self.remap_wire_indices(
|
||||
self.wires.query_xy_unordered(aabb),
|
||||
slots,
|
||||
false,
|
||||
)),
|
||||
segments: Some(self.remap_refs(
|
||||
self.segments.query_xy_unordered(aabb),
|
||||
slots,
|
||||
|value| value.wire,
|
||||
|value, wire| value.wire = wire,
|
||||
)),
|
||||
snap_points: None,
|
||||
key_vertices: None,
|
||||
key_segments: None,
|
||||
fill_triangles: Some(self.remap_refs(
|
||||
self.fill_triangles.query_xy_unordered(aabb),
|
||||
slots,
|
||||
|value| value.wire,
|
||||
|value, wire| value.wire = wire,
|
||||
)),
|
||||
pick_triangles: Some(self.remap_refs(
|
||||
self.pick_triangles.query_xy_unordered(aabb),
|
||||
slots,
|
||||
|value| value.wire,
|
||||
|value, wire| value.wire = wire,
|
||||
)),
|
||||
glyphs: Some(self.remap_refs(
|
||||
self.glyphs.query_xy_unordered(aabb),
|
||||
slots,
|
||||
|value| value.wire,
|
||||
|value, wire| value.wire = wire,
|
||||
)),
|
||||
query_aabb: Some(aabb),
|
||||
screen_rect: None,
|
||||
screen_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query_remapped_xy(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
@@ -1269,6 +1376,53 @@ impl InteractionIndex {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_xy_area(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
aabb: [f64; 4],
|
||||
) -> InteractionCandidates {
|
||||
let ((wire_indices, segments), ((fill_triangles, pick_triangles), glyphs)) =
|
||||
crate::par::join(
|
||||
|| {
|
||||
crate::par::join(
|
||||
|| {
|
||||
let mut indices = self.wires.query_xy_unordered(aabb);
|
||||
indices.extend_from_slice(&self.unbounded_wires);
|
||||
indices.sort_unstable();
|
||||
indices.dedup();
|
||||
indices
|
||||
},
|
||||
|| self.segments.query_xy_unordered(aabb),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
crate::par::join(
|
||||
|| {
|
||||
crate::par::join(
|
||||
|| self.fill_triangles.query_xy_unordered(aabb),
|
||||
|| self.pick_triangles.query_xy_unordered(aabb),
|
||||
)
|
||||
},
|
||||
|| self.glyphs.query_xy_unordered(aabb),
|
||||
)
|
||||
},
|
||||
);
|
||||
InteractionCandidates {
|
||||
wires,
|
||||
wire_indices: Some(wire_indices),
|
||||
segments: Some(segments),
|
||||
snap_points: None,
|
||||
key_vertices: None,
|
||||
key_segments: None,
|
||||
fill_triangles: Some(fill_triangles),
|
||||
pick_triangles: Some(pick_triangles),
|
||||
glyphs: Some(glyphs),
|
||||
query_aabb: Some(aabb),
|
||||
screen_rect: None,
|
||||
screen_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_xy(&self, wires: Arc<Vec<WireModel>>, aabb: [f64; 4]) -> InteractionCandidates {
|
||||
let mut wire_indices = self.wires.query_xy(aabb);
|
||||
wire_indices.extend_from_slice(&self.unbounded_wires);
|
||||
@@ -1505,6 +1659,29 @@ impl InteractionCandidates {
|
||||
.map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds))
|
||||
}
|
||||
|
||||
pub(crate) fn extend_indexed_area(&mut self, other: Self) {
|
||||
debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));
|
||||
|
||||
fn append<T>(target: &mut Option<Vec<T>>, incoming: Option<Vec<T>>) {
|
||||
let (Some(target), Some(incoming)) = (target.as_mut(), incoming) else {
|
||||
return;
|
||||
};
|
||||
target.extend(incoming);
|
||||
}
|
||||
|
||||
if let (Some(target), Some(incoming)) =
|
||||
(self.wire_indices.as_mut(), other.wire_indices)
|
||||
{
|
||||
target.extend(incoming);
|
||||
target.sort_unstable();
|
||||
target.dedup();
|
||||
}
|
||||
append(&mut self.segments, other.segments);
|
||||
append(&mut self.fill_triangles, other.fill_triangles);
|
||||
append(&mut self.pick_triangles, other.pick_triangles);
|
||||
append(&mut self.glyphs, other.glyphs);
|
||||
}
|
||||
|
||||
pub(crate) fn extend_indexed(&mut self, other: Self) {
|
||||
debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));
|
||||
|
||||
@@ -1830,3 +2007,138 @@ fn aabb3_projects_into(
|
||||
fn aabb_overlaps(a: [f64; 4], b: [f64; 4]) -> bool {
|
||||
a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] && a[1] <= b[3]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod area_query_tests {
|
||||
use super::*;
|
||||
use iced::{Point, Rectangle};
|
||||
|
||||
fn wire(name: &str, pts: Vec<[f32; 3]>, aabb: [f32; 4]) -> WireModel {
|
||||
let mut w = WireModel::solid(name.to_string(), pts, [1.0; 4], false);
|
||||
w.aabb = aabb;
|
||||
w
|
||||
}
|
||||
|
||||
fn sample() -> Vec<WireModel> {
|
||||
vec![
|
||||
wire("5", vec![[-0.5, -0.5, 0.0], [0.5, 0.5, 0.0]], [-0.5, -0.5, 0.5, 0.5]),
|
||||
wire("9", vec![[0.1, -0.9, 0.0], [0.9, -0.1, 0.0]], [0.1, -0.9, 0.9, -0.1]),
|
||||
wire("13", vec![[-0.9, 0.2, 0.0], [-0.2, 0.9, 0.0]], [-0.9, 0.2, -0.2, 0.9]),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn area_query_matches_full_query_where_it_is_read() {
|
||||
let wires = sample();
|
||||
let index = InteractionIndex::build(&wires);
|
||||
let arc = Arc::new(wires);
|
||||
let aabb = [-2.0, -2.0, 2.0, 2.0];
|
||||
|
||||
let full = index.query_xy(Arc::clone(&arc), aabb);
|
||||
let area = index.query_xy_area(Arc::clone(&arc), aabb);
|
||||
|
||||
fn normalise<T: Copy + Ord>(v: &Option<Vec<T>>) -> Option<Vec<T>> {
|
||||
v.as_ref().map(|items| {
|
||||
let mut items = items.clone();
|
||||
items.sort_unstable();
|
||||
items.dedup();
|
||||
items
|
||||
})
|
||||
}
|
||||
assert_eq!(area.wire_indices, full.wire_indices);
|
||||
assert_eq!(normalise(&area.segments), normalise(&full.segments));
|
||||
assert_eq!(
|
||||
normalise(&area.fill_triangles),
|
||||
normalise(&full.fill_triangles),
|
||||
);
|
||||
assert_eq!(
|
||||
normalise(&area.pick_triangles),
|
||||
normalise(&full.pick_triangles),
|
||||
);
|
||||
assert_eq!(normalise(&area.glyphs), normalise(&full.glyphs));
|
||||
assert_eq!(area.query_aabb, full.query_aabb);
|
||||
|
||||
// The sample carries no hatches or text, so the equality above is
|
||||
// thin for those three. What matters is that they stay indexed at all:
|
||||
// dropping one to `None` is what would silently unselect a hatch.
|
||||
assert!(area.fill_triangles.is_some());
|
||||
assert!(area.pick_triangles.is_some());
|
||||
assert!(area.glyphs.is_some());
|
||||
|
||||
assert!(area.snap_points.is_none());
|
||||
assert!(area.key_vertices.is_none());
|
||||
assert!(area.key_segments.is_none());
|
||||
assert!(full.snap_points.is_some(), "the full query still snaps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_area_merge_dedups_wire_indices_and_nothing_else() {
|
||||
let arc = Arc::new(sample());
|
||||
let base = |indices: Vec<u32>, segments: Vec<SegmentRef>| InteractionCandidates {
|
||||
wires: Arc::clone(&arc),
|
||||
wire_indices: Some(indices),
|
||||
segments: Some(segments),
|
||||
snap_points: None,
|
||||
key_vertices: None,
|
||||
key_segments: None,
|
||||
fill_triangles: Some(Vec::new()),
|
||||
pick_triangles: Some(Vec::new()),
|
||||
glyphs: Some(Vec::new()),
|
||||
query_aabb: Some([-2.0, -2.0, 2.0, 2.0]),
|
||||
screen_rect: None,
|
||||
screen_view: None,
|
||||
};
|
||||
let seg = |wire: u32| SegmentRef { wire, start: 0 };
|
||||
|
||||
let mut merged = base(vec![2, 0], vec![seg(2), seg(0)]);
|
||||
merged.extend_indexed_area(base(vec![1, 0], vec![seg(1), seg(0)]));
|
||||
|
||||
assert_eq!(merged.wire_indices, Some(vec![0, 1, 2]));
|
||||
assert_eq!(
|
||||
merged.segments,
|
||||
Some(vec![seg(2), seg(0), seg(1), seg(0)]),
|
||||
"the other categories are appended as they come",
|
||||
);
|
||||
}
|
||||
|
||||
// World (x,y) → screen ((x+1)*100, (1-y)*100) over a 200×200 viewport, the
|
||||
// same identity ortho view the hit-test tests use.
|
||||
#[test]
|
||||
fn box_selection_picks_the_same_handles_from_either_query() {
|
||||
let wires = sample();
|
||||
let index = InteractionIndex::build(&wires);
|
||||
let arc = Arc::new(wires);
|
||||
let aabb = [-2.0, -2.0, 2.0, 2.0];
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
};
|
||||
let eye = glam::DVec3::ZERO;
|
||||
|
||||
for crossing in [false, true] {
|
||||
let full = index.query_xy(Arc::clone(&arc), aabb);
|
||||
let area = index.query_xy_area(Arc::clone(&arc), aabb);
|
||||
let hit = |c: &InteractionCandidates| {
|
||||
let mut names = crate::scene::pick::hit_test::box_hit(
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(200.0, 200.0),
|
||||
crossing,
|
||||
c,
|
||||
glam::Mat4::IDENTITY,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
let from_full = hit(&full);
|
||||
assert!(!from_full.is_empty(), "the box covers the whole sample");
|
||||
assert_eq!(hit(&area), from_full, "crossing={crossing}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user