forked from 1b-agent/opencadstudio-upstream
perf: keep heavy work off UI thread
This commit is contained in:
1 parent
12bf126646
commit
deec48ad99
75 files changed
+11702
-518
No files matched your search
@@ -1052,9 +1052,10 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.push_undo_snapshot(i, "GROUP");
|
||||
let undo = self.begin_group_undo(i, "GROUP");
|
||||
self.tabs[i].scene.create_group(name.clone(), handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_group_undo(i, undo);
|
||||
self.command_line
|
||||
.push_info(&format!("Group \"{}\" created.", name));
|
||||
}
|
||||
@@ -1062,9 +1063,10 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.push_undo_snapshot(i, "UNGROUP");
|
||||
let undo = self.begin_group_undo(i, "UNGROUP");
|
||||
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_group_undo(i, undo);
|
||||
if count > 0 {
|
||||
self.command_line
|
||||
.push_info(&format!("{} group(s) dissolved.", count));
|
||||
|
||||
@@ -817,10 +817,15 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_error(&format!("DIMSTYLE: '{}' already exists.", name));
|
||||
} else {
|
||||
let undo = self.begin_dim_style_undo(
|
||||
i,
|
||||
"DIMSTYLE NEW",
|
||||
std::slice::from_ref(&name),
|
||||
);
|
||||
let style = DimStyle::new(&name);
|
||||
let _ = self.tabs[i].scene.document.dim_styles.add(style);
|
||||
self.push_undo_snapshot(i, "DIMSTYLE NEW");
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_dim_style_undo(i, undo);
|
||||
self.command_line
|
||||
.push_output(&format!("DIMSTYLE: '{}' created.", name));
|
||||
}
|
||||
@@ -832,6 +837,11 @@ impl OpenCADStudio {
|
||||
let prop = parts.get(2).map(|s| s.to_lowercase()).unwrap_or_default();
|
||||
let val_str = parts.get(3).map(|s| s.trim()).unwrap_or("");
|
||||
if let Ok(val) = val_str.parse::<f64>() {
|
||||
let undo = self.begin_dim_style_undo(
|
||||
i,
|
||||
"DIMSTYLE SET",
|
||||
std::slice::from_ref(&style_name),
|
||||
);
|
||||
if let Some(ds) =
|
||||
self.tabs[i].scene.document.dim_styles.get_mut(&style_name)
|
||||
{
|
||||
@@ -882,8 +892,10 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "DIMSTYLE SET");
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene
|
||||
.invalidate_dim_style_dependencies(&style_name);
|
||||
self.commit_dim_style_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"DIMSTYLE: '{style_name}'.{prop} = {val:.3}"
|
||||
));
|
||||
@@ -1121,10 +1133,15 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_error(&format!("{prefix}: style '{name}' already exists."));
|
||||
} else {
|
||||
let undo = self.begin_text_style_undo(
|
||||
i,
|
||||
"STYLE NEW",
|
||||
std::slice::from_ref(&name),
|
||||
);
|
||||
let style = acadrust::tables::TextStyle::new(&name);
|
||||
let _ = self.tabs[i].scene.document.text_styles.add(style);
|
||||
self.push_undo_snapshot(i, "STYLE NEW");
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_text_style_undo(i, undo);
|
||||
self.command_line
|
||||
.push_output(&format!("{prefix}: style '{name}' created."));
|
||||
}
|
||||
@@ -1136,18 +1153,28 @@ impl OpenCADStudio {
|
||||
if style_name.is_empty() || font.is_empty() {
|
||||
self.command_line
|
||||
.push_error(&format!("Usage: {prefix} FONT <style> <font_file>"));
|
||||
} else if let Some(s) =
|
||||
} else {
|
||||
let undo = self.begin_text_style_undo(
|
||||
i,
|
||||
"STYLE FONT",
|
||||
std::slice::from_ref(&style_name),
|
||||
);
|
||||
if let Some(style) =
|
||||
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||
{
|
||||
s.font_file = font.clone();
|
||||
self.push_undo_snapshot(i, "STYLE FONT");
|
||||
style.font_file = font.clone();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies(&style_name);
|
||||
self.commit_text_style_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{prefix}: '{style_name}' font set to '{font}'."
|
||||
));
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
||||
}
|
||||
}
|
||||
}
|
||||
"WIDTH" | "W" => {
|
||||
@@ -1155,12 +1182,20 @@ impl OpenCADStudio {
|
||||
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
||||
let factor_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
||||
if let Ok(factor) = factor_str.parse::<f64>() {
|
||||
if let Some(s) =
|
||||
let undo = self.begin_text_style_undo(
|
||||
i,
|
||||
"STYLE WIDTH",
|
||||
std::slice::from_ref(&style_name),
|
||||
);
|
||||
if let Some(style) =
|
||||
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||
{
|
||||
s.width_factor = factor;
|
||||
self.push_undo_snapshot(i, "STYLE WIDTH");
|
||||
style.width_factor = factor;
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies(&style_name);
|
||||
self.commit_text_style_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{prefix}: '{style_name}' width factor set to {factor:.3}."
|
||||
));
|
||||
@@ -1179,12 +1214,20 @@ impl OpenCADStudio {
|
||||
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
||||
let angle_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
||||
if let Ok(deg) = angle_str.parse::<f64>() {
|
||||
if let Some(s) =
|
||||
let undo = self.begin_text_style_undo(
|
||||
i,
|
||||
"STYLE OBLIQUE",
|
||||
std::slice::from_ref(&style_name),
|
||||
);
|
||||
if let Some(style) =
|
||||
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||
{
|
||||
s.oblique_angle = deg.to_radians();
|
||||
self.push_undo_snapshot(i, "STYLE OBLIQUE");
|
||||
style.oblique_angle = deg.to_radians();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies(&style_name);
|
||||
self.commit_text_style_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{prefix}: '{style_name}' oblique angle set to {deg:.1}°."
|
||||
));
|
||||
|
||||
+50
-29
@@ -23,7 +23,8 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|(_, e)| e.common().layer.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYOFF");
|
||||
let names: Vec<String> = layers.iter().cloned().collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYOFF", &names);
|
||||
for name in &layers {
|
||||
if name == "0" {
|
||||
continue;
|
||||
@@ -32,8 +33,9 @@ impl OpenCADStudio {
|
||||
dl.turn_off();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("Layer(s) turned off.");
|
||||
}
|
||||
@@ -58,7 +60,8 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|(_, e)| e.common().layer.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYFRZ");
|
||||
let names: Vec<String> = layers.iter().cloned().collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYFRZ", &names);
|
||||
for name in &layers {
|
||||
if name == "0" {
|
||||
continue;
|
||||
@@ -67,8 +70,9 @@ impl OpenCADStudio {
|
||||
dl.freeze();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("Layer(s) frozen.");
|
||||
}
|
||||
@@ -266,10 +270,18 @@ impl OpenCADStudio {
|
||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||
));
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "LAYERSTATE");
|
||||
let names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYERSTATE", &names);
|
||||
let n = self.tabs[i].restore_layer_state(arg).unwrap_or(0);
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_output(&format!(
|
||||
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
||||
@@ -312,14 +324,16 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|(_, e)| e.common().layer.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYLCK");
|
||||
let names: Vec<String> = layers.iter().cloned().collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYLCK", &names);
|
||||
for name in &layers {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.lock();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
// Layer locking changes editability only.
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("Layer(s) locked.");
|
||||
}
|
||||
@@ -359,41 +373,43 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"LAYON" => {
|
||||
self.push_undo_snapshot(i, "LAYON");
|
||||
for name in self.tabs[i]
|
||||
let names = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
.collect::<Vec<_>>();
|
||||
let undo = self.begin_layer_undo(i, "LAYON", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||
dl.turn_on();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("All layers turned on.");
|
||||
}
|
||||
|
||||
"LAYTHW" => {
|
||||
self.push_undo_snapshot(i, "LAYTHW");
|
||||
for name in self.tabs[i]
|
||||
let names = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
.collect::<Vec<_>>();
|
||||
let undo = self.begin_layer_undo(i, "LAYTHW", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||
dl.thaw();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("All layers thawed.");
|
||||
}
|
||||
@@ -417,14 +433,16 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|(_, e)| e.common().layer.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYULK");
|
||||
let names: Vec<String> = layers.iter().cloned().collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYULK", &names);
|
||||
for name in &layers {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.unlock();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
// Layer unlocking changes editability only.
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_info("Layer(s) unlocked.");
|
||||
}
|
||||
@@ -442,7 +460,6 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_error("LAYISO: select entities on the layers to isolate first.");
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "LAYISO");
|
||||
let names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
@@ -450,15 +467,17 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
for name in names {
|
||||
if !sel_layers.contains(&name) {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||
let undo = self.begin_layer_undo(i, "LAYISO", &names);
|
||||
for name in &names {
|
||||
if !sel_layers.contains(name) {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.turn_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line
|
||||
.push_info(&format!("LAYISO: isolated {} layer(s).", sel_layers.len()));
|
||||
@@ -511,7 +530,6 @@ impl OpenCADStudio {
|
||||
|
||||
// LAYUNISO — restore all layers that were turned off by LAYISO (turn all on)
|
||||
"LAYUNISO" => {
|
||||
self.push_undo_snapshot(i, "LAYUNISO");
|
||||
let names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
@@ -519,13 +537,15 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
for name in names {
|
||||
let undo = self.begin_layer_undo(i, "LAYUNISO", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||
dl.turn_on();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line
|
||||
.push_info("LAYUNISO: all layers restored.");
|
||||
@@ -587,9 +607,10 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "UNGROUP");
|
||||
let undo = self.begin_group_undo(i, "UNGROUP");
|
||||
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_group_undo(i, undo);
|
||||
if count > 0 {
|
||||
self.command_line
|
||||
.push_info(&format!("{} group(s) dissolved.", count));
|
||||
|
||||
+59
-2
@@ -520,7 +520,7 @@ impl HistorySnapshot {
|
||||
.saturating_add(
|
||||
d.structure
|
||||
.as_ref()
|
||||
.map_or(0, |doc| doc.objects.len().saturating_mul(192)),
|
||||
.map_or(0, StructureSnapshot::estimated_bytes),
|
||||
)
|
||||
.saturating_add(d.selected_before.len().saturating_mul(16))
|
||||
.saturating_add(d.selected_after.len().saturating_mul(16))
|
||||
@@ -546,10 +546,67 @@ pub(super) struct DeltaSnapshot {
|
||||
pub(super) dirty_after: bool,
|
||||
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
||||
/// the live structure, so the same allocation shuttles between undo/redo.
|
||||
pub(super) structure: Option<CadDocument>,
|
||||
pub(super) structure: Option<StructureSnapshot>,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum StructureSnapshot {
|
||||
/// Compatibility fallback for genuinely broad structural commands.
|
||||
Full(CadDocument),
|
||||
/// Exact layer-table entries touched by one command.
|
||||
Layers(Vec<TableEntryDelta<acadrust::tables::Layer>>),
|
||||
/// Exact text-style entries touched by one command.
|
||||
TextStyles(Vec<TableEntryDelta<acadrust::tables::TextStyle>>),
|
||||
/// Exact dimension-style entries touched by one command.
|
||||
DimStyles(Vec<TableEntryDelta<acadrust::tables::DimStyle>>),
|
||||
/// Exact object-map entries touched by one command. This supports commands
|
||||
/// such as groups/dictionaries without retaining every unrelated object.
|
||||
Objects(Vec<ObjectEntryDelta>),
|
||||
/// The bounded set of style tables, style objects, current-style pointers,
|
||||
/// and matching ribbon state touched by one Style Manager transaction.
|
||||
Styles {
|
||||
before: super::style_ops::StyleStateSnapshot,
|
||||
after: super::style_ops::StyleStateSnapshot,
|
||||
text_names: Vec<String>,
|
||||
dim_names: Vec<String>,
|
||||
object_handles: Vec<Handle>,
|
||||
},
|
||||
}
|
||||
|
||||
impl StructureSnapshot {
|
||||
pub(super) fn estimated_bytes(&self) -> usize {
|
||||
match self {
|
||||
Self::Full(doc) => doc.objects.len().saturating_mul(192),
|
||||
Self::Layers(entries) => entries.len().saturating_mul(256),
|
||||
Self::TextStyles(entries) => entries.len().saturating_mul(320),
|
||||
Self::DimStyles(entries) => entries.len().saturating_mul(1024),
|
||||
Self::Objects(entries) => entries.len().saturating_mul(384),
|
||||
Self::Styles { before, after, .. } => before
|
||||
.estimated_bytes()
|
||||
.saturating_add(after.estimated_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_full(&self) -> bool {
|
||||
matches!(self, Self::Full(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct TableEntryDelta<T> {
|
||||
pub(super) name: String,
|
||||
pub(super) before: Option<T>,
|
||||
pub(super) after: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ObjectEntryDelta {
|
||||
pub(super) handle: Handle,
|
||||
pub(super) before: Option<acadrust::objects::ObjectType>,
|
||||
pub(super) after: Option<acadrust::objects::ObjectType>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HistoryState {
|
||||
pub(super) undo_stack: Vec<HistorySnapshot>,
|
||||
|
||||
+417
-14
@@ -1,5 +1,8 @@
|
||||
use super::{
|
||||
document::{DeltaSnapshot, HistorySnapshot, PendingHistorySnapshot},
|
||||
document::{
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, PendingHistorySnapshot,
|
||||
StructureSnapshot, TableEntryDelta,
|
||||
},
|
||||
OpenCADStudio,
|
||||
};
|
||||
use acadrust::{EntityType, Handle};
|
||||
@@ -71,6 +74,38 @@ pub(super) struct PendingDelta {
|
||||
structure_before: Option<acadrust::CadDocument>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingLayerDelta {
|
||||
label: String,
|
||||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
before: Vec<(String, Option<acadrust::tables::Layer>)>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingTextStyleDelta {
|
||||
label: String,
|
||||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
before: Vec<(String, Option<acadrust::tables::TextStyle>)>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingDimStyleDelta {
|
||||
label: String,
|
||||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
before: Vec<(String, Option<acadrust::tables::DimStyle>)>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingObjectDelta {
|
||||
label: String,
|
||||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
before: FxHashMap<Handle, acadrust::objects::ObjectType>,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
|
||||
self.tabs[i]
|
||||
@@ -248,7 +283,7 @@ impl OpenCADStudio {
|
||||
selected_after,
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after,
|
||||
structure: structure_changed.then_some(pending.structure_before),
|
||||
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
@@ -304,6 +339,263 @@ impl OpenCADStudio {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn begin_layer_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
names: &[String],
|
||||
) -> PendingLayerDelta {
|
||||
self.finish_pending_history(i);
|
||||
PendingLayerDelta {
|
||||
label: label.into(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
before: names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
self.tabs[i].scene.document.layers.get(name).cloned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit_layer_undo(&mut self, i: usize, pending: PendingLayerDelta) {
|
||||
let entries: Vec<_> = pending
|
||||
.before
|
||||
.into_iter()
|
||||
.filter_map(|(name, before)| {
|
||||
let after = self.tabs[i].scene.document.layers.get(&name).cloned();
|
||||
(before != after).then_some(TableEntryDelta {
|
||||
name,
|
||||
before,
|
||||
after,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let delta = DeltaSnapshot {
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after,
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
structure: Some(StructureSnapshot::Layers(entries)),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn begin_text_style_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
names: &[String],
|
||||
) -> PendingTextStyleDelta {
|
||||
self.finish_pending_history(i);
|
||||
PendingTextStyleDelta {
|
||||
label: label.into(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
before: names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
self.tabs[i].scene.document.text_styles.get(name).cloned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit_text_style_undo(&mut self, i: usize, pending: PendingTextStyleDelta) {
|
||||
let entries: Vec<_> = pending
|
||||
.before
|
||||
.into_iter()
|
||||
.filter_map(|(name, before)| {
|
||||
let after = self.tabs[i].scene.document.text_styles.get(&name).cloned();
|
||||
(before != after).then_some(TableEntryDelta {
|
||||
name,
|
||||
before,
|
||||
after,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
structure: Some(StructureSnapshot::TextStyles(entries)),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn begin_dim_style_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
names: &[String],
|
||||
) -> PendingDimStyleDelta {
|
||||
self.finish_pending_history(i);
|
||||
PendingDimStyleDelta {
|
||||
label: label.into(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
before: names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
self.tabs[i].scene.document.dim_styles.get(name).cloned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit_dim_style_undo(&mut self, i: usize, pending: PendingDimStyleDelta) {
|
||||
let entries: Vec<_> = pending
|
||||
.before
|
||||
.into_iter()
|
||||
.filter_map(|(name, before)| {
|
||||
let after = self.tabs[i].scene.document.dim_styles.get(&name).cloned();
|
||||
(before != after).then_some(TableEntryDelta {
|
||||
name,
|
||||
before,
|
||||
after,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
structure: Some(StructureSnapshot::DimStyles(entries)),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
fn group_object_state(&self, i: usize) -> FxHashMap<Handle, acadrust::objects::ObjectType> {
|
||||
use acadrust::objects::ObjectType;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let dictionary = document.header.acad_group_dict_handle;
|
||||
document
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(handle, object)| {
|
||||
**handle == dictionary || matches!(object, ObjectType::Group(_))
|
||||
})
|
||||
.map(|(handle, object)| (*handle, object.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn begin_group_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
) -> PendingObjectDelta {
|
||||
self.finish_pending_history(i);
|
||||
PendingObjectDelta {
|
||||
label: label.into(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
before: self.group_object_state(i),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit_group_undo(&mut self, i: usize, pending: PendingObjectDelta) {
|
||||
let after = self.group_object_state(i);
|
||||
let mut handles: HashSet<Handle> = pending.before.keys().copied().collect();
|
||||
handles.extend(after.keys().copied());
|
||||
let entries: Vec<_> = handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
let before = pending.before.get(&handle).cloned();
|
||||
let after = after.get(&handle).cloned();
|
||||
(before != after).then_some(ObjectEntryDelta {
|
||||
handle,
|
||||
before,
|
||||
after,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
structure: Some(StructureSnapshot::Objects(entries)),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn commit_style_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
before: super::style_ops::StyleStateSnapshot,
|
||||
after: super::style_ops::StyleStateSnapshot,
|
||||
dirty_before: bool,
|
||||
) {
|
||||
if before == after {
|
||||
return;
|
||||
}
|
||||
self.finish_pending_history(i);
|
||||
let (text_names, dim_names, object_handles) = after.changed_keys(&before);
|
||||
let delta = DeltaSnapshot {
|
||||
entities: Vec::new(),
|
||||
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before,
|
||||
dirty_after: true,
|
||||
structure: Some(StructureSnapshot::Styles {
|
||||
before,
|
||||
after,
|
||||
text_names,
|
||||
dim_names,
|
||||
object_handles,
|
||||
}),
|
||||
label: "STYLE".to_string(),
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
/// Copy is delta-safe only when no target is a Dimension and no complete
|
||||
/// group is copied: dimensions clone fresh anonymous `*D` block records,
|
||||
/// and complete group copies add Group objects / dictionary entries. Both
|
||||
@@ -397,11 +689,12 @@ impl OpenCADStudio {
|
||||
.collect();
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let mut structure = pending.structure_before;
|
||||
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
|
||||
if let Some(before_structure) = structure.as_mut() {
|
||||
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||
let added_handles: Vec<Handle> = entities
|
||||
.iter()
|
||||
if let StructureSnapshot::Full(before_structure) = before_structure {
|
||||
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||
let added_handles: Vec<Handle> = entities
|
||||
.iter()
|
||||
.filter_map(|(handle, before, after)| {
|
||||
(before.is_none() && after.is_some()).then_some(*handle)
|
||||
})
|
||||
@@ -414,6 +707,7 @@ impl OpenCADStudio {
|
||||
if *before_structure == after_structure {
|
||||
structure = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities,
|
||||
@@ -443,9 +737,113 @@ impl OpenCADStudio {
|
||||
// Install the chosen side of every entity image. Derived-cache, geometry
|
||||
// and UI invalidation are deferred until every requested undo/redo step
|
||||
// has been applied.
|
||||
if let Some(structure) = d.structure.take() {
|
||||
let inverse = self.tabs[i].scene.document.swap_structure(structure);
|
||||
d.structure = Some(inverse);
|
||||
if let Some(structure) = d.structure.as_mut() {
|
||||
match structure {
|
||||
StructureSnapshot::Full(stored) => {
|
||||
let inverse = self.tabs[i].scene.document.swap_structure(
|
||||
std::mem::replace(stored, acadrust::CadDocument::new()),
|
||||
);
|
||||
*stored = inverse;
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
}
|
||||
StructureSnapshot::Layers(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(layer) = value {
|
||||
self.tabs[i].scene.document.layers.add_or_replace(layer);
|
||||
} else {
|
||||
self.tabs[i].scene.document.layers.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
}
|
||||
StructureSnapshot::TextStyles(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.text_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_text_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::DimStyles(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i].scene.document.dim_styles.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.dim_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_dim_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Objects(entries) => {
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(object) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(entry.handle, object);
|
||||
} else {
|
||||
self.tabs[i].scene.document.objects.remove(&entry.handle);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
}
|
||||
StructureSnapshot::Styles {
|
||||
before,
|
||||
after,
|
||||
text_names,
|
||||
dim_names,
|
||||
object_handles,
|
||||
} => {
|
||||
let snapshot = if undo { before } else { after };
|
||||
self.restore_style_state(i, snapshot);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies_many(text_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_style_dependencies_many(dim_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_object_style_dependencies(object_handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
@@ -479,6 +877,7 @@ impl OpenCADStudio {
|
||||
&mut self,
|
||||
i: usize,
|
||||
had_full: bool,
|
||||
structure_changed: bool,
|
||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||
) {
|
||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||
@@ -524,7 +923,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].active_grip = None;
|
||||
if had_full {
|
||||
if structure_changed {
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i]
|
||||
@@ -555,6 +954,7 @@ impl OpenCADStudio {
|
||||
|
||||
let mut last_label = String::new();
|
||||
let mut had_full = false;
|
||||
let mut structure_changed = false;
|
||||
let mut changes = Vec::new();
|
||||
for _ in 0..steps {
|
||||
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
||||
@@ -566,7 +966,8 @@ impl OpenCADStudio {
|
||||
// Symmetric: undo applies the before side, then the same
|
||||
// delta rides to the redo stack (it still holds the after
|
||||
// side) — no current-state capture needed.
|
||||
had_full |= d.structure.is_some();
|
||||
structure_changed |= d.structure.is_some();
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||
self.tabs[i]
|
||||
.history
|
||||
@@ -575,7 +976,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, &changes);
|
||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
||||
self.command_line
|
||||
.push_output(&format!("Undo: {last_label}"));
|
||||
}
|
||||
@@ -592,6 +993,7 @@ impl OpenCADStudio {
|
||||
|
||||
let mut last_label = String::new();
|
||||
let mut had_full = false;
|
||||
let mut structure_changed = false;
|
||||
let mut changes = Vec::new();
|
||||
for _ in 0..steps {
|
||||
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
||||
@@ -600,7 +1002,8 @@ impl OpenCADStudio {
|
||||
last_label = snapshot.label().to_string();
|
||||
match snapshot {
|
||||
HistorySnapshot::Delta(mut d) => {
|
||||
had_full |= d.structure.is_some();
|
||||
structure_changed |= d.structure.is_some();
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||
self.tabs[i]
|
||||
.history
|
||||
@@ -609,7 +1012,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, &changes);
|
||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
||||
self.command_line
|
||||
.push_output(&format!("Redo: {last_label}"));
|
||||
}
|
||||
|
||||
+16
-4
@@ -168,7 +168,6 @@ use acadrust::CadDocument;
|
||||
use iced::time::Instant;
|
||||
use iced::window;
|
||||
use iced::{mouse, Point, Task, Theme};
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) const POLY_START_DELAY_MS: u128 = 150;
|
||||
@@ -179,14 +178,15 @@ pub(super) const VARIES_LABEL: &str = "*VARIES*";
|
||||
// loader thread, read by the UI overlay on every frame.
|
||||
pub const OPEN_PHASE_READING: u8 = 0;
|
||||
pub const OPEN_PHASE_PARSING: u8 = 1;
|
||||
pub const OPEN_PHASE_CACHING: u8 = 2;
|
||||
pub const OPEN_PHASE_FINALIZING: u8 = 3;
|
||||
pub const OPEN_PHASE_XREF: u8 = 2;
|
||||
pub const OPEN_PHASE_CACHING: u8 = 3;
|
||||
pub const OPEN_PHASE_FINALIZING: u8 = 4;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpenProgress {
|
||||
pub name: String,
|
||||
pub size_bytes: u64,
|
||||
pub phase: Arc<AtomicU8>,
|
||||
pub state: Arc<crate::io::OpenProgressState>,
|
||||
pub started: Instant,
|
||||
}
|
||||
|
||||
@@ -2098,6 +2098,9 @@ pub enum Message {
|
||||
PlotWindowExport,
|
||||
/// Callback after the user picks (or cancels) the window-export path.
|
||||
PlotWindowExportPath(Option<std::path::PathBuf>),
|
||||
/// Completion of a PDF/preview/print job performed outside the UI thread.
|
||||
/// The boolean restores the Plot dialog after a preview.
|
||||
BackgroundIoFinished(Result<String, String>, bool),
|
||||
/// Send current layout to the system printer (via lp / lpr).
|
||||
PrintToPrinter,
|
||||
/// Callback from the async printer job.
|
||||
@@ -2299,6 +2302,8 @@ pub enum Message {
|
||||
WblockSave(String),
|
||||
/// Result of the WBLOCK save path dialog.
|
||||
WblockSaveResult(String, Option<std::path::PathBuf>),
|
||||
/// Background extraction/write completion.
|
||||
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
|
||||
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
||||
/// Save the pre-built CSV string to a file chosen by the user.
|
||||
DataExtractionSave(String),
|
||||
@@ -2309,16 +2314,23 @@ pub enum Message {
|
||||
StlExport,
|
||||
/// Callback after the user picks (or cancels) the STL save path.
|
||||
StlExportPath(Option<std::path::PathBuf>),
|
||||
StlExportFinished(std::path::PathBuf, Result<(), String>),
|
||||
// ── STEP export ───────────────────────────────────────────────────────
|
||||
/// Trigger STEP AP203 export: show save dialog.
|
||||
StepExport,
|
||||
/// Callback after the user picks (or cancels) the STEP save path.
|
||||
StepExportPath(Option<std::path::PathBuf>),
|
||||
StepExportFinished(std::path::PathBuf, Result<(), String>),
|
||||
// ── OBJ import ────────────────────────────────────────────────────────
|
||||
/// Trigger OBJ import: show open-file dialog.
|
||||
ObjImport,
|
||||
/// Callback after the user picks (or cancels) the OBJ file path.
|
||||
ObjImportPath(Option<std::path::PathBuf>),
|
||||
ObjImportFinished(
|
||||
u64,
|
||||
std::path::PathBuf,
|
||||
Result<crate::scene::model::mesh_model::MeshModel, String>,
|
||||
),
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
|
||||
+81
-13
@@ -539,8 +539,7 @@ impl OpenCADStudio {
|
||||
|
||||
/// Overwrite the live style state with a snapshot (used by commit's undo
|
||||
/// dance and by discard).
|
||||
fn restore_style_state(&mut self, snap: &StyleStateSnapshot) {
|
||||
let i = self.active_tab;
|
||||
pub(super) fn restore_style_state(&mut self, i: usize, snap: &StyleStateSnapshot) {
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
doc.text_styles = snap.text_styles.clone();
|
||||
doc.dim_styles = snap.dim_styles.clone();
|
||||
@@ -560,8 +559,10 @@ impl OpenCADStudio {
|
||||
doc.header.multiline_style = snap.multiline_style.clone();
|
||||
doc.header.current_table_style_name = snap.current_table.clone();
|
||||
doc.header.current_mleader_style_name = snap.current_mleader.clone();
|
||||
self.ribbon.active_table_style = snap.active_table.clone();
|
||||
self.ribbon.active_mleader_style = snap.active_mleader.clone();
|
||||
if i == self.active_tab {
|
||||
self.ribbon.active_table_style = snap.active_table.clone();
|
||||
self.ribbon.active_mleader_style = snap.active_mleader.clone();
|
||||
}
|
||||
self.tabs[i].active_mleader_style = snap.tab_active_mleader.clone();
|
||||
}
|
||||
|
||||
@@ -587,18 +588,26 @@ impl OpenCADStudio {
|
||||
self.sync_ribbon_styles();
|
||||
return;
|
||||
};
|
||||
// Capture the edited state, rewind to the baseline so the undo entry
|
||||
// restores the pre-edit document, then re-apply the edits on top.
|
||||
let edited = self.capture_style_state();
|
||||
self.restore_style_state(&stage.baseline);
|
||||
self.push_undo_snapshot(i, "STYLE");
|
||||
self.restore_style_state(&edited);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
let changed = edited != stage.baseline;
|
||||
if changed {
|
||||
self.tabs[i].dirty = true;
|
||||
let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
|
||||
self.tabs[i].scene.invalidate_text_style_dependencies_many(&text_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_style_dependencies_many(&dim_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_object_style_dependencies(&object_handles);
|
||||
self.commit_style_undo(i, stage.baseline, edited.clone(), stage.dirty_at_open);
|
||||
} else {
|
||||
self.tabs[i].dirty = stage.dirty_at_open;
|
||||
}
|
||||
self.sync_ribbon_styles();
|
||||
// Re-baseline so further edits in the still-open window stage afresh.
|
||||
self.style_stage = Some(StyleStage {
|
||||
dirty_at_open: true,
|
||||
dirty_at_open: self.tabs[i].dirty,
|
||||
baseline: edited,
|
||||
});
|
||||
}
|
||||
@@ -609,13 +618,14 @@ impl OpenCADStudio {
|
||||
let Some(stage) = self.style_stage.take() else {
|
||||
return;
|
||||
};
|
||||
self.restore_style_state(&stage.baseline);
|
||||
self.restore_style_state(self.active_tab, &stage.baseline);
|
||||
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
||||
self.sync_ribbon_styles();
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of every document field a style manager can touch.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(super) struct StyleStateSnapshot {
|
||||
text_styles: acadrust::tables::Table<TextStyle>,
|
||||
dim_styles: acadrust::tables::Table<DimStyle>,
|
||||
@@ -630,6 +640,64 @@ pub(super) struct StyleStateSnapshot {
|
||||
tab_active_mleader: String,
|
||||
}
|
||||
|
||||
impl StyleStateSnapshot {
|
||||
pub(super) fn estimated_bytes(&self) -> usize {
|
||||
self.text_styles
|
||||
.iter()
|
||||
.count()
|
||||
.saturating_mul(320)
|
||||
.saturating_add(self.dim_styles.iter().count().saturating_mul(1024))
|
||||
.saturating_add(self.style_objects.len().saturating_mul(512))
|
||||
}
|
||||
|
||||
pub(super) fn changed_keys(&self, other: &Self) -> (Vec<String>, Vec<String>, Vec<Handle>) {
|
||||
let text_names = self
|
||||
.text_styles
|
||||
.iter()
|
||||
.filter(|style| other.text_styles.get(&style.name) != Some(*style))
|
||||
.map(|style| style.name.clone())
|
||||
.chain(
|
||||
other
|
||||
.text_styles
|
||||
.iter()
|
||||
.filter(|style| self.text_styles.get(&style.name) != Some(*style))
|
||||
.map(|style| style.name.clone()),
|
||||
)
|
||||
.collect::<rustc_hash::FxHashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let dim_names = self
|
||||
.dim_styles
|
||||
.iter()
|
||||
.filter(|style| other.dim_styles.get(&style.name) != Some(*style))
|
||||
.map(|style| style.name.clone())
|
||||
.chain(
|
||||
other
|
||||
.dim_styles
|
||||
.iter()
|
||||
.filter(|style| self.dim_styles.get(&style.name) != Some(*style))
|
||||
.map(|style| style.name.clone()),
|
||||
)
|
||||
.collect::<rustc_hash::FxHashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let self_objects: rustc_hash::FxHashMap<_, _> =
|
||||
self.style_objects.iter().map(|(h, o)| (*h, o)).collect();
|
||||
let other_objects: rustc_hash::FxHashMap<_, _> =
|
||||
other.style_objects.iter().map(|(h, o)| (*h, o)).collect();
|
||||
let object_handles = self
|
||||
.style_objects
|
||||
.iter()
|
||||
.chain(other.style_objects.iter())
|
||||
.map(|(handle, _)| *handle)
|
||||
.collect::<rustc_hash::FxHashSet<_>>()
|
||||
.into_iter()
|
||||
.filter(|handle| self_objects.get(handle) != other_objects.get(handle))
|
||||
.collect();
|
||||
(text_names, dim_names, object_handles)
|
||||
}
|
||||
}
|
||||
|
||||
/// An in-progress style-manager transaction.
|
||||
pub(super) struct StyleStage {
|
||||
dirty_at_open: bool,
|
||||
|
||||
+179
-209
@@ -20,6 +20,28 @@ use acadrust::{EntityType as AcadEntityType, Handle};
|
||||
use iced::time::Instant;
|
||||
use iced::{mouse, Point, Task};
|
||||
|
||||
pub(super) fn background_task<T, F, M>(work: F, map: M) -> Task<Message>
|
||||
where
|
||||
T: Send + 'static,
|
||||
F: FnOnce() -> T + Send + 'static,
|
||||
M: FnOnce(T) -> Message + Send + 'static,
|
||||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let (tx, rx) = iced::futures::channel::oneshot::channel();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(work());
|
||||
});
|
||||
Task::perform(
|
||||
async move { rx.await.expect("background export worker dropped") },
|
||||
map,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(async move { work() }, map)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Before a save, give every cached truck solid that still has no ACIS
|
||||
@@ -362,15 +384,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
// progress, so mark one. The browser picker + parse happen
|
||||
// inside `pick_and_load_web`; the real name is unknown until
|
||||
// then, so show a generic label meanwhile.
|
||||
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||
crate::app::OPEN_PHASE_READING,
|
||||
));
|
||||
self.opening = Some(crate::app::OpenProgress {
|
||||
name: "Opening…".into(),
|
||||
size_bytes: 0,
|
||||
phase: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
|
||||
crate::app::OPEN_PHASE_READING,
|
||||
)),
|
||||
state: state.clone(),
|
||||
started: Instant::now(),
|
||||
});
|
||||
Task::perform(crate::io::pick_and_load_web(), Message::FileOpened)
|
||||
Task::perform(crate::io::pick_and_load_web(state), Message::FileOpened)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,13 +426,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument, caches: crate::scene::DerivedCaches) -> Task<Message> {
|
||||
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument,
|
||||
mut caches: crate::scene::DerivedCaches,
|
||||
) -> Task<Message> {
|
||||
// If the user clicked Cancel while the parser was running, the
|
||||
// overlay state was cleared and we silently drop the result.
|
||||
if self.opening.is_none() {
|
||||
return Task::none();
|
||||
}
|
||||
let open_started = self.opening.take().map(|p| p.started);
|
||||
let open_started = self.opening.as_ref().map(|p| p.started);
|
||||
let timings = caches.timings;
|
||||
let entity_count = doc.entities().count();
|
||||
self.command_line
|
||||
@@ -420,6 +445,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
caches.corrupt_dropped
|
||||
));
|
||||
}
|
||||
if caches.xref_dropped > 0 {
|
||||
self.command_line.push_error(&format!(
|
||||
"Warning: {} corrupt xref entities dropped",
|
||||
caches.xref_dropped
|
||||
));
|
||||
}
|
||||
for info in &caches.xrefs {
|
||||
match info.status {
|
||||
crate::io::xref::XrefStatus::Loaded => {
|
||||
self.command_line
|
||||
.push_output(&format!("XREF Loaded \"{}\"", info.name));
|
||||
}
|
||||
crate::io::xref::XrefStatus::NotFound => {
|
||||
self.command_line.push_error(&format!(
|
||||
"XREF Not found: \"{}\" ({})",
|
||||
info.name, info.path
|
||||
));
|
||||
}
|
||||
crate::io::xref::XrefStatus::Unloaded => {
|
||||
self.command_line
|
||||
.push_info(&format!("XREF Unloaded (skipped): \"{}\"", info.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
let thumbs_task = self.push_recent(path.clone());
|
||||
|
||||
let current_is_empty = {
|
||||
@@ -472,47 +521,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
1.0
|
||||
};
|
||||
|
||||
// Auto-resolve XREFs relative to the opened file's directory.
|
||||
let mut xref_ms = 0u32;
|
||||
let mut xref_merged = false;
|
||||
if let Some(base_dir) = path.parent() {
|
||||
// xref content arrives un-purged: parser-garbage entities
|
||||
// inside the referenced file can trigger infinite loops in
|
||||
// tessellation. `resolve_xrefs` runs the corrupt-entity
|
||||
// guard inline as it merges each xref, so no second
|
||||
// full-document walk is needed here.
|
||||
let t_xref = Instant::now();
|
||||
let (xrefs, extra_dropped) =
|
||||
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, base_dir);
|
||||
xref_ms = t_xref.elapsed().as_millis() as u32;
|
||||
if extra_dropped > 0 {
|
||||
self.command_line.push_error(&format!(
|
||||
"Warning: {extra_dropped} corrupt xref entities dropped"
|
||||
));
|
||||
}
|
||||
for info in &xrefs {
|
||||
match info.status {
|
||||
crate::io::xref::XrefStatus::Loaded => {
|
||||
xref_merged = true;
|
||||
self.command_line
|
||||
.push_output(&format!("XREF Loaded \"{}\"", info.name));
|
||||
}
|
||||
crate::io::xref::XrefStatus::NotFound => {
|
||||
self.command_line.push_error(&format!(
|
||||
"XREF Not found: \"{}\" ({})",
|
||||
info.name, info.path
|
||||
));
|
||||
}
|
||||
crate::io::xref::XrefStatus::Unloaded => {
|
||||
self.command_line.push_info(&format!(
|
||||
"XREF Unloaded (skipped): \"{}\"",
|
||||
info.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open-time breakdown so regressions are visible immediately.
|
||||
// `total` is wall time from the Open click to here (post-xref,
|
||||
// pre-first-frame); the phase figures are the background-thread
|
||||
@@ -522,7 +530,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
.unwrap_or(0);
|
||||
self.command_line.push_info(&format!(
|
||||
" parse {}ms · purge {}ms · caches {}ms · xref {}ms · total {}ms",
|
||||
timings.parse_ms, timings.purge_ms, timings.caches_ms, xref_ms, total_ms
|
||||
timings.parse_ms, timings.purge_ms, timings.caches_ms, timings.xref_ms, total_ms
|
||||
));
|
||||
|
||||
// Caches were built on the background thread inside open_path().
|
||||
@@ -532,18 +540,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].scene.images = caches.images;
|
||||
self.tabs[i].scene.meshes = caches.meshes;
|
||||
self.tabs[i].scene.block_meshes = caches.block_meshes;
|
||||
let prepared_geometry = caches.prepared_geometry.take();
|
||||
// Invalidate the wire cache so the new document is tessellated.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
// XREFs are merged into the document AFTER the background worker
|
||||
// built the mesh caches above, so those caches contain none of
|
||||
// the xref'd geometry. The wire pass rebuilds from the document
|
||||
// each frame (bump_geometry covers it), but 3D-solid meshes are
|
||||
// only tessellated by populate — run the incremental variant so
|
||||
// the already-cached host solids are kept and only the newly
|
||||
// merged xref solids (walls, floors, roofs) are tessellated,
|
||||
// avoiding a full re-tessellation of the whole drawing. (#203)
|
||||
if xref_merged {
|
||||
self.tabs[i].scene.populate_missing_meshes_from_document();
|
||||
if let Some(prepared) = prepared_geometry {
|
||||
self.tabs[i].scene.install_prepared_open_geometry(prepared);
|
||||
}
|
||||
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
||||
self.tabs[i].scene.preview_wires = vec![];
|
||||
@@ -625,42 +626,38 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let interaction_task = Task::none();
|
||||
if let Some(opening) = &self.opening {
|
||||
opening
|
||||
.state
|
||||
.set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1);
|
||||
}
|
||||
self.opening.take();
|
||||
let pending_open_task = self.drain_pending_open();
|
||||
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
||||
}
|
||||
|
||||
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf) -> Task<Message> {
|
||||
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let result = if block_name == "*" {
|
||||
let document = self.tabs[i].scene.document.clone();
|
||||
let handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let worker_name = block_name.clone();
|
||||
let worker_path = path.clone();
|
||||
background_task(
|
||||
move || {
|
||||
let document = if worker_name == "*" {
|
||||
crate::modules::insert::wblock::extract_entities_to_doc(
|
||||
&self.tabs[i].scene.document,
|
||||
&document,
|
||||
&handles,
|
||||
)
|
||||
} else {
|
||||
crate::modules::insert::wblock::extract_block_to_doc(
|
||||
&self.tabs[i].scene.document,
|
||||
&block_name,
|
||||
)
|
||||
};
|
||||
match result {
|
||||
Ok(doc) => match crate::io::save(&doc, &path) {
|
||||
Ok(()) => {
|
||||
let fname = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
||||
self.command_line.push_output(&format!(
|
||||
"WBLOCK Saved \"{block_name}\" → \"{fname}\""
|
||||
));
|
||||
}
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("WBLOCK save failed: {e}")),
|
||||
},
|
||||
Err(e) => self.command_line.push_error(&format!("WBLOCK: {e}")),
|
||||
&document,
|
||||
&worker_name)
|
||||
}
|
||||
Task::none()
|
||||
.map_err(|e| e.to_string())?; crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
|
||||
},
|
||||
move |result| Message::WblockWriteFinished(block_name, path, result),)
|
||||
}
|
||||
|
||||
pub(super) fn on_stl_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
@@ -675,21 +672,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
.values()
|
||||
.filter_map(|s| s.lods.first().cloned())
|
||||
.collect();
|
||||
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
||||
match crate::io::stl::build_stl(&mesh_refs) {
|
||||
Some(bytes) => match std::fs::write(&path, bytes) {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("STLOUT: write error: {e}")),
|
||||
let worker_path = path.clone();
|
||||
background_task(
|
||||
move || {
|
||||
let mesh_refs: Vec<_> = meshes.iter().collect();
|
||||
let bytes = crate::io::stl::build_stl(&mesh_refs)
|
||||
.ok_or_else(|| "no mesh data to export".to_string())?;
|
||||
std::fs::write(&worker_path, bytes).map_err(|e| e.to_string())
|
||||
},
|
||||
None => self
|
||||
.command_line
|
||||
.push_error("STLOUT: no mesh data to export."),
|
||||
}
|
||||
Task::none()
|
||||
move |result| Message::StlExportFinished(path, result),)
|
||||
}
|
||||
|
||||
pub(super) fn on_step_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
@@ -701,63 +692,26 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
.values()
|
||||
.filter_map(|s| s.lods.first().cloned())
|
||||
.collect();
|
||||
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
||||
match crate::io::step::build_step(&mesh_refs) {
|
||||
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("STEPOUT: write error: {e}")),
|
||||
let worker_path = path.clone();
|
||||
background_task(
|
||||
move || {
|
||||
let mesh_refs: Vec<_> = meshes.iter().collect();
|
||||
let text = crate::io::step::build_step(&mesh_refs)
|
||||
.ok_or_else(|| "no mesh data to export".to_string())?;
|
||||
std::fs::write(&worker_path, text.as_bytes()).map_err(|e| e.to_string())
|
||||
},
|
||||
None => self
|
||||
.command_line
|
||||
.push_error("STEPOUT: no mesh data to export."),
|
||||
}
|
||||
Task::none()
|
||||
move |result| Message::StepExportFinished(path, result),)
|
||||
}
|
||||
|
||||
pub(super) fn on_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
let src = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
self.command_line
|
||||
.push_error(&format!("IMPORTOBJ: read error: {e}"));
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let color = [0.7f32, 0.7, 0.85, 1.0];
|
||||
match crate::io::obj::parse_obj(&src, color) {
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error("IMPORTOBJ: no usable geometry in file.");
|
||||
}
|
||||
Some(mut mesh) => {
|
||||
let i = self.active_tab;
|
||||
let file_stem = path
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "obj_mesh".into());
|
||||
mesh.name = file_stem.clone();
|
||||
self.push_undo_snapshot(i, "IMPORTOBJ");
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
let entity = empty_solid3d();
|
||||
let handle = self.tabs[i].scene.add_entity(entity);
|
||||
if !handle.is_null() {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"IMPORTOBJ: imported \"{}\" as mesh.",
|
||||
file_stem
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
let tab_id = self.tabs[self.active_tab].id;
|
||||
let worker_path = path.clone();
|
||||
background_task(
|
||||
move || {
|
||||
let src = std::fs::read_to_string(&worker_path).map_err(|e| e.to_string())?; crate::io::obj::parse_obj(&src, [0.7, 0.7, 0.85, 1.0])
|
||||
.ok_or_else(|| "no usable geometry in file".to_string())
|
||||
},
|
||||
move |result| Message::ObjImportFinished(tab_id, path, result),)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -770,6 +724,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn stamp_thumbnail(&mut self, i: usize, version: acadrust::DxfVersion) {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let preview = crate::io::thumbnail::from_snapshot(
|
||||
&scene.entity_wires(),
|
||||
&scene.camera.borrow(),
|
||||
scene.bg_color,
|
||||
version >= acadrust::DxfVersion::AC1027,
|
||||
self.vp_size,
|
||||
);
|
||||
self.tabs[i].scene.document.preview = preview;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn queue_native_save(
|
||||
&mut self,
|
||||
@@ -1484,7 +1451,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
_ => (paper_w, paper_h),
|
||||
};
|
||||
|
||||
match crate::io::pdf_export::export_pdf(
|
||||
let plot_style = self.active_plot_style.clone();
|
||||
let worker_path = path.clone();
|
||||
background_task(
|
||||
move || {
|
||||
crate::io::pdf_export::export_pdf(
|
||||
&wires,
|
||||
hatches.as_slice(),
|
||||
wipeouts.as_slice(),
|
||||
@@ -1495,23 +1466,20 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
rotation_deg,
|
||||
1.0,
|
||||
None,
|
||||
&path,
|
||||
self.active_plot_style.as_ref(),
|
||||
) {
|
||||
// Full path, not just the file name — when the export was
|
||||
// driven by EXPORTPDF <path> the user needs to see where
|
||||
// the file actually landed. (#369)
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_info(&format!("Exported: {}", path.display())),
|
||||
Err(e) => self.command_line.push_error(&format!("Export failed: {e}")),
|
||||
}
|
||||
Task::none()
|
||||
&worker_path,
|
||||
plot_style.as_ref(),
|
||||
)
|
||||
.map(|_| format!("Exported: {}", worker_path.display()))
|
||||
.map_err(|e| format!("Export failed: {e}"))
|
||||
},
|
||||
|result| Message::BackgroundIoFinished(result, false),
|
||||
)
|
||||
}
|
||||
|
||||
/// Export the pending model-space plot window (set by PLOTWINDOW while on
|
||||
/// the Model tab) to PDF, using the chosen paper size/orientation/scale.
|
||||
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.current_layout != "Model" {
|
||||
@@ -1569,7 +1537,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
// same as the wires) so the final sheet-mm rect lands at (ox, oy).
|
||||
let clip = Some(((ox / scale) as f32, (oy / scale) as f32, win_w as f32, win_h as f32));
|
||||
|
||||
let res = crate::io::pdf_export::export_pdf(
|
||||
let plot_style = self.active_plot_style.clone();
|
||||
let worker_path = path.clone();
|
||||
self.close_active_modal();
|
||||
background_task(
|
||||
move || {
|
||||
crate::io::pdf_export::export_pdf(
|
||||
&wires,
|
||||
hatches.as_slice(),
|
||||
wipeouts.as_slice(),
|
||||
@@ -1580,20 +1553,18 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
0,
|
||||
scale as f32,
|
||||
clip,
|
||||
&path,
|
||||
self.active_plot_style.as_ref(),
|
||||
);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.command_line.push_info(&format!(
|
||||
&worker_path,
|
||||
plot_style.as_ref(),
|
||||
)
|
||||
.map(|_| {format!(
|
||||
"Plotted window to {}",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
));
|
||||
self.close_active_modal();
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")),
|
||||
}
|
||||
Task::none()
|
||||
worker_path
|
||||
.file_name().unwrap_or_default().to_string_lossy()
|
||||
)
|
||||
}).map_err(|e| format!("Plot failed: {e}"))
|
||||
},
|
||||
|result| Message::BackgroundIoFinished(result, false),
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the render inputs and page geometry for a full-layout plot: wires
|
||||
@@ -1678,12 +1649,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.layout_plot_params();
|
||||
let plot_style = self.active_plot_style.clone();
|
||||
self.command_line.push_info("Sending to system printer…");
|
||||
Task::perform(
|
||||
async move {
|
||||
background_task(
|
||||
move || {
|
||||
iced::futures::executor::block_on(
|
||||
crate::io::print_to_printer::print_wires(
|
||||
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
|
||||
)
|
||||
.await
|
||||
))
|
||||
},
|
||||
Message::PrintResult,
|
||||
)
|
||||
@@ -2220,16 +2191,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
return Task::none();
|
||||
};
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
||||
let exp = crate::io::pdf_export::export_pdf(
|
||||
return background_task(
|
||||
move || {
|
||||
crate::io::pdf_export::export_pdf(
|
||||
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||
plot_style.as_ref(),
|
||||
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
|
||||
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
|
||||
},
|
||||
|result| Message::BackgroundIoFinished(result, true),
|
||||
);
|
||||
match exp.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
||||
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
||||
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
}
|
||||
if d.to_file {
|
||||
// Tested clipped export (opens a save dialog).
|
||||
@@ -2244,25 +2215,27 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
return Task::none();
|
||||
};
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
||||
let exp = crate::io::pdf_export::export_pdf(
|
||||
let opts = self.plot_print_options(&d);
|
||||
return background_task(
|
||||
move || {
|
||||
crate::io::pdf_export::export_pdf(
|
||||
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||
plot_style.as_ref(),
|
||||
).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts))
|
||||
.map(|printer| format!("Sent to printer: {printer}"))
|
||||
.map_err(|e| format!("Print failed: {e}"))
|
||||
},
|
||||
|result| Message::BackgroundIoFinished(result, false),
|
||||
);
|
||||
let opts = self.plot_print_options(&d);
|
||||
match exp.and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) {
|
||||
Ok(printer) => self
|
||||
.command_line
|
||||
.push_info(&format!("Sent to printer: {printer}")),
|
||||
Err(e) => self.command_line.push_error(&format!("Print failed: {e}")),
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot) = self.layout_plot_params();
|
||||
|
||||
if preview {
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
||||
let res = crate::io::pdf_export::export_pdf(
|
||||
return background_task(
|
||||
move || {
|
||||
crate::io::pdf_export::export_pdf(
|
||||
&wires,
|
||||
&hatches,
|
||||
&wipeouts,
|
||||
@@ -2275,14 +2248,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
None,
|
||||
&tmp,
|
||||
plot_style.as_ref(),
|
||||
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
|
||||
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
|
||||
},
|
||||
|result| Message::BackgroundIoFinished(result, true),
|
||||
);
|
||||
match res.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
||||
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
||||
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
||||
}
|
||||
// Preview leaves the dialog open for further tweaks.
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if d.to_file {
|
||||
@@ -2292,12 +2262,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
|
||||
let opts = self.plot_print_options(&d);
|
||||
self.command_line.push_info("Sending to system printer…");
|
||||
Task::perform(
|
||||
async move {
|
||||
background_task(
|
||||
move || {
|
||||
iced::futures::executor::block_on(
|
||||
crate::io::print_to_printer::print_wires_with(
|
||||
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
|
||||
)
|
||||
.await
|
||||
))
|
||||
},
|
||||
Message::PrintResult,
|
||||
)
|
||||
|
||||
+103
-12
@@ -449,20 +449,26 @@ impl OpenCADStudio {
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let phase = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
|
||||
let progress = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||
super::OPEN_PHASE_READING,
|
||||
));
|
||||
self.opening = Some(super::OpenProgress {
|
||||
name: name.clone(),
|
||||
size_bytes,
|
||||
phase: phase.clone(),
|
||||
state: progress.clone(),
|
||||
started: Instant::now(),
|
||||
});
|
||||
let size_label = format_size(size_bytes);
|
||||
self.command_line
|
||||
.push_info(&format!("Opening \"{name}\" ({size_label})…"));
|
||||
let model_bg = self.default_bg_color.unwrap_or([
|
||||
33.0 / 255.0,
|
||||
40.0 / 255.0,
|
||||
48.0 / 255.0,
|
||||
1.0,
|
||||
]);
|
||||
Task::perform(
|
||||
crate::io::open_path_with_phase(path, phase),
|
||||
crate::io::open_path_with_phase(path, progress, model_bg),
|
||||
Message::FileOpened,
|
||||
)
|
||||
}
|
||||
@@ -579,6 +585,19 @@ impl OpenCADStudio {
|
||||
|
||||
Message::WblockSaveResult(_, None) => Task::none(),
|
||||
|
||||
Message::WblockWriteFinished(block_name, path, result) => {
|
||||
match result {
|
||||
Ok(()) => self.command_line.push_output(&format!(
|
||||
"WBLOCK Saved \"{block_name}\" → \"{}\"",
|
||||
path.display()
|
||||
)),
|
||||
Err(error) => self
|
||||
.command_line
|
||||
.push_error(&format!("WBLOCK save failed: {error}")),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::DataExtractionSave(csv) => {
|
||||
let csv_clone = csv.clone();
|
||||
Task::perform(
|
||||
@@ -643,6 +662,16 @@ impl OpenCADStudio {
|
||||
|
||||
Message::StlExportPath(None) => Task::none(),
|
||||
|
||||
Message::StlExportFinished(path, result) => {
|
||||
match result {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
|
||||
Err(error) => self.command_line.push_error(&format!("STLOUT: {error}")),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── STEP AP203 export ─────────────────────────────────────────
|
||||
Message::StepExport => {
|
||||
let i = self.active_tab;
|
||||
@@ -670,6 +699,16 @@ impl OpenCADStudio {
|
||||
|
||||
Message::StepExportPath(None) => Task::none(),
|
||||
|
||||
Message::StepExportFinished(path, result) => {
|
||||
match result {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
|
||||
Err(error) => self.command_line.push_error(&format!("STEPOUT: {error}")),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── OBJ import ────────────────────────────────────────────────
|
||||
Message::ObjImport => Task::perform(
|
||||
async {
|
||||
@@ -688,6 +727,38 @@ impl OpenCADStudio {
|
||||
|
||||
Message::ObjImportPath(None) => Task::none(),
|
||||
|
||||
Message::ObjImportFinished(tab_id, path, result) => {
|
||||
match result {
|
||||
Err(error) => self.command_line.push_error(&format!("IMPORTOBJ: {error}")),
|
||||
Ok(mut mesh) => {
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||
self.command_line
|
||||
.push_info("IMPORTOBJ: target drawing was closed.");
|
||||
return Task::none();
|
||||
};
|
||||
let file_stem = path
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "obj_mesh".into());
|
||||
mesh.name = file_stem.clone();
|
||||
self.push_undo_snapshot(i, "IMPORTOBJ");
|
||||
let entity = crate::modules::insert::solid3d_cmds::empty_solid3d();
|
||||
let handle = self.tabs[i].scene.add_entity(entity);
|
||||
if !handle.is_null() {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"IMPORTOBJ: imported \"{file_stem}\" as mesh."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SaveFile => self.on_save_file(),
|
||||
|
||||
Message::SaveAs => {
|
||||
@@ -1132,7 +1203,7 @@ impl OpenCADStudio {
|
||||
let targets = self.layer_row_action_targets(i, idx);
|
||||
if let Some(on) = on {
|
||||
if !targets.is_empty() {
|
||||
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
||||
let undo = self.begin_layer_undo(i, "LAYER OFF/ON", &targets);
|
||||
for name in &targets {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.flags.off = !on;
|
||||
@@ -1143,8 +1214,9 @@ impl OpenCADStudio {
|
||||
pl.visible = on;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{} layer(s) turned {}",
|
||||
targets.len(),
|
||||
@@ -1171,7 +1243,7 @@ impl OpenCADStudio {
|
||||
let targets = self.layer_row_action_targets(i, idx);
|
||||
if let Some(locked) = locked {
|
||||
if !targets.is_empty() {
|
||||
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
||||
let undo = self.begin_layer_undo(i, "LAYER LOCK/UNLOCK", &targets);
|
||||
for name in &targets {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.flags.locked = locked;
|
||||
@@ -1182,8 +1254,9 @@ impl OpenCADStudio {
|
||||
pl.locked = locked;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
// Lock state affects editability, not rendered geometry.
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{} layer(s) {}",
|
||||
targets.len(),
|
||||
@@ -1201,7 +1274,7 @@ impl OpenCADStudio {
|
||||
let targets = self.layer_row_action_targets(i, idx);
|
||||
if let Some(frozen) = frozen {
|
||||
if !targets.is_empty() {
|
||||
self.push_undo_snapshot(i, "LAYER FREEZE");
|
||||
let undo = self.begin_layer_undo(i, "LAYER FREEZE", &targets);
|
||||
for name in &targets {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
if frozen {
|
||||
@@ -1216,8 +1289,9 @@ impl OpenCADStudio {
|
||||
pl.frozen = frozen;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.command_line.push_output(&format!(
|
||||
"{} layer(s) {}",
|
||||
targets.len(),
|
||||
@@ -1321,6 +1395,7 @@ impl OpenCADStudio {
|
||||
// Apply to every selected layer (multi-select), not just one.
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||
use crate::ui::window::layers::iced_color_from_acad;
|
||||
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
||||
for name in &names {
|
||||
@@ -1334,10 +1409,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// ByLayer color is baked into the cached wires at
|
||||
// tessellation time, so bump the geometry epoch to
|
||||
// invalidate the wire cache and repaint with the new color.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].layers.color_picker_row = None;
|
||||
self.tabs[i].layers.color_full_palette = false;
|
||||
self.sync_ribbon_layers();
|
||||
@@ -1349,6 +1425,7 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER LINETYPE", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.line_type = lt.clone();
|
||||
@@ -1360,8 +1437,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// Linetype is baked into the cached wires; repaint.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -1370,6 +1448,7 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER LINEWEIGHT", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.line_weight = lw;
|
||||
@@ -1381,8 +1460,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// Lineweight is baked into the cached wires; repaint.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -3984,6 +4064,17 @@ impl OpenCADStudio {
|
||||
}
|
||||
Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path),
|
||||
|
||||
Message::BackgroundIoFinished(result, reopen_plot) => {
|
||||
match result {
|
||||
Ok(message) => self.command_line.push_info(&message),
|
||||
Err(error) => self.command_line.push_error(&error),
|
||||
}
|
||||
if reopen_plot {
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── Print to system printer ───────────────────────────────────────
|
||||
Message::PrintToPrinter => self.on_print_to_printer(),
|
||||
Message::PrintResult(Ok(printer)) => {
|
||||
|
||||
Reference in new issue
Block a user