forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/block-definition-command-line
# Conflicts: # src/app/commands/inquiry.rs # src/modules/draw/modify/block_edit.rs
This commit is contained in:
commit
99e30f1548
33 files changed
+1660
-199
No files matched your search
@@ -160,7 +160,7 @@ impl OpenCADStudio {
|
||||
if self.tabs[idx].is_start {
|
||||
continue;
|
||||
}
|
||||
if self.tabs[idx].active_block_edit.is_some() {
|
||||
if self.tabs[idx].active_block_edit.is_some() || self.tabs[idx].block_test.is_some() {
|
||||
tasks.push(self.on_tab_close(idx));
|
||||
if self.pending_block_close.is_some() || self.pending_close.is_some() {
|
||||
break;
|
||||
@@ -178,6 +178,27 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(in crate::app) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
let test_return_id = self
|
||||
.tabs
|
||||
.get(idx)
|
||||
.and_then(|tab| tab.block_test.as_ref())
|
||||
.map(|test| {
|
||||
if idx == self.active_tab {
|
||||
test.source_id
|
||||
} else {
|
||||
self.tabs[self.active_tab].id
|
||||
}
|
||||
});
|
||||
if test_return_id.is_some() {
|
||||
if idx == self.active_tab {
|
||||
self.cancel_active_grip_edit();
|
||||
self.grip_pending = None;
|
||||
self.visibility_popup = None;
|
||||
self.grip_popup = None;
|
||||
self.grip_hover = None;
|
||||
}
|
||||
self.tabs[idx].dirty = false;
|
||||
}
|
||||
// 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();
|
||||
@@ -249,6 +270,16 @@ impl OpenCADStudio {
|
||||
// The active tab is now either a brand-new blank or a
|
||||
// different existing tab; in both cases the ribbon needs
|
||||
// to track that doc's defaults / selection. #21.
|
||||
if let Some(index) =
|
||||
test_return_id.and_then(|id| self.tabs.iter().position(|tab| tab.id == id))
|
||||
{
|
||||
self.active_tab = index;
|
||||
self.adopt_header_sysvars(index);
|
||||
}
|
||||
self.ribbon
|
||||
.set_block_testing(self.tabs[self.active_tab].block_test.is_some());
|
||||
self.ribbon
|
||||
.set_block_editing(self.tabs[self.active_tab].active_block_edit.is_some());
|
||||
self.sync_ribbon_layers();
|
||||
self.sync_ribbon_styles();
|
||||
self.sync_ribbon_from_selection();
|
||||
|
||||
@@ -22,6 +22,9 @@ use iced::{mouse, Point, Task};
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
if self.refuse_block_test_save(tab_idx) {
|
||||
return Task::none();
|
||||
}
|
||||
// Every entry here is Save-As semantics (the SAVEAS command, the
|
||||
// recovery save-as, the native failure redirects): the user is
|
||||
// choosing a destination, so on the web the FS Access picker must
|
||||
|
||||
+21
-4
@@ -1779,6 +1779,11 @@ impl OpenCADStudio {
|
||||
version: codec::DxfVersion,
|
||||
set_current_path: bool,
|
||||
) -> Result<(), crate::io::SaveFailure> {
|
||||
if self.tabs[i].block_test.is_some() {
|
||||
return Err(crate::io::SaveFailure::other(
|
||||
"Test Block is temporary and cannot be saved",
|
||||
));
|
||||
}
|
||||
let previous_autosave = self.autosave_target(i);
|
||||
if self.tabs[i].recovery_save_as_required
|
||||
&& self.tabs[i]
|
||||
@@ -2539,7 +2544,7 @@ impl OpenCADStudio {
|
||||
let mut skipped = 0usize;
|
||||
let mut pending: Vec<(crate::app::WebSaveallEntry, codec::CadDocument)> = Vec::new();
|
||||
for t in 0..self.tabs.len() {
|
||||
if self.tabs[t].is_start {
|
||||
if self.tabs[t].is_start || self.tabs[t].block_test.is_some() {
|
||||
continue;
|
||||
}
|
||||
let Some(path) = self.tabs[t].current_path.clone() else {
|
||||
@@ -3199,6 +3204,9 @@ impl OpenCADStudio {
|
||||
set_current_path: bool,
|
||||
check_external_change: bool,
|
||||
) -> Task<Message> {
|
||||
if self.refuse_block_test_save(i) {
|
||||
return Task::none();
|
||||
}
|
||||
let tab_id = self.tabs[i].id;
|
||||
if self
|
||||
.pending_native_thumbnail_save
|
||||
@@ -3867,6 +3875,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
||||
if self.refuse_block_test_save(self.active_tab) {
|
||||
return Task::none();
|
||||
}
|
||||
if self.read_only {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Read-only session (--read-only): saving is disabled.").as_ref(),
|
||||
@@ -3923,6 +3934,9 @@ impl OpenCADStudio {
|
||||
/// Used by plain Save (QSAVE) on an as-yet-unsaved drawing and by the
|
||||
/// save-before-close flow — the version picker is reserved for Save As.
|
||||
pub(in crate::app) fn save_with_default_format(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
if self.refuse_block_test_save(tab_idx) {
|
||||
return Task::none();
|
||||
}
|
||||
self.active_tab = tab_idx;
|
||||
// Every caller of this entry (Save/QSAVE, the save-before-close
|
||||
// flow) is plain-save semantics: on the web, the tab's retained
|
||||
@@ -4315,7 +4329,10 @@ impl OpenCADStudio {
|
||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||
let mut tasks = Vec::new();
|
||||
for i in 0..self.tabs.len() {
|
||||
if !self.tabs[i].dirty || self.active_save_jobs.contains_key(&self.tabs[i].id) {
|
||||
if self.tabs[i].block_test.is_some()
|
||||
|| !self.tabs[i].dirty
|
||||
|| self.active_save_jobs.contains_key(&self.tabs[i].id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.prepare_snapshot_save(i);
|
||||
@@ -4350,7 +4367,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
let mut pending = Vec::new();
|
||||
for i in 0..self.tabs.len() {
|
||||
if !self.tabs[i].dirty {
|
||||
if self.tabs[i].block_test.is_some() || !self.tabs[i].dirty {
|
||||
continue;
|
||||
}
|
||||
// Same pre-serialize state sync the web Save flow performs
|
||||
@@ -4677,7 +4694,7 @@ impl OpenCADStudio {
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
let mut active = 0;
|
||||
for (index, tab) in self.tabs.iter().enumerate() {
|
||||
if tab.is_start {
|
||||
if tab.is_start || tab.block_test.is_some() {
|
||||
continue;
|
||||
}
|
||||
if index == self.active_tab {
|
||||
|
||||
+98
-3
@@ -50,7 +50,7 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool {
|
||||
|
||||
fn perf_message_label(msg: &Message) -> &'static str {
|
||||
match msg {
|
||||
Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down",
|
||||
Message::ViewportLeftPressAt(_) | Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down",
|
||||
Message::ViewportLeftRelease | Message::PaneRelease(_) => "pointer-up",
|
||||
Message::ViewportMove(_) | Message::PaneMove(_, _) => "pointer-move",
|
||||
Message::CommandFinalize => "command-finalize",
|
||||
@@ -230,6 +230,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
Some(BlockDefinition) => self.block_definition = None,
|
||||
Some(BlockEditPicker) => self.block_edit_picker = None,
|
||||
Some(BlockSaveAs) => self.block_save_as = None,
|
||||
Some(PdfAttach) => self.pdf_attach = None,
|
||||
Some(UnderlayLayers) => self.underlay_layers = None,
|
||||
@@ -346,7 +347,17 @@ impl OpenCADStudio {
|
||||
.tabs
|
||||
.get(self.active_tab)
|
||||
.is_some_and(|tab| tab.active_block_edit.is_some());
|
||||
self.ribbon.set_block_editing(block_editing);
|
||||
let testing = self
|
||||
.tabs
|
||||
.get(self.active_tab)
|
||||
.is_some_and(|tab| tab.block_test.is_some());
|
||||
if testing {
|
||||
self.ribbon.set_block_editing(false);
|
||||
self.ribbon.set_block_testing(true);
|
||||
} else {
|
||||
self.ribbon.set_block_testing(false);
|
||||
self.ribbon.set_block_editing(block_editing);
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
@@ -445,6 +456,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockEditPicker)
|
||||
&& (matches!(msg, Message::CommandFinalize)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN")))
|
||||
{
|
||||
return self.apply_bedit_dialog();
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockSaveAs)
|
||||
&& (matches!(msg, Message::CommandFinalize)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN")))
|
||||
@@ -595,7 +612,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
crate::sys::set_unsaved_changes_warning(self.tabs.iter().any(|tab| tab.dirty));
|
||||
crate::sys::set_unsaved_changes_warning(
|
||||
self.tabs
|
||||
.iter()
|
||||
.any(|tab| tab.dirty && tab.block_test.is_none()),
|
||||
);
|
||||
if self.tabs[i].active_cmd.is_none() {
|
||||
self.block_palette.placing = None;
|
||||
}
|
||||
@@ -4390,6 +4411,7 @@ impl OpenCADStudio {
|
||||
|
||||
// A multi-finger gesture owns the pointer while it runs.
|
||||
Message::ViewportMove(_)
|
||||
| Message::ViewportLeftPressAt(_)
|
||||
| Message::ViewportLeftPress
|
||||
| Message::ViewportLeftRelease
|
||||
| Message::PaneMove(..)
|
||||
@@ -4474,6 +4496,17 @@ impl OpenCADStudio {
|
||||
self.update(Message::ViewportScroll(d))
|
||||
}
|
||||
|
||||
Message::ViewportLeftPressAt(position) => {
|
||||
// Use the pointer location captured by the actual button
|
||||
// event. The last hover can lag behind fast cursor motion,
|
||||
// particularly while expensive OSNAP/OTRACK previews run.
|
||||
// Refresh the snap and world point before accepting the pick.
|
||||
let moved = self.on_viewport_move(position);
|
||||
self.graphic_attributes.open_menu = None;
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
Task::batch(vec![moved, sweep, self.on_viewport_left_press()])
|
||||
}
|
||||
|
||||
Message::ViewportLeftPress => {
|
||||
self.graphic_attributes.open_menu = None;
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
@@ -6255,6 +6288,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsOsnapApertureChanged(value) => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.osnap_aperture_input = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsToggleOsnap => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.osnap_on = !state.osnap_on;
|
||||
@@ -10445,6 +10484,62 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
|
||||
Message::BlockEditPickerName(name) => {
|
||||
if let Some(state) = self.block_edit_picker.as_mut() {
|
||||
state.name = name.clone();
|
||||
let trimmed = name.trim();
|
||||
state.error = if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
crate::scene::block_service::validate_block_name(trimmed)
|
||||
.err()
|
||||
.map(|e| e.to_string())
|
||||
};
|
||||
if let Some(tab_idx) =
|
||||
self.tabs.iter().position(|t| t.id == state.document_id)
|
||||
{
|
||||
if let Some(matched) = state
|
||||
.existing_names
|
||||
.iter()
|
||||
.find(|n| n.eq_ignore_ascii_case(trimmed))
|
||||
{
|
||||
state.preview =
|
||||
self.tabs[tab_idx].scene.block_preview_wires(matched);
|
||||
state.description = self.tabs[tab_idx]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(matched)
|
||||
.map(|b| b.description.clone())
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
state.preview.clear();
|
||||
state.description.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockEditPickerSelect(name) => {
|
||||
if let Some(state) = self.block_edit_picker.as_mut() {
|
||||
state.name = name.clone();
|
||||
state.error = None;
|
||||
if let Some(tab_idx) =
|
||||
self.tabs.iter().position(|t| t.id == state.document_id)
|
||||
{
|
||||
state.preview = self.tabs[tab_idx].scene.block_preview_wires(&name);
|
||||
state.description = self.tabs[tab_idx]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&name)
|
||||
.map(|b| b.description.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockEditPickerApply => self.apply_bedit_dialog(),
|
||||
Message::BlockEditChangesDiscard => self.resolve_block_edit_close(false),
|
||||
Message::BlockEditChangesCancel => {
|
||||
self.cancel_block_edit_close();
|
||||
|
||||
@@ -2663,7 +2663,15 @@ impl OpenCADStudio {
|
||||
.map(|snap| snap.world)
|
||||
.unwrap_or(cursor_world);
|
||||
axis_lock_apply(point, base, dir)
|
||||
} else if let Some(h) = otrack_hit {
|
||||
} else if let Some(h) = otrack_hit.filter(|_| {
|
||||
// A real object snap must win over tracking alignment.
|
||||
// Otherwise the endpoint glyph can appear at the
|
||||
// corner while the preview/accepted point follows the
|
||||
// tracking line instead.
|
||||
!self.tabs[i].snap_result.is_some_and(|s| {
|
||||
s.snap_type != crate::snap::SnapType::Grid
|
||||
})
|
||||
}) {
|
||||
h.aligned
|
||||
} else {
|
||||
// Snap runs in model space (viewport camera or the
|
||||
@@ -4498,7 +4506,11 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(h) = otrack {
|
||||
if let Some(h) = otrack.filter(|_| {
|
||||
// Match the hover path: never override a genuine OSNAP
|
||||
// endpoint/intersection with a tracking projection.
|
||||
!snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
|
||||
}) {
|
||||
pt = h.aligned;
|
||||
if clamp_world_xy {
|
||||
pt.z = 0.0;
|
||||
|
||||
Reference in new issue
Block a user