forked from 1b-agent/opencadstudio-upstream
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:
1 parent
b83c982bc2
commit
45f9f96c93
6 files changed
+265
-233
No files matched your search
@@ -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],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user