forked from 1b-agent/opencadstudio-upstream
Merge pull request #1015: The grid overlay was rebuilding the whole drawing
This commit is contained in:
commit
52d9c047d9
3 files changed
+208
-29
No files matched your search
+68
-3
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user