diff --git a/crates/gdsr-viewer/src/app.rs b/crates/gdsr-viewer/src/app.rs index a2ced3f..070139c 100644 --- a/crates/gdsr-viewer/src/app.rs +++ b/crates/gdsr-viewer/src/app.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::thread; -use crate::drawable::Drawable; +use crate::drawable::{Drawable, WorldBBox}; use crate::panels; use crate::quick_pick::{QuickPick, QuickPickResult}; use crate::recent::{RecentProjectItem, RecentProjects}; @@ -119,15 +119,23 @@ impl ViewerApp { cell.elements.clear(); cell.layers.clear(); cell.spatial_grid = None; + cell.tessellation_cache.clear(); cell.cell_stats = cell.library.get_cell(name).map(gdsr::CellStats::from_cell); + let depth = cell.render_depth; + if depth == 0 { + cell.elements_loading = false; + return; + } + if let Some(cell_data) = cell.library.get_cell(name) { let cell_data = cell_data.clone(); let library = cell.library.clone(); let (tx, rx) = mpsc::channel(); + let stream_depth = Some((depth - 1) as usize); thread::spawn(move || { - cell_data.stream_elements(None, &library, &tx); + cell_data.stream_elements(stream_depth, &library, &tx); }); cell.element_receiver = Some(rx); @@ -139,7 +147,21 @@ impl ViewerApp { /// Adjusts the viewport to fit all currently loaded elements. fn zoom_to_fit(&mut self) { if let Some(cell) = self.cell.as_ref() { - if let Some(bounds) = viewport::compute_bounds(&cell.elements) { + let bounds = if cell.render_depth == 0 { + cell.selected_cell.as_ref().and_then(|name| { + let c = cell.library.get_cell(name)?; + let (min_pt, max_pt) = gdsr::Dimensions::bounding_box(c); + Some(WorldBBox::new( + min_pt.x().absolute_value(), + min_pt.y().absolute_value(), + max_pt.x().absolute_value(), + max_pt.y().absolute_value(), + )) + }) + } else { + viewport::compute_bounds(&cell.elements) + }; + if let Some(bounds) = bounds { let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::new(800.0, 600.0)); self.viewport.zoom_to_fit(&bounds, rect); @@ -328,6 +350,7 @@ impl eframe::App for ViewerApp { }); // Bottom activity bar + let mut depth_changed = false; egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { ui.horizontal(|ui| { let is_tree = self.side_panel_tab == SidePanelTab::Cells @@ -411,6 +434,19 @@ impl eframe::App for ViewerApp { ui.selectable_value(&mut self.grid_spacing, preset, label); } }); + if let Some(cell) = &mut self.cell { + let prev_depth = cell.render_depth; + if ui.small_button("+").clicked() && cell.render_depth < 99 { + cell.render_depth += 1; + } + ui.label(format!("Depth: {}", cell.render_depth)); + if ui.small_button("−").clicked() && cell.render_depth > 0 { + cell.render_depth -= 1; + } + if cell.render_depth != prev_depth { + depth_changed = true; + } + } if self.ruler.active { ui.label("Ruler: click to place point (Esc to cancel)"); } @@ -421,6 +457,13 @@ impl eframe::App for ViewerApp { }); }); + if depth_changed { + if let Some(name) = self.cell.as_ref().and_then(|c| c.selected_cell.clone()) { + self.select_cell(&name); + } + self.render_cache.clear(); + } + // Cell picker (⌘P) self.cell_picker.set_items( self.cell @@ -498,6 +541,9 @@ impl eframe::App for ViewerApp { let grid_spacing = self.grid_spacing; let hovered_element = &mut self.hovered_element; let query_buf = &mut self.query_buf; + let render_depth = cell.as_ref().map_or(1, |c| c.render_depth); + let selected_cell_name: Option = + cell.as_ref().and_then(|c| c.selected_cell.clone()); egui::CentralPanel::default().show(ctx, |ui| { let mut empty_cache = std::collections::HashMap::new(); let (elements, spatial_grid, library, tessellation_cache) = @@ -524,6 +570,8 @@ impl eframe::App for ViewerApp { show_grid, grid_spacing, *hovered_element, + render_depth, + selected_cell_name.as_deref(), ); let prev_hovered = *hovered_element; diff --git a/crates/gdsr-viewer/src/drawable.rs b/crates/gdsr-viewer/src/drawable.rs index e5d4fcb..17ee664 100644 --- a/crates/gdsr-viewer/src/drawable.rs +++ b/crates/gdsr-viewer/src/drawable.rs @@ -63,6 +63,9 @@ pub struct DrawContext<'a> { pub screen_pts_buf: &'a mut Vec, /// When true, the element is drawn with a brighter fill and bolder outline. pub highlight: bool, + /// When true, un-flattened cell references draw as outlined bounding boxes + /// with the cell name centered, instead of expanding their contents. + pub show_ref_bbox: bool, } /// Fill alpha for normal and highlighted elements. @@ -826,6 +829,98 @@ impl Drawable for gdsr::Node { } } +/// Draws an un-flattened reference as an outlined bounding box with a centered cell name label. +fn draw_ref_as_bbox(reference: &gdsr::Reference, ctx: &mut DrawContext) { + let (label, bbox) = if let Some(cell_name) = reference.instance().as_cell() { + let Some(lib) = ctx.library else { return }; + let Some(cell) = lib.get_cell(cell_name) else { + return; + }; + let (min_pt, max_pt) = cell.bounding_box(); + let bbox_poly = gdsr::Polygon::new( + vec![ + min_pt, + gdsr::Point::new(max_pt.x(), min_pt.y()), + max_pt, + gdsr::Point::new(min_pt.x(), max_pt.y()), + min_pt, + ], + Layer::new(0), + DataType::new(0), + ); + let bbox_element = Element::Polygon(bbox_poly); + let mut merged: Option = None; + for el in reference.get_elements_in_grid(&bbox_element) { + if let Some(bb) = el.world_bbox() { + merged = Some(match merged { + Some(acc) => acc.merge(&bb), + None => bb, + }); + } + } + match merged { + Some(bb) => (Some(cell_name.as_str()), bb), + None => return, + } + } else if let Some(element) = reference.instance().as_element() { + let mut merged: Option = None; + for el in reference.get_elements_in_grid(element) { + if let Some(bb) = el.world_bbox() { + merged = Some(match merged { + Some(acc) => acc.merge(&bb), + None => bb, + }); + } + } + match merged { + Some(bb) => (None, bb), + None => return, + } + } else { + return; + }; + + if !bbox.overlaps(ctx.visible) { + return; + } + + let s_min = ctx + .viewport + .world_to_screen(bbox.min_x, bbox.min_y, ctx.rect); + let s_max = ctx + .viewport + .world_to_screen(bbox.max_x, bbox.max_y, ctx.rect); + let screen_rect = Rect::from_two_pos(s_min, s_max); + let stroke_color = Color32::from_rgb(180, 180, 180); + ctx.rect_stroke( + screen_rect, + 0.0, + Stroke::new(1.0, stroke_color), + StrokeKind::Outside, + ); + + if let Some(name) = label { + let sw = (s_max.x - s_min.x).abs(); + let sh = (s_min.y - s_max.y).abs(); + if sw >= 40.0 && sh >= 20.0 { + let char_count = name.len().max(1) as f32; + let fit_w = sw * 0.9 / (char_count * 0.6); + let fit_h = sh * 0.4; + let font_size = fit_w.min(fit_h).min(48.0); + if font_size >= 8.0 { + let center = screen_rect.center(); + ctx.text( + center, + egui::Align2::CENTER_CENTER, + name, + FontId::monospace(font_size), + stroke_color, + ); + } + } + } +} + impl Drawable for gdsr::Reference { fn layer_keys(&self) -> Vec<(Layer, DataType)> { match self.instance().as_element() { @@ -860,6 +955,10 @@ impl Drawable for gdsr::Reference { } fn draw(&self, ctx: &mut DrawContext) { + if ctx.show_ref_bbox { + draw_ref_as_bbox(self, ctx); + return; + } if let Some(element) = self.instance().as_element() { for el in self.get_elements_in_grid(element) { el.draw(ctx); @@ -952,6 +1051,7 @@ pub fn draw_highlight( tessellation_cache, screen_pts_buf: &mut screen_pts_buf, highlight: true, + show_ref_bbox: false, }; element.draw(&mut ctx); for (_, mesh) in layer_meshes { diff --git a/crates/gdsr-viewer/src/state.rs b/crates/gdsr-viewer/src/state.rs index 8839bda..a911e7c 100644 --- a/crates/gdsr-viewer/src/state.rs +++ b/crates/gdsr-viewer/src/state.rs @@ -34,6 +34,7 @@ pub struct CellState { pub spatial_grid: Option, pub tessellation_cache: HashMap>, pub cell_stats: Option, + pub render_depth: u32, } impl CellState { @@ -57,6 +58,7 @@ impl CellState { spatial_grid: None, tessellation_cache: HashMap::new(), cell_stats: None, + render_depth: 1, } } } @@ -78,6 +80,7 @@ pub struct RenderCache { /// Invalidation metadata — if any of these change, full re-render. hidden_layers: Vec<(Layer, DataType)>, element_count: usize, + render_depth: u32, populated: bool, } @@ -92,6 +95,7 @@ impl Default for RenderCache { render_rect_center: Pos2::ZERO, hidden_layers: Vec::new(), element_count: 0, + render_depth: 1, populated: false, } } @@ -103,6 +107,7 @@ impl RenderCache { &self, hidden_layers: &[(Layer, DataType)], element_count: usize, + render_depth: u32, current_center_x: f64, current_center_y: f64, current_zoom: f64, @@ -111,7 +116,10 @@ impl RenderCache { if !self.populated { return true; } - if self.hidden_layers != hidden_layers || self.element_count != element_count { + if self.hidden_layers != hidden_layers + || self.element_count != element_count + || self.render_depth != render_depth + { return true; } @@ -162,6 +170,7 @@ impl RenderCache { rect_center: Pos2, hidden_layers: Vec<(Layer, DataType)>, element_count: usize, + render_depth: u32, ) { self.layer_meshes = layer_meshes; self.extra_shapes = extra_shapes; @@ -171,6 +180,7 @@ impl RenderCache { self.render_rect_center = rect_center; self.hidden_layers = hidden_layers; self.element_count = element_count; + self.render_depth = render_depth; self.populated = true; } @@ -207,6 +217,7 @@ mod tests { test_rect().center(), vec![], 42, + 1, ); cache } @@ -215,20 +226,20 @@ mod tests { fn no_rerender_on_same_state() { let cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 0.0, 0.0, 1.0, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 1.0, rect)); } #[test] fn rerender_when_empty() { let cache = RenderCache::default(); - assert!(cache.needs_full_render(&[], 42, 0.0, 0.0, 1.0, test_rect())); + assert!(cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 1.0, test_rect())); } #[test] fn no_rerender_on_small_pan() { let cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 100.0, 0.0, 1.0, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 100.0, 0.0, 1.0, rect)); } #[test] @@ -237,14 +248,14 @@ mod tests { let rect = test_rect(); // 800px viewport width, margin budget = 800, 80% = 640px. // dx_screen = (0.0 - 700.0) * 1.0 = -700, |700| > 640 → re-render - assert!(cache.needs_full_render(&[], 42, 700.0, 0.0, 1.0, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 700.0, 0.0, 1.0, rect)); } #[test] fn no_rerender_on_small_zoom() { let cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 0.0, 0.0, 1.5, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 1.5, rect)); } #[test] @@ -252,7 +263,7 @@ mod tests { let cache = populated_cache(); let rect = test_rect(); // zoom ratio 3.0 / 1.0 = 3.0, outside [0.5, 2.0] - assert!(cache.needs_full_render(&[], 42, 0.0, 0.0, 3.0, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 3.0, rect)); } #[test] @@ -261,6 +272,7 @@ mod tests { assert!(cache.needs_full_render( &[(Layer::new(1), DataType::new(0))], 42, + 1, 0.0, 0.0, 1.0, @@ -271,7 +283,13 @@ mod tests { #[test] fn rerender_on_element_count_change() { let cache = populated_cache(); - assert!(cache.needs_full_render(&[], 43, 0.0, 0.0, 1.0, test_rect())); + assert!(cache.needs_full_render(&[], 43, 1, 0.0, 0.0, 1.0, test_rect())); + } + + #[test] + fn rerender_on_render_depth_change() { + let cache = populated_cache(); + assert!(cache.needs_full_render(&[], 42, 2, 0.0, 0.0, 1.0, test_rect())); } #[test] @@ -321,6 +339,7 @@ mod tests { rect.center(), vec![], 1, + 1, ); let new_vp = Viewport { @@ -352,28 +371,28 @@ mod tests { fn no_rerender_at_zoom_ratio_boundary_low() { let cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 0.0, 0.0, 0.5, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 0.5, rect)); } #[test] fn rerender_just_below_zoom_ratio_boundary() { let cache = populated_cache(); let rect = test_rect(); - assert!(cache.needs_full_render(&[], 42, 0.0, 0.0, 0.49, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 0.49, rect)); } #[test] fn no_rerender_at_zoom_ratio_boundary_high() { let cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 0.0, 0.0, 2.0, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 2.0, rect)); } #[test] fn rerender_just_above_zoom_ratio_boundary() { let cache = populated_cache(); let rect = test_rect(); - assert!(cache.needs_full_render(&[], 42, 0.0, 0.0, 2.01, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 2.01, rect)); } #[test] @@ -381,14 +400,14 @@ mod tests { let cache = populated_cache(); let rect = test_rect(); // margin_x = 800 * 0.8 = 640, dx_screen = |0 - 640| = 640, NOT > 640 - assert!(!cache.needs_full_render(&[], 42, 640.0, 0.0, 1.0, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 640.0, 0.0, 1.0, rect)); } #[test] fn rerender_just_beyond_margin() { let cache = populated_cache(); let rect = test_rect(); - assert!(cache.needs_full_render(&[], 42, 641.0, 0.0, 1.0, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 641.0, 0.0, 1.0, rect)); } #[test] @@ -414,9 +433,9 @@ mod tests { fn clear_forces_rerender() { let mut cache = populated_cache(); let rect = test_rect(); - assert!(!cache.needs_full_render(&[], 42, 0.0, 0.0, 1.0, rect)); + assert!(!cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 1.0, rect)); cache.clear(); - assert!(cache.needs_full_render(&[], 42, 0.0, 0.0, 1.0, rect)); + assert!(cache.needs_full_render(&[], 42, 1, 0.0, 0.0, 1.0, rect)); } #[test] diff --git a/crates/gdsr-viewer/src/viewport/mod.rs b/crates/gdsr-viewer/src/viewport/mod.rs index dc09687..bdf379d 100644 --- a/crates/gdsr-viewer/src/viewport/mod.rs +++ b/crates/gdsr-viewer/src/viewport/mod.rs @@ -5,7 +5,7 @@ pub use bounds::compute_bounds; use std::collections::HashMap; use egui::{Color32, Pos2, Rect, Sense}; -use gdsr::{DataType, Element, Layer, Library}; +use gdsr::{DataType, Dimensions, Element, Layer, Library}; use crate::drawable::{DrawContext, Drawable, WorldBBox, draw_highlight}; use crate::grid; @@ -106,6 +106,8 @@ impl Viewport { show_grid: bool, grid_spacing: GridSpacing, hovered_element: Option, + render_depth: u32, + selected_cell: Option<&str>, ) -> Option<(f64, f64)> { let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag()); let rect = response.rect; @@ -178,6 +180,7 @@ impl Viewport { if !render_cache.needs_full_render( &hidden_layers, elements.len(), + render_depth, self.center_x, self.center_y, self.zoom, @@ -221,6 +224,53 @@ impl Viewport { return mouse_world; } + // Depth-0: draw just the selected cell's bounding box with its name. + if render_depth == 0 { + if let Some(cell_name) = selected_cell { + if let Some(lib) = library { + if let Some(cell) = lib.get_cell(cell_name) { + let (min_pt, max_pt) = cell.bounding_box(); + let min_x = min_pt.x().absolute_value(); + let min_y = min_pt.y().absolute_value(); + let max_x = max_pt.x().absolute_value(); + let max_y = max_pt.y().absolute_value(); + let s_min = self.world_to_screen(min_x, min_y, rect); + let s_max = self.world_to_screen(max_x, max_y, rect); + let screen_rect = Rect::from_two_pos(s_min, s_max); + let stroke_color = Color32::from_rgb(180, 180, 180); + painter.rect_stroke( + screen_rect, + 0.0, + egui::Stroke::new(1.0, stroke_color), + egui::StrokeKind::Outside, + ); + let sw = (s_max.x - s_min.x).abs(); + let sh = (s_min.y - s_max.y).abs(); + if sw >= 40.0 && sh >= 20.0 { + let char_count = cell_name.len().max(1) as f32; + let fit_w = sw * 0.9 / (char_count * 0.6); + let fit_h = sh * 0.4; + let font_size = fit_w.min(fit_h).min(48.0); + if font_size >= 8.0 { + painter.text( + screen_rect.center(), + egui::Align2::CENTER_CENTER, + cell_name, + egui::FontId::monospace(font_size), + stroke_color, + ); + } + } + } + } + } + let mouse_world = response + .hover_pos() + .map(|pos| self.screen_to_world(pos.x, pos.y, rect)); + ruler.draw(&painter, self, rect, mouse_world); + return mouse_world; + } + // Full render: query a 3× expanded region so the cache has margin for panning. let visible = self.visible_world_rect(rect); let w = visible.max_x - visible.min_x; @@ -235,6 +285,7 @@ impl Viewport { let mut layer_meshes = HashMap::new(); let mut extra_shapes = Vec::new(); let mut screen_pts_buf = Vec::new(); + let show_ref_bbox = render_depth > 0; let mut ctx = DrawContext { painter: &painter, layer_meshes: &mut layer_meshes, @@ -248,6 +299,7 @@ impl Viewport { tessellation_cache, screen_pts_buf: &mut screen_pts_buf, highlight: false, + show_ref_bbox, }; if let Some(grid) = spatial_grid { @@ -295,6 +347,17 @@ impl Viewport { } } } + + // Cell references (Instance::Cell) have no world_bbox so the spatial + // grid never contains them. Draw any unseen references in a second pass. + if show_ref_bbox { + for (i, element) in elements.iter().enumerate() { + if !seen[i] && matches!(element, Element::Reference(_)) { + ctx.current_element_idx = Some(i as u32); + element.draw(&mut ctx); + } + } + } } else { for (i, element) in elements.iter().enumerate() { ctx.current_element_idx = Some(i as u32); @@ -317,6 +380,7 @@ impl Viewport { rect.center(), hidden_layers, elements.len(), + render_depth, ); if let Some(idx) = hovered_element {