perf: implement L–S performance improvements

- L: pre-project wire endpoints to screen before ApparentIntersection pair loops
- O+S: tight per-segment AABB cull + move a0/a1 Vec3 outside inner snap loop
- P: epoch-cached entity→block reverse map replaces O(E×B) scan
- Q: lazy per-point projection in hit tests eliminates per-wire Vec<Point> alloc
- R: HatchModel.boundary Arc<Vec> so clone is pointer bump not heap alloc
This commit is contained in:
Hakan Seven committed 2026-05-06 22:42:22 +03:00
1 parent b83c982bc2
commit 45f9f96c93
6 files changed
+265 -233

No files matched your search

+3 -3
View File
@@ -113,7 +113,7 @@ impl HatchCommand {
}]
});
HatchModel {
boundary,
boundary: std::sync::Arc::new(boundary),
pattern: HatchPattern::Pattern(families),
name: pat_name.into(),
color: [0.75, 0.75, 0.75, 0.85],
@@ -245,7 +245,7 @@ impl GradientCommand {
fn make_hatch(&self, boundary: Vec<[f32; 2]>) -> HatchModel {
HatchModel {
boundary,
boundary: std::sync::Arc::new(boundary),
pattern: HatchPattern::Gradient {
angle_deg: 0.0,
color2: [0.18, 0.18, 0.18, 0.0],
@@ -396,7 +396,7 @@ impl CadCommand for BoundaryCommand {
self.missed = false;
// Store as a Hatch entity (solid fill) so it is selectable.
let model = HatchModel {
boundary: outline.clone(),
boundary: std::sync::Arc::new(outline.clone()),
pattern: HatchPattern::Solid,
name: "SOLID".into(),
color: [0.45, 0.45, 0.45, 0.60],
+3 -1
View File
@@ -1,4 +1,5 @@
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
use std::sync::Arc;
//
// The boundary is a closed polygon in world XY coordinates (max 64 vertices).
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
@@ -41,7 +42,8 @@ pub enum HatchPattern {
#[derive(Clone, Debug)]
pub struct HatchModel {
/// World XY coordinates of the boundary polygon vertices (max 64).
pub boundary: Vec<[f32; 2]>,
/// Arc so HatchModel::clone() is a pointer bump rather than a full Vec copy.
pub boundary: Arc<Vec<[f32; 2]>>,
/// Fill pattern.
pub pattern: HatchPattern,
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
+66 -54
View File
@@ -31,19 +31,23 @@ pub fn click_hit<'a>(
let mut best_dist = CLICK_THRESHOLD_PX;
let mut best: Option<&str> = None;
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
for wire in wires {
let screen: Vec<Point> = wire
.points
.iter()
.map(|&p| world_to_screen(Vec3::from(p), view_proj, bounds))
.collect();
for seg in screen.windows(2) {
let d = dist_point_to_segment(cursor, seg[0], seg[1]);
if d < best_dist {
best_dist = d;
best = Some(&wire.name);
let mut prev: Option<Point> = None;
for &[px, py, pz] in &wire.points {
if px.is_nan() {
prev = None;
continue;
}
let cur = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
if let Some(p0) = prev {
let d = dist_point_to_segment(cursor, p0, cur);
if d < best_dist {
best_dist = d;
best = Some(&wire.name);
}
}
prev = Some(cur);
}
}
@@ -86,6 +90,7 @@ pub fn box_hit<'a>(
let box_bl = Point { x: min_x, y: max_y };
let box_br = Point { x: max_x, y: max_y };
// Q: lazy projection — accumulate screen points without allocating per-wire Vec.
wires
.iter()
.filter_map(|wire| {
@@ -93,32 +98,34 @@ pub fn box_hit<'a>(
return None;
}
let screen: Vec<Point> = wire
.points
.iter()
.map(|&p| world_to_screen(Vec3::from(p), view_proj, bounds))
.collect();
let mut hit = false;
let mut all_inside = true;
let mut prev: Option<Point> = None;
let hit = if crossing {
// A wire is hit when any vertex is inside the box OR any of its
// segments crosses one of the four box edges.
screen.iter().any(|&sp| inside(sp))
|| screen.windows(2).any(|seg| {
let (a, b) = (seg[0], seg[1]);
segments_intersect(a, b, box_tl, box_tr)
|| segments_intersect(a, b, box_tr, box_br)
|| segments_intersect(a, b, box_br, box_bl)
|| segments_intersect(a, b, box_bl, box_tl)
})
} else {
screen.iter().all(|&sp| inside(sp))
};
if hit {
Some(wire.name.as_str())
} else {
None
for &[px, py, pz] in &wire.points {
if px.is_nan() {
prev = None;
continue;
}
let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, 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; }
}
prev = Some(sp);
}
let result = if crossing { hit } else { all_inside && prev.is_some() };
if result { Some(wire.name.as_str()) } else { None }
})
.collect()
}
@@ -141,6 +148,7 @@ pub fn poly_hit<'a>(
return vec![];
}
// Q: lazy projection — no Vec allocation per wire.
wires
.iter()
.filter_map(|wire| {
@@ -148,27 +156,31 @@ pub fn poly_hit<'a>(
return None;
}
let screen: Vec<Point> = wire
.points
.iter()
.map(|&p| world_to_screen(Vec3::from(p), view_proj, bounds))
.collect();
let mut hit = false;
let mut all_inside = true;
let mut prev: Option<Point> = None;
let hit = if crossing {
// Any vertex inside OR any wire segment crosses any polygon edge.
screen.iter().any(|&sp| point_in_polygon(sp, poly))
|| screen
.windows(2)
.any(|seg| segment_crosses_polygon(seg[0], seg[1], poly))
} else {
screen.iter().all(|&sp| point_in_polygon(sp, poly))
};
if hit {
Some(wire.name.as_str())
} else {
None
for &[px, py, pz] in &wire.points {
if px.is_nan() {
prev = None;
continue;
}
let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
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; }
}
prev = Some(sp);
}
let result = if crossing { hit } else { all_inside && prev.is_some() };
if result { Some(wire.name.as_str()) } else { None }
})
.collect()
}
+90 -19
View File
@@ -134,6 +134,12 @@ pub struct Scene {
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
pub annotation_scale: f32,
/// Cached model-space bounding box, keyed by geometry_epoch.
/// Avoids re-tessellating all entities on every ZOOM E / auto-fit call.
model_extents_cache: RefCell<Option<(u64, Option<(glam::Vec3, glam::Vec3)>)>>,
/// Reverse map: entity_handle → block_record_handle, built from entity_handles lists.
/// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block.
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
}
impl Scene {
@@ -167,6 +173,8 @@ impl Scene {
world_offset: [0.0; 3],
local_extent_max: 1e9,
annotation_scale: 1.0,
model_extents_cache: RefCell::new(None),
entity_block_map_cache: RefCell::new(None),
}
}
@@ -577,12 +585,16 @@ impl Scene {
}
}
let layout_block = self.current_layout_block_handle();
// Reuse the paper_sheet_cache to avoid a duplicate tessellation pass
// when both entity_wires_arc() and paper_canvas_wires() are called in the same frame.
let mut wires = (*self.paper_sheet_wires_arc()).clone();
if self.current_layout != "Model" {
wires.extend(self.viewport_content_wires(layout_block, None, None));
// Model space: paper_sheet_wires_arc IS the full entity wire set — share the Arc,
// no Vec clone needed.
if self.current_layout == "Model" {
let arc = self.paper_sheet_wires_arc();
*self.wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
return arc;
}
// Paper space: extend sheet wires with projected viewport content.
let mut wires = (*self.paper_sheet_wires_arc()).clone();
wires.extend(self.viewport_content_wires(layout_block, None, None));
let arc = Arc::new(wires);
*self.wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
arc
@@ -778,13 +790,38 @@ impl Scene {
}
}
// entity_handles not populated: fall back to "not listed in any other block".
!self
.document
.block_records
.iter()
.filter(|br| br.handle != block_handle)
.any(|br| br.entity_handles.contains(&entity_handle))
// P: epoch-cached reverse map replaces O(B) block_records scan.
let map = self.entity_block_map();
map.get(&entity_handle)
.map_or(true, |&owner| owner == block_handle)
}
/// Build (and epoch-cache) a reverse map: entity_handle → block_record_handle,
/// covering every entity explicitly listed in a block_record's entity_handles.
fn entity_block_map(&self) -> std::cell::Ref<'_, HashMap<Handle, Handle>> {
{
let cache = self.entity_block_map_cache.borrow();
if let Some((epoch, _)) = *cache {
if epoch == self.geometry_epoch {
drop(cache);
return std::cell::Ref::map(
self.entity_block_map_cache.borrow(),
|c| &c.as_ref().unwrap().1,
);
}
}
}
let mut map: HashMap<Handle, Handle> = HashMap::new();
for br in self.document.block_records.iter() {
for &eh in &br.entity_handles {
map.insert(eh, br.handle);
}
}
*self.entity_block_map_cache.borrow_mut() = Some((self.geometry_epoch, map));
std::cell::Ref::map(
self.entity_block_map_cache.borrow(),
|c| &c.as_ref().unwrap().1,
)
}
/// Full tessellation pipeline for one entity.
@@ -817,17 +854,51 @@ impl Scene {
.unwrap_or(Handle::NULL)
}
/// Compute the axis-aligned bounding box of all model-space entities by
/// collecting their `key_vertices`. Returns `None` when there are no
/// vertices (empty drawing).
/// Compute the axis-aligned bounding box of all model-space entities.
/// Result is epoch-cached so repeated ZOOM E / auto-fit calls are O(1).
pub fn model_space_extents(&self) -> Option<(glam::Vec3, glam::Vec3)> {
{
let cache = self.model_extents_cache.borrow();
if let Some((epoch, ext)) = *cache {
if epoch == self.geometry_epoch {
return ext;
}
}
}
let result = self.compute_model_space_extents();
*self.model_extents_cache.borrow_mut() = Some((self.geometry_epoch, result));
result
}
fn compute_model_space_extents(&self) -> Option<(glam::Vec3, glam::Vec3)> {
let model_block = self.model_space_block_handle();
if model_block.is_null() {
return None;
}
let [ox, oy, _] = self.world_offset;
let mut min = glam::Vec3::splat(f32::INFINITY);
let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
let mut any = false;
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
if self.current_layout == "Model" {
let cache = self.wire_cache.borrow();
if let Some((epoch, ref arc)) = *cache {
if epoch == self.geometry_epoch {
for wire in arc.iter() {
let [ax, ay, bx, by] = wire.aabb;
if ax.is_finite() && bx.is_finite() {
min = min.min(glam::Vec3::new(ax + ox as f32, ay + oy as f32, 0.0));
max = max.max(glam::Vec3::new(bx + ox as f32, by + oy as f32, 0.0));
any = true;
}
}
return if any { Some((min, max)) } else { None };
}
}
}
// Fallback: tessellate (first call or paper-space context).
for entity in self.document.entities() {
let c = entity.common();
if c.owner_handle != model_block || c.invisible {
@@ -1649,7 +1720,7 @@ impl Scene {
let color = if selected { [0.15, 0.55, 1.00, 1.0] } else { base_color };
for boundary in fills {
models.push(HatchModel {
boundary,
boundary: Arc::new(boundary),
pattern: hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
@@ -1693,7 +1764,7 @@ impl Scene {
fill_color = [0.15, 0.55, 1.00, 0.35];
}
models.push(HatchModel {
boundary,
boundary: Arc::new(boundary),
pattern: hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: fill_color,
@@ -1936,7 +2007,7 @@ impl Scene {
dxf.pattern.name.clone()
};
Some(HatchModel {
boundary,
boundary: std::sync::Arc::new(boundary),
pattern,
name,
color,
@@ -2062,7 +2133,7 @@ impl Scene {
[(solid.third_corner.x - ox) as f32, (solid.third_corner.y - oy) as f32],
];
HatchModel {
boundary,
boundary: std::sync::Arc::new(boundary),
pattern: hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
+37 -21
View File
@@ -410,24 +410,26 @@ impl Snapper {
// ── Intersection — segment-segment intersections ──────────
if self.is_on(SnapType::Intersection) {
let r_world = self.snap_radius_px * 0.5; // rough world-space cull
for i in 0..wires.len() {
if !wire_in_range(&wires[i]) { continue; }
for j in (i + 1)..wires.len() {
if !wire_in_range(&wires[j]) { continue; }
for seg_a in wires[i].points.windows(2) {
// S: pre-convert outside inner loop
let a0 = Vec3::from(seg_a[0]);
let a1 = Vec3::from(seg_a[1]);
let a_min_x = a0.x.min(a1.x);
let a_max_x = a0.x.max(a1.x);
let a_min_y = a0.y.min(a1.y);
let a_max_y = a0.y.max(a1.y);
for seg_b in wires[j].points.windows(2) {
let a0 = Vec3::from(seg_a[0]);
let a1 = Vec3::from(seg_a[1]);
let b0 = Vec3::from(seg_b[0]);
let b1 = Vec3::from(seg_b[1]);
// Quick AABB cull before expensive calculation.
let seg_mid_a = (a0 + a1) * 0.5;
let seg_mid_b = (b0 + b1) * 0.5;
if (seg_mid_a - cursor_world).length_squared()
> (r_world * 60.0).powi(2)
&& (seg_mid_b - cursor_world).length_squared()
> (r_world * 60.0).powi(2)
// O: tight per-segment AABB overlap cull
if a_max_x < b0.x.min(b1.x)
|| a_min_x > b0.x.max(b1.x)
|| a_max_y < b0.y.min(b1.y)
|| a_min_y > b0.y.max(b1.y)
{
continue;
}
@@ -481,23 +483,37 @@ impl Snapper {
}
// ── Apparent Intersection — screen-space intersections ─────────────
// L: pre-project each in-range wire's points to screen once, not once per segment pair.
if self.is_on(SnapType::ApparentIntersection) {
let screen_pts: Vec<Option<Vec<Point>>> = wires
.iter()
.map(|w| {
if !wire_in_range(w) {
return None;
}
Some(
w.points
.iter()
.map(|&p| world_to_screen(Vec3::from(p), view_proj, bounds))
.collect::<Vec<_>>(),
)
})
.collect();
for i in 0..wires.len() {
if !wire_in_range(&wires[i]) { continue; }
let Some(ref si) = screen_pts[i] else { continue };
for j in (i + 1)..wires.len() {
if !wire_in_range(&wires[j]) { continue; }
for seg_a in wires[i].points.windows(2) {
for seg_b in wires[j].points.windows(2) {
let sa0 = world_to_screen(Vec3::from(seg_a[0]), view_proj, bounds);
let sa1 = world_to_screen(Vec3::from(seg_a[1]), view_proj, bounds);
let sb0 = world_to_screen(Vec3::from(seg_b[0]), view_proj, bounds);
let sb1 = world_to_screen(Vec3::from(seg_b[1]), view_proj, bounds);
let Some(ref sj) = screen_pts[j] else { continue };
for (ai, seg_a) in wires[i].points.windows(2).enumerate() {
let sa0 = si[ai];
let sa1 = si[ai + 1];
for (bi, _) in wires[j].points.windows(2).enumerate() {
let sb0 = sj[bi];
let sb1 = sj[bi + 1];
if let Some((ta, _)) = seg_intersect_2d(sa0, sa1, sb0, sb1) {
// World position: interpolate along segment A.
let wa0 = Vec3::from(seg_a[0]);
let wa1 = Vec3::from(seg_a[1]);
let world = wa0 + ta * (wa1 - wa0);
try_pt(world, SnapType::ApparentIntersection);
try_pt(wa0 + ta * (wa1 - wa0), SnapType::ApparentIntersection);
}
}
}