Merge pull request #1015: The grid overlay was rebuilding the whole drawing

This commit is contained in:
Hakan Seven committed 2026-09-05 19:28:30 +03:00
commit 52d9c047d9
3 files changed
+208 -29

No files matched your search

+68 -3
View File
@@ -197,6 +197,20 @@ impl OpenCADStudio {
/// `view` so the single-window web build can render it directly, bypassing
/// the multi-window id dispatch above (the web build has no extra windows).
pub fn view_main(&self) -> Element<'_, Message> {
// Section marks for `view-detail`. A closure over a RefCell rather
// than one binding per section: two of these sections sit inside a
// nested block and would otherwise be out of scope at the report.
let view_t0 = crate::perf::enabled().then(iced::time::Instant::now);
let view_marks: std::cell::RefCell<Vec<(&'static str, f64)>> =
std::cell::RefCell::new(Vec::with_capacity(8));
let mark = |name: &'static str| {
if let Some(t0) = view_t0 {
view_marks
.borrow_mut()
.push((name, t0.elapsed().as_secs_f64() * 1000.0));
}
};
let i = self.active_tab;
let tab = &self.tabs[i];
let thumbnail_capture_clean = self.thumbnail_capture_clean;
@@ -257,6 +271,7 @@ impl OpenCADStudio {
// the same shader as model space: a full-canvas top-locked "sheet"
// viewport draws the layout's own geometry (white sheet + entities +
// borders) and the floating content viewports blit on top.
mark("viewport_3d");
let viewport_3d: Element<'_, Message> = if tab.is_start {
start_page_view(
&self.patrons,
@@ -334,6 +349,7 @@ impl OpenCADStudio {
// the shader pane_grid (same `model_panes` → identical layout). Layered
// above the crosshair overlay so it actually receives mouse events, and
// it owns the divider resize. Only built for the Model layout.
mark("model_input");
let model_input_layer: Option<Element<'_, Message>> = if is_paper || tab.is_start {
None
} else {
@@ -351,6 +367,7 @@ impl OpenCADStudio {
)
};
mark("grid");
let grid_overlay = {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let model_basis = {
@@ -365,9 +382,15 @@ impl OpenCADStudio {
);
(o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
};
let grid: Vec<crate::ui::overlay::GridParams> = tab
.scene
.grid_views(vw, vh)
// The first paper frame spends seconds in this block and `grid`
// alone cannot say which part. Reported only when it costs
// something, so ordinary frames stay quiet.
let t_grid = crate::perf::enabled().then(iced::time::Instant::now);
let views = tab.scene.grid_views(vw, vh);
let views_ms = crate::perf::elapsed_ms(t_grid);
let view_count = views.len();
let t_params = crate::perf::enabled().then(iced::time::Instant::now);
let grid: Vec<crate::ui::overlay::GridParams> = views
.into_iter()
.map(|(bounds, cam, handle)| {
let (origin, mut axes): (glam::DVec3, _) = if is_paper {
@@ -411,7 +434,16 @@ impl OpenCADStudio {
}
})
.collect();
let params_ms = crate::perf::elapsed_ms(t_params);
let t_bg = crate::perf::enabled().then(iced::time::Instant::now);
let bg = crosshair_background(tab, is_paper);
let bg_ms = crate::perf::elapsed_ms(t_bg);
if views_ms + params_ms + bg_ms >= 1.0 {
crate::perf_record!(
"[perf] grid-detail views={views_ms:.1}ms params={params_ms:.1}ms \
bg={bg_ms:.1}ms n={view_count}"
);
}
let bg_lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2];
let grid_style = crate::ui::overlay::GridStyle {
opacity: self.model_space.grid_opacity,
@@ -420,6 +452,7 @@ impl OpenCADStudio {
crate::ui::overlay::grid_overlay(grid, grid_style)
};
mark("selection_overlay");
let selection_overlay = {
// Hold a single `Ref<'_, SelectionState>` for the whole overlay
// block. The `vp_size` and `last_move_pos` reads below become
@@ -790,6 +823,7 @@ impl OpenCADStudio {
)
};
mark("viewport_mouse");
let viewport_mouse = mouse_area(container(
iced::widget::Space::new().width(Fill).height(Fill),
))
@@ -935,6 +969,7 @@ impl OpenCADStudio {
None
};
mark("viewport_stack");
let mut viewport_stack = if tab.is_start {
// Start tab: only the welcome widget over a flat background.
// Skip every drawing-only overlay (selection markers, snap info,
@@ -1053,6 +1088,7 @@ impl OpenCADStudio {
// both layered ABOVE the viewport mouse_area so they receive
// clicks (the shader viewport sits below it). Positioned with
// leading Spaces sized to the viewport's screen rectangle.
mark("active_vp_rect");
let active_vp_rect: Option<(acadrust::Handle, iced::Rectangle)> =
if is_paper && !tab.is_start {
tab.scene.active_viewport.and_then(|h| {
@@ -1383,6 +1419,7 @@ impl OpenCADStudio {
}
// Reserve the overlaid command line when placing cursor-anchored panels.
mark("chrome");
let command_line_inset = if self.command_line.history_open {
self.command_line.history_height.clamp(
crate::ui::command_line::HISTORY_HEIGHT_MIN,
@@ -2102,6 +2139,34 @@ impl OpenCADStudio {
};
// Shared CAD colour picker. Indexed ACI colours use OpenCADStudio's own
// dialog; True Color keeps the existing iced_aw gradient picker.
// Which part of the widget tree the frame went into. Reported as a
// breakdown rather than one number, because a first switch to a paper
// layout spends seconds here and `view` alone cannot say where.
if let Some(t0) = view_t0 {
mark("end");
let total = t0.elapsed().as_secs_f64() * 1000.0;
if total >= 1.0 {
let marks = view_marks.borrow();
// A mark opens a section; the following mark closes it, so a
// section's cost is the gap to its successor. The first mark
// is preceded by the preamble, reported as `head`.
let mut detail = String::new();
if let Some((_, first)) = marks.first() {
let _ = std::fmt::Write::write_fmt(
&mut detail,
format_args!(" head={first:.1}"),
);
}
for pair in marks.windows(2) {
let _ = std::fmt::Write::write_fmt(
&mut detail,
format_args!(" {}={:.1}", pair[0].0, pair[1].1 - pair[0].1),
);
}
crate::perf_record!("[perf] view-detail total={total:.1}ms{detail}");
}
}
if let Some((_, current)) = self.color_pick_target.as_ref() {
match self.color_picker_tab {
super::ColorPickerTab::Index => {
+111 -26
View File
@@ -9080,6 +9080,11 @@ impl Scene {
let mut materialize_ms = 0.0f64;
let mut memo_hits = 0usize;
let memo_misses;
// Two consecutive identical resident builds both missed every entry,
// which a stable guard cannot explain — the memo is being cleared
// between them. Report the guard and the inputs that can move it.
let mut guard_dbg = 0u64;
let mut guard_was_stale = false;
let t_build = perf.then(iced::time::Instant::now);
let mut wires: Vec<WireModel> = if memo_active {
// Guard hash of everything tessellate_entity output depends on
@@ -9114,7 +9119,9 @@ impl Scene {
} else {
(&self.tess_memo, &self.tess_memo_guard)
};
if guard_cell.get() != guard {
guard_dbg = guard;
guard_was_stale = guard_cell.get() != guard;
if guard_was_stale {
memo_cell.borrow_mut().clear();
guard_cell.set(guard);
}
@@ -9228,7 +9235,8 @@ impl Scene {
crate::perf_record!(
"[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} \
build={:.1} [classify={:.1} hits={:.1} tess={:.1} materialize={:.1}] sort={:.1}({}) \
entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={}",
entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={} \
guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
crate::perf::elapsed_ms(t_fn),
sort_cache_ms,
visible_ms,
@@ -9247,6 +9255,13 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
if memo_active { "on" } else { "off" },
visible_path,
visible_candidates,
guard_dbg,
guard_was_stale,
avp.map(|h| h.value()).unwrap_or(0),
anno,
annotation_scale_handle.map(|h| h.value()).unwrap_or(0),
all_visible,
crate::scene::text::sdf_atlas::generation(),
);
}
wires
@@ -10014,20 +10029,31 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
}
/// Full tessellation pipeline for one entity.
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
let bg = if self.current_layout == "Model" {
/// The document-wide inputs `tessellate_entity` takes, resolved from the
/// current layout and viewport.
///
/// Every entity in a walk gets the same four, and resolving them per entity
/// is not free — `block_cache_arc_for` takes a `RefCell` borrow and a cache
/// lookup. Callers that tessellate more than one entity resolve them once
/// and pass them down; sharing the resolution keeps those callers from
/// drifting away from `tessellate_one`.
fn tessellation_context(
&self,
) -> ([f32; 4], f32, Option<Handle>, Arc<cache::block_cache::BlockCache>, bool) {
let is_model = self.current_layout == "Model";
let bg = if is_model {
self.bg_color
} else {
self.paper_bg_color
};
let anno = if self.current_layout == "Model" {
let anno = if is_model {
self.annotation_scale
} else if let Some(viewport) = self.active_viewport {
self.viewport_annotation_multiplier(viewport)
} else {
1.0
};
let annotation_scale_handle = if self.current_layout == "Model" {
let annotation_scale_handle = if is_model {
crate::scene::annotative::scale_handle_by_name(
&self.document,
&self.document.header.current_annotation_scale,
@@ -10042,6 +10068,12 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
self.annotation_all_visible(),
self.active_viewport,
);
(bg, anno, annotation_scale_handle, blk_cache, !is_model)
}
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
let (bg, anno, annotation_scale_handle, blk_cache, paper) =
self.tessellation_context();
// tessellate_one is used for one-off lookups (hit test, properties).
// Skip culling here so the caller always gets the full geometry.
tessellate_entity(
@@ -10055,7 +10087,7 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
Some(&blk_cache),
None,
None,
self.current_layout != "Model",
paper,
)
}
@@ -10240,27 +10272,80 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
// offset-rel coords there silently double-subtracts world_offset
// inside `camera_for_viewport` and points the viewport at the
// wrong location on UTM-scale drawings.
for entity in self.document.entities() {
let c = entity.common();
if c.owner_handle != model_block
|| c.invisible
|| self.entity_temporarily_hidden(c.handle)
{
continue;
}
for wire in self.tessellate_one(entity) {
for &[x, y, z] in &wire.key_vertices {
let v = glam::Vec3::new(x as f32, y as f32, z as f32);
// Check finiteness *after* the f32 cast: a ray/xline endpoint
// is a huge-but-finite f64 that overflows to inf in f32, which
// the f64 `is_finite` test would have let through.
if v.is_finite() {
min = min.min(v);
max = max.max(v);
any = true;
//
// `tessellate_one` resolves the background, annotation scale and block
// cache from `&self` on every call, and this walk calls it once per
// model entity — 186 k times on a large drawing, each one taking a
// `RefCell` borrow for a block cache that is the same every time. Those
// inputs do not vary across the loop, so they are resolved once and the
// shared tessellator is called directly, which also lets the walk run
// in parallel the way the wire builder's own tessellation pass does.
// Same arguments, same output: only who computes them changed.
let (bg_for_tess, anno_for_tess, anno_handle_for_tess, blk_for_tess, paper_for_tess) =
self.tessellation_context();
let subjects: Vec<&EntityType> = self
.document
.entities()
.filter(|entity| {
let c = entity.common();
c.owner_handle == model_block
&& !c.invisible
&& !self.entity_temporarily_hidden(c.handle)
})
.collect();
let doc = &self.document;
let sel = &self.selected;
let avp = self.active_viewport;
let blk_ref: &cache::block_cache::BlockCache = &blk_for_tess;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let reduced = subjects
.into_par_iter()
.map(|entity| {
let mut lo = glam::Vec3::splat(f32::INFINITY);
let mut hi = glam::Vec3::splat(f32::NEG_INFINITY);
let mut hit = false;
for wire in tessellate_entity(
doc,
sel,
avp,
bg_for_tess,
anno_for_tess,
anno_handle_for_tess,
entity,
Some(blk_ref),
None,
None,
paper_for_tess,
) {
for &[x, y, z] in &wire.key_vertices {
let v = glam::Vec3::new(x as f32, y as f32, z as f32);
// Check finiteness *after* the f32 cast: a ray/xline
// endpoint is a huge-but-finite f64 that overflows to
// inf in f32, which the f64 `is_finite` test would have
// let through.
if v.is_finite() {
lo = lo.min(v);
hi = hi.max(v);
hit = true;
}
}
}
}
(lo, hi, hit)
})
.reduce(
|| {
(
glam::Vec3::splat(f32::INFINITY),
glam::Vec3::splat(f32::NEG_INFINITY),
false,
)
},
|a, b| (a.0.min(b.0), a.1.max(b.1), a.2 || b.2),
);
if reduced.2 {
min = min.min(reduced.0);
max = max.max(reduced.1);
any = true;
}
// Same mesh inclusion for the tessellate fallback path.
for (&handle, set) in &self.meshes {
+29
View File
@@ -69,7 +69,36 @@ impl Scene {
(layout_block, sheet, content)
}
/// Could any viewport in this layout draw a grid?
///
/// Read from the same fields `active_viewports` copies into
/// `ViewportInstance::grid_on`, and deliberately without the visibility
/// filters that function also applies: answering "maybe" only costs the
/// work that would have happened anyway, while answering "no" wrongly
/// would drop a grid the user asked for.
fn any_viewport_grid_on(&self) -> bool {
if self.current_layout == "Model" {
return self.model_tiles.borrow().iter().any(|tile| tile.grid_on);
}
let (_, sheet, content) = self.paper_viewport_handles();
let grid_on = |handle: Handle| {
matches!(
self.document.get_entity(handle),
Some(EntityType::Viewport(vp)) if vp.status.grid_on
)
};
grid_on(sheet) || content.iter().copied().any(grid_on)
}
pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera, Handle)> {
// `active_viewports` resolves a camera for every viewport, and
// `camera_for_viewport` reaches `model_space_extents`, which outside the
// Model layout tessellates the whole drawing. Filtering on `grid_on`
// afterwards threw all of that away: on the reproducer the first paper
// frame spent 7.8 s here to produce `n=0` views. Ask first.
if !self.any_viewport_grid_on() {
return Vec::new();
}
self.active_viewports(vw, vh, acadrust::entities::ViewportRenderMode::Wireframe2D)
.into_iter()
.filter(|inst| inst.grid_on)