forked from 1b-agent/opencadstudio-upstream
feat(block-editor): isolate session history and add save checkpoints
This commit is contained in:
1 parent
535710e5d1
commit
f39fb31137
15 files changed
+1002
-255
No files matched your search
@@ -700,6 +700,9 @@ impl OpenCADStudio {
|
||||
};
|
||||
match req["op"].as_str().unwrap_or("") {
|
||||
"new" => {
|
||||
if self.tabs[self.active_tab].block_edit_history.is_some() {
|
||||
return err("Close the block editor before replacing the drawing.");
|
||||
}
|
||||
if let Some(template) = req["template"].as_str() {
|
||||
let purged = match self.apply_template(template) {
|
||||
Ok(purged) => purged,
|
||||
@@ -723,6 +726,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
"open" => {
|
||||
if self.tabs[self.active_tab].block_edit_history.is_some() {
|
||||
return err("Close the block editor before replacing the drawing.");
|
||||
}
|
||||
let Some(path) = req["path"].as_str() else {
|
||||
return err("open: missing \"path\"");
|
||||
};
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
use super::document::{
|
||||
DeltaSnapshot, HistorySnapshot, HistoryState, ParametricConstraintsEntryDelta,
|
||||
StructureSnapshot,
|
||||
};
|
||||
use super::{Message, ModalKind, OpenCADStudio};
|
||||
use codec::{CadDocument, EntityType};
|
||||
use iced::Task;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
/// A single save checkpoint. Entity payloads remain shared through the
|
||||
/// document's Arc storage; only modified entities detach during editing.
|
||||
pub(super) struct BlockEditCheckpoint {
|
||||
document: CadDocument,
|
||||
constraints: Vec<crate::scene::parametric_constraints::ParametricConstraintSet>,
|
||||
parameters: crate::scene::named_parameters::ParameterTable,
|
||||
dirty: bool,
|
||||
active_layer: String,
|
||||
}
|
||||
|
||||
pub(super) struct BlockEditHistory {
|
||||
pub main: HistoryState,
|
||||
checkpoint: BlockEditCheckpoint,
|
||||
return_isolation: crate::scene::ObjectIsolationState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum BlockEditContinuation {
|
||||
Close,
|
||||
Switch(String),
|
||||
Layout(String),
|
||||
Tab(u64),
|
||||
Quit,
|
||||
}
|
||||
|
||||
fn same_constraints(
|
||||
before: &crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
after: &crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
) -> bool {
|
||||
before.constraints == after.constraints
|
||||
&& before.local_parameters == after.local_parameters
|
||||
&& before.retained_standard_groups == after.retained_standard_groups
|
||||
&& before.dimensions == after.dimensions
|
||||
}
|
||||
|
||||
fn normalize_allocation(document: &mut CadDocument, next: u64) {
|
||||
if document.next_handle() < next {
|
||||
document.header.handle_seed = next - 1;
|
||||
document.allocate_handle();
|
||||
}
|
||||
document.header.handle_seed = next;
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn reconcile_block_edit_membership(&mut self, i: usize) {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
return;
|
||||
};
|
||||
let name = session.block_name.clone();
|
||||
let document = &mut self.tabs[i].scene.document;
|
||||
let Some(record) = document.block_records.get(&name) else {
|
||||
return;
|
||||
};
|
||||
let members: Vec<_> = record
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| document.get_entity(*handle).is_some())
|
||||
.collect();
|
||||
if members != record.entity_handles {
|
||||
document
|
||||
.block_records
|
||||
.get_mut(&name)
|
||||
.unwrap()
|
||||
.entity_handles = members;
|
||||
}
|
||||
}
|
||||
|
||||
fn block_edit_checkpoint(&self, i: usize) -> BlockEditCheckpoint {
|
||||
let tab = &self.tabs[i];
|
||||
BlockEditCheckpoint {
|
||||
document: tab.scene.document.clone(),
|
||||
constraints: tab.scene.parametric_constraints.clone(),
|
||||
parameters: tab.scene.named_parameters.clone(),
|
||||
dirty: tab.dirty,
|
||||
active_layer: tab.active_layer.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn begin_block_edit_history(&mut self, i: usize) {
|
||||
self.finish_pending_history(i);
|
||||
let checkpoint = self.block_edit_checkpoint(i);
|
||||
let main = std::mem::take(&mut self.tabs[i].history);
|
||||
let return_isolation = self.tabs[i].scene.object_isolation.clone();
|
||||
self.tabs[i].block_edit_history = Some(BlockEditHistory {
|
||||
main,
|
||||
checkpoint,
|
||||
return_isolation,
|
||||
});
|
||||
}
|
||||
|
||||
fn block_edit_delta(&mut self, i: usize) -> Option<DeltaSnapshot> {
|
||||
self.reconcile_block_edit_membership(i);
|
||||
self.finish_pending_history(i);
|
||||
let current_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||
let tab = &self.tabs[i];
|
||||
let history = tab.block_edit_history.as_ref()?;
|
||||
let checkpoint = &history.checkpoint;
|
||||
let session = tab.active_block_edit_session()?;
|
||||
let before: FxHashMap<_, _> = checkpoint
|
||||
.document
|
||||
.entities()
|
||||
.map(|e| {
|
||||
let h = e.common().handle;
|
||||
(h, checkpoint.document.get_entity_arc(h).unwrap())
|
||||
})
|
||||
.collect();
|
||||
let after: FxHashMap<_, _> = tab
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|e| {
|
||||
let h = e.common().handle;
|
||||
(h, tab.scene.document.get_entity_arc(h).unwrap())
|
||||
})
|
||||
.collect();
|
||||
let mut handles: FxHashSet<_> = before.keys().copied().collect();
|
||||
handles.extend(after.keys().copied());
|
||||
let mut entities: Vec<_> = handles
|
||||
.into_iter()
|
||||
.filter_map(|h| {
|
||||
let a = before.get(&h);
|
||||
let b = after.get(&h);
|
||||
(a != b).then(|| (h, a.cloned(), b.cloned()))
|
||||
})
|
||||
.collect();
|
||||
entities.sort_by_key(|(handle, _, _)| handle.value());
|
||||
let mut before_structure = checkpoint.document.clone();
|
||||
before_structure = before_structure.snapshot_structure();
|
||||
let mut after_structure = current_structure;
|
||||
// The editor temporarily uses Model rendering. Drawing history must
|
||||
// retain the layout to which the editor returns.
|
||||
let model = session.return_layout == "Model";
|
||||
before_structure.header.show_model_space = model;
|
||||
after_structure.header.show_model_space = model;
|
||||
if !model {
|
||||
crate::io::set_saved_active_layout(&mut before_structure, &session.return_layout);
|
||||
crate::io::set_saved_active_layout(&mut after_structure, &session.return_layout);
|
||||
}
|
||||
let next = before_structure
|
||||
.next_handle()
|
||||
.max(after_structure.next_handle())
|
||||
.max(before_structure.header.handle_seed)
|
||||
.max(after_structure.header.handle_seed);
|
||||
// Reserving handles is not a content edit. Undoing every edit should
|
||||
// return a session to clean without reusing allocated handles.
|
||||
normalize_allocation(&mut before_structure, next);
|
||||
normalize_allocation(&mut after_structure, next);
|
||||
let structure = (before_structure != after_structure)
|
||||
.then_some(StructureSnapshot::Full(before_structure));
|
||||
let mut scopes: Vec<_> = checkpoint.constraints.iter().map(|c| c.scope).collect();
|
||||
for set in &tab.scene.parametric_constraints {
|
||||
if !scopes.contains(&set.scope) {
|
||||
scopes.push(set.scope);
|
||||
}
|
||||
}
|
||||
let parametric_constraints: Vec<_> = scopes
|
||||
.into_iter()
|
||||
.filter_map(|scope| {
|
||||
let empty =
|
||||
|| crate::scene::parametric_constraints::ParametricConstraintSet::new(scope);
|
||||
let a = checkpoint
|
||||
.constraints
|
||||
.iter()
|
||||
.find(|c| c.scope == scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(empty);
|
||||
let b = tab
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.find(|c| c.scope == scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(empty);
|
||||
(!same_constraints(&a, &b)).then_some(ParametricConstraintsEntryDelta {
|
||||
scope,
|
||||
before: a,
|
||||
after: b,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let named_parameters = (checkpoint.parameters != tab.scene.named_parameters).then(|| {
|
||||
(
|
||||
checkpoint.parameters.clone(),
|
||||
tab.scene.named_parameters.clone(),
|
||||
)
|
||||
});
|
||||
let active_layer = (checkpoint.active_layer != tab.active_layer)
|
||||
.then(|| (checkpoint.active_layer.clone(), tab.active_layer.clone()));
|
||||
if entities.is_empty()
|
||||
&& structure.is_none()
|
||||
&& parametric_constraints.is_empty()
|
||||
&& named_parameters.is_none()
|
||||
&& active_layer.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(DeltaSnapshot {
|
||||
entities,
|
||||
current_layout_before: session.return_layout.clone(),
|
||||
current_layout_after: session.return_layout.clone(),
|
||||
selected_before: Vec::new(),
|
||||
selected_after: Vec::new(),
|
||||
dirty_before: checkpoint.dirty,
|
||||
dirty_after: true,
|
||||
active_layer,
|
||||
structure,
|
||||
parametric_constraints,
|
||||
named_parameters,
|
||||
label: format!("Edit block {}", session.block_name),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn block_edit_dirty(&mut self, i: usize) -> bool {
|
||||
self.block_edit_delta(i).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn save_block_edit(&mut self, i: usize) -> bool {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No block editor is open.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let name = session.block_name.clone();
|
||||
if let Err(error) = crate::scene::block_service::validate_block_name(&name) {
|
||||
// Existing anonymous definitions are editable; name validation
|
||||
// applies to named definitions rather than generated variants.
|
||||
if !name.starts_with('*') {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let nested: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities_in_block(&name)
|
||||
.filter_map(|entity| match entity {
|
||||
EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for child in nested {
|
||||
if crate::scene::block_service::would_create_cycle(
|
||||
&self.tabs[i].scene.document,
|
||||
&name,
|
||||
&child,
|
||||
) {
|
||||
self.command_line.push_error(
|
||||
&crate::scene::block_service::circular_dependency_error(&name, &child),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(delta) = self.block_edit_delta(i) {
|
||||
let mut history = self.tabs[i].block_edit_history.take().unwrap();
|
||||
std::mem::swap(&mut self.tabs[i].history, &mut history.main);
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
std::mem::swap(&mut self.tabs[i].history, &mut history.main);
|
||||
self.tabs[i].dirty = true;
|
||||
history.checkpoint = self.block_edit_checkpoint(i);
|
||||
self.tabs[i].block_edit_history = Some(history);
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
}
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Block {} saved.", name).as_ref());
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn close_block_edit(&mut self, i: usize, discard: bool) {
|
||||
self.finish_pending_history(i);
|
||||
let Some(session) = self.tabs[i].block_edits.pop() else {
|
||||
return;
|
||||
};
|
||||
if let Some(history) = self.tabs[i].block_edit_history.take() {
|
||||
if discard {
|
||||
self.tabs[i].scene.document = history.checkpoint.document;
|
||||
self.tabs[i].scene.parametric_constraints = history.checkpoint.constraints;
|
||||
self.tabs[i].scene.named_parameters = history.checkpoint.parameters;
|
||||
self.tabs[i].active_layer = history.checkpoint.active_layer;
|
||||
self.tabs[i].dirty = history.checkpoint.dirty;
|
||||
}
|
||||
self.tabs[i].history = history.main;
|
||||
self.tabs[i].scene.object_isolation = history.return_isolation;
|
||||
}
|
||||
self.tabs[i].active_block_edit = None;
|
||||
self.restore_after_block_edit_close(
|
||||
i,
|
||||
None,
|
||||
&session.return_layout,
|
||||
&session.return_camera,
|
||||
);
|
||||
self.cancel_attr_editor();
|
||||
}
|
||||
|
||||
pub(super) fn request_block_edit_close(
|
||||
&mut self,
|
||||
i: usize,
|
||||
continuation: BlockEditContinuation,
|
||||
) -> Task<Message> {
|
||||
if self.tabs[i].active_block_edit_session().is_some() {
|
||||
if self.block_edit_dirty(i) {
|
||||
self.active_tab = i;
|
||||
self.pending_block_close = Some((self.tabs[i].id, continuation));
|
||||
self.active_modal = Some(ModalKind::BlockEditChanges);
|
||||
return Task::none();
|
||||
}
|
||||
self.close_block_edit(i, true);
|
||||
}
|
||||
self.continue_after_block_edit(continuation, i)
|
||||
}
|
||||
|
||||
fn continue_after_block_edit(
|
||||
&mut self,
|
||||
continuation: BlockEditContinuation,
|
||||
i: usize,
|
||||
) -> Task<Message> {
|
||||
match continuation {
|
||||
BlockEditContinuation::Close => Task::none(),
|
||||
BlockEditContinuation::Switch(name) => {
|
||||
self.active_tab = i;
|
||||
self.dispatch_command(&format!("BEDIT_OPEN:{name}"))
|
||||
}
|
||||
BlockEditContinuation::Layout(name) => {
|
||||
self.active_tab = i;
|
||||
self.on_layout_switch(name)
|
||||
}
|
||||
BlockEditContinuation::Tab(id) => self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab| tab.id == id)
|
||||
.map(|index| {
|
||||
let close = self.on_tab_close(index);
|
||||
if self.pending_close.is_none() && self.pending_block_close.is_none() {
|
||||
Task::batch([close, self.continue_tab_close_queue()])
|
||||
} else {
|
||||
close
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(Task::none),
|
||||
BlockEditContinuation::Quit => self.request_quit_with_block_edits(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_block_edit_close(&mut self, save: bool) -> Task<Message> {
|
||||
let Some((id, continuation)) = self.pending_block_close.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == id) else {
|
||||
self.cancel_block_edit_close();
|
||||
return Task::none();
|
||||
};
|
||||
if save && !self.save_block_edit(i) {
|
||||
return Task::none();
|
||||
}
|
||||
self.pending_block_close = None;
|
||||
self.active_modal = None;
|
||||
self.close_block_edit(i, !save);
|
||||
self.continue_after_block_edit(continuation, i)
|
||||
}
|
||||
|
||||
pub(super) fn cancel_block_edit_close(&mut self) {
|
||||
self.pending_block_close = None;
|
||||
self.pending_tab_closes.clear();
|
||||
if self.active_modal == Some(ModalKind::BlockEditChanges) {
|
||||
self.active_modal = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn request_quit_with_block_edits(&mut self) -> Task<Message> {
|
||||
if let Some(i) = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab| tab.active_block_edit_session().is_some())
|
||||
{
|
||||
return self.request_block_edit_close(i, BlockEditContinuation::Quit);
|
||||
}
|
||||
if self.tabs.iter().any(|tab| tab.dirty) {
|
||||
self.pending_close = Some(super::PendingClose::Quit);
|
||||
self.open_unsaved_dialog_window()
|
||||
} else {
|
||||
self.exit_app()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
use super::{Message, ModalKind, OpenCADStudio};
|
||||
use codec::entities::{Insert, Line};
|
||||
use codec::types::Vector3;
|
||||
use codec::{EntityType, Handle};
|
||||
|
||||
fn fixture() -> (OpenCADStudio, Handle) {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.define_block_raw(
|
||||
"EDIT_ME",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
))],
|
||||
);
|
||||
let reference = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO)))
|
||||
.unwrap();
|
||||
app.tabs[i].dirty = false;
|
||||
(app, reference)
|
||||
}
|
||||
|
||||
fn begin(app: &mut OpenCADStudio, reference: Handle) {
|
||||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
}
|
||||
|
||||
fn circle(app: &mut OpenCADStudio, x: f64) {
|
||||
let result = app.automation_op(&format!(r#"{{"op":"run","cmd":"CIRCLE {x},0 2"}}"#));
|
||||
assert_eq!(result["ok"], true, "{result}");
|
||||
app.finish_pending_history(app.active_tab);
|
||||
}
|
||||
|
||||
fn members(app: &OpenCADStudio) -> Vec<Handle> {
|
||||
app.tabs[app.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn main_depth(app: &OpenCADStudio) -> usize {
|
||||
let tab = &app.tabs[app.active_tab];
|
||||
tab.block_edit_history
|
||||
.as_ref()
|
||||
.map(|h| h.main.undo_stack.len())
|
||||
.unwrap_or(tab.history.undo_stack.len())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_undo_preserves_membership_and_cannot_reach_drawing_history() {
|
||||
let (mut app, reference) = fixture();
|
||||
circle(&mut app, 50.0);
|
||||
let depth = main_depth(&app);
|
||||
begin(&mut app, reference);
|
||||
assert_eq!(main_depth(&app), depth);
|
||||
circle(&mut app, 1.0);
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
assert!(!app.block_edit_dirty(app.active_tab));
|
||||
app.undo_steps(100);
|
||||
assert_eq!(main_depth(&app), depth);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
app.redo_steps(1);
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
assert!(members(&app).iter().all(|h| app.tabs[app.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*h)
|
||||
.is_some()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_saves_stay_open_and_commit_one_drawing_step_each() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
circle(&mut app, 2.0);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
assert_eq!(main_depth(&app), 1);
|
||||
assert!(!app.block_edit_dirty(app.active_tab));
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert_eq!(main_depth(&app), 1, "a clean save is not a new edit");
|
||||
circle(&mut app, 3.0);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert_eq!(main_depth(&app), 2);
|
||||
circle(&mut app, 4.0);
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert_eq!(members(&app).len(), 4, "discard restores the last save");
|
||||
assert_eq!(main_depth(&app), 2);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab]
|
||||
.history
|
||||
.undo_stack
|
||||
.last()
|
||||
.unwrap()
|
||||
.label(),
|
||||
"Edit block EDIT_ME"
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app).len(), 3);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
app.redo_steps(2);
|
||||
assert_eq!(members(&app).len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_preserves_drawing_redo_and_never_leaks_local_edits() {
|
||||
let (mut app, reference) = fixture();
|
||||
circle(&mut app, 50.0);
|
||||
app.undo_steps(1);
|
||||
let main_redo = app.tabs[app.active_tab].history.redo_stack.len();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
assert_eq!(app.tabs[app.active_tab].history.redo_stack.len(), main_redo);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_close_cancel_discard_and_save_follow_the_checkpoint() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
||||
let _ = app.update(Message::BlockEditChangesCancel);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let _ = app.update(Message::BlockEditChangesDiscard);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
assert_eq!(main_depth(&app), 0);
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 2.0);
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let _ = app.update(Message::BlockEditChangesSave);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
assert_eq!(main_depth(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_close_does_not_prompt_or_dirty_the_drawing() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
assert!(!app.tabs[app.active_tab].dirty);
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||
assert!(app.active_modal.is_none());
|
||||
assert!(!app.tabs[app.active_tab].dirty);
|
||||
assert_eq!(main_depth(&app), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_switch_can_cancel_and_never_nests_sessions() {
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.define_block_raw("OTHER", Vector3::ZERO, vec![]);
|
||||
let other = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)))
|
||||
.unwrap();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
||||
let _ = app.update(Message::BlockEditChangesCancel);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
||||
"EDIT_ME"
|
||||
);
|
||||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
|
||||
let _ = app.update(Message::BlockEditChangesSave);
|
||||
assert_eq!(app.tabs[i].block_edits.len(), 1);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
||||
"OTHER"
|
||||
);
|
||||
assert_eq!(main_depth(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_close_prompts_for_block_edits_before_drawing_changes() {
|
||||
let (mut app, reference) = fixture();
|
||||
let id = app.tabs[app.active_tab].id;
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.update(Message::TabClose(id));
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
||||
let _ = app.update(Message::BlockEditChangesCancel);
|
||||
assert!(app.tabs.iter().any(|t| t.id == id));
|
||||
let _ = app.update(Message::TabClose(id));
|
||||
let _ = app.update(Message::BlockEditChangesDiscard);
|
||||
assert!(!app.tabs.iter().any(|t| t.id == id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_refuses_a_cycle_introduced_outside_the_insert_command() {
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.define_block_raw(
|
||||
"OTHER",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO))],
|
||||
);
|
||||
begin(&mut app, reference);
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)));
|
||||
assert!(!app.save_block_edit(i));
|
||||
assert_eq!(main_depth(&app), 0);
|
||||
assert!(app.tabs[i].active_block_edit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_history_retains_the_paper_layout_after_save_and_undo() {
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.add_layout("Sheet").unwrap();
|
||||
app.tabs[i].scene.set_current_layout("Sheet".into());
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.dispatch_command("BEDIT_SAVE");
|
||||
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
|
||||
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
|
||||
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
||||
assert_eq!(
|
||||
crate::io::saved_active_layout(&app.tabs[i].scene.document).as_deref(),
|
||||
Some("Sheet")
|
||||
);
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
app.redo_steps(1);
|
||||
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_parameters_are_saved_and_discarded_with_the_definition() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let scope = app.tabs[i].current_parametric_scope();
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.local_parameters
|
||||
.set("Width", "7")
|
||||
.unwrap();
|
||||
assert!(app.save_block_edit(i));
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.local_parameters
|
||||
.set("Width", "9")
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.unwrap()
|
||||
.local_parameters
|
||||
.resolve("Width")
|
||||
.unwrap(),
|
||||
7.0
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert!(!app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.unwrap()
|
||||
.local_parameters
|
||||
.contains("Width"));
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.unwrap()
|
||||
.local_parameters
|
||||
.resolve("Width")
|
||||
.unwrap(),
|
||||
7.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quit_prompts_for_block_edits_before_unsaved_drawing_changes() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.request_quit_with_block_edits();
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
||||
let _ = app.update(Message::BlockEditChangesCancel);
|
||||
assert!(app.pending_close.is_none());
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
let _ = app.request_quit_with_block_edits();
|
||||
let _ = app.update(Message::BlockEditChangesSave);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||
assert_eq!(app.active_modal, Some(ModalKind::Unsaved));
|
||||
assert!(matches!(app.pending_close, Some(super::PendingClose::Quit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_replacement_is_refused_until_the_editor_is_closed() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false);
|
||||
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erasing_the_last_member_saves_an_empty_definition_and_undo_restores_it() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let line = members(&app)[0];
|
||||
app.push_undo_snapshot(i, "Erase block member");
|
||||
app.tabs[i].scene.erase_entities(&[line]);
|
||||
app.finish_pending_history(i);
|
||||
assert!(members(&app).is_empty());
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app), vec![line]);
|
||||
app.redo_steps(1);
|
||||
assert!(members(&app).is_empty());
|
||||
let _ = app.dispatch_command("BEDIT_SAVE");
|
||||
assert!(members(&app).is_empty());
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app), vec![line]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_all_resumes_after_discarding_a_dirty_block() {
|
||||
let (mut app, reference) = fixture();
|
||||
let first = app.tabs[app.active_tab].id;
|
||||
app.tabs.push(super::document::DocumentTab::new_drawing(2));
|
||||
let second = app.tabs.last().unwrap().id;
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let _ = app.update(Message::DocTabCloseAll);
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
||||
let _ = app.update(Message::BlockEditChangesDiscard);
|
||||
assert!(!app
|
||||
.tabs
|
||||
.iter()
|
||||
.any(|tab| tab.id == first || tab.id == second));
|
||||
assert!(app.pending_tab_closes.is_empty());
|
||||
}
|
||||
@@ -1284,12 +1284,9 @@ mod bedit_characterization_tests {
|
||||
assert!(app.tabs[tab].block_edits.is_empty());
|
||||
}
|
||||
|
||||
/// Documenting: the REFEDIT guard lives only in the `BEDIT` picker arm.
|
||||
/// `BEDIT_BEGIN:<handle>` (used by the palette "Edit" action) has no
|
||||
/// REFEDIT check and opens a session while REFEDIT is active. Expected to
|
||||
/// flip in Phase 1 once the guard moves into the begin path.
|
||||
/// Every entry point must refuse while REFEDIT is active.
|
||||
#[test]
|
||||
fn documenting_bedit_begin_bypasses_refedit_guard() {
|
||||
fn locked_bedit_begin_refuses_active_refedit() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
@@ -1297,8 +1294,8 @@ mod bedit_characterization_tests {
|
||||
assert!(app.tabs[tab].refedit_session.is_some());
|
||||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value()));
|
||||
assert!(
|
||||
app.tabs[tab].active_block_edit.is_some(),
|
||||
"documenting: BEDIT_BEGIN opens a session despite an active REFEDIT"
|
||||
app.tabs[tab].active_block_edit.is_none(),
|
||||
"begin must refuse while REFEDIT is active"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1323,12 +1320,9 @@ mod bedit_characterization_tests {
|
||||
|
||||
// ---- documenting: today's behavior, expected to flip in Phase 1 ----
|
||||
|
||||
/// Documenting: modify an existing member, discard, then undo once. The
|
||||
/// undo target is the BEDIT_BEGIN entry. Records whatever the stack does
|
||||
/// with the stale before-image (ISS-02). The assertion pins the current
|
||||
/// result; Phase 1 must justify any change.
|
||||
/// Discard removes local before-images before drawing undo resumes.
|
||||
#[test]
|
||||
fn documenting_modify_discard_then_undo_probes_stale_before_image() {
|
||||
fn locked_modify_discard_then_undo_does_not_restore_local_edits() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, members) = setup_block_and_insert(&mut app);
|
||||
let line = members[0];
|
||||
@@ -1367,10 +1361,9 @@ mod bedit_characterization_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: undo during a session crosses the entry boundary. A
|
||||
/// single undo after an in-session edit may revert the BEDIT_BEGIN step.
|
||||
/// Local undo restores both the entity store and block membership.
|
||||
#[test]
|
||||
fn documenting_undo_inside_session_crosses_entry_boundary() {
|
||||
fn locked_session_undo_restores_membership_without_crossing_entry() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
begin_bedit(&mut app, insert);
|
||||
@@ -1381,38 +1374,33 @@ mod bedit_characterization_tests {
|
||||
assert_eq!(block_members(&app, "BLK").len(), 2);
|
||||
|
||||
app.undo_steps(1);
|
||||
// Documenting: undo removes the circle from the entity store but leaves
|
||||
// its handle in the block record, so the definition holds a dangling
|
||||
// member. Expected to flip in Phase 1 (members should match the store).
|
||||
let members = block_members(&app, "BLK");
|
||||
assert_eq!(
|
||||
members.len(),
|
||||
2,
|
||||
"documenting: block record keeps the undone handle"
|
||||
1,
|
||||
"block membership must match the undone entity store"
|
||||
);
|
||||
let dangling = members
|
||||
.iter()
|
||||
.filter(|h| app.tabs[tab].scene.document.get_entity(**h).is_none())
|
||||
.count();
|
||||
assert_eq!(dangling, 1, "documenting: one member handle has no entity");
|
||||
assert_eq!(dangling, 0, "every member must have an entity");
|
||||
assert!(
|
||||
app.tabs[tab].active_block_edit.is_some(),
|
||||
"documenting: undo does not close the session"
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: undoing the BEDIT_BEGIN step itself, while the session is
|
||||
/// still open, leaves the editor open. Records whether the undo reaches
|
||||
/// across the entry boundary.
|
||||
/// Opening starts an empty local history; undo cannot cross entry.
|
||||
#[test]
|
||||
fn documenting_undo_across_entry_leaves_session_open() {
|
||||
fn locked_session_entry_has_no_undo_step() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
begin_bedit(&mut app, insert);
|
||||
app.finish_pending_history(tab);
|
||||
let depth_after_begin = undo_depth(&app);
|
||||
assert!(depth_after_begin >= 1, "BEDIT_BEGIN recorded an undo step");
|
||||
assert_eq!(depth_after_begin, 0, "opening must not record an undo step");
|
||||
|
||||
app.undo_steps(1);
|
||||
assert!(
|
||||
@@ -1421,10 +1409,9 @@ mod bedit_characterization_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: BEDIT on a block while another block is open nests a
|
||||
/// second session. Records the resulting stack.
|
||||
/// Switching a clean block replaces the single session.
|
||||
#[test]
|
||||
fn documenting_nested_bedit_opens_second_session() {
|
||||
fn locked_clean_switch_replaces_the_session() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
@@ -1448,15 +1435,14 @@ mod bedit_characterization_tests {
|
||||
begin_bedit(&mut app, other_insert);
|
||||
assert_eq!(
|
||||
app.tabs[tab].block_edits.len(),
|
||||
2,
|
||||
"documenting: a second BEDIT pushes a nested session"
|
||||
1,
|
||||
"a drawing has only one block-editor session"
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: BEDIT_BEGIN pushes an undo entry, and BEDIT_SAVE and
|
||||
/// BEDIT_DISCARD push none. Phase 1 may change this.
|
||||
/// No-change Save and Discard leave the drawing history untouched.
|
||||
#[test]
|
||||
fn documenting_save_and_discard_push_no_undo_entry() {
|
||||
fn locked_clean_save_and_discard_leave_history_unchanged() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
begin_bedit(&mut app, insert);
|
||||
|
||||
+62
-175
@@ -511,30 +511,40 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
cmd if cmd.starts_with("BEDIT_BEGIN:") => {
|
||||
cmd if cmd.starts_with("BEDIT_BEGIN:") || cmd.starts_with("BEDIT_OPEN:") => {
|
||||
if self.tabs[i].refedit_session.is_some() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
use crate::modules::draw::modify::block_edit::BlockEditSession;
|
||||
use codec::Handle;
|
||||
|
||||
let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
||||
let insert_handle = Handle::new(handle_u64);
|
||||
if self.reject_locked_edit(i, insert_handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
let block_name = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||
Some(codec::EntityType::Insert(ins)) => ins.block_name.clone(),
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT: selected object is not a block reference.").as_ref(),
|
||||
);
|
||||
let block_name = if let Some(name) = cmd.strip_prefix("BEDIT_OPEN:") {
|
||||
name.to_string()
|
||||
} else {
|
||||
let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
||||
let insert_handle = Handle::new(handle_u64);
|
||||
if self.reject_locked_edit(i, insert_handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||
Some(codec::EntityType::Insert(ins)) => ins.block_name.clone(),
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT: selected object is not a block reference.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the block record; reject external references (xrefs).
|
||||
let (br_handle, is_xref) =
|
||||
match self.tabs[i].scene.document.block_records.get(&block_name) {
|
||||
Some(br) => (br.handle, br.flags.is_xref),
|
||||
Some(br) => (br.handle, br.flags.is_xref || br.flags.is_xref_overlay),
|
||||
None => {
|
||||
self.command_line.push_error(
|
||||
crate::tf!("BEDIT: block \"{}\" not found.", block_name).as_ref(),
|
||||
@@ -548,52 +558,16 @@ impl OpenCADStudio {
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
if self.tabs[i]
|
||||
.block_edits
|
||||
.iter()
|
||||
.any(|session| session.br_handle == br_handle)
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::done(Message::BlockEditSwitch(block_name.clone())));
|
||||
if let Some(session) = self.tabs[i].active_block_edit_session() {
|
||||
if session.br_handle == br_handle {
|
||||
return Some(Task::none());
|
||||
}
|
||||
return Some(self.request_block_edit_close(
|
||||
i,
|
||||
crate::app::block_editor::BlockEditContinuation::Switch(block_name),
|
||||
));
|
||||
}
|
||||
|
||||
// Snapshot the complete block definition so Discard can restore
|
||||
// structural markers, ATTDEFs and geometry with exact handles.
|
||||
let snapshot: Vec<_> = {
|
||||
let br = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&block_name)
|
||||
.unwrap();
|
||||
br.entity_handles
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.collect()
|
||||
};
|
||||
let dependent_snapshot: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.block_definition_dependent_handles(br_handle)
|
||||
.into_iter()
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity_arc(handle))
|
||||
.collect();
|
||||
let reference_attributes: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|entity| match entity {
|
||||
codec::EntityType::Insert(reference)
|
||||
if reference.block_name.eq_ignore_ascii_case(&block_name)
|
||||
&& !reference.attributes.is_empty() =>
|
||||
{
|
||||
Some((reference.common.handle, reference.attributes.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.push_undo_snapshot(i, "BEDIT");
|
||||
|
||||
// Capture the camera before the editor reframes it, so leaving
|
||||
// the block editor returns the view exactly where it was (#425).
|
||||
let current_camera = self.tabs[i].scene.camera.borrow().clone();
|
||||
@@ -627,9 +601,6 @@ impl OpenCADStudio {
|
||||
br_handle,
|
||||
return_layout,
|
||||
return_block,
|
||||
snapshot,
|
||||
dependent_snapshot,
|
||||
reference_attributes,
|
||||
return_camera,
|
||||
editor_camera: current_camera,
|
||||
editor_ucs: None,
|
||||
@@ -664,125 +635,37 @@ impl OpenCADStudio {
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
self.refresh_properties();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_info(
|
||||
crate::tf!(
|
||||
"BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
|
||||
block_name
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.begin_block_edit_history(i);
|
||||
self.command_line
|
||||
.push_info(crate::tf!("Block Editor: editing {}", block_name).as_ref());
|
||||
}
|
||||
|
||||
"BSAVE" => {
|
||||
self.save_block_edit(i);
|
||||
}
|
||||
"BCLOSE" => {
|
||||
if self.tabs[i].active_block_edit.is_none() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No block editor is open.").as_ref());
|
||||
} else {
|
||||
return Some(self.request_block_edit_close(
|
||||
i,
|
||||
crate::app::block_editor::BlockEditContinuation::Close,
|
||||
));
|
||||
}
|
||||
}
|
||||
"BEDIT_SAVE" => {
|
||||
let session = match self.tabs[i].active_block_edit.take() {
|
||||
Some(index) if index < self.tabs[i].block_edits.len() => {
|
||||
self.tabs[i].block_edits.remove(index)
|
||||
}
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("BEDIT_SAVE: no block editor is open.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
Some(_) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT_SAVE: invalid block editor state.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
// Edits are live on the block record — just leave the block space.
|
||||
self.restore_after_block_edit_close(
|
||||
i,
|
||||
session.return_block.as_deref(),
|
||||
&session.return_layout,
|
||||
&session.return_camera,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"BEDIT: Block \"{}\" saved. All references updated.",
|
||||
session.block_name
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
if self.save_block_edit(i) {
|
||||
self.close_block_edit(i, false);
|
||||
}
|
||||
}
|
||||
|
||||
"BEDIT_DISCARD" => {
|
||||
let session = match self.tabs[i].active_block_edit.take() {
|
||||
Some(index) if index < self.tabs[i].block_edits.len() => {
|
||||
self.tabs[i].block_edits.remove(index)
|
||||
}
|
||||
None => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT_DISCARD: no block editor is open.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
Some(_) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("BEDIT_DISCARD: invalid block editor state.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
// Restore the block definition to its on-entry snapshot: remove the
|
||||
// block's current entities, then re-add the snapshot ones (mirrors
|
||||
// the REFCLOSE_SAVE write-back).
|
||||
let old_handles: Vec<_> = match self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&session.block_name)
|
||||
{
|
||||
Some(br) => br.entity_handles.clone(),
|
||||
None => vec![],
|
||||
};
|
||||
for h in &old_handles {
|
||||
self.tabs[i].scene.document.remove_entity(*h);
|
||||
if self.tabs[i].active_block_edit.is_none() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No block editor is open.").as_ref());
|
||||
} else {
|
||||
self.close_block_edit(i, true);
|
||||
}
|
||||
if let Some(br) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get_mut(&session.block_name)
|
||||
{
|
||||
br.entity_handles.clear();
|
||||
}
|
||||
let block_name = session.block_name.clone();
|
||||
let return_block = session.return_block.clone();
|
||||
let return_layout = session.return_layout.clone();
|
||||
let return_camera = session.return_camera.clone();
|
||||
for mut entity in session.snapshot {
|
||||
// Preserve the original handle so ATTDEF references and
|
||||
// other handle-based relationships survive Discard.
|
||||
entity.common_mut().owner_handle = session.br_handle;
|
||||
let _ = self.tabs[i].scene.document.add_entity(entity);
|
||||
}
|
||||
for (handle, attributes) in session.reference_attributes {
|
||||
if let Some(codec::EntityType::Insert(reference)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
reference.attributes = attributes;
|
||||
}
|
||||
}
|
||||
for entity in session.dependent_snapshot {
|
||||
let handle = entity.common().handle;
|
||||
let _ = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.replace_entity_arc(handle, entity);
|
||||
}
|
||||
self.restore_after_block_edit_close(
|
||||
i,
|
||||
return_block.as_deref(),
|
||||
&return_layout,
|
||||
&return_camera,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("BEDIT: Block \"{}\" edit discarded.", block_name).as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
||||
@@ -1462,7 +1345,7 @@ impl OpenCADStudio {
|
||||
Some(self.finish_dispatch(cmd))
|
||||
}
|
||||
|
||||
fn restore_after_block_edit_close(
|
||||
pub(in crate::app) fn restore_after_block_edit_close(
|
||||
&mut self,
|
||||
i: usize,
|
||||
return_block: Option<&str>,
|
||||
@@ -1510,6 +1393,10 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
let layers = self.tabs[i].scene.document.layers.clone();
|
||||
let viewports = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i].layers.sync_with_viewports(&layers, viewports);
|
||||
self.sync_ribbon_layers();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.refresh_properties();
|
||||
self.adopt_view_display(i);
|
||||
|
||||
@@ -205,6 +205,8 @@ pub(super) struct DocumentTab {
|
||||
/// Index of the field that TAB has focused (the one keystrokes edit).
|
||||
pub(super) dyn_active: usize,
|
||||
pub(super) history: HistoryState,
|
||||
/// Drawing history is suspended while `history` belongs to BEDIT.
|
||||
pub(super) block_edit_history: Option<super::block_editor::BlockEditHistory>,
|
||||
pub(super) active_layer: String,
|
||||
/// Block references whose properties panel shows per-axis Scale X/Y/Z even
|
||||
/// though the three factors are currently equal — the user unchecked the
|
||||
@@ -653,6 +655,7 @@ impl DocumentTab {
|
||||
dyn_ref_screen: None,
|
||||
dyn_active: 0,
|
||||
history: HistoryState::default(),
|
||||
block_edit_history: None,
|
||||
active_layer: "0".to_string(),
|
||||
props_asym_scale: std::collections::HashSet::new(),
|
||||
graph: Default::default(),
|
||||
|
||||
+34
-3
@@ -152,7 +152,7 @@ impl OpenCADStudio {
|
||||
defer_history_drop(discarded);
|
||||
}
|
||||
|
||||
fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||
pub(super) fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||
self.tabs[i].history.undo_stack.push(snapshot);
|
||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||
self.clear_redo_history(i);
|
||||
@@ -283,6 +283,7 @@ impl OpenCADStudio {
|
||||
return;
|
||||
};
|
||||
self.tabs[i].scene.document.end_entity_change_recording();
|
||||
self.reconcile_block_edit_membership(i);
|
||||
let structure_after = self.tabs[i].scene.document.snapshot_structure();
|
||||
let before_images = pending.recorder.take_before_images();
|
||||
let added_handles: Vec<Handle> = before_images
|
||||
@@ -292,11 +293,25 @@ impl OpenCADStudio {
|
||||
.then_some(*handle)
|
||||
})
|
||||
.collect();
|
||||
let block_members = self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.and_then(|session| {
|
||||
pending
|
||||
.structure_before
|
||||
.block_records
|
||||
.get(&session.block_name)
|
||||
.map(|record| (session.block_name.clone(), record.entity_handles.clone()))
|
||||
});
|
||||
codec::CadDocument::align_added_entity_structure(
|
||||
&mut pending.structure_before,
|
||||
&structure_after,
|
||||
&added_handles,
|
||||
);
|
||||
if let Some((name, members)) = block_members {
|
||||
if let Some(record) = pending.structure_before.block_records.get_mut(&name) {
|
||||
record.entity_handles = members;
|
||||
}
|
||||
}
|
||||
let structure_changed = pending.structure_before != structure_after;
|
||||
let entities: Vec<_> = before_images
|
||||
.into_iter()
|
||||
@@ -402,8 +417,8 @@ impl OpenCADStudio {
|
||||
) -> Option<PendingDelta> {
|
||||
self.finish_pending_history(i);
|
||||
let label = label.into();
|
||||
let structure_before =
|
||||
(!delta_safe).then(|| self.tabs[i].scene.document.snapshot_structure());
|
||||
let structure_before = (!delta_safe || self.tabs[i].active_block_edit.is_some())
|
||||
.then(|| self.tabs[i].scene.document.snapshot_structure());
|
||||
self.tabs[i].scene.begin_undo_recording();
|
||||
Some(PendingDelta {
|
||||
label,
|
||||
@@ -740,6 +755,7 @@ impl OpenCADStudio {
|
||||
let Some(rec) = self.tabs[i].scene.take_undo_recording() else {
|
||||
return;
|
||||
};
|
||||
self.reconcile_block_edit_membership(i);
|
||||
// The predicate should have kept the command entity-only; if a primitive
|
||||
// still poisoned the recording we've already mutated and the pre-command
|
||||
// full state is gone, so the entity delta is the best we can do (its
|
||||
@@ -813,6 +829,16 @@ impl OpenCADStudio {
|
||||
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
|
||||
if let Some(before_structure) = structure.as_mut() {
|
||||
if let StructureSnapshot::Full(before_structure) = before_structure {
|
||||
let block_members = self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.and_then(|session| {
|
||||
before_structure
|
||||
.block_records
|
||||
.get(&session.block_name)
|
||||
.map(|record| {
|
||||
(session.block_name.clone(), record.entity_handles.clone())
|
||||
})
|
||||
});
|
||||
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||
let added_handles: Vec<Handle> = entities
|
||||
.iter()
|
||||
@@ -825,6 +851,11 @@ impl OpenCADStudio {
|
||||
&after_structure,
|
||||
&added_handles,
|
||||
);
|
||||
if let Some((name, members)) = block_members {
|
||||
if let Some(record) = before_structure.block_records.get_mut(&name) {
|
||||
record.entity_handles = members;
|
||||
}
|
||||
}
|
||||
if *before_structure == after_structure {
|
||||
structure = None;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ pub(crate) mod config;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use automation::{export_headless, serve};
|
||||
mod annotation_data;
|
||||
mod block_editor;
|
||||
#[cfg(test)]
|
||||
mod block_editor_tests;
|
||||
mod command_driver;
|
||||
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace};
|
||||
pub(crate) mod commands;
|
||||
@@ -1520,6 +1523,7 @@ pub(super) struct OpenCADStudio {
|
||||
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
||||
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
||||
pending_close: Option<PendingClose>,
|
||||
pending_block_close: Option<(u64, block_editor::BlockEditContinuation)>,
|
||||
/// Stable document ids waiting to be closed by "Close All" or
|
||||
/// "Close All Other Drawings". Dirty drawings pause this queue at the
|
||||
/// existing unsaved-changes dialog and resume after Save or Discard.
|
||||
@@ -2158,6 +2162,7 @@ pub enum ModalKind {
|
||||
MLeaderStyle,
|
||||
DimStyle,
|
||||
Unsaved,
|
||||
BlockEditChanges,
|
||||
SaveDialog,
|
||||
Recovery,
|
||||
/// Fonts referenced by the drawing are missing on this machine.
|
||||
@@ -2784,6 +2789,9 @@ pub enum Message {
|
||||
UnsavedDialogDiscard,
|
||||
/// User clicked "Cancel" in the unsaved-changes dialog.
|
||||
UnsavedDialogCancel,
|
||||
BlockEditChangesSave,
|
||||
BlockEditChangesDiscard,
|
||||
BlockEditChangesCancel,
|
||||
// ── AEC / unsupported-object drop warning (lossy Save-As) ──────────────
|
||||
/// Save as the source DWG version so the unsupported objects survive.
|
||||
AecDropSameVersion,
|
||||
@@ -4844,6 +4852,7 @@ impl OpenCADStudio {
|
||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||
pending_close: None,
|
||||
pending_tab_closes: std::collections::VecDeque::new(),
|
||||
pending_block_close: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
active_save_jobs: std::collections::HashMap::new(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -887,11 +887,9 @@ fn locked_palette_remove_refuses_a_block_with_an_open_bedit_session() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Nested BEDIT sessions all count: the outer and the inner record are both
|
||||
/// open, so both are refused — the whole session stack is handed to the
|
||||
/// service, not just the active tab.
|
||||
/// Switching replaces the open session; the active definition stays protected.
|
||||
#[test]
|
||||
fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() {
|
||||
fn locked_palette_remove_refuses_the_open_block_after_switching() {
|
||||
let mut app = app();
|
||||
let inner = "BO_NEST_INNER";
|
||||
define_block(&mut app, inner);
|
||||
@@ -915,10 +913,10 @@ fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() {
|
||||
begin_bedit(&mut app, inner_reference);
|
||||
assert_eq!(
|
||||
app.tabs[tab(&app)].block_edits.len(),
|
||||
2,
|
||||
"the nested session did not open"
|
||||
1,
|
||||
"switching must keep one session"
|
||||
);
|
||||
for name in [outer, inner] {
|
||||
for name in [inner] {
|
||||
palette_remove(&mut app, name);
|
||||
assert!(record_exists(&app, name), "{name} went while open");
|
||||
let expected = command_line_error(&format!(
|
||||
|
||||
@@ -151,7 +151,7 @@ impl OpenCADStudio {
|
||||
/// Close queued drawings until a dirty tab requires confirmation. Queue
|
||||
/// entries use stable document ids because removing an earlier tab changes
|
||||
/// every later vector index.
|
||||
pub(super) fn continue_tab_close_queue(&mut self) -> Task<Message> {
|
||||
pub(in crate::app) fn continue_tab_close_queue(&mut self) -> Task<Message> {
|
||||
let mut tasks = Vec::new();
|
||||
while let Some(tab_id) = self.pending_tab_closes.pop_front() {
|
||||
let Some(idx) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||
@@ -160,6 +160,13 @@ impl OpenCADStudio {
|
||||
if self.tabs[idx].is_start {
|
||||
continue;
|
||||
}
|
||||
if self.tabs[idx].active_block_edit.is_some() {
|
||||
tasks.push(self.on_tab_close(idx));
|
||||
if self.pending_block_close.is_some() || self.pending_close.is_some() {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if self.tabs[idx].dirty {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(tab_id));
|
||||
tasks.push(self.open_unsaved_dialog_window());
|
||||
@@ -170,11 +177,21 @@ impl OpenCADStudio {
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
pub(in crate::app) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
// Start tab is fixed — close requests on it are no-ops.
|
||||
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
|
||||
return Task::none();
|
||||
}
|
||||
if self
|
||||
.tabs
|
||||
.get(idx)
|
||||
.is_some_and(|tab| tab.active_block_edit.is_some())
|
||||
{
|
||||
return self.request_block_edit_close(
|
||||
idx,
|
||||
crate::app::block_editor::BlockEditContinuation::Tab(self.tabs[idx].id),
|
||||
);
|
||||
}
|
||||
// Closing a tab shifts indices / the active tab. The attribute
|
||||
// editor holds a document-local handle into one tab, so drop it
|
||||
// now rather than risk it applying to a different tab's document.
|
||||
@@ -4344,7 +4361,7 @@ impl OpenCADStudio {
|
||||
/// Close the attribute editor without applying, if it is open. Used when
|
||||
/// the tab it belongs to closes or the active tab switches — its handle is
|
||||
/// document-local and must not outlive its tab.
|
||||
pub(super) fn cancel_attr_editor(&mut self) {
|
||||
pub(in crate::app) fn cancel_attr_editor(&mut self) {
|
||||
if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) {
|
||||
self.active_modal = None;
|
||||
self.reset_modal_geometry();
|
||||
|
||||
@@ -215,6 +215,7 @@ impl OpenCADStudio {
|
||||
match self.active_modal {
|
||||
// Dismissing these via ✕ is the cancel/decline path.
|
||||
Some(Unsaved) => self.pending_close = None,
|
||||
Some(BlockEditChanges) => self.cancel_block_edit_close(),
|
||||
Some(AssocPrompt) => self.mark_assoc_prompted(),
|
||||
// Cancel: leave the layer (and its objects) untouched.
|
||||
Some(LayerDeleteWarning) => self.layer_delete_pending = None,
|
||||
@@ -3895,11 +3896,7 @@ impl OpenCADStudio {
|
||||
|
||||
Message::WindowCloseRequested(id) => {
|
||||
if self.main_window == Some(id) {
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
self.pending_close = Some(super::PendingClose::Quit);
|
||||
return self.open_unsaved_dialog_window();
|
||||
}
|
||||
return self.exit_app();
|
||||
return self.request_quit_with_block_edits();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -10434,6 +10431,12 @@ impl OpenCADStudio {
|
||||
Message::UnsavedDialogDiscard => self.on_unsaved_dialog_discard(),
|
||||
|
||||
Message::UnsavedDialogSave => self.on_unsaved_dialog_save(),
|
||||
Message::BlockEditChangesSave => self.resolve_block_edit_close(true),
|
||||
Message::BlockEditChangesDiscard => self.resolve_block_edit_close(false),
|
||||
Message::BlockEditChangesCancel => {
|
||||
self.cancel_block_edit_close();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::AecDropSameVersion => self.on_aec_drop_same_version(),
|
||||
Message::AecDropProceed => self.on_aec_drop_proceed(),
|
||||
|
||||
@@ -6446,6 +6446,12 @@ properties={:.1}ms picked={}",
|
||||
}
|
||||
|
||||
pub(crate) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||
if self.tabs[self.active_tab].active_block_edit.is_some() {
|
||||
return self.request_block_edit_close(
|
||||
self.active_tab,
|
||||
crate::app::block_editor::BlockEditContinuation::Layout(name),
|
||||
);
|
||||
}
|
||||
self.on_layout_switch_inner(name, false)
|
||||
}
|
||||
|
||||
@@ -6508,6 +6514,12 @@ properties={:.1}ms picked={}",
|
||||
/// define a view and then returns to its paper layout. This is the only
|
||||
/// layout transition allowed to preserve an active command.
|
||||
pub(crate) fn on_layout_switch_preserving_command(&mut self, name: String) -> Task<Message> {
|
||||
if self.tabs[self.active_tab].active_block_edit.is_some() {
|
||||
return self.request_block_edit_close(
|
||||
self.active_tab,
|
||||
crate::app::block_editor::BlockEditContinuation::Layout(name),
|
||||
);
|
||||
}
|
||||
self.on_layout_switch_inner(name, true)
|
||||
}
|
||||
|
||||
|
||||
+51
-8
@@ -76,6 +76,7 @@ impl OpenCADStudio {
|
||||
Some(K::ExternalChange) => crate::tr!("modal", "drawing-changed"),
|
||||
Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"),
|
||||
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
|
||||
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
|
||||
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
|
||||
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
|
||||
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
|
||||
@@ -1742,7 +1743,19 @@ impl OpenCADStudio {
|
||||
.unwrap_or_default(),
|
||||
None => String::new(),
|
||||
};
|
||||
automatic_flow(ex, |flow| unsaved_changes_dialog_window(&tab_name, flow))
|
||||
automatic_flow(ex, |flow| {
|
||||
unsaved_changes_dialog_window(&tab_name, flow, false)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockEditChanges => {
|
||||
let name = self
|
||||
.pending_block_close
|
||||
.as_ref()
|
||||
.and_then(|(id, _)| self.tabs.iter().find(|tab| tab.id == *id))
|
||||
.and_then(|tab| tab.active_block_edit_session())
|
||||
.map(|session| session.block_name.clone())
|
||||
.unwrap_or_default();
|
||||
automatic_flow(ex, |flow| unsaved_changes_dialog_window(&name, flow, true))
|
||||
}
|
||||
super::super::ModalKind::PointStyle => sized_flow(ex, 360, 470, |flow| {
|
||||
crate::ui::style::point_style::view_window(
|
||||
@@ -2060,23 +2073,53 @@ fn save_as_dialog_window<'a>(
|
||||
fn unsaved_changes_dialog_window(
|
||||
name: &str,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
block_edit: bool,
|
||||
) -> Element<'static, Message> {
|
||||
container(
|
||||
column![
|
||||
text(name.to_owned()).size(14),
|
||||
text(if block_edit {
|
||||
crate::tf!("Save changes to {}?", name).into_owned()
|
||||
} else {
|
||||
name.to_owned()
|
||||
})
|
||||
.size(14),
|
||||
iced::widget::Space::new().height(4),
|
||||
text(crate::tr!("modal", "unsaved-save-prompt"))
|
||||
.size(12)
|
||||
.style(dialog_muted_text_style),
|
||||
text(if block_edit {
|
||||
String::new()
|
||||
} else {
|
||||
crate::tr!("modal", "unsaved-save-prompt")
|
||||
})
|
||||
.size(12)
|
||||
.style(dialog_muted_text_style),
|
||||
iced::widget::Space::new().height(20),
|
||||
row![
|
||||
dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary),
|
||||
dialog_button(
|
||||
t!("Save"),
|
||||
if block_edit {
|
||||
Message::BlockEditChangesSave
|
||||
} else {
|
||||
Message::UnsavedDialogSave
|
||||
},
|
||||
button::primary
|
||||
),
|
||||
iced::widget::Space::new().width(8),
|
||||
dialog_button(t!("Discard"), Message::UnsavedDialogDiscard, button::danger),
|
||||
dialog_button(
|
||||
t!("Discard"),
|
||||
if block_edit {
|
||||
Message::BlockEditChangesDiscard
|
||||
} else {
|
||||
Message::UnsavedDialogDiscard
|
||||
},
|
||||
button::danger
|
||||
),
|
||||
iced::widget::Space::new().width(8),
|
||||
dialog_button(
|
||||
t!("Cancel"),
|
||||
Message::UnsavedDialogCancel,
|
||||
if block_edit {
|
||||
Message::BlockEditChangesCancel
|
||||
} else {
|
||||
Message::UnsavedDialogCancel
|
||||
},
|
||||
button::secondary
|
||||
),
|
||||
],
|
||||
|
||||
@@ -37,13 +37,13 @@ impl CadModule for BlockEditorModule {
|
||||
include_bytes!("../../../assets/icons/edit_block.svg"),
|
||||
),
|
||||
command(
|
||||
"BEDIT_SAVE",
|
||||
"BSAVE",
|
||||
"Save Block",
|
||||
include_bytes!("../../../assets/icons/mt_ok.svg"),
|
||||
),
|
||||
command(
|
||||
"BEDIT_DISCARD",
|
||||
"Discard Block Edit",
|
||||
"BCLOSE",
|
||||
"Close Block Editor",
|
||||
include_bytes!("../../../assets/icons/mt_cancel.svg"),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// block record in place, so there is no copy-back and no transform — the space
|
||||
// render filter (`current_layout_block_handle`) already draws only the block.
|
||||
|
||||
use codec::{EntityType, Handle};
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
@@ -26,16 +26,16 @@ use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
pub fn block_edit_tools() -> Vec<ToolDef> {
|
||||
vec![
|
||||
ToolDef {
|
||||
id: "BEDIT_SAVE",
|
||||
id: "BSAVE",
|
||||
label: "Save Block",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_ok.svg")),
|
||||
event: ModuleEvent::Command("BEDIT_SAVE".to_string()),
|
||||
event: ModuleEvent::Command("BSAVE".to_string()),
|
||||
},
|
||||
ToolDef {
|
||||
id: "BEDIT_DISCARD",
|
||||
label: "Discard Block Edit",
|
||||
id: "BCLOSE",
|
||||
label: "Close Block Editor",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_cancel.svg")),
|
||||
event: ModuleEvent::Command("BEDIT_DISCARD".to_string()),
|
||||
event: ModuleEvent::Command("BCLOSE".to_string()),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -55,17 +55,6 @@ pub struct BlockEditSession {
|
||||
/// Parent block tab to return to for a nested BEDIT session. `None` returns
|
||||
/// to `return_layout`.
|
||||
pub return_block: Option<String>,
|
||||
/// The complete block definition captured on entry, so Discard restores
|
||||
/// structural markers and content with their original handles.
|
||||
pub snapshot: Vec<EntityType>,
|
||||
/// Baked anonymous dimension-block entities transformed alongside
|
||||
/// dimensions in this definition. They remain in place, so Discard can
|
||||
/// restore their shared before-images directly.
|
||||
pub dependent_snapshot: Vec<std::sync::Arc<EntityType>>,
|
||||
/// Inline ATTRIB values/positions on every existing INSERT of this block.
|
||||
/// Rebasing the origin moves them with the definition; Discard restores
|
||||
/// their exact on-entry state.
|
||||
pub reference_attributes: Vec<(Handle, Vec<codec::entities::AttributeEntity>)>,
|
||||
/// Camera state of the space that was active when BEDIT began, restored
|
||||
/// on Save/Discard so the view returns exactly where it was (#425).
|
||||
pub return_camera: crate::scene::view::camera::Camera,
|
||||
@@ -121,3 +110,6 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BEDIT_DISCARD"]
|
||||
});
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BSAVE", "BCLOSE"]
|
||||
});
|
||||
Reference in new issue
Block a user