forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt across workspace (as on main)
Formats the branch the way main's 9cf9ff2 did, so merging main only
conflicts where the code itself differs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
532 files changed
+56995
-33416
No files matched your search
@@ -56,7 +56,10 @@ impl OpenCADStudio {
|
||||
async {
|
||||
let handle = rfd::AsyncFileDialog::new()
|
||||
.set_title(crate::t!("Select Drawing to Insert as Block").as_ref())
|
||||
.add_filter(crate::t!("DWG/DXF Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
|
||||
.add_filter(
|
||||
crate::t!("DWG/DXF Files").as_ref(),
|
||||
&["dwg", "dxf", "DWG", "DXF"],
|
||||
)
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
@@ -67,7 +70,9 @@ impl OpenCADStudio {
|
||||
|r| Message::BlockPalette(BlockPaletteMsg::FilePicked(r)),
|
||||
);
|
||||
}
|
||||
BlockPaletteMsg::FilePicked(Ok(path)) => return self.palette_insert(Item::File(path), None),
|
||||
BlockPaletteMsg::FilePicked(Ok(path)) => {
|
||||
return self.palette_insert(Item::File(path), None)
|
||||
}
|
||||
BlockPaletteMsg::FilePicked(Err(e)) | BlockPaletteMsg::LibraryPicked(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
self.command_line.push_error(&e);
|
||||
@@ -99,7 +104,12 @@ impl OpenCADStudio {
|
||||
BlockPaletteMsg::LibraryPicked(Ok(path)) => self.open_block_library(path),
|
||||
BlockPaletteMsg::LibrarySelect(path) => self.open_block_library(PathBuf::from(path)),
|
||||
BlockPaletteMsg::LibraryUp => {
|
||||
if let Some(parent) = self.block_palette.library_dir.as_ref().and_then(|d| d.parent()) {
|
||||
if let Some(parent) = self
|
||||
.block_palette
|
||||
.library_dir
|
||||
.as_ref()
|
||||
.and_then(|d| d.parent())
|
||||
{
|
||||
let parent = parent.to_path_buf();
|
||||
self.list_block_library(parent);
|
||||
}
|
||||
@@ -165,17 +175,30 @@ impl OpenCADStudio {
|
||||
return None;
|
||||
}
|
||||
if path.is_dir() {
|
||||
Some(LibraryEntry { path, folder: true, thumb: None })
|
||||
Some(LibraryEntry {
|
||||
path,
|
||||
folder: true,
|
||||
thumb: None,
|
||||
})
|
||||
} else if is_drawing(&path) {
|
||||
let thumb = crate::io::thumbnail::read_handle(&path);
|
||||
Some(LibraryEntry { path, folder: false, thumb })
|
||||
Some(LibraryEntry {
|
||||
path,
|
||||
folder: false,
|
||||
thumb,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by_key(|e| {
|
||||
(!e.folder, e.path.file_name().map(|n| n.to_string_lossy().to_lowercase()))
|
||||
(
|
||||
!e.folder,
|
||||
e.path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_lowercase()),
|
||||
)
|
||||
});
|
||||
self.block_palette.library_dir = Some(dir);
|
||||
self.block_palette.library_entries = entries;
|
||||
@@ -270,7 +293,11 @@ impl OpenCADStudio {
|
||||
|
||||
/// Insert `item` (a click, or a drop at `drop_at`), bringing its block
|
||||
/// into the drawing first when it lives elsewhere.
|
||||
pub(crate) fn palette_insert(&mut self, item: Item, drop_at: Option<glam::DVec3>) -> Task<Message> {
|
||||
pub(crate) fn palette_insert(
|
||||
&mut self,
|
||||
item: Item,
|
||||
drop_at: Option<glam::DVec3>,
|
||||
) -> Task<Message> {
|
||||
match self.resolve_palette_block(&item) {
|
||||
Ok(name) => {
|
||||
self.refresh_block_palette();
|
||||
@@ -286,7 +313,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn block_exists(&self, name: &str) -> bool {
|
||||
self.tabs[self.active_tab].scene.document.block_records.get(name).is_some()
|
||||
self.tabs[self.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(name)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn current_drawing_path(&self) -> String {
|
||||
@@ -395,12 +427,16 @@ impl OpenCADStudio {
|
||||
MenuAction::Insert => return self.palette_insert(item, None),
|
||||
MenuAction::Redefine => {
|
||||
let result = match &item {
|
||||
Item::Ref(r) if r.drawing => self.import_drawing_block(PathBuf::from(&r.source), true),
|
||||
Item::Ref(r) if r.drawing => {
|
||||
self.import_drawing_block(PathBuf::from(&r.source), true)
|
||||
}
|
||||
Item::File(path) => self.import_drawing_block(path.clone(), true),
|
||||
Item::Ref(r) if !r.source.is_empty() => crate::io::load_file(Path::new(&r.source))
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|doc| self.import_block_definition(&doc, &r.name, true))
|
||||
.map(|_| r.name.clone()),
|
||||
Item::Ref(r) if !r.source.is_empty() => {
|
||||
crate::io::load_file(Path::new(&r.source))
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|doc| self.import_block_definition(&doc, &r.name, true))
|
||||
.map(|_| r.name.clone())
|
||||
}
|
||||
_ => return Task::none(),
|
||||
};
|
||||
match result {
|
||||
@@ -430,14 +466,22 @@ impl OpenCADStudio {
|
||||
MenuAction::Edit => {
|
||||
let name = item.name();
|
||||
let i = self.active_tab;
|
||||
let reference = self.tabs[i].scene.document.entities().find_map(|e| match e {
|
||||
codec::EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&name) => {
|
||||
Some(ins.common.handle)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let reference = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.find_map(|e| match e {
|
||||
codec::EntityType::Insert(ins)
|
||||
if ins.block_name.eq_ignore_ascii_case(&name) =>
|
||||
{
|
||||
Some(ins.common.handle)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
match reference {
|
||||
Some(handle) => return self.dispatch_command(&format!("BEDIT_BEGIN:{}", handle.value())),
|
||||
Some(handle) => {
|
||||
return self.dispatch_command(&format!("BEDIT_BEGIN:{}", handle.value()))
|
||||
}
|
||||
None => self.command_line.push_error(&crate::tf!(
|
||||
"Block \"{name}\" has no reference in the drawing to edit."
|
||||
)),
|
||||
@@ -458,7 +502,8 @@ impl OpenCADStudio {
|
||||
self.push_undo_snapshot(i, "PURGE");
|
||||
crate::app::control::erase_block_definition(&mut self.tabs[i].scene, &name);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&crate::tf!("Block \"{name}\" removed."));
|
||||
self.command_line
|
||||
.push_output(&crate::tf!("Block \"{name}\" removed."));
|
||||
self.refresh_block_palette();
|
||||
}
|
||||
}
|
||||
@@ -492,12 +537,20 @@ impl OpenCADStudio {
|
||||
|
||||
/// Put `name` at the top of the Recent list (BLOCKMRULIST long).
|
||||
pub(crate) fn note_recent_block(&mut self, name: &str) {
|
||||
let (source, drawing) = match self.block_palette.file_sources.get(&name.to_ascii_uppercase()) {
|
||||
let (source, drawing) = match self
|
||||
.block_palette
|
||||
.file_sources
|
||||
.get(&name.to_ascii_uppercase())
|
||||
{
|
||||
Some(path) => (path.clone(), true),
|
||||
None => (self.current_drawing_path(), false),
|
||||
};
|
||||
let entry = PaletteBlockRef {
|
||||
name: if drawing { file_stem(Path::new(&source)) } else { name.to_string() },
|
||||
name: if drawing {
|
||||
file_stem(Path::new(&source))
|
||||
} else {
|
||||
name.to_string()
|
||||
},
|
||||
source,
|
||||
time: now_secs(),
|
||||
drawing,
|
||||
@@ -509,7 +562,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(crate) fn trim_recent_blocks(&mut self) {
|
||||
self.block_palette.recent.truncate(self.block_mru_list as usize);
|
||||
self.block_palette
|
||||
.recent
|
||||
.truncate(self.block_mru_list as usize);
|
||||
}
|
||||
|
||||
/// Rebuild the Current Drawing list, and the previews kept for blocks
|
||||
@@ -541,7 +596,9 @@ impl OpenCADStudio {
|
||||
let here = self.current_drawing_path();
|
||||
for entry in &entries {
|
||||
let key = format!("{here}|{}", entry.name.to_ascii_uppercase());
|
||||
self.block_palette.ref_wires.insert(key, entry.wires.clone());
|
||||
self.block_palette
|
||||
.ref_wires
|
||||
.insert(key, entry.wires.clone());
|
||||
}
|
||||
self.block_palette.blocks = entries;
|
||||
let drawings: Vec<String> = self
|
||||
@@ -580,7 +637,10 @@ impl OpenCADStudio {
|
||||
let mut unnamed = 0;
|
||||
let mut units = rustc_hash::FxHashMap::default();
|
||||
for br in doc.block_records.iter() {
|
||||
if br.flags.is_xref || br.name.eq_ignore_ascii_case("*Model_Space") || br.name.to_ascii_uppercase().starts_with("*PAPER_SPACE") {
|
||||
if br.flags.is_xref
|
||||
|| br.name.eq_ignore_ascii_case("*Model_Space")
|
||||
|| br.name.to_ascii_uppercase().starts_with("*PAPER_SPACE")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if br.name.starts_with('*') {
|
||||
@@ -602,6 +662,11 @@ impl OpenCADStudio {
|
||||
let search_path = std::env::current_dir()
|
||||
.map(|d| d.display().to_string())
|
||||
.unwrap_or_default();
|
||||
BlockCatalog { user, unnamed, units, search_path }
|
||||
BlockCatalog {
|
||||
user,
|
||||
unnamed,
|
||||
units,
|
||||
search_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
+2990
-3031
File diff suppressed because it is too large.
Load diff
+128
-41
@@ -71,7 +71,8 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.selection
|
||||
.borrow()
|
||||
.menu.ui
|
||||
.menu
|
||||
.ui
|
||||
.open_submenu;
|
||||
build_context_menu(&self.context_menu_context(), open)
|
||||
}
|
||||
@@ -88,18 +89,22 @@ impl OpenCADStudio {
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn on_context_menu_submenu_toggle(&mut self, id: SubmenuId) -> Task<Message> {
|
||||
pub(in crate::app) fn on_context_menu_submenu_toggle(
|
||||
&mut self,
|
||||
id: SubmenuId,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
let ui = &mut sel.menu.ui;
|
||||
ui.open_submenu = if ui.open_submenu == Some(id) { None } else { Some(id) };
|
||||
ui.open_submenu = if ui.open_submenu == Some(id) {
|
||||
None
|
||||
} else {
|
||||
Some(id)
|
||||
};
|
||||
drop(sel);
|
||||
// Keep the keyboard highlight on the header that was toggled.
|
||||
let menu = self.current_context_menu();
|
||||
let header = menu
|
||||
.selectable()
|
||||
.iter()
|
||||
.position(|s| s.header == Some(id));
|
||||
let header = menu.selectable().iter().position(|s| s.header == Some(id));
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
if sel.menu.ui.highlighted.is_some() {
|
||||
sel.menu.ui.highlighted = header;
|
||||
@@ -164,7 +169,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
/// Keyboard navigation while the menu is open.
|
||||
pub(in crate::app) fn on_context_menu_navigate(&mut self, nav: ContextMenuNav) -> Task<Message> {
|
||||
pub(in crate::app) fn on_context_menu_navigate(
|
||||
&mut self,
|
||||
nav: ContextMenuNav,
|
||||
) -> Task<Message> {
|
||||
if !self.context_menu_open() {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -193,7 +201,13 @@ impl OpenCADStudio {
|
||||
None => menu.default_index(),
|
||||
Some(cur) => next_enabled(cur, 1),
|
||||
};
|
||||
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.menu
|
||||
.ui
|
||||
.highlighted = Some(idx);
|
||||
Task::none()
|
||||
}
|
||||
ContextMenuNav::Up => {
|
||||
@@ -201,7 +215,13 @@ impl OpenCADStudio {
|
||||
None => menu.default_index(),
|
||||
Some(cur) => next_enabled(cur, -1),
|
||||
};
|
||||
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.menu
|
||||
.ui
|
||||
.highlighted = Some(idx);
|
||||
Task::none()
|
||||
}
|
||||
ContextMenuNav::Enter => {
|
||||
@@ -220,8 +240,13 @@ impl OpenCADStudio {
|
||||
if unique {
|
||||
self.on_context_menu_pick(rows[idx].action.clone())
|
||||
} else {
|
||||
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted =
|
||||
Some(idx);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.menu
|
||||
.ui
|
||||
.highlighted = Some(idx);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
@@ -235,7 +260,10 @@ impl OpenCADStudio {
|
||||
/// normal handlers (after closing the menu when the key is not a menu key,
|
||||
/// so typing a command name or a coordinate just works — the menu gets
|
||||
/// out of the way exactly as it does in commercial solutions).
|
||||
pub(in crate::app) fn intercept_context_menu_key(&mut self, msg: &Message) -> Option<Task<Message>> {
|
||||
pub(in crate::app) fn intercept_context_menu_key(
|
||||
&mut self,
|
||||
msg: &Message,
|
||||
) -> Option<Task<Message>> {
|
||||
match msg {
|
||||
Message::ArrowKeyPressed { direction, .. }
|
||||
| Message::CommandLineArrowProbe { direction, .. } => match direction {
|
||||
@@ -247,7 +275,10 @@ impl OpenCADStudio {
|
||||
let ui = self.tabs[i].scene.selection.borrow().menu.ui.clone();
|
||||
let menu = self.current_context_menu();
|
||||
let rows = menu.selectable();
|
||||
let header = ui.highlighted.and_then(|c| rows.get(c)).and_then(|r| r.header);
|
||||
let header = ui
|
||||
.highlighted
|
||||
.and_then(|c| rows.get(c))
|
||||
.and_then(|r| r.header);
|
||||
let want_open = matches!(direction, ArrowKey::Right);
|
||||
match header {
|
||||
Some(id) if (ui.open_submenu == Some(id)) != want_open => {
|
||||
@@ -476,7 +507,11 @@ mod tests {
|
||||
right_click(&mut app);
|
||||
assert!(menu_open(&app));
|
||||
let acts = menu_actions(&app);
|
||||
assert_eq!(acts[0], MenuAction::Command("LINE".into()), "Repeat LINE first");
|
||||
assert_eq!(
|
||||
acts[0],
|
||||
MenuAction::Command("LINE".into()),
|
||||
"Repeat LINE first"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -491,8 +526,8 @@ mod tests {
|
||||
let _ = app.update(Message::ViewportRightPress);
|
||||
{
|
||||
let mut sel = app.tabs[0].scene.selection.borrow_mut();
|
||||
sel.input.right_press_time = iced::time::Instant::now()
|
||||
.checked_sub(std::time::Duration::from_millis(400));
|
||||
sel.input.right_press_time =
|
||||
iced::time::Instant::now().checked_sub(std::time::Duration::from_millis(400));
|
||||
}
|
||||
let _ = app.update(Message::ViewportRightRelease);
|
||||
assert!(menu_open(&app));
|
||||
@@ -558,7 +593,14 @@ mod tests {
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Command("LINE".into())));
|
||||
assert!(!menu_open(&app));
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
assert!(!app.tabs[0].scene.selection.borrow().input.right_click_entered);
|
||||
assert!(
|
||||
!app.tabs[0]
|
||||
.scene
|
||||
.selection
|
||||
.borrow()
|
||||
.input
|
||||
.right_click_entered
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -601,9 +643,11 @@ mod tests {
|
||||
let _ = app.update(Message::CommandAppendChar("c".into()));
|
||||
assert!(!menu_open(&app));
|
||||
assert_eq!(active(&app), None);
|
||||
let closed = app.tabs[0].scene.document.entities().any(
|
||||
|e| matches!(e, codec::EntityType::LwPolyline(p) if p.is_closed),
|
||||
);
|
||||
let closed = app.tabs[0]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.any(|e| matches!(e, codec::EntityType::LwPolyline(p) if p.is_closed));
|
||||
assert!(closed);
|
||||
});
|
||||
}
|
||||
@@ -619,7 +663,11 @@ mod tests {
|
||||
let _ = app.update(Message::CommandAppendChar("7".into()));
|
||||
assert!(!menu_open(&app));
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
assert!(app.command_line.input.contains('7'), "{:?}", app.command_line.input);
|
||||
assert!(
|
||||
app.command_line.input.contains('7'),
|
||||
"{:?}",
|
||||
app.command_line.input
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -635,7 +683,9 @@ mod tests {
|
||||
let _ = app.update(Message::ContextMenuSubmenuToggle(SubmenuId::RecentInput));
|
||||
let acts = menu_actions(&app);
|
||||
assert!(acts.contains(&MenuAction::FeedInput("10,10".into())));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::FeedInput("10,10".into())));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::FeedInput(
|
||||
"10,10".into(),
|
||||
)));
|
||||
assert!(!menu_open(&app));
|
||||
let lines = app.tabs[0]
|
||||
.scene
|
||||
@@ -690,10 +740,12 @@ mod tests {
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
let handle = app.tabs[0].scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
let handle = app.tabs[0]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
app.tabs[0].scene.selected.insert(handle);
|
||||
app.refresh_selected_grips();
|
||||
app.tabs[0].active_grip = Some(crate::scene::pick::grip::GripEdit::single(
|
||||
@@ -710,7 +762,10 @@ mod tests {
|
||||
.scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(world, iced::Rectangle::with_size(iced::Size::new(800.0, 600.0)))
|
||||
.project(
|
||||
world,
|
||||
iced::Rectangle::with_size(iced::Size::new(800.0, 600.0)),
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y));
|
||||
}
|
||||
@@ -732,7 +787,10 @@ mod tests {
|
||||
assert_eq!(acts[0], MenuAction::Enter);
|
||||
assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Move)));
|
||||
assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Exit)));
|
||||
assert!(app.tabs[0].active_grip.is_some(), "the grip stays hot under the menu");
|
||||
assert!(
|
||||
app.tabs[0].active_grip.is_some(),
|
||||
"the grip stays hot under the menu"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -741,11 +799,17 @@ mod tests {
|
||||
with_stack(|| {
|
||||
let (mut app, _) = grip_app();
|
||||
right_click(&mut app);
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Move)));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(
|
||||
GripMenuCmd::Move,
|
||||
)));
|
||||
assert!(!menu_open(&app));
|
||||
assert!(app.tabs[0].active_grip.is_none());
|
||||
assert_eq!(active(&app), Some("MOVE"));
|
||||
let prompt = app.tabs[0].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default();
|
||||
let prompt = app.tabs[0]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.prompt())
|
||||
.unwrap_or_default();
|
||||
let base_prompt = crate::tr!("command-move", "base", count = 1i64);
|
||||
assert_ne!(prompt, base_prompt, "base already consumed");
|
||||
assert_eq!(app.last_point, Some(DVec3::ZERO));
|
||||
@@ -758,12 +822,20 @@ mod tests {
|
||||
let (mut app, handle) = grip_app();
|
||||
move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0));
|
||||
let (x, y) = line_start(&app, handle);
|
||||
assert!((x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3, "drag applied");
|
||||
assert!(
|
||||
(x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3,
|
||||
"drag applied"
|
||||
);
|
||||
right_click(&mut app);
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Exit)));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(
|
||||
GripMenuCmd::Exit,
|
||||
)));
|
||||
assert!(app.tabs[0].active_grip.is_none());
|
||||
let (x, y) = line_start(&app, handle);
|
||||
assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "Exit restores the original");
|
||||
assert!(
|
||||
x.abs() < 1e-9 && y.abs() < 1e-9,
|
||||
"Exit restores the original"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -773,7 +845,9 @@ mod tests {
|
||||
let (mut app, handle) = grip_app();
|
||||
move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0));
|
||||
right_click(&mut app);
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Undo)));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(
|
||||
GripMenuCmd::Undo,
|
||||
)));
|
||||
let (x, y) = line_start(&app, handle);
|
||||
assert!(x.abs() < 1e-9 && y.abs() < 1e-9);
|
||||
let grip = app.tabs[0].active_grip.as_ref().expect("grip still hot");
|
||||
@@ -786,7 +860,9 @@ mod tests {
|
||||
with_stack(|| {
|
||||
let (mut app, handle) = grip_app();
|
||||
right_click(&mut app);
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::CopyToggle)));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(
|
||||
GripMenuCmd::CopyToggle,
|
||||
)));
|
||||
assert!(app.tabs[0].grip_copy);
|
||||
move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0));
|
||||
let _ = app.update(Message::CommandFinalize);
|
||||
@@ -802,8 +878,13 @@ mod tests {
|
||||
assert_eq!(lines.len(), 2, "{lines:?}");
|
||||
let (x, y) = line_start(&app, handle);
|
||||
assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "original untouched");
|
||||
assert!(lines.iter().any(|&(x, y)| (x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3));
|
||||
assert!(app.tabs[0].active_grip.is_some(), "grip re-armed for the next copy");
|
||||
assert!(lines
|
||||
.iter()
|
||||
.any(|&(x, y)| (x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3));
|
||||
assert!(
|
||||
app.tabs[0].active_grip.is_some(),
|
||||
"grip re-armed for the next copy"
|
||||
);
|
||||
// One undo step removes the copy again.
|
||||
app.undo_steps(1);
|
||||
let count = app.tabs[0]
|
||||
@@ -821,7 +902,9 @@ mod tests {
|
||||
with_stack(|| {
|
||||
let (mut app, handle) = grip_app();
|
||||
right_click(&mut app);
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::BasePoint)));
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(
|
||||
GripMenuCmd::BasePoint,
|
||||
)));
|
||||
assert!(app.tabs[0].grip_base_pending);
|
||||
move_cursor_to(&mut app, DVec3::new(1.0, 1.0, 0.0));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
@@ -989,7 +1072,9 @@ mod transparent_tests {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!((lines[1].start.x - 50.0).abs() < 1e-6 && (lines[1].start.y - 0.0).abs() < 1e-6);
|
||||
assert!(
|
||||
(lines[1].start.x - 50.0).abs() < 1e-6 && (lines[1].start.y - 0.0).abs() < 1e-6
|
||||
);
|
||||
assert!((lines[1].end.x - 50.0).abs() < 1e-6 && (lines[1].end.y - 25.0).abs() < 1e-6);
|
||||
|
||||
// Point cursor in -X direction from (50, 25) using feed_active_cmd
|
||||
@@ -1005,7 +1090,9 @@ mod transparent_tests {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
assert!((lines[2].start.x - 50.0).abs() < 1e-6 && (lines[2].start.y - 25.0).abs() < 1e-6);
|
||||
assert!(
|
||||
(lines[2].start.x - 50.0).abs() < 1e-6 && (lines[2].start.y - 25.0).abs() < 1e-6
|
||||
);
|
||||
assert!((lines[2].end.x - 0.0).abs() < 1e-6 && (lines[2].end.y - 25.0).abs() < 1e-6);
|
||||
});
|
||||
}
|
||||
|
||||
+362
-259
@@ -4,13 +4,12 @@
|
||||
use super::util::*;
|
||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||
use crate::app::helpers::{
|
||||
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||
CoordKind,
|
||||
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
|
||||
};
|
||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::model::object::GripApply;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::{
|
||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
};
|
||||
@@ -20,7 +19,6 @@ use codec::{EntityType as AcadEntityType, Handle};
|
||||
use iced::time::Instant;
|
||||
use iced::{mouse, Point, Task};
|
||||
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
// Default the format dropdown to the loaded file's own format — its
|
||||
@@ -64,7 +62,6 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
||||
pub(in crate::app) fn close_save_dialog_window(&mut self) -> Task<Message> {
|
||||
self.aec_drop_acknowledged = false;
|
||||
if self.active_modal == Some(crate::app::ModalKind::SaveDialog) {
|
||||
@@ -74,7 +71,6 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
||||
pub(in crate::app) fn open_unsaved_dialog_window(&mut self) -> Task<Message> {
|
||||
self.active_modal = Some(crate::app::ModalKind::Unsaved);
|
||||
// The unsaved-changes prompt renders inside the main window, so bring
|
||||
@@ -95,7 +91,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(in crate::app) fn close_unsaved_dialog_window(&mut self) -> Task<Message> {
|
||||
if self.active_modal == Some(crate::app::ModalKind::Unsaved) {
|
||||
self.active_modal = None;
|
||||
@@ -104,131 +99,132 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
||||
pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEvent) -> Task<Message> {
|
||||
// Commands use `start_allowed`; other events need a drawing
|
||||
// and stay blocked on the Start page (#299, #388, #389).
|
||||
if self.tabs[self.active_tab].is_start && !matches!(event, ModuleEvent::Command(_)) {
|
||||
self.ribbon.close_dropdown();
|
||||
self.command_line
|
||||
.push_info(crate::t!("No drawing open — use New or Open first.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
// Dismiss any open dropdown / collapsed-panel flyout on tool use,
|
||||
// and remember this tool as its panel's last-used one.
|
||||
self.ribbon.close_dropdown();
|
||||
self.ribbon.note_panel_tool(&tool_id);
|
||||
self.ribbon.activate_tool(&tool_id);
|
||||
match event {
|
||||
// `dispatch_command` turns a one-shot tool's highlight off
|
||||
// again once nothing is left running (#355).
|
||||
ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd),
|
||||
ModuleEvent::OpenFileDialog => {
|
||||
pub(super) fn on_ribbon_tool_click(
|
||||
&mut self,
|
||||
tool_id: String,
|
||||
event: ModuleEvent,
|
||||
) -> Task<Message> {
|
||||
// Commands use `start_allowed`; other events need a drawing
|
||||
// and stay blocked on the Start page (#299, #388, #389).
|
||||
if self.tabs[self.active_tab].is_start && !matches!(event, ModuleEvent::Command(_)) {
|
||||
self.ribbon.close_dropdown();
|
||||
self.command_line
|
||||
.push_info(crate::t!("No drawing open — use New or Open first.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
// Dismiss any open dropdown / collapsed-panel flyout on tool use,
|
||||
// and remember this tool as its panel's last-used one.
|
||||
self.ribbon.close_dropdown();
|
||||
self.ribbon.note_panel_tool(&tool_id);
|
||||
self.ribbon.activate_tool(&tool_id);
|
||||
match event {
|
||||
// `dispatch_command` turns a one-shot tool's highlight off
|
||||
// again once nothing is left running (#355).
|
||||
ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd),
|
||||
ModuleEvent::OpenFileDialog => {
|
||||
self.command_line
|
||||
.push_info(crate::t!("Open DWG/DXF: not yet implemented.").as_ref());
|
||||
}
|
||||
ModuleEvent::ClearModels => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear();
|
||||
self.tabs[i].properties = PropertiesPanel::empty();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Scene cleared.").as_ref());
|
||||
}
|
||||
ModuleEvent::SetVisualStyle(name) => {
|
||||
use crate::modules::view::visual_style;
|
||||
match visual_style::mode_for_keyword(&name) {
|
||||
Some(mode) => return Task::done(Message::SetRenderMode(mode)),
|
||||
None => {
|
||||
// Name the styles that do exist, from the same
|
||||
// list every other caller reads.
|
||||
self.command_line
|
||||
.push_info(crate::t!("Open DWG/DXF: not yet implemented.").as_ref());
|
||||
}
|
||||
ModuleEvent::ClearModels => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear();
|
||||
self.tabs[i].properties = PropertiesPanel::empty();
|
||||
self.command_line.push_output(crate::t!("Scene cleared.").as_ref());
|
||||
}
|
||||
ModuleEvent::SetVisualStyle(name) => {
|
||||
use crate::modules::view::visual_style;
|
||||
match visual_style::mode_for_keyword(&name) {
|
||||
Some(mode) => return Task::done(Message::SetRenderMode(mode)),
|
||||
None => {
|
||||
// Name the styles that do exist, from the same
|
||||
// list every other caller reads.
|
||||
self.command_line.push_error(
|
||||
crate::tf!("Unknown visual style \"{name}\".").as_ref(),
|
||||
);
|
||||
self.command_line
|
||||
.push_info(crate::t!(visual_style::keyword_prompt()).as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleEvent::ToggleLayers => {
|
||||
return Task::done(Message::ToggleLayers);
|
||||
}
|
||||
ModuleEvent::PluginFileDialog {
|
||||
command,
|
||||
title,
|
||||
filter_name,
|
||||
extensions,
|
||||
} => {
|
||||
return Task::perform(
|
||||
async move {
|
||||
let exts: Vec<&str> =
|
||||
extensions.iter().map(|s| s.as_str()).collect();
|
||||
let path = crate::sys::file_dialog()
|
||||
.set_title(title)
|
||||
.add_filter(filter_name, &exts)
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|h| crate::sys::handle_path(&h));
|
||||
(command, path)
|
||||
},
|
||||
|(command, path)| Message::PluginFileDialogResult { command, path },
|
||||
);
|
||||
.push_error(crate::tf!("Unknown visual style \"{name}\".").as_ref());
|
||||
self.command_line
|
||||
.push_info(crate::t!(visual_style::keyword_prompt()).as_ref());
|
||||
}
|
||||
}
|
||||
// Every non-Command event above is a one-shot (state toggle,
|
||||
// clear, dialog spawn) — nothing stays running to clear the
|
||||
// highlight later, so turn it off here. (#355)
|
||||
self.ribbon.deactivate_tool();
|
||||
Task::none()
|
||||
}
|
||||
ModuleEvent::ToggleLayers => {
|
||||
return Task::done(Message::ToggleLayers);
|
||||
}
|
||||
ModuleEvent::PluginFileDialog {
|
||||
command,
|
||||
title,
|
||||
filter_name,
|
||||
extensions,
|
||||
} => {
|
||||
return Task::perform(
|
||||
async move {
|
||||
let exts: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect();
|
||||
let path = crate::sys::file_dialog()
|
||||
.set_title(title)
|
||||
.add_filter(filter_name, &exts)
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|h| crate::sys::handle_path(&h));
|
||||
(command, path)
|
||||
},
|
||||
|(command, path)| Message::PluginFileDialogResult { command, path },
|
||||
);
|
||||
}
|
||||
}
|
||||
// Every non-Command event above is a one-shot (state toggle,
|
||||
// clear, dialog spawn) — nothing stays running to clear the
|
||||
// highlight later, so turn it off here. (#355)
|
||||
self.ribbon.deactivate_tool();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_unsaved_dialog_discard(&mut self) -> Task<Message> {
|
||||
match self.pending_close.take() {
|
||||
Some(crate::app::PendingClose::Tab(tab_id)) => {
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
// Resolve the id now: the tab may have closed while
|
||||
// the dialog was open (batched close, automation),
|
||||
// in which case there is nothing left to discard.
|
||||
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {
|
||||
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
||||
};
|
||||
// Discarded — drop this tab's autosave recovery copy.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
||||
if self.tabs.len() == 1 {
|
||||
self.tab_counter += 1;
|
||||
self.tabs[0] =
|
||||
crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
||||
self.active_tab = 0;
|
||||
self.apply_display_defaults(0);
|
||||
} else {
|
||||
self.tabs.remove(idx);
|
||||
if self.active_tab >= self.tabs.len() {
|
||||
self.active_tab = self.tabs.len() - 1;
|
||||
}
|
||||
}
|
||||
// The active tab is now a fresh blank or a
|
||||
// different existing tab; sync ribbon chips so
|
||||
// they don't keep showing the discarded tab's
|
||||
// last selection. #21.
|
||||
self.sync_ribbon_layers();
|
||||
self.sync_ribbon_from_selection();
|
||||
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
||||
match self.pending_close.take() {
|
||||
Some(crate::app::PendingClose::Tab(tab_id)) => {
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
// Resolve the id now: the tab may have closed while
|
||||
// the dialog was open (batched close, automation),
|
||||
// in which case there is nothing left to discard.
|
||||
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {
|
||||
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
||||
};
|
||||
// Discarded — drop this tab's autosave recovery copy.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
||||
if self.tabs.len() == 1 {
|
||||
self.tab_counter += 1;
|
||||
self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
||||
self.active_tab = 0;
|
||||
self.apply_display_defaults(0);
|
||||
} else {
|
||||
self.tabs.remove(idx);
|
||||
if self.active_tab >= self.tabs.len() {
|
||||
self.active_tab = self.tabs.len() - 1;
|
||||
}
|
||||
Some(crate::app::PendingClose::Quit) => {
|
||||
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
||||
self.tabs[idx].dirty = false;
|
||||
}
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
// More dirty tabs remain — keep window open.
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
} else {
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
return Task::batch(vec![close_win, self.exit_app()]);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Task::none()
|
||||
// The active tab is now a fresh blank or a
|
||||
// different existing tab; sync ribbon chips so
|
||||
// they don't keep showing the discarded tab's
|
||||
// last selection. #21.
|
||||
self.sync_ribbon_layers();
|
||||
self.sync_ribbon_from_selection();
|
||||
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
||||
}
|
||||
Some(crate::app::PendingClose::Quit) => {
|
||||
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
||||
self.tabs[idx].dirty = false;
|
||||
}
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
// More dirty tabs remain — keep window open.
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
} else {
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
return Task::batch(vec![close_win, self.exit_app()]);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_unsaved_dialog_save(&mut self) -> Task<Message> {
|
||||
@@ -254,10 +250,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
crate::app::PendingClose::Quit => {
|
||||
let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) else {
|
||||
self.pending_close = None;
|
||||
return Task::batch([
|
||||
self.close_unsaved_dialog_window(),
|
||||
self.exit_app(),
|
||||
]);
|
||||
return Task::batch([self.close_unsaved_dialog_window(), self.exit_app()]);
|
||||
};
|
||||
(idx, crate::app::SaveContinuation::Quit)
|
||||
}
|
||||
@@ -319,10 +312,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
let save = self.save_with_default_format(idx);
|
||||
Task::batch([close, save])
|
||||
} else {
|
||||
Task::batch([
|
||||
self.close_unsaved_dialog_window(),
|
||||
self.exit_app(),
|
||||
])
|
||||
Task::batch([self.close_unsaved_dialog_window(), self.exit_app()])
|
||||
}
|
||||
}
|
||||
None => Task::none(),
|
||||
@@ -356,7 +346,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
/// one new block in the active drawing. Returns the new block's name, or an
|
||||
/// error message. Nested block definitions are imported first so nested
|
||||
/// INSERTs render (AutoCAD's "inserting a drawing imports its block defs").
|
||||
pub(in crate::app) fn import_file_as_block(&mut self, path: std::path::PathBuf) -> Result<String, String> {
|
||||
pub(in crate::app) fn import_file_as_block(
|
||||
&mut self,
|
||||
path: std::path::PathBuf,
|
||||
) -> Result<String, String> {
|
||||
self.import_drawing_block(path, false)
|
||||
}
|
||||
|
||||
@@ -417,7 +410,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
.iter()
|
||||
.filter_map(|h| doc.get_entity(*h))
|
||||
.filter(|e| {
|
||||
!matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_))
|
||||
!matches!(
|
||||
e,
|
||||
codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
@@ -430,7 +426,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
o == model_br || o.is_null()
|
||||
})
|
||||
.filter(|e| {
|
||||
!matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_))
|
||||
!matches!(
|
||||
e,
|
||||
codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
@@ -445,7 +444,12 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
doc.header.model_space_insertion_base.z,
|
||||
);
|
||||
let redefine = redefine
|
||||
&& self.tabs[i].scene.document.block_records.get(stem.trim()).is_some();
|
||||
&& self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(stem.trim())
|
||||
.is_some();
|
||||
let name = if redefine {
|
||||
stem.trim().to_string()
|
||||
} else {
|
||||
@@ -622,12 +626,16 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
.entities()
|
||||
.filter_map(|entity| match entity {
|
||||
codec::EntityType::RasterImage(image)
|
||||
if image.definition_handle.is_some_and(|key| self.tabs[i].xref_unloaded.is_unloaded(key.value())) =>
|
||||
if image
|
||||
.definition_handle
|
||||
.is_some_and(|key| self.tabs[i].xref_unloaded.is_unloaded(key.value())) =>
|
||||
{
|
||||
Some(entity.common().handle)
|
||||
}
|
||||
codec::EntityType::Underlay(underlay)
|
||||
if self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
|
||||
if self.tabs[i]
|
||||
.xref_unloaded
|
||||
.is_unloaded(underlay.definition_handle.value()) =>
|
||||
{
|
||||
Some(entity.common().handle)
|
||||
}
|
||||
@@ -646,12 +654,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
/// selection. Same engine fns as the CLI arms, batched with per-item
|
||||
/// report lines in the CLI wording; finishes with [`post_ref_op`] plus
|
||||
/// a palette rescan (mirror of the CLI post-op sequence).
|
||||
pub(crate) fn xref_manager_op(
|
||||
&mut self,
|
||||
op: crate::ui::window::xref_manager::XrefPaletteOp,
|
||||
) {
|
||||
pub(crate) fn xref_manager_op(&mut self, op: crate::ui::window::xref_manager::XrefPaletteOp) {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Reference changes are not available on web — the reference list is read-only."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
@@ -664,14 +674,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
idx.sort_unstable();
|
||||
idx.iter()
|
||||
.filter_map(|idx| {
|
||||
self.xref_manager.entries.get(*idx).map(|e| {
|
||||
(
|
||||
e.key,
|
||||
e.name.clone(),
|
||||
e.kind,
|
||||
e.parent_key.is_some(),
|
||||
)
|
||||
})
|
||||
self.xref_manager
|
||||
.entries
|
||||
.get(*idx)
|
||||
.map(|e| (e.key, e.name.clone(), e.kind, e.parent_key.is_some()))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -686,8 +692,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
);
|
||||
let host: Option<std::path::PathBuf> = self.tabs[i].current_path.clone();
|
||||
if needs_host && host.is_none() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let label = match op {
|
||||
@@ -712,15 +719,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
).as_ref());
|
||||
continue;
|
||||
}
|
||||
match crate::io::xref::detach_reference(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*key,
|
||||
) {
|
||||
match crate::io::xref::detach_reference(&mut self.tabs[i].scene.document, *key)
|
||||
{
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: detached \"{}\".",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line
|
||||
.push_output(crate::tf!("XREF: detached \"{}\".", name).as_ref());
|
||||
self.tabs[i].xref_unloaded.remove(key);
|
||||
self.tabs[i].xref_stat_cache.remove(key);
|
||||
done += 1;
|
||||
@@ -738,15 +741,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
).as_ref());
|
||||
continue;
|
||||
}
|
||||
match crate::io::xref::unload_reference(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*key,
|
||||
) {
|
||||
match crate::io::xref::unload_reference(&mut self.tabs[i].scene.document, *key)
|
||||
{
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: unloaded \"{}\".",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line
|
||||
.push_output(crate::tf!("XREF: unloaded \"{}\".", name).as_ref());
|
||||
self.tabs[i].xref_unloaded.add(*key);
|
||||
done += 1;
|
||||
}
|
||||
@@ -810,10 +809,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
} else if *kind != crate::io::xref_model::RefKind::DwgXref {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"{}: reload applies to drawing references only.",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::tf!("{}: reload applies to drawing references only.", name)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (key, _) in &dwg_picked {
|
||||
@@ -853,18 +852,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
}
|
||||
for (_, name) in &dwg_picked {
|
||||
match infos
|
||||
.iter()
|
||||
.find(|n| n.name.eq_ignore_ascii_case(name))
|
||||
{
|
||||
match infos.iter().find(|n| n.name.eq_ignore_ascii_case(name)) {
|
||||
Some(info) => {
|
||||
self.report_xref_status(info);
|
||||
done += 1;
|
||||
}
|
||||
None => self.command_line.push_error(crate::tf!(
|
||||
"XREF: no references match '{}'.",
|
||||
name
|
||||
).as_ref()),
|
||||
None => self.command_line.push_error(
|
||||
crate::tf!("XREF: no references match '{}'.", name).as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,10 +878,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
crate::io::xref_model::RefType::Overlay,
|
||||
) {
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: \"{}\" set to Overlay.",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!("XREF: \"{}\" set to Overlay.", name).as_ref(),
|
||||
);
|
||||
done += 1;
|
||||
}
|
||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
||||
@@ -908,10 +902,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
crate::io::xref_model::RefType::Attach,
|
||||
) {
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: \"{}\" set to Attach.",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!("XREF: \"{}\" set to Attach.", name).as_ref(),
|
||||
);
|
||||
done += 1;
|
||||
}
|
||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
||||
@@ -929,10 +922,13 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
for (key, name, _, is_nested) in &picked {
|
||||
if *is_nested {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
self.command_line.push_error(
|
||||
crate::tf!(
|
||||
"XREF: cannot bind nested reference '{}'. Bind it in its host drawing.",
|
||||
name
|
||||
).as_ref());
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
match crate::io::xref::bind_reference(
|
||||
@@ -943,10 +939,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
) {
|
||||
Ok(outcome) => {
|
||||
if outcome.unremapped == 0 {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: bound \"{}\".",
|
||||
outcome.name
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!("XREF: bound \"{}\".", outcome.name).as_ref(),
|
||||
);
|
||||
} else {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: bound \"{}\" with {} unremapped style handles (see bind limitations).",
|
||||
@@ -978,10 +973,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
&host,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: Path set for \"{}\" — Reload to apply.",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!("XREF: Path set for \"{}\" — Reload to apply.", name)
|
||||
.as_ref(),
|
||||
);
|
||||
done += 1;
|
||||
}
|
||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
||||
@@ -1003,18 +998,25 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
/// full resolve, per-ref report, stat baselines refreshed.
|
||||
pub(crate) fn xref_manager_reload_all(&mut self) {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Reference changes are not available on web — the reference list is read-only."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let Some(path) = self.tabs[i].current_path.clone() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(base_dir) = path.parent().map(|p| p.to_path_buf()) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let reload_keys: Vec<u64> = crate::io::xref::collect_entries_with_prev(
|
||||
@@ -1096,7 +1098,9 @@ mod tests {
|
||||
fn import_document_as_block_defines_block() {
|
||||
let mut app = fresh();
|
||||
let doc = foreign_doc(&app);
|
||||
let name = app.import_document_as_block(doc, "Fixture".to_string()).unwrap();
|
||||
let name = app
|
||||
.import_document_as_block(doc, "Fixture".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(name, "Fixture");
|
||||
assert!(app.tabs[app.active_tab]
|
||||
.scene
|
||||
@@ -1119,9 +1123,7 @@ mod tests {
|
||||
.objects
|
||||
.values()
|
||||
.find_map(|o| match o {
|
||||
codec::objects::ObjectType::Layout(l) if l.name == "Model" => {
|
||||
Some(l.block_record)
|
||||
}
|
||||
codec::objects::ObjectType::Layout(l) if l.name == "Model" => Some(l.block_record),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1191,15 +1193,14 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The nested-INSERT's block name inside a defined block record.
|
||||
fn nested_insert_target(
|
||||
doc: &codec::CadDocument,
|
||||
block: &str,
|
||||
) -> Option<String> {
|
||||
fn nested_insert_target(doc: &codec::CadDocument, block: &str) -> Option<String> {
|
||||
let br = doc.block_records.get(block)?;
|
||||
br.entity_handles.iter().find_map(|h| match doc.get_entity(*h)? {
|
||||
EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
br.entity_handles
|
||||
.iter()
|
||||
.find_map(|h| match doc.get_entity(*h)? {
|
||||
EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A foreign document whose model space INSERTs a `Fixture` block whose
|
||||
@@ -1276,7 +1277,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _ = app.import_document_as_block(doc, "Imported".to_string()).unwrap();
|
||||
let _ = app
|
||||
.import_document_as_block(doc, "Imported".to_string())
|
||||
.unwrap();
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
// The file's own "Door (2)" is kept intact and targets the file's
|
||||
// renamed "Door" (now "Door (3)" — "Door (2)" was taken by the source).
|
||||
@@ -1323,7 +1326,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let name = app.import_document_as_block(doc, "Imported".to_string()).unwrap();
|
||||
let name = app
|
||||
.import_document_as_block(doc, "Imported".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(name, "Imported");
|
||||
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
@@ -1354,11 +1359,16 @@ mod tests {
|
||||
fn blockpalette_refresh_lists_and_places_block() {
|
||||
let mut app = fresh();
|
||||
let doc = foreign_doc(&app);
|
||||
let name = app.import_document_as_block(doc, "Fixture".to_string()).unwrap();
|
||||
let name = app
|
||||
.import_document_as_block(doc, "Fixture".to_string())
|
||||
.unwrap();
|
||||
app.refresh_block_palette();
|
||||
assert!(app.block_palette.blocks.iter().any(|b| b.name == "Fixture"));
|
||||
app.start_block_placement(&name);
|
||||
let cmd = app.tabs[app.active_tab].active_cmd.as_ref().expect("INSERT running");
|
||||
let cmd = app.tabs[app.active_tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.expect("INSERT running");
|
||||
assert_eq!(cmd.name(), "-INSERT");
|
||||
assert_eq!(app.block_palette.placing.as_deref(), Some("Fixture"));
|
||||
}
|
||||
@@ -1415,7 +1425,11 @@ mod tests {
|
||||
line.end = Vector3::new(1.0, 0.0, 0.0);
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.define_block_from_owned_entities(vec![EntityType::Line(line)], "Chair", glam::DVec3::ZERO)
|
||||
.define_block_from_owned_entities(
|
||||
vec![EntityType::Line(line)],
|
||||
"Chair",
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(app.block_name_from_file("Chair"), "Chair (2)");
|
||||
assert_eq!(app.block_name_from_file("Table"), "Table");
|
||||
@@ -1444,12 +1458,7 @@ mod tests {
|
||||
br.flags.is_xref = true;
|
||||
br.xref_path = "refs/plan.dwg".to_string();
|
||||
br.handle = app.tabs[i].scene.document.allocate_handle();
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.add(br)
|
||||
.unwrap();
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(old_dir.join("host.dwg"));
|
||||
let tab_id = app.tabs[i].id;
|
||||
let job_id = 4242u64;
|
||||
@@ -1510,9 +1519,9 @@ mod tests {
|
||||
// Behavioral matrix: every row op on a NESTED row via the GUI
|
||||
// message path. Nested rows must report per-entry errors (CLI
|
||||
// wording), never silently skip.
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use codec::tables::BlockRecord;
|
||||
let dir = palette_tmpdir("nestedmatrix");
|
||||
let mut host_doc = codec::CadDocument::new();
|
||||
let mut inner = BlockRecord::new("INNER");
|
||||
@@ -1530,7 +1539,12 @@ mod tests {
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(dir.join("app.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "INNER").expect("nested INNER listed");
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.name == "INNER")
|
||||
.expect("nested INNER listed");
|
||||
assert!(app.xref_manager.entries[idx].parent_key.is_some());
|
||||
for op in [
|
||||
XrefPaletteOp::Detach,
|
||||
@@ -1542,9 +1556,17 @@ mod tests {
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, op));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("INNER"), "op {op:?} on nested row gave: {out:?}");
|
||||
assert!(
|
||||
app.tabs[i].scene.document.block_records.get("HOST").is_some(),
|
||||
out.contains("INNER"),
|
||||
"op {op:?} on nested row gave: {out:?}"
|
||||
);
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("HOST")
|
||||
.is_some(),
|
||||
"op {op:?} must not mutate the direct reference"
|
||||
);
|
||||
}
|
||||
@@ -1555,9 +1577,9 @@ mod tests {
|
||||
fn palette_detach_unload_rowop_gui_path() {
|
||||
// Reproduction for "Detach/Unload from the palette do nothing":
|
||||
// drive the exact GUI message path (row right-click menu item).
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use codec::tables::BlockRecord;
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
for name in ["PLAN", "SITE"] {
|
||||
@@ -1569,19 +1591,42 @@ mod tests {
|
||||
}
|
||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_repro_host.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "PLAN").expect("PLAN listed");
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.name == "PLAN")
|
||||
.expect("PLAN listed");
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Detach));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "detach output missing, got: {out:?}");
|
||||
assert!(app.tabs[i].scene.document.block_records.get("PLAN").is_none(), "PLAN definition must be gone");
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "SITE").expect("SITE listed");
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("PLAN")
|
||||
.is_none(),
|
||||
"PLAN definition must be gone"
|
||||
);
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.name == "SITE")
|
||||
.expect("SITE listed");
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Unload));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("SITE"), "unload output missing, got: {out:?}");
|
||||
assert_eq!(
|
||||
app.xref_manager.entries.iter().find(|e| e.name == "SITE").unwrap().status,
|
||||
app.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.name == "SITE")
|
||||
.unwrap()
|
||||
.status,
|
||||
crate::io::xref_model::RefStatus::Unloaded
|
||||
);
|
||||
}
|
||||
@@ -1590,10 +1635,10 @@ mod tests {
|
||||
fn palette_overlay_and_pathtype_rowop_gui_path() {
|
||||
// Row-menu Overlay + Change-Path row ops on a direct row via the
|
||||
// GUI message path: type flag flips, saved path clears.
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::io::xref_model::{Pathtype, RefType};
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use codec::tables::BlockRecord;
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
let mut br = BlockRecord::new("PLAN");
|
||||
@@ -1603,20 +1648,49 @@ mod tests {
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_rowop_host.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "PLAN").expect("PLAN listed");
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.name == "PLAN")
|
||||
.expect("PLAN listed");
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Overlay));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "got: {out:?}");
|
||||
let br = app.tabs[i].scene.document.block_records.get("PLAN").unwrap();
|
||||
let br = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("PLAN")
|
||||
.unwrap();
|
||||
assert!(br.flags.is_xref_overlay && !br.flags.is_xref);
|
||||
assert_eq!(app.xref_manager.entries.iter().find(|e| e.name == "PLAN").unwrap().ref_type, RefType::Overlay);
|
||||
assert_eq!(
|
||||
app.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.name == "PLAN")
|
||||
.unwrap()
|
||||
.ref_type,
|
||||
RefType::Overlay
|
||||
);
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Pathtype(Pathtype::None)));
|
||||
let _ = app.update(Message::XrefRowOp(
|
||||
idx,
|
||||
XrefPaletteOp::Pathtype(Pathtype::None),
|
||||
));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "got: {out:?}");
|
||||
let br = app.tabs[i].scene.document.block_records.get("PLAN").unwrap();
|
||||
assert_eq!(br.xref_path, "plan.dwg", "Remove Path strips to the bare filename");
|
||||
let br = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("PLAN")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
br.xref_path, "plan.dwg",
|
||||
"Remove Path strips to the bare filename"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1639,9 +1713,21 @@ mod tests {
|
||||
let start = app.command_line.history.len();
|
||||
app.xref_manager_reload_all();
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "reload-all must report the reference, got: {out:?}");
|
||||
let entry = app.xref_manager.entries.iter().find(|e| e.name == "PLAN").expect("PLAN listed");
|
||||
assert_eq!(entry.status, crate::io::xref_model::RefStatus::Loaded, "PLAN must resolve Loaded");
|
||||
assert!(
|
||||
out.contains("PLAN"),
|
||||
"reload-all must report the reference, got: {out:?}"
|
||||
);
|
||||
let entry = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.name == "PLAN")
|
||||
.expect("PLAN listed");
|
||||
assert_eq!(
|
||||
entry.status,
|
||||
crate::io::xref_model::RefStatus::Loaded,
|
||||
"PLAN must resolve Loaded"
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
@@ -1659,8 +1745,8 @@ mod tests {
|
||||
fn palette_unload_nested_reports_per_entry() {
|
||||
// F6: palette Unload on a nested row reports the per-entry nested
|
||||
// error (CLI wording) instead of the confusing 'no loaded reference'.
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use codec::tables::BlockRecord;
|
||||
let dir = palette_tmpdir("nested");
|
||||
let mut host_doc = codec::CadDocument::new();
|
||||
let mut inner = BlockRecord::new("INNER");
|
||||
@@ -1678,7 +1764,12 @@ mod tests {
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(dir.join("app.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "INNER").expect("nested INNER listed");
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.name == "INNER")
|
||||
.expect("nested INNER listed");
|
||||
assert!(app.xref_manager.entries[idx].parent_key.is_some());
|
||||
app.xref_manager.selected.insert(idx);
|
||||
let start = app.command_line.history.len();
|
||||
@@ -1694,27 +1785,36 @@ mod tests {
|
||||
// F7: palette Reload on an image/PDF row reports the drawing-only
|
||||
// error and leaves its unloaded flag untouched (no spurious
|
||||
// no-match after a flag clear).
|
||||
use codec::objects::{ImageDefinition, ObjectType};
|
||||
use crate::io::xref_model::RefKind;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use codec::objects::{ImageDefinition, ObjectType};
|
||||
let dir = palette_tmpdir("imgreload");
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
let h = app.tabs[i].scene.document.allocate_handle();
|
||||
let mut def = ImageDefinition::with_dimensions("img.png", 8, 8);
|
||||
def.handle = h;
|
||||
app.tabs[i].scene.document.objects.insert(h, ObjectType::ImageDefinition(def));
|
||||
let mut img = codec::entities::RasterImage::new(
|
||||
"img.png",
|
||||
codec::types::Vector3::ZERO,
|
||||
8.0,
|
||||
8.0,
|
||||
);
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(h, ObjectType::ImageDefinition(def));
|
||||
let mut img =
|
||||
codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0);
|
||||
img.definition_handle = Some(h);
|
||||
app.tabs[i].scene.document.add_entity(codec::EntityType::RasterImage(img)).unwrap();
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::RasterImage(img))
|
||||
.unwrap();
|
||||
app.tabs[i].current_path = Some(dir.join("host.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.kind == RefKind::Image).expect("image listed");
|
||||
let idx = app
|
||||
.xref_manager
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.kind == RefKind::Image)
|
||||
.expect("image listed");
|
||||
let key = app.xref_manager.entries[idx].key;
|
||||
app.xref_manager.selected.insert(idx);
|
||||
app.tabs[i].xref_unloaded.add(key);
|
||||
@@ -1723,7 +1823,10 @@ mod tests {
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("img.png"), "got: {out:?}");
|
||||
assert_eq!(app.command_line.history.len(), start + 1);
|
||||
assert!(app.tabs[i].xref_unloaded.is_unloaded(key), "image flag must stay untouched");
|
||||
assert!(
|
||||
app.tabs[i].xref_unloaded.is_unloaded(key),
|
||||
"image flag must stay untouched"
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
+42
-42
@@ -4,13 +4,12 @@
|
||||
use super::util::*;
|
||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||
use crate::app::helpers::{
|
||||
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||
CoordKind,
|
||||
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
|
||||
};
|
||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::model::object::GripApply;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::{
|
||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
};
|
||||
@@ -83,9 +82,7 @@ impl OpenCADStudio {
|
||||
.active_grip
|
||||
.as_ref()
|
||||
.map(|grip| match grip.mode {
|
||||
crate::scene::pick::grip::GripEditMode::Stretch => {
|
||||
(grip.origin_world, None)
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::Stretch => (grip.origin_world, None),
|
||||
crate::scene::pick::grip::GripEditMode::Lengthen => {
|
||||
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
||||
}
|
||||
@@ -111,10 +108,18 @@ impl OpenCADStudio {
|
||||
|
||||
if let Some((origin, scalar_role)) = grip_input {
|
||||
let wanted_roles: Vec<crate::command::DynRole> = if rectangle_frame.is_some() {
|
||||
vec![crate::command::DynRole::Width, crate::command::DynRole::Height]
|
||||
vec![
|
||||
crate::command::DynRole::Width,
|
||||
crate::command::DynRole::Height,
|
||||
]
|
||||
} else {
|
||||
scalar_role.map_or_else(
|
||||
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|
||||
|| {
|
||||
vec![
|
||||
crate::command::DynRole::Distance,
|
||||
crate::command::DynRole::Angle,
|
||||
]
|
||||
},
|
||||
|role| vec![role],
|
||||
)
|
||||
};
|
||||
@@ -259,8 +264,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
// Derive the guide + anchor for the legacy field set so the overlay
|
||||
// draws the right construction without each command opting in.
|
||||
let comps: Vec<DynComponent> =
|
||||
self.tabs[i].dyn_fields.iter().map(|f| f.component).collect();
|
||||
let comps: Vec<DynComponent> = self.tabs[i]
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.map(|f| f.component)
|
||||
.collect();
|
||||
self.tabs[i].dyn_guide = match comps.as_slice() {
|
||||
[DynComponent::Distance, DynComponent::Angle] | [DynComponent::Angle] => {
|
||||
crate::command::DynGuide::Polar
|
||||
@@ -278,13 +286,15 @@ impl OpenCADStudio {
|
||||
|
||||
pub(in crate::app) fn apply_dyn_spec(&mut self, i: usize, spec: crate::command::DynSpec) {
|
||||
use crate::app::document::DynFieldEntry;
|
||||
let new_roles: Vec<crate::command::DynRole> =
|
||||
spec.fields.iter().map(|f| f.role).collect();
|
||||
let new_roles: Vec<crate::command::DynRole> = spec.fields.iter().map(|f| f.role).collect();
|
||||
let cur_roles: Vec<crate::command::DynRole> =
|
||||
self.tabs[i].dyn_fields.iter().map(|f| f.role).collect();
|
||||
if cur_roles != new_roles {
|
||||
self.tabs[i].dyn_fields =
|
||||
spec.fields.iter().map(|f| DynFieldEntry::from_role(f.role)).collect();
|
||||
self.tabs[i].dyn_fields = spec
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| DynFieldEntry::from_role(f.role))
|
||||
.collect();
|
||||
self.tabs[i].dyn_active = 0;
|
||||
}
|
||||
self.tabs[i].dyn_guide = spec.guide;
|
||||
@@ -394,12 +404,12 @@ impl OpenCADStudio {
|
||||
let dz = d_ucs.z;
|
||||
let live_d = (dx * dx + dy * dy).sqrt();
|
||||
let live_a = dy.atan2(dx); // radians, in the UCS plane
|
||||
// A typed angle is shown unsigned (0..180); give it the sign of the
|
||||
// cursor's current side so an entry made below the X axis sweeps
|
||||
// downward to match the arc instead of mirroring up. An EXPLICIT
|
||||
// `-`/`+` prefix keeps its literal sign though — "-30" must mean
|
||||
// −30° (= 330°) regardless of where the cursor sits (#417).
|
||||
// Untyped → live.
|
||||
// A typed angle is shown unsigned (0..180); give it the sign of the
|
||||
// cursor's current side so an entry made below the X axis sweeps
|
||||
// downward to match the arc instead of mirroring up. An EXPLICIT
|
||||
// `-`/`+` prefix keeps its literal sign though — "-30" must mean
|
||||
// −30° (= 330°) regardless of where the cursor sits (#417).
|
||||
// Untyped → live.
|
||||
let angle_rad = |idx: usize| -> f64 {
|
||||
let raw = fields[idx]
|
||||
.buffer
|
||||
@@ -420,9 +430,7 @@ impl OpenCADStudio {
|
||||
// perpendicular through the anchor at that offset; the command projects
|
||||
// it. Untyped tracks the cursor's signed offset; typed takes the
|
||||
// cursor's side.
|
||||
if let (Some(ref_pt), [DynComponent::Distance]) =
|
||||
(self.tabs[i].dyn_ref, comps.as_slice())
|
||||
{
|
||||
if let (Some(ref_pt), [DynComponent::Distance]) = (self.tabs[i].dyn_ref, comps.as_slice()) {
|
||||
let axis = (ref_pt - base).normalize_or_zero();
|
||||
let perp = glam::DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let signed = (w - base).dot(perp);
|
||||
@@ -449,22 +457,14 @@ impl OpenCADStudio {
|
||||
Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), 0.0)))
|
||||
}
|
||||
[DynComponent::X, DynComponent::Y] => {
|
||||
Some(xf.to_wcs(glam::DVec3::new(
|
||||
val(0, w_ucs.x),
|
||||
val(1, w_ucs.y),
|
||||
0.0,
|
||||
)))
|
||||
Some(xf.to_wcs(glam::DVec3::new(val(0, w_ucs.x), val(1, w_ucs.y), 0.0)))
|
||||
}
|
||||
[DynComponent::X, DynComponent::Y, DynComponent::Z] if relative => {
|
||||
Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), val(2, dz))))
|
||||
}
|
||||
[DynComponent::X, DynComponent::Y, DynComponent::Z] => {
|
||||
Some(xf.to_wcs(glam::DVec3::new(
|
||||
val(0, w_ucs.x),
|
||||
val(1, w_ucs.y),
|
||||
val(2, w_ucs.z),
|
||||
)))
|
||||
}
|
||||
[DynComponent::X, DynComponent::Y, DynComponent::Z] => Some(xf.to_wcs(
|
||||
glam::DVec3::new(val(0, w_ucs.x), val(1, w_ucs.y), val(2, w_ucs.z)),
|
||||
)),
|
||||
[DynComponent::Distance, DynComponent::Angle] => {
|
||||
let d = val(0, live_d);
|
||||
let a = angle_rad(1);
|
||||
@@ -485,7 +485,11 @@ impl OpenCADStudio {
|
||||
.as_deref()
|
||||
.and_then(eval_dynamic_angle)
|
||||
.unwrap_or(live_a);
|
||||
Some(rel(glam::DVec3::new(live_d * a.cos(), live_d * a.sin(), 0.0)))
|
||||
Some(rel(glam::DVec3::new(
|
||||
live_d * a.cos(),
|
||||
live_d * a.sin(),
|
||||
0.0,
|
||||
)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -641,7 +645,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(in crate::app) fn try_dyn_commit(&mut self) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if !self.dyn_input
|
||||
@@ -818,16 +821,13 @@ mod tests {
|
||||
context.aunits = 2;
|
||||
set_unit_context(context);
|
||||
assert!(
|
||||
(eval_dynamic_angle("2 * 50").unwrap() - std::f64::consts::FRAC_PI_2).abs()
|
||||
< 1e-12
|
||||
(eval_dynamic_angle("2 * 50").unwrap() - std::f64::consts::FRAC_PI_2).abs() < 1e-12
|
||||
);
|
||||
|
||||
context.aunits = 3;
|
||||
set_unit_context(context);
|
||||
assert!(
|
||||
(eval_dynamic_angle("1.5707963267948966").unwrap()
|
||||
- std::f64::consts::FRAC_PI_2)
|
||||
.abs()
|
||||
(eval_dynamic_angle("1.5707963267948966").unwrap() - std::f64::consts::FRAC_PI_2).abs()
|
||||
< 1e-12
|
||||
);
|
||||
|
||||
|
||||
+428
-157
File diff suppressed because it is too large.
Load diff
+128
-83
@@ -90,9 +90,9 @@ fn reorder_insertion_index(from: usize, to: usize, after: bool, len: usize) -> O
|
||||
(insertion != from).then_some(insertion)
|
||||
}
|
||||
|
||||
mod blocks_palette;
|
||||
mod command;
|
||||
mod context_menu;
|
||||
mod blocks_palette;
|
||||
mod dialog;
|
||||
mod dynamic;
|
||||
mod file;
|
||||
@@ -351,7 +351,8 @@ impl OpenCADStudio {
|
||||
2 => "ATTMODE2",
|
||||
_ => "ATTMODE1",
|
||||
};
|
||||
self.ribbon.set_dropdown_current("ATTMODE_DROPDOWN", attmode);
|
||||
self.ribbon
|
||||
.set_dropdown_current("ATTMODE_DROPDOWN", attmode);
|
||||
}
|
||||
|
||||
fn update_message(&mut self, msg: Message) -> Task<Message> {
|
||||
@@ -381,8 +382,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
// Esc backs out of a pending ALIASEDIT draft first, mirroring
|
||||
// the shortcut editor's capture cancel; the next Esc closes.
|
||||
if self.active_modal == Some(super::ModalKind::Aliases) && self.alias_pending_add
|
||||
{
|
||||
if self.active_modal == Some(super::ModalKind::Aliases) && self.alias_pending_add {
|
||||
return self.update(Message::AliasEditorDraftCancel);
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockDefinition) {
|
||||
@@ -444,9 +444,12 @@ impl OpenCADStudio {
|
||||
// a mouse move included — without allocating.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let active_command = |app: &Self| -> Option<(u64, Option<&'static str>)> {
|
||||
app.tabs
|
||||
.get(app.active_tab)
|
||||
.map(|tab| (tab.id, tab.active_cmd.as_ref().map(|command| command.name())))
|
||||
app.tabs.get(app.active_tab).map(|tab| {
|
||||
(
|
||||
tab.id,
|
||||
tab.active_cmd.as_ref().map(|command| command.name()),
|
||||
)
|
||||
})
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let command_before = active_command(self);
|
||||
@@ -561,9 +564,8 @@ impl OpenCADStudio {
|
||||
Message::SpaceMouseWake => self.on_spacemouse_wake(),
|
||||
Message::TrackpadPinch(magnification) => self.on_pinch_zoom(magnification),
|
||||
Message::SpaceMouseFrame(time) => {
|
||||
self.spacemouse.frame(
|
||||
time.saturating_duration_since(self.start).as_secs_f64() * 1000.,
|
||||
);
|
||||
self.spacemouse
|
||||
.frame(time.saturating_duration_since(self.start).as_secs_f64() * 1000.);
|
||||
Task::none()
|
||||
}
|
||||
Message::SpaceMouseFocus(id, focused) => {
|
||||
@@ -615,9 +617,8 @@ impl OpenCADStudio {
|
||||
if response["ok"] == false {
|
||||
let code = response["code"].as_str().unwrap_or("failed");
|
||||
let detail = response["error"].as_str().unwrap_or("");
|
||||
self.command_line.push_error(&format!(
|
||||
"automation {op}: {code} — {detail}"
|
||||
));
|
||||
self.command_line
|
||||
.push_error(&format!("automation {op}: {code} — {detail}"));
|
||||
} else if matches!(
|
||||
response["status"].as_str(),
|
||||
Some("completed" | "cancelled" | "waiting_input")
|
||||
@@ -963,7 +964,8 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.menu.open_at = None;
|
||||
.menu
|
||||
.open_at = None;
|
||||
// Same guard as typed MTP/M2P: point step, not entity pick.
|
||||
let i = self.active_tab;
|
||||
let allowed = self.tabs[i].active_cmd.as_ref().is_some_and(|c| {
|
||||
@@ -1223,19 +1225,23 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ImageEmbedPick => {
|
||||
Task::perform(crate::io::pick_embedded_image_file(), Message::ImageEmbedPickResult)
|
||||
}
|
||||
Message::ImageEmbedPick => Task::perform(
|
||||
crate::io::pick_embedded_image_file(),
|
||||
Message::ImageEmbedPickResult,
|
||||
),
|
||||
|
||||
Message::ImageEmbedPickResult(Ok(image)) => {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::draw::raster_image::ImageCommand;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"IMAGEEMBED \"{name}\": {w}×{h} px (embedded)",
|
||||
name = image.name.as_str(),
|
||||
w = image.pixel_width,
|
||||
h = image.pixel_height,
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"IMAGEEMBED \"{name}\": {w}×{h} px (embedded)",
|
||||
name = image.name.as_str(),
|
||||
w = image.pixel_width,
|
||||
h = image.pixel_height,
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
let cmd = ImageCommand::new_embedded(image);
|
||||
let i = self.active_tab;
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
@@ -1322,10 +1328,13 @@ impl OpenCADStudio {
|
||||
.insert(crate::io::font_repo::font_key(name));
|
||||
}
|
||||
for (name, path) in &pairs {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"FONT Downloaded {name} → {path}",
|
||||
path = path.display()
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"FONT Downloaded {name} → {path}",
|
||||
path = path.display()
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
if !unavailable.is_empty() {
|
||||
self.command_line.push_info(crate::tf!(
|
||||
@@ -1341,14 +1350,16 @@ impl OpenCADStudio {
|
||||
if let Some(path) = path {
|
||||
return Task::done(Message::OpenExternal(path));
|
||||
}
|
||||
self.command_line.push_info(crate::t!(
|
||||
"Save and reopen the drawing to apply the new fonts."
|
||||
).as_ref());
|
||||
self.command_line.push_info(
|
||||
crate::t!("Save and reopen the drawing to apply the new fonts.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// Keep the prompt open: the user can correct a custom
|
||||
// source or retry a transient network failure.
|
||||
self.command_line.push_error(crate::tf!("Font download failed: {e}").as_ref());
|
||||
self.command_line
|
||||
.push_error(crate::tf!("Font download failed: {e}").as_ref());
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
@@ -1886,9 +1897,9 @@ impl OpenCADStudio {
|
||||
self.refresh_layer_panel();
|
||||
self.sync_ribbon_styles();
|
||||
}
|
||||
Err(error) => self.command_line.push_error(
|
||||
error["error"].as_str().unwrap_or("template"),
|
||||
),
|
||||
Err(error) => self
|
||||
.command_line
|
||||
.push_error(error["error"].as_str().unwrap_or("template")),
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
@@ -2700,16 +2711,13 @@ impl OpenCADStudio {
|
||||
};
|
||||
let new_raw = path.to_string_lossy().into_owned();
|
||||
self.push_undo_snapshot(i, "XREF-PATH");
|
||||
match crate::io::xref::set_ref_path(
|
||||
&mut self.tabs[i].scene.document,
|
||||
key,
|
||||
&new_raw,
|
||||
) {
|
||||
match crate::io::xref::set_ref_path(&mut self.tabs[i].scene.document, key, &new_raw)
|
||||
{
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: Path set for \"{}\" — Reload to apply.",
|
||||
name
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!("XREF: Path set for \"{}\" — Reload to apply.", name)
|
||||
.as_ref(),
|
||||
);
|
||||
self.post_ref_op(i);
|
||||
}
|
||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
||||
@@ -2719,7 +2727,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
Message::XrefPathPickResult(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
self.command_line.push_error(crate::tf!("XREF: {e}").as_ref());
|
||||
self.command_line
|
||||
.push_error(crate::tf!("XREF: {e}").as_ref());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -2744,9 +2753,11 @@ impl OpenCADStudio {
|
||||
if let Some(found) = entry.found_at.clone() {
|
||||
let is_dwg = found.to_ascii_lowercase().ends_with(".dwg")
|
||||
|| found.to_ascii_lowercase().ends_with(".dxf");
|
||||
self.command_line.push_output(crate::tf!("XOPEN: opening \"{}\".", found).as_ref());
|
||||
self.command_line
|
||||
.push_output(crate::tf!("XOPEN: opening \"{}\".", found).as_ref());
|
||||
if is_dwg {
|
||||
return self.update(Message::OpenRecent(std::path::PathBuf::from(found)));
|
||||
return self
|
||||
.update(Message::OpenRecent(std::path::PathBuf::from(found)));
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = open::that_detached(&found);
|
||||
@@ -2782,7 +2793,10 @@ impl OpenCADStudio {
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
let prefill = if let Some(entry) = self.xref_manager.entries.get(index) {
|
||||
let saved = &entry.saved_path;
|
||||
if let Some(parent) = std::path::Path::new(saved).parent().and_then(|p| p.to_str()) {
|
||||
if let Some(parent) = std::path::Path::new(saved)
|
||||
.parent()
|
||||
.and_then(|p| p.to_str())
|
||||
{
|
||||
if !parent.is_empty() {
|
||||
format!("XREF Path Find \"{}\" ", parent)
|
||||
} else {
|
||||
@@ -3124,20 +3138,15 @@ impl OpenCADStudio {
|
||||
super::LayerStateProperty::Frozen => codec::LayerStateMask::FROZEN,
|
||||
super::LayerStateProperty::Locked => codec::LayerStateMask::LOCKED,
|
||||
super::LayerStateProperty::Plot => codec::LayerStateMask::PLOT,
|
||||
super::LayerStateProperty::NewViewport => {
|
||||
codec::LayerStateMask::NEW_VIEWPORT
|
||||
}
|
||||
super::LayerStateProperty::NewViewport => codec::LayerStateMask::NEW_VIEWPORT,
|
||||
super::LayerStateProperty::Color => codec::LayerStateMask::COLOR,
|
||||
super::LayerStateProperty::LineType => codec::LayerStateMask::LINE_TYPE,
|
||||
super::LayerStateProperty::LineWeight => codec::LayerStateMask::LINE_WEIGHT,
|
||||
super::LayerStateProperty::PlotStyle => codec::LayerStateMask::PLOT_STYLE,
|
||||
super::LayerStateProperty::Transparency => {
|
||||
codec::LayerStateMask::TRANSPARENCY
|
||||
}
|
||||
super::LayerStateProperty::Transparency => codec::LayerStateMask::TRANSPARENCY,
|
||||
};
|
||||
if let Some(state) = self.layer_state_edit_draft.as_mut() {
|
||||
state.mask =
|
||||
codec::LayerStateMask::from_bits(state.mask.bits() ^ flag.bits());
|
||||
state.mask = codec::LayerStateMask::from_bits(state.mask.bits() ^ flag.bits());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -3755,7 +3764,8 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.input.last_move_pos = Some(p);
|
||||
.input
|
||||
.last_move_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
self.focus_model_pane(idx);
|
||||
@@ -3852,7 +3862,8 @@ impl OpenCADStudio {
|
||||
let was_click = !sel.input.right_dragging;
|
||||
// How long the button was held, for the time-sensitive mode.
|
||||
let held_ms = sel
|
||||
.input.right_press_time
|
||||
.input
|
||||
.right_press_time
|
||||
.map_or(0, |t| t.elapsed().as_millis() as i32);
|
||||
sel.input.right_down = false;
|
||||
sel.input.right_press_pos = None;
|
||||
@@ -4044,7 +4055,8 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.selection
|
||||
.borrow()
|
||||
.input.last_move_pos
|
||||
.input
|
||||
.last_move_pos
|
||||
.unwrap_or(self.cursor_pos);
|
||||
self.update_grip_hover(i, p);
|
||||
Task::none()
|
||||
@@ -4439,7 +4451,9 @@ impl OpenCADStudio {
|
||||
continue;
|
||||
};
|
||||
let current = (
|
||||
crate::scene::pe_url_of(entity).unwrap_or_default().to_owned(),
|
||||
crate::scene::pe_url_of(entity)
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
crate::scene::pe_url_description_of(entity)
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
@@ -4891,7 +4905,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.refresh_properties();
|
||||
crate::entities::common::set_unit_context(
|
||||
crate::entities::common::UnitContext::from_header(&self.tabs[i].scene.document.header),
|
||||
crate::entities::common::UnitContext::from_header(
|
||||
&self.tabs[i].scene.document.header,
|
||||
),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
@@ -5050,12 +5066,14 @@ impl OpenCADStudio {
|
||||
};
|
||||
let name = state.name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
state.error_message = Some(crate::t!("Block name cannot be empty.").into_owned());
|
||||
state.error_message =
|
||||
Some(crate::t!("Block name cannot be empty.").into_owned());
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(_ch) = state.invalid_name_char() {
|
||||
state.error_message = Some(
|
||||
crate::t!("Block name cannot contain: \\ / : * ? \" < > | = `").into_owned(),
|
||||
crate::t!("Block name cannot contain: \\ / : * ? \" < > | = `")
|
||||
.into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
@@ -5066,13 +5084,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
if !state.objects_specify_onscreen && state.selected_handles.is_empty() {
|
||||
state.error_message = Some(
|
||||
crate::t!("No objects selected. You must select objects to define a block.")
|
||||
.into_owned(),
|
||||
crate::t!(
|
||||
"No objects selected. You must select objects to define a block."
|
||||
)
|
||||
.into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.document.block_records.get(&name).is_some() {
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&name)
|
||||
.is_some()
|
||||
{
|
||||
state.confirm_redefine = Some(name);
|
||||
return Task::none();
|
||||
}
|
||||
@@ -5098,12 +5124,16 @@ impl OpenCADStudio {
|
||||
let current_path = std::path::Path::new(&state.file_path);
|
||||
let file_stem = current_path.file_stem().and_then(|s| s.to_str());
|
||||
let is_default_or_block = file_stem == Some("new_block")
|
||||
|| state.existing_blocks.iter().any(|b| Some(b.as_str()) == file_stem);
|
||||
|| state
|
||||
.existing_blocks
|
||||
.iter()
|
||||
.any(|b| Some(b.as_str()) == file_stem);
|
||||
if is_default_or_block {
|
||||
let new_file_name = format!("{}.dwg", trimmed);
|
||||
if let Some(parent) = current_path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
state.file_path = parent.join(new_file_name).to_string_lossy().to_string();
|
||||
state.file_path =
|
||||
parent.join(new_file_name).to_string_lossy().to_string();
|
||||
} else {
|
||||
state.file_path = new_file_name;
|
||||
}
|
||||
@@ -5224,12 +5254,19 @@ impl OpenCADStudio {
|
||||
WblockSourceMode::Block => {
|
||||
let name = state.block_name.trim();
|
||||
if name.is_empty() {
|
||||
state.error_message =
|
||||
Some(crate::t!("Please select or enter a block name.").into_owned());
|
||||
state.error_message = Some(
|
||||
crate::t!("Please select or enter a block name.").into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.document.block_records.get(name).is_none() {
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(name)
|
||||
.is_none()
|
||||
{
|
||||
state.error_message = Some(
|
||||
crate::tf!("Block \"{}\" does not exist in drawing.", name)
|
||||
.into_owned(),
|
||||
@@ -5354,10 +5391,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
WblockSourceMode::Objects => {
|
||||
crate::modules::insert::wblock::extract_entities_to_doc_with_base(
|
||||
&document,
|
||||
&handles,
|
||||
base_point,
|
||||
unit,
|
||||
&document, &handles, base_point, unit,
|
||||
)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
@@ -5416,7 +5450,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.refresh_properties();
|
||||
crate::entities::common::set_unit_context(
|
||||
crate::entities::common::UnitContext::from_header(&self.tabs[i].scene.document.header),
|
||||
crate::entities::common::UnitContext::from_header(
|
||||
&self.tabs[i].scene.document.header,
|
||||
),
|
||||
);
|
||||
let label = crate::modules::draw::units::linear_format_label(code);
|
||||
self.command_line
|
||||
@@ -5746,8 +5782,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::AutoConstrainReset => {
|
||||
self.auto_constrain_settings =
|
||||
super::settings::AutoConstrainSettings::default();
|
||||
self.auto_constrain_settings = super::settings::AutoConstrainSettings::default();
|
||||
self.auto_constrain_selected_row = 0;
|
||||
self.auto_constrain_distance_input =
|
||||
format!("{}", self.auto_constrain_settings.distance_tolerance);
|
||||
@@ -5786,8 +5821,7 @@ impl OpenCADStudio {
|
||||
self.auto_constrain_settings.distance_tolerance = distance;
|
||||
self.auto_constrain_settings.angle_tolerance_deg = angle;
|
||||
self.auto_constrain_settings.sanitize();
|
||||
self.auto_constrain_saved =
|
||||
Some(self.auto_constrain_settings.clone());
|
||||
self.auto_constrain_saved = Some(self.auto_constrain_settings.clone());
|
||||
self.persist_settings_if_changed();
|
||||
if matches!(action, Message::AutoConstrainOk) {
|
||||
self.close_active_modal();
|
||||
@@ -8657,7 +8691,9 @@ impl OpenCADStudio {
|
||||
Message::CloseModal => {
|
||||
// The Invalid Area dialog's ✕ undoes the boundary change, as its Cancel does.
|
||||
if self.active_modal == Some(super::ModalKind::CountInvalidArea) {
|
||||
return self.update(Message::Count(crate::ui::window::count_palette::CountMsg::InvalidUndo));
|
||||
return self.update(Message::Count(
|
||||
crate::ui::window::count_palette::CountMsg::InvalidUndo,
|
||||
));
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::SheetSet) {
|
||||
self.close_sheet_set_dialog();
|
||||
@@ -8674,7 +8710,11 @@ impl OpenCADStudio {
|
||||
self.attdef_dialog = None;
|
||||
let editing = self.attdef_edit.take().is_some();
|
||||
self.close_active_modal();
|
||||
return if editing { self.post_editor_closed(false) } else { Task::none() };
|
||||
return if editing {
|
||||
self.post_editor_closed(false)
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
}
|
||||
// Cancelling the section settings drops the waiting extraction.
|
||||
if self.active_modal == Some(super::ModalKind::PcSection) {
|
||||
@@ -11230,7 +11270,9 @@ mod free_text_entry_tests {
|
||||
app.clipboard = vec![codec::EntityType::Line(codec::entities::Line::default())];
|
||||
|
||||
// A dialog field pasted natively: no command-line text, no PASTECLIP.
|
||||
let _ = app.update(Message::PasteShortcutResolved(crate::app::PasteFocus::Field));
|
||||
let _ = app.update(Message::PasteShortcutResolved(
|
||||
crate::app::PasteFocus::Field,
|
||||
));
|
||||
assert_eq!(app.command_line.input, "EXISTING");
|
||||
assert!(app.tabs[0].active_cmd.is_none());
|
||||
}
|
||||
@@ -11249,7 +11291,10 @@ mod free_text_entry_tests {
|
||||
// Copied objects paste even though the command line holds focus.
|
||||
app.clipboard = vec![codec::EntityType::Line(codec::entities::Line::default())];
|
||||
let _ = app.update(Message::PasteShortcutResolved(PasteFocus::CommandLine));
|
||||
assert!(app.tabs[0].active_cmd.as_ref().is_some_and(|cmd| cmd.name() == "PASTECLIP"));
|
||||
assert!(app.tabs[0]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|cmd| cmd.name() == "PASTECLIP"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.import_draft = Some(match result {
|
||||
Ok((file, setups)) => PageSetupImportDraft {
|
||||
file,
|
||||
setups: setups.into_iter().map(|(name, ps)| (name, ps, true)).collect(),
|
||||
setups: setups
|
||||
.into_iter()
|
||||
.map(|(name, ps)| (name, ps, true))
|
||||
.collect(),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => PageSetupImportDraft {
|
||||
@@ -179,8 +182,7 @@ impl OpenCADStudio {
|
||||
match available.iter().find(|(n, _)| n.eq_ignore_ascii_case(want)) {
|
||||
Some(found) => chosen.push(found.clone()),
|
||||
None => self.command_line.push_error(
|
||||
crate::tf!("PSETUPIN: no page setup named \"{want}\" in {file}.")
|
||||
.as_ref(),
|
||||
crate::tf!("PSETUPIN: no page setup named \"{want}\" in {file}.").as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -214,7 +216,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn last_line(app: &OpenCADStudio) -> String {
|
||||
app.command_line.history.last().map(|line| line.text.clone()).unwrap_or_default()
|
||||
app.command_line
|
||||
.history
|
||||
.last()
|
||||
.map(|line| line.text.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The fixture's page setups as the chooser would hold them, read
|
||||
@@ -281,7 +287,10 @@ mod tests {
|
||||
let site = app.tabs[i].scene.page_setup_get("Site").unwrap();
|
||||
assert_eq!(site.paper_size, "ISO_A3_(297.00_x_420.00_MM)");
|
||||
assert_eq!(site.rotation, PlotRotation::Degrees90);
|
||||
assert!(site.plot_view_handle.is_null(), "foreign handles are dropped");
|
||||
assert!(
|
||||
site.plot_view_handle.is_null(),
|
||||
"foreign handles are dropped"
|
||||
);
|
||||
assert!(app.tabs[i].dirty);
|
||||
// Undo takes the whole import back.
|
||||
let _ = app.update(Message::Undo);
|
||||
@@ -314,20 +323,40 @@ mod tests {
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Loaded(loaded)));
|
||||
let draft = app.plot_dialog.import_draft.as_ref().expect("chooser open");
|
||||
assert_eq!(draft.file, "other.dwg");
|
||||
assert_eq!(draft.selected().count(), 3, "everything ticked to start with");
|
||||
assert_eq!(
|
||||
draft.selected().count(),
|
||||
3,
|
||||
"everything ticked to start with"
|
||||
);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::All(false)));
|
||||
assert_eq!(app.plot_dialog.import_draft.as_ref().unwrap().selected().count(), 0);
|
||||
assert_eq!(
|
||||
app.plot_dialog
|
||||
.import_draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected()
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Toggle("B".into())));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Toggle("C".into())));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Apply));
|
||||
assert!(app.plot_dialog.import_draft.is_none(), "Apply closes the chooser");
|
||||
assert!(
|
||||
app.plot_dialog.import_draft.is_none(),
|
||||
"Apply closes the chooser"
|
||||
);
|
||||
assert_eq!(app.tabs[i].scene.page_setup_names(), ["B", "C"]);
|
||||
assert_eq!(app.plot_dialog.selected_setup, "B");
|
||||
assert!(app.plot_dialog.page_setups.iter().any(|name| name == "C"));
|
||||
// A failed read shows its reason in the chooser; Cancel closes it.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Loaded(Err("unreadable".into()))));
|
||||
assert_eq!(
|
||||
app.plot_dialog.import_draft.as_ref().unwrap().error.as_deref(),
|
||||
app.plot_dialog
|
||||
.import_draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.error
|
||||
.as_deref(),
|
||||
Some("unreadable")
|
||||
);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Import(I::Cancel));
|
||||
@@ -340,7 +369,10 @@ mod tests {
|
||||
let _ = app.update(Message::LayoutCreate);
|
||||
assert_ne!(app.active_modal, Some(crate::app::ModalKind::Plot));
|
||||
let _ = app.update(Message::PageSetupOnNewLayoutChanged(true));
|
||||
assert!(app.current_config().plot.page_setup_on_new_layout, "persisted");
|
||||
assert!(
|
||||
app.current_config().plot.page_setup_on_new_layout,
|
||||
"persisted"
|
||||
);
|
||||
let _ = app.update(Message::LayoutCreate);
|
||||
assert_eq!(app.active_modal, Some(crate::app::ModalKind::Plot));
|
||||
assert_eq!(app.plot_dialog.selected_setup, "*Layout3*");
|
||||
|
||||
+569
-575
File diff suppressed because it is too large.
Load diff
@@ -15,8 +15,7 @@ pub(super) fn sync_annotation_scale_header(scene: &mut Scene) {
|
||||
};
|
||||
let current = scene.document.header.current_annotation_scale.clone();
|
||||
let current_matches = scene.scale_list().into_iter().any(|(name, factor, _)| {
|
||||
name.eq_ignore_ascii_case(¤t)
|
||||
&& (factor - anno).abs() < 0.001 * anno.max(0.001)
|
||||
name.eq_ignore_ascii_case(¤t) && (factor - anno).abs() < 0.001 * anno.max(0.001)
|
||||
});
|
||||
let name = if current_matches {
|
||||
current
|
||||
|
||||
+559
-256
File diff suppressed because it is too large.
Load diff
@@ -233,8 +233,7 @@ impl OpenCADStudio {
|
||||
let saved_grid = self.snapper.grid_snap_on;
|
||||
let saved_from = self.snapper.from_point;
|
||||
self.snapper.grid_snap_on = false;
|
||||
self.snapper.from_point =
|
||||
saved_from.map(|p| frame.paper_to_model(p));
|
||||
self.snapper.from_point = saved_from.map(|p| frame.paper_to_model(p));
|
||||
let hit = self.snapper.snap(
|
||||
model_cursor,
|
||||
local,
|
||||
|
||||
Reference in new issue
Block a user