fix: keep exact snapping, source drawing data and adaptive options layout

Resolve curve intersections through cadkernel, retain empty exact results, gather analytic candidates independently of tessellation, and bound material uploads. Preserve large MINSERT data while limiting rendering work; remove obsolete review narratives.
This commit is contained in:
Hakan Seven committed 2026-09-09 13:17:30 +03:00
1 parent 4bb6aac0b6
commit 973af87d58
16 files changed
+57 -724

No files matched your search

-41
View File
@@ -1271,28 +1271,6 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
segment_hits(a, b).then_some(wire.name.as_str())
};
// A crossing selection over a whole drawing spends its time here, not in
// the window branch of `box_hit`: every measured `select-commit` line read
// `crossing=true`.
//
// The scan stops projecting a wire once that wire is in. A circle or a
// polyline contributes one segment per tessellated span, and every span
// after the first that hits can only re-add a name already present — at 24
// spans per wire that is 23 projection pairs out of 24 skipped, which took
// a 4.4 M-segment scan from ~1 s to 42 ms. The wire is keyed by index, an
// integer to hash rather than a string, and the name set still runs behind
// it because one entity can produce several wires sharing a name.
//
// Fanning this out across cores was tried and removed: with the early-out
// the remaining work is small enough that per-chunk sets, which cannot see
// wires already hit in another chunk, measured *slower* than one pass
// (45.6 ms against 42.4 ms at 186 468 wires).
// A bitmap over wire indices rather than a hash set: this is consulted once
// per segment, triangle and glyph inside the box — millions of times on a
// whole-drawing selection — and an array index beats hashing a `u32`, let
// alone hashing the wire's name, which is what the primitive loops below
// used to do. `seen` stays for the names, because several wires can share
// one entity's name and the output dedups by name.
let mut wire_hit: Vec<bool> = Vec::new();
fn mark(wire_hit: &mut Vec<bool>, wire: u32) {
let index = wire as usize;
@@ -1377,10 +1355,6 @@ 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() {
// Structural tests first: `||` short-circuits, so putting the name
// lookup last means a wire with two or more points — very nearly all
// of them — is dropped without hashing its name. This loop walks every
// candidate, so that was one string hash per wire in the box.
if wire.points.len() >= 2
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
@@ -1591,10 +1565,6 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
}
}
for wire in wires.iter() {
// Structural tests first: `||` short-circuits, so putting the name
// lookup last means a wire with two or more points — very nearly all
// of them — is dropped without hashing its name. This loop walks every
// candidate, so that was one string hash per wire in the box.
if wire.points.len() >= 2
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
@@ -2773,13 +2743,6 @@ mod parallel_selection_tests {
use crate::scene::pick::interaction_index::{InteractionCandidates, InteractionIndex};
use std::sync::Arc;
// Enough wires to clear the 4096 default several times over, laid on a
// diagonal so the selection box catches a known prefix of them rather than
// all or nothing — an "everything hits" case would not notice a reordering.
// A drawing is not made of single segments: a circle or an arc tessellates
// into dozens of spans, and a polyline carries as many as it has vertices.
// A one-segment-per-wire sample makes the per-wire early-out invisible and
// the whole measurement optimistic, so the benchmark uses `spans` per wire.
fn many_wires_with_spans(count: usize, spans: usize) -> Vec<WireModel> {
(0..count)
.map(|i| {
@@ -2804,10 +2767,6 @@ mod parallel_selection_tests {
.collect()
}
// Not a correctness test — a measurement, at the size the user's drawing
// actually is. Run it deliberately:
//
// cargo test --release --lib selection_scaling -- --ignored --nocapture
#[test]
#[ignore = "measurement, not a check"]
fn selection_scaling_numbers() {
-71
View File
@@ -458,20 +458,6 @@ impl SpatialGrid {
}
}
/// The same set as [`SpatialGrid::query`], in cell order and with
/// duplicates left in.
///
/// `query` sorts twice over the result — once to dedup the entry indices an
/// entry gets for spanning several cells, once to dedup the values — which
/// over a box covering the drawing is two O(n log n) passes across 4.4 M
/// elements to remove a handful of repeats. Everything an area selection
/// does with the result is order-independent and dedups anyway: the
/// crossing scan keys wires by index and names by string, and the box test
/// on a segment does not care when it runs.
///
/// Click picking keeps [`SpatialGrid::query`]: it resolves ties between
/// overlapping geometry by scan order, so its order is part of which entity
/// a click selects.
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);
@@ -1211,13 +1197,6 @@ impl InteractionIndex {
.collect()
}
/// [`InteractionIndex::query_remapped_xy`] for an area selection.
///
/// The overlay path runs whenever the drawing has been edited since the
/// interaction index was built — that is, for most of a working session —
/// so leaving it on the full query would have meant the area selection got
/// its saving only until the user drew something. Same five categories,
/// same unordered gather.
pub(crate) fn query_remapped_xy_area(
&self,
wires: Arc<Vec<WireModel>>,
@@ -1397,34 +1376,11 @@ impl InteractionIndex {
}
}
/// Candidates for an area selection — box, crossing or fence.
///
/// `query_xy` runs eight spatial queries because hover and snapping need
/// them all. An area selection needs five: the wires and their segments,
/// and — because `indexed_box_crossing_hits` reads them to catch hatches
/// and text inside the box — the fill triangles, pick triangles and
/// glyphs. Only the three that exist for snapping (`snap_points`,
/// `key_vertices`, `key_segments`, read solely by `pick::snap::snap`) go
/// unread, so only those are skipped.
///
/// Over a box covering the whole drawing each query gathers nearly
/// everything, which is why this is worth splitting: gathering candidates
/// was 407 ms of an 825 ms selection of 186 468 entities.
///
/// The skipped categories stay `None`. That is safe *only* for consumers
/// that never read them — `snap` would silently stop snapping rather than
/// fail, since the accessors treat `None` as "no index". Route anything
/// that snaps through `query_xy`.
pub fn query_xy_area(
&self,
wires: Arc<Vec<WireModel>>,
aabb: [f64; 4],
) -> InteractionCandidates {
// The five are independent tree walks over the same box, and over a box
// covering the drawing each one gathers nearly everything — 259 ms
// together. Running them concurrently costs the largest instead of the
// sum. Each closure still calls the same query, so the results are
// identical to the sequential ones, order included.
let ((wire_indices, segments), ((fill_triangles, pick_triangles), glyphs)) =
crate::par::join(
|| {
@@ -1703,16 +1659,6 @@ impl InteractionCandidates {
.map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds))
}
/// [`InteractionCandidates::extend_indexed`] for an area selection.
///
/// `extend_indexed` sorts and dedups every category it merges, which over a
/// box covering the drawing would put back exactly the sorts the unordered
/// query exists to avoid.
///
/// `wire_indices` keeps its dedup, and must: the window branch of `box_hit`
/// walks it directly, so a wire listed twice would be selected twice. The
/// other categories are read by scans that dedup by wire index and by name
/// already, so a repeat there costs one extra test and changes nothing.
pub(crate) fn extend_indexed_area(&mut self, other: Self) {
debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));
@@ -2081,11 +2027,6 @@ mod area_query_tests {
]
}
// The area query drops the three snap-only categories and must leave every
// category an area selection reads untouched. `unwrap_or_default()` in
// `indexed_box_crossing_hits` would turn a wrongly-dropped category into a
// silent miss — a hatch or a text that stops being selectable — rather than
// a failure, so the equivalence is asserted rather than assumed.
#[test]
fn area_query_matches_full_query_where_it_is_read() {
let wires = sample();
@@ -2096,13 +2037,6 @@ mod area_query_tests {
let full = index.query_xy(Arc::clone(&arc), aabb);
let area = index.query_xy_area(Arc::clone(&arc), aabb);
// The area query skips the sort and the dedup `query` pays for, so it
// returns the same *set* in cell order, with an entry repeated once per
// cell it spans. Every consumer on this path dedups already — the
// crossing scan by wire index and by name — so the set is the contract,
// and comparing sequences here would only pin down an order nothing
// reads. `wire_indices` is the exception: `query_xy_area` sorts and
// dedups it before returning, so it must match exactly.
fn normalise<T: Copy + Ord>(v: &Option<Vec<T>>) -> Option<Vec<T>> {
v.as_ref().map(|items| {
let mut items = items.clone();
@@ -2137,11 +2071,6 @@ mod area_query_tests {
assert!(full.snap_points.is_some(), "the full query still snaps");
}
// The overlay merge drops the sort on every category but one. That one
// matters: the window branch of `box_hit` walks `wire_indices` directly, so
// a wire left in twice would be selected twice — a duplicate handle rather
// than an error, which is the kind of thing that surfaces as a wrong count
// somewhere far away.
#[test]
fn the_area_merge_dedups_wire_indices_and_nothing_else() {
let arc = Arc::new(sample());