perf: keep heavy work off UI thread

This commit is contained in:
Hakan Seven committed 2026-07-25 18:49:18 +03:00
1 parent 12bf126646
commit deec48ad99
75 files changed
+11702 -518

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
//! Shared broad phase for interactive modify commands.
//!
//! The scene already narrows the cursor pick. Commands still need fast access
//! to the picked analytic entity and, for intersection-heavy operations such
//! as TRIM, to nearby boundary entities. Keeping this compact command-local
//! index avoids cloning/scanning the complete drawing on every mouse move.
use acadrust::{EntityType, Handle};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::scene::convert::tess::entity_world_aabb_f64;
use crate::scene::pick::quadtree::QuadTree;
pub(super) struct ModifyEntityIndex {
by_handle: FxHashMap<Handle, usize>,
tree: Option<QuadTree>,
unbounded: Vec<Handle>,
}
impl ModifyEntityIndex {
pub(super) fn build(entities: &[EntityType]) -> Self {
let mut by_handle = FxHashMap::default();
let mut bounded = Vec::new();
let mut unbounded = Vec::new();
let mut world = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for (index, entity) in entities.iter().enumerate() {
let handle = entity.common().handle;
by_handle.insert(handle, index);
if let Some(aabb) = entity_world_aabb_f64(entity) {
world[0] = world[0].min(aabb[0]);
world[1] = world[1].min(aabb[1]);
world[2] = world[2].max(aabb[2]);
world[3] = world[3].max(aabb[3]);
bounded.push((handle, aabb));
} else {
unbounded.push(handle);
}
}
let tree = if bounded.is_empty() {
None
} else {
let span = (world[2] - world[0]).max(world[3] - world[1]).max(1.0);
let pad = span * 1.0e-9 + 1.0e-6;
let mut tree = QuadTree::new([
world[0] - pad,
world[1] - pad,
world[2] + pad,
world[3] + pad,
]);
for (handle, aabb) in bounded {
tree.insert(handle, aabb);
}
Some(tree)
};
Self {
by_handle,
tree,
unbounded,
}
}
#[inline]
pub(super) fn get<'a>(
&self,
entities: &'a [EntityType],
handle: Handle,
) -> Option<&'a EntityType> {
self.by_handle
.get(&handle)
.and_then(|index| entities.get(*index))
}
pub(super) fn nearby_handles(
&self,
entities: &[EntityType],
handle: Handle,
) -> Option<FxHashSet<Handle>> {
let entity = self.get(entities, handle)?;
let aabb = entity_world_aabb_f64(entity)?;
let span = (aabb[2] - aabb[0]).max(aabb[3] - aabb[1]).max(1.0);
let pad = span * 1.0e-10 + 1.0e-7;
let query = [aabb[0] - pad, aabb[1] - pad, aabb[2] + pad, aabb[3] + pad];
let mut handles: FxHashSet<Handle> = self
.tree
.as_ref()
.map(|tree| tree.query_rect(query).into_iter().collect())
.unwrap_or_default();
handles.extend(self.unbounded.iter().copied());
Some(handles)
}
}
+23 -24
View File
@@ -21,6 +21,8 @@ use crate::modules::draw::defaults;
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "fillet_chamfer";
@@ -1204,6 +1206,7 @@ pub struct FilletCommand {
radius: f64,
step: FilletStep,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// First-object pick to restore after a radius entry made mid-selection
/// (i.e. "R" pressed after the first object was already picked), so the
/// command resumes at the second pick instead of restarting selection.
@@ -1212,10 +1215,12 @@ pub struct FilletCommand {
impl FilletCommand {
pub fn new(radius: f64, all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
radius: radius as f64,
step: FilletStep::First,
all_entities,
entity_index,
resume_second: None,
}
}
@@ -1348,9 +1353,8 @@ impl CadCommand for FilletCommand {
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
FilletStep::First => {
let e1 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@@ -1380,9 +1384,8 @@ impl CadCommand for FilletCommand {
}
let e2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@@ -1426,9 +1429,8 @@ impl CadCommand for FilletCommand {
FilletStep::WaitingForRadius => vec![],
FilletStep::First => {
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| match e {
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
_ => FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type())),
@@ -1449,9 +1451,8 @@ impl CadCommand for FilletCommand {
let e1 = e1.clone();
let click1 = *click1;
let e2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@@ -1583,6 +1584,7 @@ pub struct ChamferCommand {
dist2: f64,
step: ChamferStep,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// First-object pick (line or polyline segment) to restore after a
/// distance entry made mid-selection, so the command resumes at the
/// second pick instead of restarting selection.
@@ -1591,11 +1593,13 @@ pub struct ChamferCommand {
impl ChamferCommand {
pub fn new(dist: f64, all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
dist1: dist as f64,
dist2: defaults::get_chamfer_dist2(),
step: ChamferStep::First,
all_entities,
entity_index,
resume_pick: None,
}
}
@@ -1777,9 +1781,7 @@ impl CadCommand for ChamferCommand {
}
ChamferStep::First => {
match self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
Some(EntityType::Line(l)) => {
self.step = ChamferStep::Second {
@@ -1821,9 +1823,8 @@ impl CadCommand for ChamferCommand {
}
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
@@ -1857,9 +1858,8 @@ impl CadCommand for ChamferCommand {
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![],
ChamferStep::First => {
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| match e {
EntityType::Line(l) => Some(line_pts(l)),
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
@@ -1880,9 +1880,8 @@ impl CadCommand for ChamferCommand {
let l1 = l1.clone();
let click1 = *click1;
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
+1
View File
@@ -4,6 +4,7 @@ pub mod attedit;
pub mod break_cmd;
pub mod copy;
pub mod delete;
mod entity_index;
pub mod explode;
pub mod fillet;
pub mod join;
+9 -6
View File
@@ -25,6 +25,8 @@ use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Ribbon definition ──────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
@@ -691,6 +693,7 @@ enum Step {
pub struct OffsetCommand {
step: Step,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
/// distance step resolves.
preselected: Vec<EntityType>,
@@ -712,9 +715,11 @@ pub fn is_offsettable(e: &EntityType) -> bool {
impl OffsetCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
step: Step::Distance,
all_entities,
entity_index,
preselected: Vec::new(),
}
}
@@ -722,9 +727,11 @@ impl OffsetCommand {
/// Pick-first flow (#422): the distance step still comes first, then the
/// pre-selected objects go straight to the side step.
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
step: Step::Distance,
all_entities,
entity_index,
preselected: targets,
}
}
@@ -801,9 +808,7 @@ impl CadCommand for OffsetCommand {
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
.cloned();
// Accept every type compute_offset can offset — including XLine (#296),
@@ -881,9 +886,7 @@ impl CadCommand for OffsetCommand {
return vec![];
}
if let Some(entity) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
let pts = entity_wire_pts(entity);
if !pts.is_empty() {
+58 -22
View File
@@ -27,6 +27,8 @@ use crate::modules::draw::modify::spline_ops::{
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "trim_extend";
@@ -269,6 +271,20 @@ enum Geo {
},
}
impl Geo {
fn handle(&self) -> Handle {
match self {
Self::Line { handle, .. }
| Self::Arc { handle, .. }
| Self::Circle { handle, .. }
| Self::Ray { handle, .. }
| Self::InfLine { handle, .. }
| Self::Ellipse { handle, .. }
| Self::Spline { handle, .. } => *handle,
}
}
}
fn build_geos(entities: &[EntityType]) -> Vec<Geo> {
let mut out = Vec::new();
for e in entities {
@@ -2797,6 +2813,7 @@ fn crossing_preview_wire(p1: [f64; 2], cursor: [f64; 2], name: &str) -> WireMode
pub struct TrimCommand {
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
geos: Vec<Geo>,
mode: TrimMode,
/// Cutting-edge selection; empty = every object cuts (quick mode).
@@ -2809,9 +2826,11 @@ pub struct TrimCommand {
impl TrimCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
let geos = build_geos(&all_entities);
Self {
all_entities,
entity_index,
geos,
mode: TrimMode::Pick,
edge_set: Vec::new(),
@@ -2823,6 +2842,7 @@ impl TrimCommand {
/// Boundary geometry from the edge selection (or everything when none),
/// with the Edge option's implied extrapolation applied on top.
fn rebuild_geos(&mut self) {
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
self.geos = if self.edge_set.is_empty() {
build_geos(&self.all_entities)
} else {
@@ -2839,6 +2859,23 @@ impl TrimCommand {
}
}
/// Exact analytic boundaries whose boxes overlap the picked entity. The
/// host already narrowed the cursor pick; this second broad phase keeps
/// TRIM preview/intersection work local on dense drawings.
fn nearby_geos(&self, handle: Handle) -> Vec<Geo> {
if self.implied_edges {
return self.geos.clone();
}
let Some(handles) = self.entity_index.nearby_handles(&self.all_entities, handle) else {
return self.geos.clone();
};
self.geos
.iter()
.filter(|geo| handles.contains(&geo.handle()))
.cloned()
.collect()
}
fn fence_run(
&mut self,
fence: &[[f64; 2]],
@@ -2974,7 +3011,8 @@ impl CadCommand for TrimCommand {
pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
.map(|e| vec![e])
} else {
pick_trim_at(&self.all_entities, &self.geos, handle, px, py)
let geos = self.nearby_geos(handle);
pick_trim_at(&self.all_entities, &geos, handle, px, py)
};
if let Some(new_entities) = new_entities {
// Snapshot is updated in on_entity_replaced once we know
@@ -3108,10 +3146,10 @@ impl CadCommand for TrimCommand {
return vec![];
}
let nearby_geos = self.nearby_geos(handle);
let geos = nearby_geos.as_slice();
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
.entity_index.get(&self.all_entities, handle);
let mut hover_wires = match entity {
Some(EntityType::Line(l)) => {
@@ -3119,7 +3157,7 @@ impl CadCommand for TrimCommand {
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
let ts = line_seg_ts(ax, ay, bx, by, handle, geos);
if ts.is_empty() {
return vec![];
}
@@ -3152,7 +3190,7 @@ impl CadCommand for TrimCommand {
let cy = a.center.y;
let a0 = a.start_angle;
let a1 = a.end_angle;
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, geos);
if ts.is_empty() {
return vec![];
}
@@ -3176,7 +3214,7 @@ impl CadCommand for TrimCommand {
Some(EntityType::Circle(c)) => {
let cx = c.center.x;
let cy = c.center.y;
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, &self.geos);
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, geos);
if ts.len() < 2 {
return vec![];
}
@@ -3205,7 +3243,7 @@ impl CadCommand for TrimCommand {
let by = r.base_point.y;
let ex = bx + r.direction.x * TRIM_EXTENT;
let ey = by + r.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(bx, by, ex, ey, handle, &self.geos);
let ts = line_seg_ts(bx, by, ex, ey, handle, geos);
if ts.is_empty() {
return vec![];
}
@@ -3245,7 +3283,7 @@ impl CadCommand for TrimCommand {
let ey_start = by - x.direction.y * TRIM_EXTENT;
let ex_end = bx + x.direction.x * TRIM_EXTENT;
let ey_end = by + x.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(ex_start, ey_start, ex_end, ey_end, handle, &self.geos);
let ts = line_seg_ts(ex_start, ey_start, ex_end, ey_end, handle, geos);
if ts.is_empty() {
return vec![];
}
@@ -3294,7 +3332,7 @@ impl CadCommand for TrimCommand {
t1 += TAU;
}
let ts = ellipse_seg_ts(
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos,
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, geos,
);
if ts.is_empty() {
return vec![];
@@ -3321,7 +3359,7 @@ impl CadCommand for TrimCommand {
out
}
Some(EntityType::Spline(s)) => {
let ts = spline_seg_ts(s, handle, &self.geos);
let ts = spline_seg_ts(s, handle, geos);
if ts.is_empty() {
return vec![];
}
@@ -3348,7 +3386,7 @@ impl CadCommand for TrimCommand {
out
}
Some(EntityType::LwPolyline(p)) => {
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos)
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, geos)
else {
return vec![];
};
@@ -3371,7 +3409,7 @@ impl CadCommand for TrimCommand {
if self.implied_edges && !hover_wires.is_empty() {
if let (Some(orig), Some(pieces)) = (
entity,
pick_trim_at(&self.all_entities, &self.geos, handle, pt.x, pt.y),
pick_trim_at(&self.all_entities, geos, handle, pt.x, pt.y),
) {
let cuts = piece_cut_points(orig, &pieces);
hover_wires.extend(implied_cut_guides(&self.all_entities, handle, &cuts));
@@ -3477,6 +3515,7 @@ impl CadCommand for TrimCommand {
pub struct ExtendCommand {
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
geos: Vec<Geo>,
mode: TrimMode,
/// Boundary-edge selection; empty = every object is a boundary.
@@ -3489,9 +3528,11 @@ pub struct ExtendCommand {
impl ExtendCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
let geos = build_geos(&all_entities);
Self {
all_entities,
entity_index,
geos,
mode: TrimMode::Pick,
edge_set: Vec::new(),
@@ -3501,6 +3542,7 @@ impl ExtendCommand {
}
fn rebuild_geos(&mut self) {
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
self.geos = if self.edge_set.is_empty() {
build_geos(&self.all_entities)
} else {
@@ -3681,18 +3723,14 @@ impl CadCommand for ExtendCommand {
let mut out: Vec<WireModel> = self
.edge_set
.iter()
.filter_map(|h| {
self.all_entities.iter().find(|e| e.common().handle == *h)
})
.filter_map(|h| self.entity_index.get(&self.all_entities, *h))
.map(|e| {
WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
})
.collect();
if !handle.is_null() && !self.edge_set.contains(&handle) {
if let Some(e) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
let mut c = OPT_YELLOW;
c[3] = 0.45;
@@ -3732,9 +3770,7 @@ impl CadCommand for ExtendCommand {
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
.entity_index.get(&self.all_entities, handle);
match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;