diff --git a/src/app/properties.rs b/src/app/properties.rs index 9281f0655..e46f91231 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -142,11 +142,198 @@ impl OpenCADStudio { let selected = self.tabs[i].scene.selected_entities(); let mut panel = match selected.len() { 0 => { - let sections = crate::entities::object_data::cached_document_sections( - &self.tabs[i].scene.object_data_cache, - ); + use crate::scene::model::object::{PropSection, PropValue, Property}; + + let tab = &self.tabs[i]; + let scene = &tab.scene; + let doc = &scene.document; + let header = &doc.header; + let camera = scene.camera.borrow(); + let (viewport_width, viewport_height) = scene.selection.borrow().vp_size; + let aspect = if viewport_height > 0.0 { + viewport_width as f64 / viewport_height as f64 + } else { + 1.0 + }; + let view_height = camera.ortho_size() as f64 * 2.0; + let view_width = view_height * aspect; + let format_length = crate::entities::common::format_length; + let read_only = |label: &str, value: String| Property { + label: label.to_string(), + field: "drawing_property", + value: PropValue::ReadOnly(value), + }; + let current_layer = if header.current_layer_name.is_empty() { + tab.active_layer.clone() + } else { + header.current_layer_name.clone() + }; + let current_linetype = if !header.current_linetype_name.is_empty() { + header.current_linetype_name.clone() + } else if !header.current_linetype_handle.is_null() { + doc.line_types + .iter() + .find(|line_type| { + line_type.handle == header.current_linetype_handle + }) + .map(|line_type| line_type.name.clone()) + .unwrap_or_else(|| "ByLayer".to_string()) + } else { + "ByLayer".to_string() + }; + let material = if header.current_material_handle.is_null() { + "ByLayer".to_string() + } else { + doc.objects + .get(&header.current_material_handle) + .and_then(|object| match object { + acadrust::objects::ObjectType::Material(material) => { + Some(material.name.clone()) + } + _ => None, + }) + .unwrap_or_else(|| "ByLayer".to_string()) + }; + let plot_style = match header.current_plotstyle_type { + 1 => "ByBlock", + 2 => "ByColor", + 3 => "ByObject", + _ => "ByLayer", + }; + let layout_plot_table = doc.objects.values().find_map(|object| { + let acadrust::objects::ObjectType::Layout(layout) = object else { + return None; + }; + (layout.name == scene.current_layout + && !layout.plot_style_sheet.trim().is_empty()) + .then(|| layout.plot_style_sheet.clone()) + }); + let plot_table = layout_plot_table + .or_else(|| { + (!header.stylesheet.trim().is_empty()) + .then(|| header.stylesheet.clone()) + }) + .unwrap_or_else(|| "None".to_string()); + let ucs_per_viewport = scene + .active_viewport + .and_then(|handle| doc.get_entity(handle)) + .and_then(|entity| match entity { + acadrust::EntityType::Viewport(viewport) => { + Some(viewport.ucs_per_viewport) + } + _ => None, + }) + .unwrap_or(false); + let ucs_name = tab + .active_ucs + .as_ref() + .map(|ucs| ucs.name.trim()) + .filter(|name| !name.is_empty()) + .unwrap_or("World") + .to_string(); + let annotation_scale = if header.current_annotation_scale.trim().is_empty() { + "1:1".to_string() + } else { + header.current_annotation_scale.clone() + }; + let sections = vec![ + PropSection { + title: "General".to_string(), + props: vec![ + read_only("AC Version", doc.version.as_str().to_string()), + Property { + label: "Color".to_string(), + field: "color", + value: PropValue::ColorChoice(header.current_entity_color), + }, + Property { + label: "Layer".to_string(), + field: "layer", + value: PropValue::LayerChoice(current_layer), + }, + Property { + label: "Linetype".to_string(), + field: "linetype", + value: PropValue::LinetypeChoice(current_linetype), + }, + read_only( + "Linetype scale", + format_length(header.current_entity_linetype_scale), + ), + Property { + label: "Lineweight".to_string(), + field: "line_weight", + value: PropValue::LwChoice( + acadrust::types::LineWeight::from_value( + header.current_line_weight, + ), + ), + }, + read_only("Transparency", "ByLayer".to_string()), + read_only("Thickness", format_length(header.thickness)), + ], + }, + PropSection { + title: "3D Visualization".to_string(), + props: vec![read_only("Material", material)], + }, + PropSection { + title: "Plot style".to_string(), + props: vec![ + read_only("Plot style", plot_style.to_string()), + read_only("Plot style table", plot_table.clone()), + read_only( + "Plot table attached to", + if plot_table == "None" { + "None".to_string() + } else { + scene.current_layout.clone() + }, + ), + read_only( + "Plot table type", + if header.plotstyle_mode { + "Named plot styles".to_string() + } else { + "Color-dependent plot styles".to_string() + }, + ), + ], + }, + PropSection { + title: "View".to_string(), + props: vec![ + read_only("Center X", format_length(camera.target.x)), + read_only("Center Y", format_length(camera.target.y)), + read_only("Center Z", format_length(camera.target.z)), + read_only("Height", format_length(view_height)), + read_only("Width", format_length(view_width)), + ], + }, + PropSection { + title: "Misc".to_string(), + props: vec![ + read_only("Annotation scale", annotation_scale), + read_only( + "UCS icon On", + if self.show_ucs_icon { "Yes" } else { "No" }.to_string(), + ), + read_only( + "UCS icon at origin", + if self.ucs_icon_at_origin { "Yes" } else { "No" } + .to_string(), + ), + read_only( + "UCS per viewport", + if ucs_per_viewport { "Yes" } else { "No" }.to_string(), + ), + read_only("UCS Name", ucs_name), + read_only("Visual Style", tab.visual_style.clone()), + ], + }, + ]; ui::PropertiesPanel { - title: "Drawing".to_string(), + title: "No selection".to_string(), sections, layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), linetype_combo: iced::widget::combo_box::State::new( diff --git a/src/app/update/file.rs b/src/app/update/file.rs index daa3516a5..56439debf 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -864,7 +864,7 @@ pub(super) fn on_open_file(&mut self) -> Task { self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; self.tabs[i].dirty = false; self.tabs[i].history = crate::app::document::HistoryState::default(); - self.refresh_selected_grips(); + self.refresh_properties(); #[cfg(not(target_arch = "wasm32"))] let interaction_task = { let wires = self.tabs[i].scene.hit_test_wires(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 8dd36de91..9c4994482 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -920,6 +920,9 @@ impl OpenCADStudio { Message::TabSwitch(idx) => { self.doc_tab_context_menu = None; + self.layout_list_open = false; + self.layout_context_menu = None; + self.layout_rename_state = None; if idx < self.tabs.len() { if idx != self.active_tab { // The attribute editor is tab-scoped; leaving its tab @@ -931,6 +934,11 @@ impl OpenCADStudio { self.stamp_header_sysvars(prev); } self.active_tab = idx; + if self.tabs[idx].is_start + && self.active_modal == Some(super::ModalKind::LayoutManager) + { + self.close_active_modal(); + } self.sync_ribbon_layers(); self.sync_ribbon_styles(); // #21: also re-seed ribbon Color / Linetype / Lineweight @@ -2334,6 +2342,10 @@ impl OpenCADStudio { Task::none() } Message::ToggleLayoutList => { + if self.tabs[self.active_tab].is_start { + self.layout_list_open = false; + return Task::none(); + } self.layout_list_open ^= true; Task::none() } @@ -2919,74 +2931,89 @@ impl OpenCADStudio { Message::PropLayerChanged(layer) => { let i = self.active_tab; let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for &handle in &handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_common_prop( - entity, "layer", &layer, - ); - } - } - self.invalidate_property_targets(i, &handles); - self.tabs[i].dirty = true; + if handles.is_empty() { + let task = self.on_ribbon_layer_changed(layer); self.refresh_properties(); + return task; } + self.push_undo_snapshot(i, "CHPROP"); + for &handle in &handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_common_prop( + entity, "layer", &layer, + ); + } + } + self.invalidate_property_targets(i, &handles); + self.tabs[i].dirty = true; + self.refresh_properties(); Task::none() } Message::PropColorChanged(color) => { let i = self.active_tab; let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for &handle in &handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_color(entity, color); - } - } - self.invalidate_property_targets(i, &handles); - self.tabs[i].properties.color_picker_open = false; + if handles.is_empty() { self.tabs[i].properties.color_palette_open = false; - self.tabs[i].dirty = true; + let task = self.on_ribbon_color_changed(color); self.refresh_properties(); + return task; } + self.push_undo_snapshot(i, "CHPROP"); + for &handle in &handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_color(entity, color); + } + } + self.invalidate_property_targets(i, &handles); + self.tabs[i].properties.color_picker_open = false; + self.tabs[i].properties.color_palette_open = false; + self.tabs[i].dirty = true; + self.refresh_properties(); Task::none() } Message::PropLwChanged(lw) => { let i = self.active_tab; let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for &handle in &handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_line_weight(entity, lw); - } - } - self.invalidate_property_targets(i, &handles); + if handles.is_empty() { + self.tabs[i].scene.document.header.current_line_weight = lw.value(); self.tabs[i].dirty = true; + self.ribbon.active_lineweight = lw; self.refresh_properties(); + return Task::none(); } + self.push_undo_snapshot(i, "CHPROP"); + for &handle in &handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_line_weight(entity, lw); + } + } + self.invalidate_property_targets(i, &handles); + self.tabs[i].dirty = true; + self.refresh_properties(); Task::none() } Message::PropLinetypeChanged(lt) => { let i = self.active_tab; let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for &handle in &handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_common_prop( - entity, "linetype", <, - ); - } - } - self.invalidate_property_targets(i, &handles); - self.tabs[i].dirty = true; + if handles.is_empty() { + let task = self.on_ribbon_linetype_changed(lt); self.refresh_properties(); + return task; } + self.push_undo_snapshot(i, "CHPROP"); + for &handle in &handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_common_prop( + entity, "linetype", <, + ); + } + } + self.invalidate_property_targets(i, &handles); + self.tabs[i].dirty = true; + self.refresh_properties(); Task::none() } @@ -3364,6 +3391,11 @@ impl OpenCADStudio { // ── Layout Manager Panel ────────────────────────────────────────── Message::LayoutManagerOpen => { let i = self.active_tab; + if self.tabs[i].is_start { + self.command_line + .push_info("Open or create a drawing to manage layouts."); + return Task::none(); + } let current = self.tabs[i].scene.current_layout.clone(); self.layout_manager_selected = current.clone(); self.layout_manager_rename_buf = if current == "Model" { @@ -3417,6 +3449,11 @@ impl OpenCADStudio { } Message::LayoutManagerNew => { let i = self.active_tab; + if self.tabs[i].is_start { + self.command_line + .push_info("Open or create a drawing to add a layout."); + return Task::none(); + } let existing = self.tabs[i].scene.layout_names(); let n = (1usize..) .find(|n| !existing.contains(&format!("Layout{n}"))) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index b30938236..99b81d9c8 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3677,6 +3677,11 @@ impl OpenCADStudio { pub(super) fn on_layout_switch(&mut self, name: String) -> Task { let i = self.active_tab; + if self.tabs[i].is_start { + self.command_line + .push_info("Open or create a drawing to switch layouts."); + return Task::none(); + } // A BEDIT block editor locks the active space; finish it with // Save Block or Discard before switching spaces. (#261) if self.tabs[i].block_edit.is_some() { @@ -3745,6 +3750,11 @@ impl OpenCADStudio { pub(super) fn on_layout_create(&mut self) -> Task { let i = self.active_tab; + if self.tabs[i].is_start { + self.command_line + .push_info("Open or create a drawing to add a layout."); + return Task::none(); + } // Find a unique name (e.g. Layout2, Layout3, ...). let existing = self.tabs[i].scene.layout_names(); let mut idx = existing.len(); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d598d3a54..cb69efa2a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1446,6 +1446,7 @@ impl OpenCADStudio { .as_ref() .map(|be| be.block_name.clone()) .unwrap_or_else(|| tab.scene.current_layout.clone()), + tab.is_start, self.layout_rename_state.as_ref(), tab.scene.first_viewport_scale(), tab.scene.viewport_count(), diff --git a/src/entities/object_data.rs b/src/entities/object_data.rs index 8decfb960..1807ee628 100644 --- a/src/entities/object_data.rs +++ b/src/entities/object_data.rs @@ -67,10 +67,6 @@ pub fn build_cache(document: &CadDocument) -> ObjectDataCache { } } -pub fn cached_document_sections(cache: &ObjectDataCache) -> Vec { - cache.document_sections.as_ref().clone() -} - pub fn cache_is_prepared(cache: &ObjectDataCache) -> bool { !cache.document_sections.is_empty() } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index ea0374dbe..d5d49303f 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -343,7 +343,11 @@ impl PropertiesPanel { // into one clickable summary row; clicking expands the components. let mut idx = 0; while idx < section.props.len() { - let group_len = coord_group_len(§ion.props, idx); + let group_len = if section.title == "View" { + 0 + } else { + coord_group_len(§ion.props, idx) + }; if group_len >= 2 { let base = coord_base(§ion.props[idx].label); let key = format!("{}:{}", section.title, base); diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index 00455c6a6..87f0efbc3 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -59,6 +59,8 @@ impl StatusBar { otrack: bool, layouts: Vec, current_layout: String, + // Start/welcome view has no drawing to own layouts. + is_start: bool, // If `Some((original, edit_value))`, the named tab shows a text input. rename_state: Option<&'a (String, String)>, // Scale of the first user viewport in the active paper layout. @@ -107,8 +109,11 @@ impl StatusBar { ) -> Element<'a, Message> { // Leftmost hamburger: opens a dropdown listing Model + every layout, so // a layout can be picked directly even when the tab strip is scrolled. - let menu_btn = button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR)) - .on_press(Message::ToggleLayoutList) + let menu_button = button(crate::ui::icons::tinted( + crate::ui::icons::MENU, + 16.0, + if is_start { DISABLED_COLOR } else { ICON_COLOR }, + )) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => PILL_BG, @@ -121,14 +126,33 @@ impl StatusBar { ..Default::default() }) .padding([4, 8]); + let menu_btn = if is_start { + tip( + menu_button.into(), + "Open or create a drawing to manage layouts.", + ) + } else { + menu_button.on_press(Message::ToggleLayoutList).into() + }; - let add_btn = button(text("+").size(12).color(ICON_COLOR)) - .on_press(Message::LayoutCreate) + let add_button = button(text("+").size(12).color(if is_start { + DISABLED_COLOR + } else { + ICON_COLOR + })) .style(|_: &Theme, _| button::Style { background: Some(Background::Color(Color::TRANSPARENT)), ..Default::default() }) .padding([4, 8]); + let add_btn = if is_start { + tip( + add_button.into(), + "Open or create a drawing to add a layout.", + ) + } else { + add_button.on_press(Message::LayoutCreate).into() + }; // ── Right side ──────────────────────────────────────────────────── let osnap_active = snapper.is_active(); @@ -353,7 +377,7 @@ impl StatusBar { let renaming = rename_state .filter(|(orig, _)| *orig == name) .map(|(_, edit)| edit.as_str()); - left.push(space_tab(name, is_active, renaming).into()); + left.push(space_tab(name, is_active, renaming, !is_start).into()); } left.push(add_btn.into()); } @@ -640,6 +664,7 @@ fn space_tab<'a>( label: String, is_active: bool, rename_edit: Option<&'a str>, + enabled: bool, ) -> Element<'a, Message> { let bg = move |is_active: bool, hovered: bool| { if is_active { @@ -661,7 +686,9 @@ fn space_tab<'a>( radius: 2.0.into(), }; - let text_color = if is_active { + let text_color = if !enabled { + DISABLED_COLOR + } else if is_active { Color::WHITE } else { Color { @@ -672,7 +699,23 @@ fn space_tab<'a>( } }; - if let Some(edit_val) = rename_edit { + if !enabled { + let display = container(text(label.clone()).size(12).color(text_color)) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(bg(is_active, false))), + border, + ..Default::default() + }) + .padding([4, 10]); + crate::ui::wrap_bar::PosReport::owned( + format!("SB_LAYOUT_TAB:{label}"), + tip( + display.into(), + "Open or create a drawing to switch layouts.", + ), + ) + .into() + } else if let Some(edit_val) = rename_edit { // Inline rename text input with a cancel (✕) button. let input = text_input("", edit_val) .id(iced::widget::Id::new(LAYOUT_RENAME_INPUT_ID)) @@ -870,6 +913,12 @@ const ICON_COLOR: Color = Color { b: 0.70, a: 1.0, }; +const DISABLED_COLOR: Color = Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, +}; const ACCENT: Color = Color { r: 0.20, g: 0.55,