From d2995ecee1a9dcbe2f47752cf0bb2fe5ccdfca9a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 15 Oct 2026 23:51:38 +0300 Subject: [PATCH] fix(hatch): pick points stay fast in large drawings, and see off layers Each picked region matched every edge of its rings against every boundary source of the drawing, twice: with 65,000 sources a click took over 30 seconds. Sources are now narrowed to those reaching the rings' box first, once per hatch and once per ring: the same click takes 9 ms. Objects on layers that are turned off (not frozen) now bound a hatch, as in the reference. Drawings switch off the layers that close the regions to be hatched; without them a click between columns found the outer frame instead. (#1813) Co-Authored-By: Claude Opus 5.5 --- src/modules/draw/draw/hatch.rs | 13 ++++---- src/scene/boundary.rs | 61 ++++++++++++++++++++++++++++++++++ src/scene/mod.rs | 3 +- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 1afaa741f..545754cb6 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -350,12 +350,13 @@ impl HatchCommand { .into_iter() .map(|depth| depth == 0) .collect(); + let near = crate::scene::sources_near_rings(&rings, &self.boundary_sources, 1.0e-6); let mut boundary_sources: Vec> = rings .iter() - .map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources)) + .map(|ring| crate::scene::ring_source_handles(ring, &near)) .collect(); - let mut boundary_paths = - crate::scene::exact_hatch_paths(&rings, &exterior, &self.boundary_sources, 1.0e-6); + let mut boundary_paths = crate::scene::exact_hatch_paths(&rings, &exterior, &near, 1.0e-6); + if !self.associative { for handles in &mut boundary_sources { handles.clear(); @@ -983,12 +984,12 @@ impl GradientCommand { .into_iter() .map(|depth| depth == 0) .collect(); + let near = crate::scene::sources_near_rings(&rings, &self.boundary_sources, 1.0e-6); let boundary_sources = rings .iter() - .map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources)) + .map(|ring| crate::scene::ring_source_handles(ring, &near)) .collect(); - let boundary_paths = - crate::scene::exact_hatch_paths(&rings, &exterior, &self.boundary_sources, 1.0e-6); + let boundary_paths = crate::scene::exact_hatch_paths(&rings, &exterior, &near, 1.0e-6); HatchModel { pattern_origin: None, render_instance: None, diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index 5c1eb1826..a23e3fba0 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -211,10 +211,41 @@ fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec }) } +/// The sources with a segment reaching the box around `rings`: the only ones +/// their edges can lie on. Matching every edge against every source of a +/// large drawing took half a minute per picked hatch region. (#1813) +pub(crate) fn sources_near_rings( + rings: &[Vec<[f64; 2]>], + sources: &rustc_hash::FxHashMap, + pad: f64, +) -> rustc_hash::FxHashMap { + let mut lo = [f64::INFINITY; 2]; + let mut hi = [f64::NEG_INFINITY; 2]; + for point in rings.iter().flatten() { + for axis in 0..2 { + lo[axis] = lo[axis].min(point[axis] - pad); + hi[axis] = hi[axis].max(point[axis] + pad); + } + } + let reaches = |line: &Line| { + (0..2).all(|axis| { + line.start[axis].min(line.end[axis]) <= hi[axis] + && line.start[axis].max(line.end[axis]) >= lo[axis] + }) + }; + sources + .iter() + .filter(|(_, source)| source.segments.iter().any(reaches)) + .map(|(handle, source)| (*handle, source.clone())) + .collect() +} + pub(crate) fn ring_source_handles( ring: &[[f64; 2]], sources: &rustc_hash::FxHashMap, ) -> Vec { + let near = sources_near_rings(&[ring.to_vec()], sources, WELD_TOLERANCE); + let sources = &near; let mut handles = rustc_hash::FxHashSet::default(); for (&start, &end) in ring .iter() @@ -411,6 +442,8 @@ pub(crate) fn exact_hatch_paths( ) -> Vec { use codec::entities::{BoundaryPath, BoundaryPathFlags}; + let near = sources_near_rings(rings, sources, tolerance.max(WELD_TOLERANCE)); + let sources = &near; rings .iter() .enumerate() @@ -568,6 +601,8 @@ fn refined_boundary_ring( if count < 3 { return (ring.to_vec(), Vec::new()); } + let near = sources_near_rings(&[ring.to_vec()], sources, tolerance.max(WELD_TOLERANCE)); + let sources = &near; let edge_curves: Vec<_> = (0..count) .map(|index| { edge_curve( @@ -1022,6 +1057,32 @@ impl Scene { .segments .extend(segments); } + // Objects on a layer that is turned off (not frozen) still bound a + // hatch, as in the reference: drawings switch off the layers that + // close the regions to be hatched. They draw no wire, so they come + // from the document. (#1813) + let space = if self.active_viewport.is_some() || self.current_layout == "Model" { + self.content_render_block_handle() + } else { + self.current_layout_block_handle() + }; + for entity in self.document.entities() { + let common = entity.common(); + if common.owner_handle != space || sources.contains_key(&common.handle) { + continue; + } + let hidden = self + .document + .layers + .get(&common.layer) + .is_some_and(|layer| layer.is_off() && !layer.is_frozen()); + if !hidden { + continue; + } + if let Some(source) = entity_boundary_source_on_plane(entity, plane, tolerance, None) { + sources.insert(common.handle, source); + } + } // Finite extents of the wire geometry, so rays and infinite // construction lines clip to something real instead of tessellating // to a unit stub (see `live_segments_for_curves`). diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ede21eb85..5a070a2d8 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -80,7 +80,8 @@ mod parametric_solve; pub(crate) use boundary::{ boundary_entities, boundary_entities_from_sources, boundary_faces, boundary_polyline_entities, exact_hatch_paths, hatch_boundary_rings, hatch_path_directions, hatch_path_ring, - remap_hatch_associations, ring_source_handles, separated_hatch_path_groups, BoundarySource, + remap_hatch_associations, ring_source_handles, separated_hatch_path_groups, sources_near_rings, + BoundarySource, }; // Parallel tessellation free functions live in `convert::tess` (alongside the