diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 29fbe2388..2a6688c4b 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -348,6 +348,10 @@ impl H7CAD { if thawed_count > 0 { self.command_line.push_info(&format!("VPLAYER: {thawed_count} layer(s) thawed in viewport.")); } + // Sync layer panel so VP freeze columns update immediately. + let doc_layers = self.tabs[i].scene.document.layers.clone(); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); } // Show updated prompt (command stays active for more operations). diff --git a/src/app/history.rs b/src/app/history.rs index 43535d4e1..403b5a24b 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -44,7 +44,8 @@ impl H7CAD { self.tabs[i].active_grip = None; self.tabs[i].dirty = snapshot.dirty; let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); self.sync_ribbon_layers(); self.refresh_properties(); } diff --git a/src/app/mod.rs b/src/app/mod.rs index f5140fe7d..f750969c1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -180,6 +180,8 @@ pub enum Message { LayerToggleVisible(usize), LayerToggleLock(usize), LayerToggleFreeze(usize), + /// Toggle per-viewport freeze: (layer_index, vp_col_index) + LayerToggleVpFreeze(usize, usize), LayerNew, LayerDelete, LayerSetCurrent, diff --git a/src/app/update.rs b/src/app/update.rs index a3d2b63f0..b4207b30b 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -56,7 +56,8 @@ impl H7CAD { crate::linetypes::populate_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); self.sync_ribbon_layers(); self.tabs[i].scene.fit_all(); self.tabs[i].dirty = false; @@ -144,7 +145,8 @@ impl H7CAD { crate::linetypes::populate_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); self.command_line .push_output("Scene cleared. Standard linetypes loaded."); self.tabs[i].current_path = None; @@ -438,6 +440,43 @@ impl H7CAD { Task::none() } + Message::LayerToggleVpFreeze(layer_idx, vp_col_idx) => { + let i = self.active_tab; + let vp_handle = self.tabs[i].layers.vp_cols.get(vp_col_idx) + .map(|c| c.handle); + let layer_name = self.tabs[i].layers.layers.get(layer_idx) + .map(|l| l.name.clone()); + + if let (Some(vp_handle), Some(layer_name)) = (vp_handle, layer_name) { + // Get the layer handle from the document + if let Some(doc_layer) = self.tabs[i].scene.document.layers.get(&layer_name) { + let layer_handle = doc_layer.handle; + self.push_undo_snapshot(i, "VPLAYER"); + + // Toggle frozen_layers on the viewport entity + for e in self.tabs[i].scene.document.entities_mut() { + if let acadrust::EntityType::Viewport(vp) = e { + if vp.common.handle == vp_handle { + if vp.frozen_layers.contains(&layer_handle) { + vp.frozen_layers.retain(|h| h != &layer_handle); + } else { + vp.frozen_layers.push(layer_handle); + } + break; + } + } + } + + // Re-sync layer panel with updated VP info + let vp_info = self.tabs[i].scene.viewport_list(); + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); + self.tabs[i].dirty = true; + } + } + Task::none() + } + Message::LayerNew => { let i = self.active_tab; let mut n = 1; @@ -453,7 +492,8 @@ impl H7CAD { let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name)); self.tabs[i].dirty = true; let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); let new_idx = self.tabs[i].layers.layers.iter() .position(|l| l.name == new_name); if let Some(idx) = new_idx { @@ -476,7 +516,8 @@ impl H7CAD { self.tabs[i].scene.document.layers.remove(&name); self.tabs[i].dirty = true; let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); self.tabs[i].layers.selected = None; self.sync_ribbon_layers(); } @@ -549,7 +590,8 @@ impl H7CAD { self.tabs[i].dirty = true; } let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); self.tabs[i].layers.edit_buf.clear(); self.sync_ribbon_layers(); } @@ -1682,6 +1724,10 @@ impl H7CAD { } else if self.ribbon.active_is_layout() { self.ribbon.select(0); } + // Refresh VP freeze columns for the new layout. + let doc_layers = self.tabs[i].scene.document.layers.clone(); + let vp_info = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); Task::none() } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ff5385a1d..bf04f246b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -240,6 +240,37 @@ impl Scene { }) } + /// List of user viewports in the current layout: (handle, label, frozen_layer_handles). + pub fn viewport_list(&self) -> Vec<(acadrust::Handle, String, Vec)> { + if self.current_layout == "Model" { + return vec![]; + } + let layout_block = self.current_layout_block_handle(); + if layout_block.is_null() { + return vec![]; + } + let mut result: Vec<(acadrust::Handle, String, Vec)> = self + .document + .entities() + .filter_map(|e| { + if let EntityType::Viewport(vp) = e { + if vp.id > 1 && vp.common.owner_handle == layout_block { + Some((vp.common.handle, vp.id, vp.frozen_layers.clone())) + } else { + None + } + } else { + None + } + }) + .collect::>() + .into_iter() + .map(|(h, id, frozen)| (h, format!("VP {}", id - 1), frozen)) + .collect(); + result.sort_by_key(|(_, label, _)| label.clone()); + result + } + /// Count of user viewports (id > 1) in the current layout. pub fn viewport_count(&self) -> usize { if self.current_layout == "Model" { diff --git a/src/ui/layers.rs b/src/ui/layers.rs index 4f4cea054..512bb25ef 100644 --- a/src/ui/layers.rs +++ b/src/ui/layers.rs @@ -6,6 +6,7 @@ use acadrust::tables::layer::Layer as DocLayer; use acadrust::tables::Table; use acadrust::types::aci_table::aci_to_rgb; use acadrust::types::{Color as AcadColor, LineWeight}; +use acadrust::Handle; use iced::widget::{ button, column, combo_box, container, mouse_area, row, scrollable, text, text_input, }; @@ -13,6 +14,14 @@ use iced::{Background, Border, Color, Element, Fill, Length, Theme}; use iced::Padding; use crate::ui::ROW_H; +// ── Per-viewport column descriptor ─────────────────────────────────────────── + +#[derive(Clone, Debug)] +pub struct VpCol { + pub handle: Handle, + pub label: String, +} + // ── Row-height-derived constants ───────────────────────────────────────── /// SVG icon size inside a layer-table cell. const ICON_SZ: f32 = ROW_H * 0.62; // ≈16 px at ROW_H=26 @@ -33,6 +42,8 @@ pub struct Layer { pub linetype: String, pub lineweight: LineWeight, pub transparency: i32, + /// Freeze state per-viewport, indexed parallel to LayerPanel::vp_cols. + pub vp_frozen: Vec, } impl Layer { @@ -46,6 +57,7 @@ impl Layer { linetype: "Continuous".to_string(), lineweight: LineWeight::Default, transparency: 0, + vp_frozen: vec![], } } } @@ -66,6 +78,8 @@ pub struct LayerPanel { pub color_full_palette: bool, pub linetype_combo: combo_box::State, pub lw_combo: combo_box::State, + /// Per-viewport columns (only populated when in a paper layout with viewports). + pub vp_cols: Vec, } impl Default for LayerPanel { @@ -84,27 +98,47 @@ impl Default for LayerPanel { LinetypeItem { name: "Continuous".into(), art: String::new() } ]), lw_combo: combo_box::State::new(lw_options()), + vp_cols: vec![], } } } impl LayerPanel { - pub fn sync_from_doc(&mut self, doc_layers: &Table) { + /// Sync layers + update per-viewport freeze columns. + /// `vp_info`: list of (vp_handle, vp_label, frozen_layer_handles) from scene. + pub fn sync_with_viewports( + &mut self, + doc_layers: &Table, + vp_info: Vec<(Handle, String, Vec)>, + ) { + self.vp_cols = vp_info + .iter() + .map(|(h, label, _)| VpCol { handle: *h, label: label.clone() }) + .collect(); + self.layers = doc_layers .iter() - .map(|l| Layer { - name: l.name.clone(), - visible: !l.flags.off, - frozen: l.flags.frozen, - locked: l.flags.locked, - color: iced_color_from_acad(&l.color), - linetype: if l.line_type.is_empty() { - "Continuous".to_string() - } else { - l.line_type.clone() - }, - lineweight: l.line_weight, - transparency: 0, + .map(|l| { + let layer_handle = l.handle; + let vp_frozen = vp_info + .iter() + .map(|(_, _, frozen_handles)| frozen_handles.contains(&layer_handle)) + .collect(); + Layer { + name: l.name.clone(), + visible: !l.flags.off, + frozen: l.flags.frozen, + locked: l.flags.locked, + color: iced_color_from_acad(&l.color), + linetype: if l.line_type.is_empty() { + "Continuous".to_string() + } else { + l.line_type.clone() + }, + lineweight: l.line_weight, + transparency: 0, + vp_frozen, + } }) .collect(); } @@ -143,28 +177,37 @@ impl LayerPanel { .padding([4, 8]); // ── Column header ───────────────────────────────────────────────── - let col_header = container( - row![ - text("Status").size(10).color(DIM).width(50), - text("Name").size(10).color(DIM).width(Length::Fixed(COL_NAME)), - text("On").size(10).color(DIM).width(Length::Fixed(COL_ICON)), - text("Freeze").size(10).color(DIM).width(Length::Fixed(COL_ICON)), - text("Lock").size(10).color(DIM).width(Length::Fixed(COL_ICON)), - text("Color").size(10).color(DIM).width(Length::Fixed(COL_COLOR)), - text("Linetype").size(10).color(DIM).width(Length::Fixed(COL_LT)), - text("Lineweight").size(10).color(DIM).width(Length::Fixed(COL_LW)), - text("Transparency").size(10).color(DIM).width(Length::Fixed(COL_TRANS)), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(COL_HEADER_BG)), - border: Border { color: BORDER_COLOR, width: 1.0, radius: 0.0.into() }, - ..Default::default() - }) - .padding([4, 8]) - .width(Fill); + let mut header_row = row![ + text("Status").size(10).color(DIM).width(50), + text("Name").size(10).color(DIM).width(Length::Fixed(COL_NAME)), + text("On").size(10).color(DIM).width(Length::Fixed(COL_ICON)), + text("Freeze").size(10).color(DIM).width(Length::Fixed(COL_ICON)), + text("Lock").size(10).color(DIM).width(Length::Fixed(COL_ICON)), + text("Color").size(10).color(DIM).width(Length::Fixed(COL_COLOR)), + text("Linetype").size(10).color(DIM).width(Length::Fixed(COL_LT)), + text("Lineweight").size(10).color(DIM).width(Length::Fixed(COL_LW)), + text("Transparency").size(10).color(DIM).width(Length::Fixed(COL_TRANS)), + ] + .spacing(4) + .align_y(iced::Center); + + for vp in &self.vp_cols { + header_row = header_row.push( + text(vp.label.as_str()) + .size(10) + .color(DIM) + .width(Length::Fixed(COL_ICON)), + ); + } + + let col_header = container(header_row) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(COL_HEADER_BG)), + border: Border { color: BORDER_COLOR, width: 1.0, radius: 0.0.into() }, + ..Default::default() + }) + .padding([4, 8]) + .width(Fill); // ── Layer rows ──────────────────────────────────────────────────── let mut rows_col = column![].spacing(0); @@ -182,6 +225,7 @@ impl LayerPanel { rows_col = rows_col.push(layer_row( i, layer, is_sel, is_current, is_editing, &self.edit_buf, color_open, ltc, lwc, + &self.vp_cols, )); // Insert color picker dropdown below this row when open. @@ -258,6 +302,7 @@ fn layer_row<'a>( color_picker_open: bool, lt_combo: Option<&'a combo_box::State>, lw_combo_state: Option<&'a combo_box::State>, + vp_cols: &'a [VpCol], ) -> Element<'a, Message> { let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> { button( @@ -443,7 +488,7 @@ fn layer_row<'a>( let bg = if is_selected { ROW_SEL } else if index % 2 == 0 { ROW_EVEN } else { ROW_ODD }; - let row_content = row![ + let mut row_content = row![ container(status_dot).width(50).align_x(iced::Center).align_y(iced::Center), name_cell, container(svg_btn(vis_svg, Message::LayerToggleVisible(index))) @@ -463,6 +508,21 @@ fn layer_row<'a>( .spacing(4) .align_y(iced::Center); + // Per-viewport freeze columns + for (vp_idx, _vp_col) in vp_cols.iter().enumerate() { + let is_vp_frozen = layer.vp_frozen.get(vp_idx).copied().unwrap_or(false); + let vp_frz_svg: &'static [u8] = if is_vp_frozen { + include_bytes!("../../assets/icons/layers/layfrz.svg") + } else { + include_bytes!("../../assets/icons/layers/laythw.svg") + }; + row_content = row_content.push( + container(svg_btn(vp_frz_svg, Message::LayerToggleVpFreeze(index, vp_idx))) + .width(Length::Fixed(COL_ICON)) + .align_x(iced::Center), + ); + } + mouse_area( container(row_content) .style(move |_: &Theme| container::Style {