Refactor src/main.rs into modular src/app/ structure
Split the 4500-line main.rs into focused submodules under src/app/: - mod.rs: H7CAD struct, Message enum, boot/new/run - document.rs: DocumentTab, HistorySnapshot, HistoryState - helpers.rs: pure helper functions - history.rs: undo/redo logic - properties.rs: properties panel logic - layers.rs: layer sync - commands.rs: dispatch_command - cmd_result.rs: apply_cmd_result - view.rs: view, subscription, tab bar - update.rs: update message handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
854f07517e
commit
ed9cbc2092
19 changed files with 4150 additions and 4519 deletions
347
src/app/properties.rs
Normal file
347
src/app/properties.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
use super::{H7CAD, VARIES_LABEL};
|
||||
use super::helpers::{entity_type_label, entity_type_key, title_case_word};
|
||||
use crate::scene::dispatch;
|
||||
use crate::ui;
|
||||
use crate::linetypes;
|
||||
use acadrust::{EntityType, Handle};
|
||||
|
||||
impl H7CAD {
|
||||
/// Rebuild the PropertiesPanel from the current entity selection.
|
||||
/// Preserves UI state (open pickers, edit buffer) across refreshes.
|
||||
pub(super) fn refresh_properties(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let color_picker_open = self.tabs[i].properties.color_picker_open;
|
||||
let color_palette_open = self.tabs[i].properties.color_palette_open;
|
||||
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
|
||||
let selected_group = self.tabs[i].properties.selected_group.clone();
|
||||
|
||||
let layer_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
let linetype_items: Vec<ui::properties::LinetypeItem> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.line_types
|
||||
.iter()
|
||||
.map(|lt| {
|
||||
let name = if lt.name.is_empty() {
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
lt.name.clone()
|
||||
};
|
||||
let art = linetypes::extract_pattern(<.description);
|
||||
ui::properties::LinetypeItem { name, art }
|
||||
})
|
||||
.collect();
|
||||
let text_style_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.map(|style| style.name.trim().to_string())
|
||||
.filter(|name| !name.is_empty())
|
||||
.collect();
|
||||
|
||||
let new_panel = {
|
||||
let selected = self.tabs[i].scene.selected_entities();
|
||||
let mut panel = match selected.len() {
|
||||
0 => ui::PropertiesPanel::empty(),
|
||||
1 => {
|
||||
let (handle, entity) = selected[0];
|
||||
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
||||
let mut sections =
|
||||
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
||||
if !group_names.is_empty() {
|
||||
let label = group_names.join(", ");
|
||||
if let Some(general) = sections.first_mut() {
|
||||
general.props.push(crate::scene::object::Property {
|
||||
label: "Group".to_string(),
|
||||
field: "group",
|
||||
value: crate::scene::object::PropValue::ReadOnly(label),
|
||||
});
|
||||
}
|
||||
}
|
||||
let title = entity_type_label(entity);
|
||||
ui::PropertiesPanel {
|
||||
choice_combos: sections
|
||||
.iter()
|
||||
.flat_map(|section| section.props.iter())
|
||||
.filter_map(|prop| match &prop.value {
|
||||
crate::scene::object::PropValue::Choice { options, .. } => Some((
|
||||
prop.field.to_string(),
|
||||
iced::widget::combo_box::State::new(options.clone()),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
sections,
|
||||
title,
|
||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
||||
crate::scene::hatch_patterns::names(),
|
||||
),
|
||||
lineweight_combo: iced::widget::combo_box::State::new(
|
||||
ui::properties::lw_options(),
|
||||
),
|
||||
linetype_items,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let groups = build_selection_groups(&selected);
|
||||
let active_group = selected_group
|
||||
.and_then(|group| groups.iter().find(|g| g.label == group.label).cloned())
|
||||
.or_else(|| groups.first().cloned());
|
||||
|
||||
let filtered: Vec<(Handle, &EntityType)> = active_group
|
||||
.as_ref()
|
||||
.map(|group| {
|
||||
selected
|
||||
.iter()
|
||||
.filter(|(handle, _)| group.handles.contains(handle))
|
||||
.copied()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let sections = aggregate_sections(&filtered, &text_style_names);
|
||||
ui::PropertiesPanel {
|
||||
choice_combos: sections
|
||||
.iter()
|
||||
.flat_map(|section| section.props.iter())
|
||||
.filter_map(|prop| match &prop.value {
|
||||
crate::scene::object::PropValue::Choice { options, .. } => Some((
|
||||
prop.field.to_string(),
|
||||
iced::widget::combo_box::State::new(options.clone()),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
sections,
|
||||
title: format!("{} objects selected", selected.len()),
|
||||
selection_group_combo: iced::widget::combo_box::State::new(groups.clone()),
|
||||
selection_groups: groups,
|
||||
selected_group: active_group,
|
||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
||||
crate::scene::hatch_patterns::names(),
|
||||
),
|
||||
lineweight_combo: iced::widget::combo_box::State::new(
|
||||
ui::properties::lw_options(),
|
||||
),
|
||||
linetype_items,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
panel.color_picker_open = color_picker_open;
|
||||
panel.color_palette_open = color_palette_open;
|
||||
panel.edit_buf = edit_buf;
|
||||
panel
|
||||
};
|
||||
|
||||
self.tabs[i].properties = new_panel;
|
||||
self.refresh_selected_grips();
|
||||
}
|
||||
|
||||
/// Rebuild the cached selected_grips from the current entity selection.
|
||||
pub(super) fn refresh_selected_grips(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let (new_handle, new_grips) = {
|
||||
let selected = self.tabs[i].scene.selected_entities();
|
||||
if selected.len() == 1 {
|
||||
let (handle, entity) = selected[0];
|
||||
let grips = dispatch::grips(entity);
|
||||
(Some(handle), grips)
|
||||
} else {
|
||||
(None, vec![])
|
||||
}
|
||||
};
|
||||
self.tabs[i].selected_handle = new_handle;
|
||||
self.tabs[i].selected_grips = new_grips;
|
||||
}
|
||||
|
||||
pub(super) fn property_target_handles(&self, i: usize) -> Vec<Handle> {
|
||||
let handles = self.tabs[i].properties.selected_handles();
|
||||
if !handles.is_empty() {
|
||||
handles
|
||||
} else {
|
||||
self.tabs[i].selected_handle.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an entity to the correct space (model or paper space layout).
|
||||
pub(super) fn commit_entity(&mut self, mut entity: acadrust::EntityType) {
|
||||
let i = self.active_tab;
|
||||
let layer = &self.tabs[i].active_layer;
|
||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||
entity.as_entity_mut().set_layer(layer.clone());
|
||||
}
|
||||
|
||||
crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
||||
crate::scene::dispatch::apply_common_prop(
|
||||
&mut entity,
|
||||
"linetype",
|
||||
&self.ribbon.active_linetype.clone(),
|
||||
);
|
||||
crate::scene::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight);
|
||||
|
||||
if matches!(&entity, acadrust::EntityType::Viewport(_))
|
||||
&& self.tabs[i].scene.current_layout != "Model"
|
||||
{
|
||||
let layout = self.tabs[i].scene.current_layout.clone();
|
||||
match self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity_to_layout(entity, &layout)
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("Viewport eklenemedi: {e}")),
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].scene.add_entity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Multi-selection property aggregation ───────────────────────────────────
|
||||
|
||||
pub(super) fn build_selection_groups(
|
||||
selected: &[(Handle, &EntityType)],
|
||||
) -> Vec<ui::properties::SelectionGroup> {
|
||||
let mut groups = vec![ui::properties::SelectionGroup {
|
||||
label: format!("All({})", selected.len()),
|
||||
handles: selected.iter().map(|(handle, _)| *handle).collect(),
|
||||
}];
|
||||
|
||||
let mut by_type: std::collections::BTreeMap<String, Vec<Handle>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for (handle, entity) in selected {
|
||||
by_type
|
||||
.entry(entity_type_key(entity))
|
||||
.or_default()
|
||||
.push(*handle);
|
||||
}
|
||||
|
||||
for (kind, handles) in by_type {
|
||||
groups.push(ui::properties::SelectionGroup {
|
||||
label: format!("{}({})", title_case_word(&kind), handles.len()),
|
||||
handles,
|
||||
});
|
||||
}
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
pub(super) fn aggregate_sections(
|
||||
selected: &[(Handle, &EntityType)],
|
||||
text_style_names: &[String],
|
||||
) -> Vec<crate::scene::object::PropSection> {
|
||||
if selected.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut all_sections: Vec<Vec<crate::scene::object::PropSection>> = selected
|
||||
.iter()
|
||||
.map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names))
|
||||
.collect();
|
||||
|
||||
let mut result = all_sections.remove(0);
|
||||
for sections in all_sections {
|
||||
result = merge_sections(&result, §ions);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn merge_sections(
|
||||
left: &[crate::scene::object::PropSection],
|
||||
right: &[crate::scene::object::PropSection],
|
||||
) -> Vec<crate::scene::object::PropSection> {
|
||||
left.iter()
|
||||
.filter_map(|section| {
|
||||
let rhs = right.iter().find(|candidate| candidate.title == section.title)?;
|
||||
let props: Vec<crate::scene::object::Property> = section
|
||||
.props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
let other =
|
||||
rhs.props.iter().find(|candidate| candidate.field == prop.field)?;
|
||||
Some(crate::scene::object::Property {
|
||||
label: prop.label.clone(),
|
||||
field: prop.field,
|
||||
value: merge_prop_value(&prop.value, &other.value),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if props.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(crate::scene::object::PropSection {
|
||||
title: section.title.clone(),
|
||||
props,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn merge_prop_value(
|
||||
left: &crate::scene::object::PropValue,
|
||||
right: &crate::scene::object::PropValue,
|
||||
) -> crate::scene::object::PropValue {
|
||||
use crate::scene::object::PropValue;
|
||||
|
||||
if left == right {
|
||||
return left.clone();
|
||||
}
|
||||
|
||||
match (left, right) {
|
||||
(PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => {
|
||||
PropValue::LayerChoice(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::ColorChoice(_), PropValue::ColorChoice(_))
|
||||
| (PropValue::ColorVaries, _)
|
||||
| (_, PropValue::ColorVaries) => PropValue::ColorVaries,
|
||||
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
||||
| (PropValue::LwVaries, _)
|
||||
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
||||
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
||||
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
||||
}
|
||||
(
|
||||
PropValue::Choice { options, .. },
|
||||
PropValue::Choice {
|
||||
options: other_options,
|
||||
..
|
||||
},
|
||||
) if options == other_options => PropValue::Choice {
|
||||
selected: VARIES_LABEL.into(),
|
||||
options: options.clone(),
|
||||
},
|
||||
(PropValue::EditText(_), PropValue::EditText(_)) => {
|
||||
PropValue::EditText(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
||||
PropValue::ReadOnly(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => {
|
||||
PropValue::HatchPatternChoice(VARIES_LABEL.into())
|
||||
}
|
||||
(
|
||||
PropValue::BoolToggle { field, .. },
|
||||
PropValue::BoolToggle {
|
||||
field: other_field,
|
||||
..
|
||||
},
|
||||
) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()),
|
||||
_ => left.clone(),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue