Merge main into codex/graphic-attributes-cleanup

Conflicts were additions on both sides, kept together: the Count palette
and Sheet Set toggles next to the Graphic Attributes toggle in the ribbon,
main's new locale groups and catalog entries next to graphic-attributes,
and module declarations. The palette code follows main's crate renames
(acadrust -> codec, cadkernel -> kernel).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VgkJN8DEojuPTumAXLHotu
This commit is contained in:
Claude committed 2026-10-07 20:11:05 +00:00
commit 7a03e1c43c
625 files changed
+104397 -28053

No files matched your search

+607
View File
@@ -0,0 +1,607 @@
//! The Blocks palette on the app side: tabs, the recent / favorite lists,
//! library folders, the insertion options, click and drag insertion, and the
//! block context menu.
use std::path::{Path, PathBuf};
use crate::app::settings::PaletteBlockRef;
use crate::app::{Message, OpenCADStudio};
use crate::modules::insert::insert_block::{BlockCatalog, InsertBlockCommand};
use crate::ui::window::block_palette::{
BlockEntry, BlockPaletteMsg, Item, LibraryEntry, MenuAction, OptionMsg, Tab,
};
use iced::Task;
/// Library folders remembered in the Libraries list.
const MAX_LIBRARIES: usize = 10;
fn now_secs() -> u64 {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(target_arch = "wasm32")]
{
0
}
}
fn file_stem(path: &Path) -> String {
path.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Block".to_string())
}
fn is_drawing(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("dwg") || e.eq_ignore_ascii_case("dxf"))
}
impl OpenCADStudio {
pub(super) fn on_block_palette(&mut self, m: BlockPaletteMsg) -> Task<Message> {
match m {
BlockPaletteMsg::Search(s) => self.block_palette.search = s,
BlockPaletteMsg::Tab(tab) => self.show_block_palette_tab(tab),
BlockPaletteMsg::View(v) => {
self.block_palette.view = v;
self.persist_settings_if_changed();
}
BlockPaletteMsg::Refresh => self.refresh_block_palette(),
BlockPaletteMsg::PickFile => {
return Task::perform(
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"])
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
|r| Message::BlockPalette(BlockPaletteMsg::FilePicked(r)),
);
}
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);
}
}
BlockPaletteMsg::Option(o) => self.on_block_option(o),
BlockPaletteMsg::ToggleOptions => {
self.block_palette.options_collapsed ^= true;
}
// Browsers expose no folder picker.
#[cfg(target_arch = "wasm32")]
BlockPaletteMsg::LibraryBrowse => {}
#[cfg(not(target_arch = "wasm32"))]
BlockPaletteMsg::LibraryBrowse => {
return Task::perform(
async {
match rfd::AsyncFileDialog::new()
.set_title(crate::t!("Select a Block Library Folder").as_ref())
.pick_folder()
.await
{
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
|r| Message::BlockPalette(BlockPaletteMsg::LibraryPicked(r)),
);
}
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()) {
let parent = parent.to_path_buf();
self.list_block_library(parent);
}
}
BlockPaletteMsg::Press(item) => self.block_palette.pressed = Some(item),
BlockPaletteMsg::Release(item) => {
if self.block_palette.pressed.take().as_ref() == Some(&item) {
return match item {
Item::Folder(dir) => {
self.list_block_library(dir);
Task::none()
}
item => self.palette_insert(item, None),
};
}
}
BlockPaletteMsg::Menu(item, action) => return self.on_block_menu(item, action),
}
Task::none()
}
/// BLOCKSPALETTE: open (expanded) on `tab`, or on the tab it was left on.
pub(crate) fn open_blocks_palette(&mut self, tab: Option<Tab>) {
self.show_block_palette = true;
self.dock_expanded = Some(crate::ui::dock::PanelId::BlockPalette);
self.show_block_palette_tab(tab.unwrap_or(self.block_palette.tab));
}
fn show_block_palette_tab(&mut self, tab: Tab) {
self.block_palette.tab = tab;
if tab == Tab::Libraries && self.block_palette.library_dir.is_none() {
let start = Some(self.block_navigate.trim())
.filter(|p| !p.is_empty() && *p != "." && Path::new(p).is_dir())
.map(PathBuf::from)
.or_else(|| self.block_palette.libraries.first().map(PathBuf::from));
if let Some(dir) = start {
self.list_block_library(dir);
}
}
self.refresh_block_palette();
}
fn open_block_library(&mut self, root: PathBuf) {
let key = root.display().to_string();
let libraries = &mut self.block_palette.libraries;
libraries.retain(|l| l != &key);
libraries.insert(0, key);
libraries.truncate(MAX_LIBRARIES);
self.list_block_library(root);
self.persist_settings_if_changed();
}
/// Show `dir`: its sub-folders, then its drawings, each by name.
fn list_block_library(&mut self, dir: PathBuf) {
let mut entries: Vec<LibraryEntry> = std::fs::read_dir(&dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let path = e.path();
// Hidden folders (".name") hold tool data, not blocks.
if e.file_name().to_string_lossy().starts_with('.') {
return None;
}
if path.is_dir() {
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 })
} else {
None
}
})
.collect();
entries.sort_by_key(|e| {
(!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;
}
fn on_block_option(&mut self, o: OptionMsg) {
let p = &mut self.block_palette;
let number = |t: &str| t.trim().parse::<f64>().ok().filter(|v| v.is_finite());
match o {
OptionMsg::InsertionPoint(v) => p.options.insertion_point = v,
OptionMsg::Scale(v) => p.options.scale = v,
OptionMsg::Uniform(v) => p.options.uniform = v,
OptionMsg::Rotation(v) => p.options.rotation = v,
OptionMsg::AutoPlacement(v) => p.options.auto_placement = v,
OptionMsg::Repeat(v) => p.options.repeat = v,
OptionMsg::Explode(v) => p.options.explode = v,
OptionMsg::X(t) => {
if let Some(v) = number(&t).filter(|v| *v != 0.0) {
p.options.x = v;
}
p.option_text[0] = t;
}
OptionMsg::Y(t) => {
if let Some(v) = number(&t).filter(|v| *v != 0.0) {
p.options.y = v;
}
p.option_text[1] = t;
}
OptionMsg::Z(t) => {
if let Some(v) = number(&t).filter(|v| *v != 0.0) {
p.options.z = v;
}
p.option_text[2] = t;
}
OptionMsg::Angle(t) => {
if let Some(v) = number(&t) {
p.options.angle = v;
}
p.option_text[3] = t;
}
}
self.persist_settings_if_changed();
}
/// Start placing `name` as a palette click would.
#[cfg(test)]
pub(super) fn start_block_placement(&mut self, name: &str) {
let _ = self.start_palette_insert(name, None);
}
fn start_palette_insert(&mut self, name: &str, drop_at: Option<glam::DVec3>) -> Task<Message> {
use crate::command::CadCommand;
let i = self.active_tab;
let wires = self
.block_palette
.blocks
.iter()
.find(|b| b.name.eq_ignore_ascii_case(name))
.map(|b| b.wires.clone())
.unwrap_or_else(|| self.tabs[i].scene.block_preview_wires(name));
let mut options = self.block_palette.options.clone();
if drop_at.is_some() {
// A dropped block goes in where it lands with the fixed values.
options.insertion_point = false;
options.scale = false;
options.rotation = false;
options.repeat = false;
}
let mut cmd = InsertBlockCommand::palette(name.to_string(), wires, &options);
if let Some(point) = drop_at {
cmd.set_point(point);
}
self.reset_command_start_state(i);
let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
cmd.set_working_plane(plane);
let first = cmd.initial();
let prompt = cmd.prompt();
self.tabs[i].active_cmd = Some(Box::new(cmd));
self.block_palette.placing = Some(name.to_string());
match first {
Some(result) => self.apply_cmd_result(result),
None => {
self.command_line.push_info(&prompt);
Task::none()
}
}
}
/// 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> {
match self.resolve_palette_block(&item) {
Ok(name) => {
self.refresh_block_palette();
self.start_palette_insert(&name, drop_at)
}
Err(e) => {
if !e.is_empty() {
self.command_line.push_error(&e);
}
Task::none()
}
}
}
fn block_exists(&self, name: &str) -> bool {
self.tabs[self.active_tab].scene.document.block_records.get(name).is_some()
}
fn current_drawing_path(&self) -> String {
self.tabs[self.active_tab]
.current_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default()
}
fn resolve_palette_block(&mut self, item: &Item) -> Result<String, String> {
match item {
Item::Current(name) => Ok(name.clone()),
Item::Ref(r) if r.drawing => self.block_from_drawing(PathBuf::from(&r.source)),
Item::Ref(r) => {
let elsewhere = !r.source.is_empty() && r.source != self.current_drawing_path();
if self.block_exists(&r.name) && !(elsewhere && self.block_redefine_mode == 2) {
return Ok(r.name.clone());
}
if !elsewhere {
return Err(crate::tf!("Block \"{}\" not found.", r.name).into_owned());
}
let doc = crate::io::load_file(Path::new(&r.source)).map_err(|e| e.to_string())?;
let redefine = self.block_exists(&r.name);
self.import_block_definition(&doc, &r.name, redefine)?;
Ok(r.name.clone())
}
Item::File(path) => self.block_from_drawing(path.clone()),
Item::Folder(_) => Err(String::new()),
}
}
/// The block standing for drawing `path`: the drawing's own block when
/// the drawing already has one of that name (redefined from the file
/// under BLOCKREDEFINEMODE 2), else the file inserted as a new block.
fn block_from_drawing(&mut self, path: PathBuf) -> Result<String, String> {
let stem = file_stem(&path);
let name = if self.block_exists(&stem) && self.block_redefine_mode != 2 {
stem
} else {
self.import_drawing_block(path.clone(), true)?
};
self.block_palette
.file_sources
.insert(name.to_ascii_uppercase(), path.display().to_string());
Ok(name)
}
/// Bring block `name` (and what it needs) from `source` into the active
/// drawing; with `redefine` an existing definition takes the source's.
fn import_block_definition(
&mut self,
source: &codec::CadDocument,
name: &str,
redefine: bool,
) -> Result<(), String> {
if source.block_records.get(name).is_none() {
return Err(crate::tf!("Block \"{}\" not found.", name).into_owned());
}
let probe = codec::EntityType::Insert(codec::entities::Insert::new(
name.to_string(),
codec::types::Vector3::ZERO,
));
let deps = crate::app::ClipboardDeps::capture(source, std::slice::from_ref(&probe));
let i = self.active_tab;
self.push_undo_snapshot(i, "INSERT");
self.merge_dependencies(i, &deps);
for def in deps.blocks {
let scene = &mut self.tabs[i].scene;
if redefine && def.name.eq_ignore_ascii_case(name) {
scene.redefine_block_raw(&def.name, def.base_point, def.entities);
} else if scene.document.block_records.get(&def.name).is_none() {
scene.define_block_raw(&def.name, def.base_point, def.entities);
}
}
self.tabs[i].scene.populate_meshes_from_document();
self.tabs[i].dirty = true;
Ok(())
}
fn palette_ref(&self, item: &Item) -> Option<PaletteBlockRef> {
let time = now_secs();
match item {
Item::Current(name) => Some(PaletteBlockRef {
name: name.clone(),
source: self.current_drawing_path(),
time,
drawing: false,
}),
Item::Ref(r) => Some(r.clone()),
Item::File(path) => Some(PaletteBlockRef {
name: file_stem(path),
source: path.display().to_string(),
time,
drawing: true,
}),
Item::Folder(_) => None,
}
}
fn on_block_menu(&mut self, item: Item, action: MenuAction) -> Task<Message> {
let same = |a: &PaletteBlockRef, b: &PaletteBlockRef| {
a.name.eq_ignore_ascii_case(&b.name) && a.source == b.source
};
match action {
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::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()),
_ => return Task::none(),
};
match result {
Ok(name) => {
self.command_line
.push_output(&crate::tf!("Block \"{name}\" redefined."));
self.refresh_block_palette();
}
Err(e) => self.command_line.push_error(&e),
}
}
MenuAction::Favorite => {
if let Some(r) = self.palette_ref(&item) {
let favorites = &mut self.block_palette.favorites;
favorites.retain(|f| !same(f, &r));
favorites.insert(0, r);
self.persist_settings_if_changed();
self.refresh_block_palette();
}
}
MenuAction::Unfavorite => {
if let Some(r) = self.palette_ref(&item) {
self.block_palette.favorites.retain(|f| !same(f, &r));
self.persist_settings_if_changed();
}
}
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,
});
match reference {
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."
)),
}
}
MenuAction::Remove => match self.block_palette.tab {
Tab::Current => {
let name = item.name();
let i = self.active_tab;
let referenced = self.tabs[i].scene.document.entities().any(|e| {
matches!(e, codec::EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&name))
});
if referenced {
self.command_line.push_error(&crate::tf!(
"Block \"{name}\" is referenced in the drawing and cannot be removed."
));
} else {
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.refresh_block_palette();
}
}
Tab::Recent | Tab::Favorites => {
if let Some(r) = self.palette_ref(&item) {
let list = if self.block_palette.tab == Tab::Recent {
&mut self.block_palette.recent
} else {
&mut self.block_palette.favorites
};
list.retain(|f| !same(f, &r));
self.persist_settings_if_changed();
}
}
Tab::Libraries => {}
},
}
Task::none()
}
/// A press in the drawing ends any palette press; a release there with
/// a block still pressed drops it at the cursor.
pub(super) fn take_block_drop(&mut self) -> Option<Item> {
self.block_palette.pressed.take()
}
pub(super) fn drop_block(&mut self, item: Item) -> Task<Message> {
let at = self.tabs[self.active_tab].last_cursor_world;
self.palette_insert(item, Some(at))
}
/// 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()) {
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() },
source,
time: now_secs(),
drawing,
};
let recent = &mut self.block_palette.recent;
recent.retain(|r| !(r.name.eq_ignore_ascii_case(&entry.name) && r.source == entry.source));
recent.insert(0, entry);
self.trim_recent_blocks();
}
pub(crate) fn trim_recent_blocks(&mut self) {
self.block_palette.recent.truncate(self.block_mru_list as usize);
}
/// Rebuild the Current Drawing list, and the previews kept for blocks
/// of other drawings and for whole drawings.
pub(crate) fn refresh_block_palette(&mut self) {
let i = self.active_tab;
let names = self.tabs[i].scene.custom_block_names();
self.block_palette.cached_names = names.clone();
self.block_palette.source_tab_id = Some(self.tabs[i].id);
self.block_palette.source_block_epoch = self.tabs[i].scene.block_epoch;
let doc = &self.tabs[i].scene.document;
let annotative = |name: &str| {
doc.block_records.get(name).is_some_and(|br| {
matches!(
doc.get_entity(br.block_entity_handle),
Some(codec::EntityType::Block(b)) if b.common.extended_data.records().iter().any(|r| r.application_name == "AcadAnnotative")
)
})
};
let entries: Vec<BlockEntry> = names
.into_iter()
.map(|name| BlockEntry {
wires: self.tabs[i].scene.block_preview_wires(&name),
annotative: annotative(&name),
dynamic: false,
name,
})
.collect();
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.blocks = entries;
let drawings: Vec<String> = self
.block_palette
.recent
.iter()
.chain(&self.block_palette.favorites)
.filter(|r| r.drawing && !self.block_palette.file_thumbs.contains_key(&r.source))
.map(|r| r.source.clone())
.collect();
for source in drawings {
let thumb = crate::io::thumbnail::read_handle(Path::new(&source));
self.block_palette.file_thumbs.insert(source, thumb);
}
}
/// Cheap per-update check: rebuild when the active drawing's definitions
/// changed, even when their names happen to stay the same.
pub(crate) fn refresh_block_palette_if_stale(&mut self) {
if !self.show_block_palette {
return;
}
let i = self.active_tab;
if self.block_palette.source_tab_id != Some(self.tabs[i].id)
|| self.block_palette.source_block_epoch != self.tabs[i].scene.block_epoch
{
self.refresh_block_palette();
}
}
/// What `-INSERT`'s name step knows about the drawing's blocks.
pub(crate) fn block_catalog(&self) -> BlockCatalog {
let doc = &self.tabs[self.active_tab].scene.document;
let host = doc.header.insertion_units;
let mut user = Vec::new();
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") {
continue;
}
if br.name.starts_with('*') {
unnamed += 1;
continue;
}
let factor = crate::app::properties::insert_unit_scale(host, br.units).unwrap_or(1.0);
units.insert(
br.name.to_ascii_uppercase(),
format!(
"Units: {} Conversion:{:>10}",
crate::app::properties::insunits_name(br.units),
crate::app::properties::format_unit_factor(factor)
),
);
user.push(br.name.clone());
}
user.sort_by_key(|n| n.to_ascii_uppercase());
let search_path = std::env::current_dir()
.map(|d| d.display().to_string())
.unwrap_or_default();
BlockCatalog { user, unnamed, units, search_path }
}
}
+822 -342
View File
File diff suppressed because it is too large. Load diff
+35 -35
View File
@@ -11,7 +11,7 @@ use crate::ui::popup::context_menu::{
build_context_menu, ContextMenu, GripMenuCmd, GripMenuContext, MenuAction, MenuContext,
SubmenuId,
};
use acadrust::Handle;
use codec::Handle;
use iced::Task;
impl OpenCADStudio {
@@ -19,7 +19,7 @@ impl OpenCADStudio {
pub(in crate::app) fn context_menu_open(&self) -> bool {
self.tabs
.get(self.active_tab)
.is_some_and(|tab| tab.scene.selection.borrow().context_menu.is_some())
.is_some_and(|tab| tab.scene.selection.borrow().menu.open_at.is_some())
}
/// Everything the menu builder needs, read from the active tab. Called by
@@ -71,7 +71,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow()
.context_menu_ui
.menu.ui
.open_submenu;
build_context_menu(&self.context_menu_context(), open)
}
@@ -80,10 +80,10 @@ impl OpenCADStudio {
pub(in crate::app) fn close_context_menu(&mut self) -> Task<Message> {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
if sel.context_menu.is_none() {
if sel.menu.open_at.is_none() {
return Task::none();
}
sel.close_context_menu();
sel.menu.close();
drop(sel);
self.focus_cmd_input()
}
@@ -91,7 +91,7 @@ impl OpenCADStudio {
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.context_menu_ui;
let ui = &mut sel.menu.ui;
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.
@@ -101,8 +101,8 @@ impl OpenCADStudio {
.iter()
.position(|s| s.header == Some(id));
let mut sel = self.tabs[i].scene.selection.borrow_mut();
if sel.context_menu_ui.highlighted.is_some() {
sel.context_menu_ui.highlighted = header;
if sel.menu.ui.highlighted.is_some() {
sel.menu.ui.highlighted = header;
}
Task::none()
}
@@ -118,10 +118,10 @@ impl OpenCADStudio {
let i = self.active_tab;
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.close_context_menu();
sel.menu.close();
// A pick counts as "another interaction" for the Enter-first
// right-click cycle: the next right-click acts as Enter again.
sel.right_click_entered = false;
sel.input.right_click_entered = false;
}
let focus = self.focus_cmd_input();
let task = match action {
@@ -174,7 +174,7 @@ impl OpenCADStudio {
if rows.is_empty() {
return Task::none();
}
let current = self.tabs[i].scene.selection.borrow().context_menu_ui.highlighted;
let current = self.tabs[i].scene.selection.borrow().menu.ui.highlighted;
let next_enabled = |from: usize, step: isize| -> usize {
let n = rows.len() as isize;
let mut idx = from as isize;
@@ -193,7 +193,7 @@ impl OpenCADStudio {
None => menu.default_index(),
Some(cur) => next_enabled(cur, 1),
};
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx);
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
Task::none()
}
ContextMenuNav::Up => {
@@ -201,7 +201,7 @@ impl OpenCADStudio {
None => menu.default_index(),
Some(cur) => next_enabled(cur, -1),
};
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx);
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
Task::none()
}
ContextMenuNav::Enter => {
@@ -220,7 +220,7 @@ impl OpenCADStudio {
if unique {
self.on_context_menu_pick(rows[idx].action.clone())
} else {
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted =
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted =
Some(idx);
Task::none()
}
@@ -244,7 +244,7 @@ impl OpenCADStudio {
ArrowKey::Left | ArrowKey::Right => {
// On a submenu header, Right expands and Left collapses.
let i = self.active_tab;
let ui = self.tabs[i].scene.selection.borrow().context_menu_ui.clone();
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);
@@ -410,7 +410,7 @@ mod tests {
fn app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.update(Message::ViewportMove(Point::new(200.0, 150.0)));
app
}
@@ -428,7 +428,7 @@ mod tests {
}
fn menu_open(app: &OpenCADStudio) -> bool {
app.tabs[0].scene.selection.borrow().context_menu.is_some()
app.tabs[0].scene.selection.borrow().menu.open_at.is_some()
}
fn active(app: &OpenCADStudio) -> Option<&'static str> {
@@ -491,7 +491,7 @@ mod tests {
let _ = app.update(Message::ViewportRightPress);
{
let mut sel = app.tabs[0].scene.selection.borrow_mut();
sel.right_press_time = iced::time::Instant::now()
sel.input.right_press_time = iced::time::Instant::now()
.checked_sub(std::time::Duration::from_millis(400));
}
let _ = app.update(Message::ViewportRightRelease);
@@ -522,7 +522,7 @@ mod tests {
.scene
.document
.entities()
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
.filter(|e| matches!(e, codec::EntityType::Line(_)))
.count();
assert_eq!(lines, 3, "{mode:?}");
}
@@ -540,7 +540,7 @@ mod tests {
assert!(!menu_open(&app));
assert_eq!(active(&app), None);
let closed = app.tabs[0].scene.document.entities().any(|e| {
matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed && p.vertices.len() == 3)
matches!(e, codec::EntityType::LwPolyline(p) if p.is_closed && p.vertices.len() == 3)
});
assert!(closed, "PLINE should have been closed by the menu pick");
});
@@ -558,7 +558,7 @@ 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().right_click_entered);
assert!(!app.tabs[0].scene.selection.borrow().input.right_click_entered);
});
}
@@ -582,7 +582,7 @@ mod tests {
right_click(&mut app);
let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Enter row
assert_eq!(
app.tabs[0].scene.selection.borrow().context_menu_ui.highlighted,
app.tabs[0].scene.selection.borrow().menu.ui.highlighted,
Some(0)
);
let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Cancel
@@ -602,7 +602,7 @@ mod tests {
assert!(!menu_open(&app));
assert_eq!(active(&app), None);
let closed = app.tabs[0].scene.document.entities().any(
|e| matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed),
|e| matches!(e, codec::EntityType::LwPolyline(p) if p.is_closed),
);
assert!(closed);
});
@@ -641,7 +641,7 @@ mod tests {
.scene
.document
.entities()
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
.filter(|e| matches!(e, codec::EntityType::Line(_)))
.count();
assert_eq!(lines, 3);
});
@@ -658,7 +658,7 @@ mod tests {
.document
.entities()
.find_map(|e| match e {
acadrust::EntityType::Line(l) => Some(l.common.handle),
codec::EntityType::Line(l) => Some(l.common.handle),
_ => None,
})
.expect("a line");
@@ -673,7 +673,7 @@ mod tests {
.scene
.document
.entities()
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
.filter(|e| matches!(e, codec::EntityType::Line(_)))
.count();
assert_eq!(lines, 0);
});
@@ -683,7 +683,7 @@ mod tests {
/// A drawing with one line, selected, its start grip hot at the origin.
fn grip_app() -> (OpenCADStudio, Handle) {
use acadrust::{entities::Line, types::Vector3, EntityType};
use codec::{entities::Line, types::Vector3, EntityType};
let mut app = app();
app.snapper.snap_enabled = false;
app.snapper.grid_snap_on = false;
@@ -717,7 +717,7 @@ mod tests {
fn line_start(app: &OpenCADStudio, handle: Handle) -> (f64, f64) {
match app.tabs[0].scene.document.get_entity(handle) {
Some(acadrust::EntityType::Line(l)) => (l.start.x, l.start.y),
Some(codec::EntityType::Line(l)) => (l.start.x, l.start.y),
other => panic!("{other:?}"),
}
}
@@ -795,7 +795,7 @@ mod tests {
.document
.entities()
.filter_map(|e| match e {
acadrust::EntityType::Line(l) => Some((l.start.x, l.start.y)),
codec::EntityType::Line(l) => Some((l.start.x, l.start.y)),
_ => None,
})
.collect();
@@ -810,7 +810,7 @@ mod tests {
.scene
.document
.entities()
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
.filter(|e| matches!(e, codec::EntityType::Line(_)))
.count();
assert_eq!(count, 1);
});
@@ -853,7 +853,7 @@ mod transparent_tests {
fn line_app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.dispatch_command("LINE");
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
app
@@ -895,7 +895,7 @@ mod transparent_tests {
.scene
.document
.entities()
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
.filter(|e| matches!(e, codec::EntityType::Line(_)))
.count();
assert_eq!(lines, 1);
});
@@ -961,7 +961,7 @@ mod transparent_tests {
.document
.entities()
.filter_map(|e| match e {
acadrust::EntityType::Line(l) => Some(l.clone()),
codec::EntityType::Line(l) => Some(l.clone()),
_ => None,
})
.collect();
@@ -984,7 +984,7 @@ mod transparent_tests {
.document
.entities()
.filter_map(|e| match e {
acadrust::EntityType::Line(l) => Some(l.clone()),
codec::EntityType::Line(l) => Some(l.clone()),
_ => None,
})
.collect();
@@ -1000,7 +1000,7 @@ mod transparent_tests {
.document
.entities()
.filter_map(|e| match e {
acadrust::EntityType::Line(l) => Some(l.clone()),
codec::EntityType::Line(l) => Some(l.clone()),
_ => None,
})
.collect();
+167 -191
View File
@@ -2,7 +2,6 @@
#![allow(unused_imports)]
use super::util::*;
use crate::ui::window::block_palette::BlockPaletteMsg;
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,
@@ -16,8 +15,8 @@ use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
use crate::ui::PropertiesPanel;
use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
use codec::types::Color as AcadColor;
use codec::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::{mouse, Point, Task};
@@ -121,25 +120,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.ribbon.note_panel_tool(&tool_id);
self.ribbon.activate_tool(&tool_id);
match event {
ModuleEvent::Command(cmd) => {
let task = self.dispatch_command(&cmd);
// One-shot tools (view changes, clipboard, toggles,
// audits…) leave nothing running: no interactive
// command and no dialog. Their highlight would stick
// forever — turn it off now. Interactive commands and
// dialog owners keep theirs; the command end / modal
// close clears those. (#355)
let i = self.active_tab;
if self.tabs[i].active_cmd.is_none()
&& self.active_modal.is_none()
&& !self.tabs[i].pan_mode
&& !self.tabs[i].orbit_mode
&& !self.tabs[i].zoom_dynamic_mode
{
self.ribbon.deactivate_tool();
}
return task;
}
// `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());
@@ -200,8 +183,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
pub(super) fn on_unsaved_dialog_discard(&mut self) -> Task<Message> {
match self.pending_close.take() {
Some(crate::app::PendingClose::Tab(idx)) => {
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));
@@ -249,7 +238,17 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
return Task::none();
};
let (idx, continuation) = match pending {
crate::app::PendingClose::Tab(idx) => {
crate::app::PendingClose::Tab(tab_id) => {
// The pending target is an id; resolve it against the
// current tab list so a tab closed in the meantime can
// neither panic the save path nor save the wrong file.
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {
self.pending_close = None;
return Task::batch([
self.close_unsaved_dialog_window(),
self.continue_tab_close_queue(),
]);
};
(idx, crate::app::SaveContinuation::CloseTab)
}
crate::app::PendingClose::Quit => {
@@ -298,8 +297,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
#[cfg(target_arch = "wasm32")]
{
match self.pending_close.take() {
Some(crate::app::PendingClose::Tab(idx)) => {
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
Some(crate::app::PendingClose::Tab(tab_id)) => {
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {
return Task::batch([
self.close_unsaved_dialog_window(),
self.continue_tab_close_queue(),
]);
};
self.pending_close = Some(crate::app::PendingClose::Tab(tab_id));
self.save_dialog_for_unsaved = true;
let close = self.close_unsaved_dialog_window();
let save = self.save_with_default_format(idx);
@@ -351,21 +356,41 @@ 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").
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)
}
/// [`Self::import_file_as_block`]; with `redefine` a block already named
/// after the file takes the file's contents instead of a new name.
pub(super) fn import_drawing_block(
&mut self,
path: std::path::PathBuf,
redefine: bool,
) -> Result<String, String> {
let doc = crate::io::load_file(&path).map_err(|e| e.to_string())?;
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Block".to_string());
self.import_document_as_block(doc, stem)
self.import_document_block(doc, stem, redefine)
}
/// Define one block in the active drawing from a loaded `CadDocument`'s
/// model-space entities (base = the file's model-space insertion base).
#[cfg(test)]
fn import_document_as_block(
&mut self,
doc: acadrust::CadDocument,
doc: codec::CadDocument,
stem: String,
) -> Result<String, String> {
self.import_document_block(doc, stem, false)
}
fn import_document_block(
&mut self,
doc: codec::CadDocument,
stem: String,
redefine: bool,
) -> Result<String, String> {
let i = self.active_tab;
// Model-space block record handle (Layout object first, name fallback).
@@ -373,15 +398,15 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.objects
.values()
.find_map(|o| {
if let acadrust::objects::ObjectType::Layout(l) = o {
if let codec::objects::ObjectType::Layout(l) = o {
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
} else {
None
}
})
.or_else(|| doc.block_records.get("*Model_Space").map(|br| br.handle))
.unwrap_or(acadrust::Handle::NULL);
let mut entities: Vec<acadrust::EntityType> = if model_br.is_null() {
.unwrap_or(codec::Handle::NULL);
let mut entities: Vec<codec::EntityType> = if model_br.is_null() {
Vec::new()
} else {
let br = doc.block_records.iter().find(|br| br.handle == model_br);
@@ -392,7 +417,7 @@ 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, acadrust::EntityType::Block(_) | acadrust::EntityType::BlockEnd(_))
!matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_))
})
.cloned()
.collect()
@@ -405,7 +430,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
o == model_br || o.is_null()
})
.filter(|e| {
!matches!(e, acadrust::EntityType::Block(_) | acadrust::EntityType::BlockEnd(_))
!matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_))
})
.cloned()
.collect()
@@ -419,7 +444,13 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
doc.header.model_space_insertion_base.y,
doc.header.model_space_insertion_base.z,
);
let name = self.block_name_from_file(&stem);
let redefine = redefine
&& self.tabs[i].scene.document.block_records.get(stem.trim()).is_some();
let name = if redefine {
stem.trim().to_string()
} else {
self.block_name_from_file(&stem)
};
// Capture every table record needed by the top-level entities and their
// nested block definitions. Importing only the definitions leaves
// source-only layers, linetypes, and text/dimension styles dangling.
@@ -472,7 +503,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.iter_mut()
.chain(defs.iter_mut().flat_map(|def| def.entities.iter_mut()))
{
if let acadrust::EntityType::Insert(ins) = entity {
if let codec::EntityType::Insert(ins) = entity {
if let Some(new_name) = rename_map.get(&ins.block_name) {
ins.block_name = new_name.clone();
}
@@ -491,74 +522,22 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.scene
.define_block_raw(&def.name, def.base_point, def.entities);
}
self.tabs[i]
.scene
.define_block_from_owned_entities(entities, &name, base)?;
if redefine {
self.tabs[i].scene.redefine_block_raw(
&name,
codec::types::Vector3::new(base.x, base.y, base.z),
entities,
);
} else {
self.tabs[i]
.scene
.define_block_from_owned_entities(entities, &name, base)?;
}
self.tabs[i].scene.populate_meshes_from_document();
self.tabs[i].dirty = true;
Ok(name)
}
pub(super) fn on_block_palette(&mut self, m: crate::ui::window::block_palette::BlockPaletteMsg) -> iced::Task<Message> {
use crate::ui::window::block_palette::{BlockEntry, BlockPaletteMsg};
match m {
BlockPaletteMsg::Search(s) => {
self.block_palette.search = s;
iced::Task::none()
}
BlockPaletteMsg::CyclePreviewSize => {
self.block_palette.preview_size =
crate::ui::window::block_palette::cycle_preview_size(
self.block_palette.preview_size,
);
iced::Task::none()
}
BlockPaletteMsg::Refresh => {
self.refresh_block_palette();
iced::Task::none()
}
BlockPaletteMsg::PickFile => iced::Task::perform(
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"])
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
|r| Message::BlockPalette(BlockPaletteMsg::FilePicked(r)),
),
BlockPaletteMsg::FilePicked(Ok(path)) => {
match self.import_file_as_block(path) {
Ok(name) => {
self.command_line
.push_output(&crate::tf!("Inserting \"{name}\" from file."));
self.refresh_block_palette();
self.start_block_placement(&name);
}
Err(e) if e != "Cancelled" => {
self.command_line.push_error(&crate::tf!("INSERT FILE: {e}"));
}
Err(_) => {}
}
iced::Task::none()
}
BlockPaletteMsg::FilePicked(Err(e)) => {
if e != "Cancelled" {
self.command_line.push_error(&e);
}
iced::Task::none()
}
BlockPaletteMsg::Insert(name) => {
self.start_block_placement(&name);
iced::Task::none()
}
}
}
/// Dock chrome interaction (grab / pin / resize / hover / move) applied to
/// whichever panel the message names.
pub(super) fn on_dock(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
@@ -605,6 +584,20 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::Browser => {
self.show_browser = false;
}
PanelId::NodeGraph => {
self.show_node_graph = false;
}
PanelId::PointCloudManager => {
self.pc_manager.show = false;
}
PanelId::Count => {
self.count_palette.show = false;
self.ribbon.set_count_palette(false);
}
PanelId::SheetSetManager => {
self.sheet_set.show = false;
self.ribbon.set_sheet_set(false);
}
PanelId::Properties => {
self.show_properties = false;
self.ribbon.set_properties(false);
@@ -653,7 +646,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
// produced a one-time jump plus a stuck drag. Column moves
// arrive via Message::XrefColMove only.
if self.dock_dragging.is_some() {
let avail = self.tabs[self.active_tab].scene.selection.borrow().vp_size.1;
let avail = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size.1;
let side = if point.x < self.win_size.0 * 0.5 {
DockSide::Left
} else {
@@ -715,6 +708,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::BlockPalette => self.show_block_palette,
PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
PanelId::Count => self.count_palette.show,
PanelId::SheetSetManager => self.sheet_set.show,
}
}
@@ -731,56 +728,6 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.count()
}
/// Start placing `name` through the INSERT command, skipping the name prompt.
fn start_block_placement(&mut self, name: &str) {
let i = self.active_tab;
let wires = self
.block_palette
.blocks
.iter()
.find(|b| b.name == name)
.map(|b| b.wires.clone())
.unwrap_or_else(|| self.tabs[i].scene.block_preview_wires(name));
use crate::modules::insert::insert_block::InsertBlockCommand;
let cmd = InsertBlockCommand::new_for_block(name.to_string(), wires, glam::Vec3::ZERO);
use crate::command::CadCommand;
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
self.block_palette.placing = Some(name.to_string());
}
/// Rebuild the panel's block list + cached wires from the active drawing.
pub(crate) fn refresh_block_palette(&mut self) {
let i = self.active_tab;
let names = self.tabs[i].scene.custom_block_names();
self.block_palette.cached_names = names.clone();
self.block_palette.source_tab_id = Some(self.tabs[i].id);
self.block_palette.source_block_epoch = self.tabs[i].scene.block_epoch;
self.block_palette.blocks = names
.into_iter()
.map(|name| {
let wires = self.tabs[i].scene.block_preview_wires(&name);
crate::ui::window::block_palette::BlockEntry { name, wires }
})
.collect();
}
/// Cheap per-update check: rebuild when the active drawing's definitions
/// changed, even when their names happen to stay the same.
pub(crate) fn refresh_block_palette_if_stale(&mut self) {
if !self.show_block_palette {
return;
}
let i = self.active_tab;
// `block_epoch` advances for every block-definition change, so it
// avoids re-scanning every block name on unrelated application updates.
if self.block_palette.source_tab_id != Some(self.tabs[i].id)
|| self.block_palette.source_block_epoch != self.tabs[i].scene.block_epoch
{
self.refresh_block_palette();
}
}
/// Rebuild the Reference Manager's entry list from the active drawing.
/// Uses the tab's session sets (unloaded + stat baseline) so CLI and
/// palette agree; writes fresh load-time mtimes back into the tab's stat
@@ -859,19 +806,18 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
// not persist an unloaded bit), so keep their source entities but
// remove their decoded render models. A reload repopulates the cache
// after clearing the corresponding session key.
let hidden_images: Vec<acadrust::types::Handle> = self.tabs[i]
let hidden_images: Vec<codec::types::Handle> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| match entity {
acadrust::EntityType::RasterImage(image)
codec::EntityType::RasterImage(image)
if image.definition_handle.is_some_and(|key| self.tabs[i].xref_unloaded.is_unloaded(key.value())) =>
{
Some(entity.common().handle)
}
acadrust::EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf)
&& self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
codec::EntityType::Underlay(underlay)
if self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
{
Some(entity.common().handle)
}
@@ -997,6 +943,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
self.tabs[i].scene.reseed_underlays();
// Point clouds redraw as unloaded (box and saved path).
self.tabs[i].scene.bump_geometry();
}
XrefPaletteOp::Reload => {
let base_dir: std::path::PathBuf = host
@@ -1023,6 +972,33 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
"XREF: cannot reload nested reference '{}'. Reload it in its host drawing.",
name
).as_ref());
} else if *kind == crate::io::xref_model::RefKind::Underlay {
// An underlay reloads by clearing its definition's unloaded state.
for (key, row_name, row_kind, _) in &picked {
if row_kind != kind || row_name != name {
continue;
}
let handle = codec::types::Handle::new(*key);
if let Some(codec::objects::ObjectType::UnderlayDefinition(def)) =
self.tabs[i].scene.document.objects.get_mut(&handle)
{
def.unloaded = false;
}
self.tabs[i].xref_unloaded.remove(key);
}
self.tabs[i].scene.reseed_underlays();
} else if *kind == crate::io::xref_model::RefKind::PointCloud {
// A point cloud reloads by setting its definition loaded.
for (key, row_name, row_kind, _) in &picked {
if row_kind == kind && row_name == name {
crate::io::xref::set_point_cloud_loaded(
&mut self.tabs[i].scene.document,
codec::types::Handle::new(*key),
true,
);
}
}
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.",
@@ -1034,7 +1010,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let handles: rustc_hash::FxHashSet<acadrust::types::Handle> = self.tabs[i]
let handles: rustc_hash::FxHashSet<codec::types::Handle> = self.tabs[i]
.scene
.document
.block_records
@@ -1273,9 +1249,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
mod tests {
use super::*;
use crate::app::OpenCADStudio;
use acadrust::entities::Line;
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::Line;
use codec::types::Vector3;
use codec::EntityType;
fn fresh() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
@@ -1284,13 +1260,13 @@ mod tests {
}
/// A foreign document: the fresh scene's document plus one model-space LINE.
fn foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
fn foreign_doc(app: &OpenCADStudio) -> codec::CadDocument {
let mut doc = app.tabs[app.active_tab].scene.document.clone();
let model_br = doc
.objects
.values()
.find_map(|o| {
if let acadrust::objects::ObjectType::Layout(l) = o {
if let codec::objects::ObjectType::Layout(l) = o {
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
} else {
None
@@ -1322,7 +1298,7 @@ mod tests {
#[test]
fn import_document_as_block_merges_source_only_layer() {
use acadrust::tables::Layer;
use codec::tables::Layer;
let mut app = fresh();
let mut doc = foreign_doc(&app);
@@ -1333,7 +1309,7 @@ mod tests {
.objects
.values()
.find_map(|o| match o {
acadrust::objects::ObjectType::Layout(l) if l.name == "Model" => {
codec::objects::ObjectType::Layout(l) if l.name == "Model" => {
Some(l.block_record)
}
_ => None,
@@ -1358,10 +1334,10 @@ mod tests {
/// definition itself INSERTs a `Door` block — so the imported `Door` is a
/// *nested dependency*, not a top-level entity. The `Door` in this file is
/// unrelated to any `Door` in the destination drawing.
fn nested_foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
use acadrust::entities::Insert;
use acadrust::tables::BlockRecord;
use acadrust::Handle;
fn nested_foreign_doc(app: &OpenCADStudio) -> codec::CadDocument {
use codec::entities::Insert;
use codec::tables::BlockRecord;
use codec::Handle;
let mut doc = foreign_doc(app);
// "Door" block definition: one LINE of geometry.
@@ -1389,7 +1365,7 @@ mod tests {
.objects
.values()
.find_map(|o| {
if let acadrust::objects::ObjectType::Layout(l) = o {
if let codec::objects::ObjectType::Layout(l) = o {
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
} else {
None
@@ -1406,7 +1382,7 @@ mod tests {
/// The nested-INSERT's block name inside a defined block record.
fn nested_insert_target(
doc: &acadrust::CadDocument,
doc: &codec::CadDocument,
block: &str,
) -> Option<String> {
let br = doc.block_records.get(block)?;
@@ -1419,10 +1395,10 @@ mod tests {
/// A foreign document whose model space INSERTs a `Fixture` block whose
/// definition INSERTs `Door (2)`, whose definition in turn INSERTs `Door`.
/// The file therefore carries *both* `Door` and `Door (2)` as nested deps.
fn doubly_nested_foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
use acadrust::entities::Insert;
use acadrust::tables::BlockRecord;
use acadrust::Handle;
fn doubly_nested_foreign_doc(app: &OpenCADStudio) -> codec::CadDocument {
use codec::entities::Insert;
use codec::tables::BlockRecord;
use codec::Handle;
let mut doc = foreign_doc(app);
let door_h = Handle::new(doc.next_handle());
@@ -1455,7 +1431,7 @@ mod tests {
.objects
.values()
.find_map(|o| {
if let acadrust::objects::ObjectType::Layout(l) = o {
if let codec::objects::ObjectType::Layout(l) = o {
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
} else {
None
@@ -1573,13 +1549,13 @@ mod tests {
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");
assert_eq!(cmd.name(), "INSERT");
assert_eq!(cmd.name(), "-INSERT");
assert_eq!(app.block_palette.placing.as_deref(), Some("Fixture"));
}
#[test]
fn blockpalette_reflects_new_block_without_reopen() {
use acadrust::types::Transform;
use codec::types::Transform;
let mut app = fresh();
let i = app.active_tab;
@@ -1702,7 +1678,7 @@ mod tests {
app.show_block_palette = false;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
@@ -1729,7 +1705,7 @@ mod tests {
app.show_block_palette = true;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
@@ -1784,7 +1760,7 @@ mod tests {
let new_dir = dir.join("new");
std::fs::create_dir_all(&old_dir).unwrap();
std::fs::create_dir_all(&new_dir).unwrap();
let mut br = acadrust::tables::BlockRecord::new("PLAN");
let mut br = codec::tables::BlockRecord::new("PLAN");
br.flags.is_xref = true;
br.xref_path = "refs/plan.dwg".to_string();
br.handle = app.tabs[i].scene.document.allocate_handle();
@@ -1854,11 +1830,11 @@ 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 acadrust::tables::BlockRecord;
use codec::tables::BlockRecord;
use crate::app::Message;
use crate::ui::window::xref_manager::XrefPaletteOp;
let dir = palette_tmpdir("nestedmatrix");
let mut host_doc = acadrust::CadDocument::new();
let mut host_doc = codec::CadDocument::new();
let mut inner = BlockRecord::new("INNER");
inner.flags.is_xref = true;
inner.xref_path = "inner.dwg".to_string();
@@ -1899,7 +1875,7 @@ 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 acadrust::tables::BlockRecord;
use codec::tables::BlockRecord;
use crate::app::Message;
use crate::ui::window::xref_manager::XrefPaletteOp;
let mut app = fresh();
@@ -1934,7 +1910,7 @@ 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 acadrust::tables::BlockRecord;
use codec::tables::BlockRecord;
use crate::app::Message;
use crate::io::xref_model::{Pathtype, RefType};
use crate::ui::window::xref_manager::XrefPaletteOp;
@@ -1967,9 +1943,9 @@ mod tests {
fn palette_reload_all_resolves_direct_refs() {
// Toolbar Reload All against a real on-disk reference: full
// resolve, per-ref report, entry back to Loaded.
use acadrust::tables::BlockRecord;
use codec::tables::BlockRecord;
let dir = palette_tmpdir("reloadall");
let ref_doc = acadrust::CadDocument::new();
let ref_doc = codec::CadDocument::new();
let bytes = crate::io::save_to_bytes(&ref_doc, "dwg", ref_doc.version).unwrap();
std::fs::write(dir.join("plan.dwg"), &bytes).unwrap();
let mut app = fresh();
@@ -2003,10 +1979,10 @@ 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 acadrust::tables::BlockRecord;
use codec::tables::BlockRecord;
use crate::ui::window::xref_manager::XrefPaletteOp;
let dir = palette_tmpdir("nested");
let mut host_doc = acadrust::CadDocument::new();
let mut host_doc = codec::CadDocument::new();
let mut inner = BlockRecord::new("INNER");
inner.flags.is_xref = true;
inner.xref_path = "inner.dwg".to_string();
@@ -2038,7 +2014,7 @@ 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 acadrust::objects::{ImageDefinition, ObjectType};
use codec::objects::{ImageDefinition, ObjectType};
use crate::io::xref_model::RefKind;
use crate::ui::window::xref_manager::XrefPaletteOp;
let dir = palette_tmpdir("imgreload");
@@ -2048,14 +2024,14 @@ mod tests {
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 = acadrust::entities::RasterImage::new(
let mut img = codec::entities::RasterImage::new(
"img.png",
acadrust::types::Vector3::ZERO,
codec::types::Vector3::ZERO,
8.0,
8.0,
);
img.definition_handle = Some(h);
app.tabs[i].scene.document.add_entity(acadrust::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");
+6 -4
View File
@@ -15,8 +15,8 @@ use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
use crate::ui::PropertiesPanel;
use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
use codec::types::Color as AcadColor;
use codec::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::{mouse, Point, Task};
@@ -191,7 +191,9 @@ impl OpenCADStudio {
let picks_object = self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.needs_entity_pick() || c.needs_structure_point_pick())
.map(|c| {
c.needs_entity_pick() || c.needs_structure_point_pick() || c.selects_by_window()
})
.unwrap_or(false);
let has_base = self.last_point.is_some();
// While aligned to an OTRACK ray, the point step reads a single
@@ -330,7 +332,7 @@ impl OpenCADStudio {
/// following `ARC_CONT` starts tangentially from where drawing ended. Set to
/// `None` for any other entity kind. Uses `last_point` (the final pick) to
/// pick the endpoint the pen actually finished on.
pub(in crate::app) fn update_cont_anchor(&mut self, entity: &acadrust::EntityType) {
pub(in crate::app) fn update_cont_anchor(&mut self, entity: &codec::EntityType) {
let last = self.last_point;
self.cont_anchor = crate::modules::draw::draw::arc::continue_anchor(entity, last);
}
+353 -158
View File
File diff suppressed because it is too large. Load diff
+34 -34
View File
@@ -7,8 +7,8 @@ use crate::ui::window::gradient_editor::{GradientColorMode, GradientEditorState,
use crate::ui::window::graphic_attributes::{
self as palette, GraphicAttribute, GraphicAttributesMsg, HatchEditorState, PendingFillClose,
};
use acadrust::types::{Color as AcadColor, LineWeight, Transparency};
use acadrust::{EntityType, Handle};
use codec::types::{Color as AcadColor, LineWeight, Transparency};
use codec::{EntityType, Handle};
use iced::Task;
use rustc_hash::{FxHashMap, FxHashSet};
@@ -52,14 +52,14 @@ fn fill_pattern(value: GraphicAttribute) -> Option<(String, HatchPattern)> {
/// Convert a selected HATCH to another fill kind in place.
fn set_hatch_graphic_attribute(
hatch: &mut acadrust::entities::Hatch,
hatch: &mut codec::entities::Hatch,
value: GraphicAttribute,
visible_color: [f32; 4],
) {
match value {
GraphicAttribute::Solid => {
hatch.pattern = acadrust::entities::HatchPattern::new("SOLID");
hatch.pattern_type = acadrust::entities::HatchPatternType::Predefined;
hatch.pattern = codec::entities::HatchPattern::new("SOLID");
hatch.pattern_type = codec::entities::HatchPatternType::Predefined;
hatch.is_solid = true;
hatch.gradient_color.enabled = false;
}
@@ -76,7 +76,7 @@ fn set_hatch_graphic_attribute(
let origin = hatch.pattern_origin();
crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y);
hatch.pattern = pattern;
hatch.pattern_type = acadrust::entities::HatchPatternType::Predefined;
hatch.pattern_type = codec::entities::HatchPatternType::Predefined;
hatch.is_solid = false;
hatch.gradient_color.enabled = false;
}
@@ -90,7 +90,7 @@ fn set_hatch_graphic_attribute(
hatch.gradient_color.shift = 0.0;
hatch.gradient_color.is_single_color = false;
hatch.gradient_color.colors = vec![
acadrust::entities::hatch::GradientColorEntry {
codec::entities::hatch::GradientColorEntry {
value: 0.0,
color: AcadColor::Rgb {
r: channel(visible_color[0]),
@@ -98,7 +98,7 @@ fn set_hatch_graphic_attribute(
b: channel(visible_color[2]),
},
},
acadrust::entities::hatch::GradientColorEntry {
codec::entities::hatch::GradientColorEntry {
value: 1.0,
color: AcadColor::Rgb {
r: 46,
@@ -163,7 +163,7 @@ fn concrete_graphic_attributes(
linetype: &str,
lineweight: LineWeight,
transparency: Transparency,
layer: Option<&acadrust::tables::Layer>,
layer: Option<&codec::tables::Layer>,
) -> ConcreteGraphicAttributes {
let layer_color = layer.map_or(AcadColor::Index(7), |layer| match layer.color {
AcadColor::ByLayer | AcadColor::ByBlock => AcadColor::Index(7),
@@ -523,7 +523,7 @@ impl OpenCADStudio {
crate::t!("Create Layer with active settings"),
std::slice::from_ref(&layer_name),
);
let mut layer = acadrust::tables::Layer::new(name);
let mut layer = codec::tables::Layer::new(name);
layer.handle = self.tabs[i].scene.document.allocate_handle();
layer.color = values.color;
layer.line_type = values.linetype;
@@ -649,11 +649,11 @@ impl OpenCADStudio {
gradient.is_single_color = state.color_mode == GradientColorMode::One;
gradient.color_tint = state.shade_tint as f64;
gradient.colors = vec![
acadrust::entities::hatch::GradientColorEntry {
codec::entities::hatch::GradientColorEntry {
value: 0.0,
color: state.color_1,
},
acadrust::entities::hatch::GradientColorEntry {
codec::entities::hatch::GradientColorEntry {
value: 1.0,
color: state.color_2,
},
@@ -795,7 +795,7 @@ impl OpenCADStudio {
};
let normal = working.z.normalize_or(glam::DVec3::Z);
let storage = crate::entities::curve::ocs_plane(
acadrust::types::Vector3::new(normal.x, normal.y, normal.z),
codec::types::Vector3::new(normal.x, normal.y, normal.z),
working.origin.dot(normal),
);
let plane = crate::command::WorkingPlane::new(
@@ -993,8 +993,8 @@ impl OpenCADStudio {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::{Line, LwPolyline};
use acadrust::types::Vector2;
use codec::entities::{Line, LwPolyline};
use codec::types::Vector2;
fn ga(message: GraphicAttributesMsg) -> Message {
Message::GraphicAttributes(message)
@@ -1024,7 +1024,7 @@ mod tests {
}
}
fn hatches(app: &OpenCADStudio) -> Vec<&acadrust::entities::Hatch> {
fn hatches(app: &OpenCADStudio) -> Vec<&codec::entities::Hatch> {
app.tabs[app.active_tab]
.scene
.document
@@ -1036,7 +1036,7 @@ mod tests {
.collect()
}
fn common(app: &OpenCADStudio, handle: Handle) -> &acadrust::entities::EntityCommon {
fn common(app: &OpenCADStudio, handle: Handle) -> &codec::entities::EntityCommon {
app.tabs[app.active_tab]
.scene
.document
@@ -1261,16 +1261,16 @@ mod tests {
let i = app.active_tab;
app.tabs[i].scene.deselect_all();
let _ = app.on_line_color(acadrust::types::Color::Index(2));
let _ = app.on_line_color(codec::types::Color::Index(2));
let _ = app.on_line_linetype("Continuous".to_string());
let _ = app.on_line_lineweight(acadrust::types::LineWeight::Value(35));
let _ = app.on_line_lineweight(codec::types::LineWeight::Value(35));
let _ = app.on_line_linetype_scale(2.5);
let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.3));
let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
let header = &app.tabs[i].scene.document.header;
assert_eq!(
header.current_entity_color,
acadrust::types::Color::Index(2)
codec::types::Color::Index(2)
);
assert_eq!(header.current_linetype_name, "Continuous");
assert_eq!(header.current_line_weight, 35);
@@ -1298,10 +1298,10 @@ mod tests {
.layers
.get_mut("0")
.expect("default layer");
layer.color = acadrust::types::Color::Index(3);
layer.color = codec::types::Color::Index(3);
layer.line_type = "Continuous".to_string();
layer.line_weight = acadrust::types::LineWeight::Value(50);
layer.transparency = acadrust::types::Transparency::from_percent(0.25);
layer.line_weight = codec::types::LineWeight::Value(50);
layer.transparency = codec::types::Transparency::from_percent(0.25);
}
let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
app.tabs[i].scene.select_entity(handle, false);
@@ -1313,9 +1313,9 @@ mod tests {
.get_entity(handle)
.expect("line")
.common();
assert_eq!(common.color, acadrust::types::Color::ByBlock);
assert_eq!(common.color, codec::types::Color::ByBlock);
assert_eq!(common.linetype, "ByBlock");
assert_eq!(common.line_weight, acadrust::types::LineWeight::ByBlock);
assert_eq!(common.line_weight, codec::types::LineWeight::ByBlock);
assert!(common.transparency.is_by_block());
let _ = app.on_graphic_attributes_set_all(true);
@@ -1326,9 +1326,9 @@ mod tests {
.get_entity(handle)
.expect("line")
.common();
assert_eq!(common.color, acadrust::types::Color::Index(3));
assert_eq!(common.color, codec::types::Color::Index(3));
assert_eq!(common.linetype, "Continuous");
assert_eq!(common.line_weight, acadrust::types::LineWeight::Value(50));
assert_eq!(common.line_weight, codec::types::LineWeight::Value(50));
assert!((common.transparency.as_percent() - 0.25).abs() < 0.01);
}
@@ -1337,10 +1337,10 @@ mod tests {
let mut app = fresh();
let i = app.active_tab;
app.tabs[i].scene.deselect_all();
let _ = app.on_line_color(acadrust::types::Color::Index(2));
let _ = app.on_line_color(codec::types::Color::Index(2));
let _ = app.on_line_linetype("Continuous".to_string());
let _ = app.on_line_lineweight(acadrust::types::LineWeight::Value(35));
let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.3));
let _ = app.on_line_lineweight(codec::types::LineWeight::Value(35));
let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
let _ = app.create_graphic_attributes_layer("Invalid\nLayer");
assert!(app.tabs[i]
@@ -1367,9 +1367,9 @@ mod tests {
.layers
.get("Graphic Settings")
.expect("created layer");
assert_eq!(layer.color, acadrust::types::Color::Index(2));
assert_eq!(layer.color, codec::types::Color::Index(2));
assert_eq!(layer.line_type, "Continuous");
assert_eq!(layer.line_weight, acadrust::types::LineWeight::Value(35));
assert_eq!(layer.line_weight, codec::types::LineWeight::Value(35));
assert!((layer.transparency.as_percent() - 0.3).abs() < 0.01);
}
@@ -1522,7 +1522,7 @@ mod tests {
fn closed_square_on(app: &mut OpenCADStudio, layer: &str) -> Handle {
let i = app.active_tab;
if app.tabs[i].scene.document.layers.get(layer).is_none() {
let mut new_layer = acadrust::tables::Layer::new(layer);
let mut new_layer = codec::tables::Layer::new(layer);
new_layer.handle = app.tabs[i].scene.document.allocate_handle();
let _ = app.tabs[i].scene.document.layers.add(new_layer);
}
+492 -162
View File
File diff suppressed because it is too large. Load diff
+7 -6
View File
@@ -11,6 +11,7 @@ use std::path::{Path, PathBuf};
/// The result names the file the way the chooser shows it.
fn read_page_setups_task(path: PathBuf) -> Task<Message> {
super::file::background_task(
crate::t!("Page setup import"),
move || {
let file = path
.file_name()
@@ -78,7 +79,7 @@ impl OpenCADStudio {
let Some(draft) = self.plot_dialog.import_draft.take() else {
return Task::none();
};
let chosen: Vec<(String, acadrust::objects::PlotSettings)> = draft
let chosen: Vec<(String, codec::objects::PlotSettings)> = draft
.selected()
.map(|(name, ps)| (name.to_string(), ps.clone()))
.collect();
@@ -113,7 +114,7 @@ impl OpenCADStudio {
/// as one undoable step. Returns the names imported.
pub(super) fn import_page_setups(
&mut self,
setups: Vec<(String, acadrust::objects::PlotSettings)>,
setups: Vec<(String, codec::objects::PlotSettings)>,
) -> Vec<String> {
if setups.is_empty() {
return Vec::new();
@@ -169,7 +170,7 @@ impl OpenCADStudio {
return Task::none();
}
};
let chosen: Vec<(String, acadrust::objects::PlotSettings)> = if names == "*" {
let chosen: Vec<(String, codec::objects::PlotSettings)> = if names == "*" {
available
} else {
let wanted: Vec<&str> = names.split(',').map(str::trim).collect();
@@ -218,7 +219,7 @@ mod tests {
/// The fixture's page setups as the chooser would hold them, read
/// through the same document reader the command uses.
fn fixture_setups() -> Vec<(String, acadrust::objects::PlotSettings)> {
fn fixture_setups() -> Vec<(String, codec::objects::PlotSettings)> {
let document = crate::io::load_file(std::path::Path::new(FIXTURE)).unwrap();
crate::scene::document_page_setups(&document)
}
@@ -242,7 +243,7 @@ mod tests {
#[test]
fn psetupin_imports_named_setups_from_a_saved_drawing() {
use acadrust::objects::PlotRotation;
use codec::objects::PlotRotation;
// Author a source drawing with two named setups and save it.
let dir = std::env::temp_dir().join(format!("ocs-psetupin-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
@@ -256,7 +257,7 @@ mod tests {
ps.paper_width = 297.0;
ps.paper_height = 420.0;
ps.rotation = PlotRotation::Degrees90;
ps.plot_view_handle = acadrust::Handle::new(0xBEEF);
ps.plot_view_handle = codec::Handle::new(0xBEEF);
app.tabs[i].scene.page_setup_save("Site", ps.clone());
ps.paper_size = "ISO_A2_(420.00_x_594.00_MM)".into();
app.tabs[i].scene.page_setup_save("Overview", ps);
+123 -46
View File
@@ -16,8 +16,8 @@ use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
use crate::ui::PropertiesPanel;
use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
use codec::types::Color as AcadColor;
use codec::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::{mouse, Point, Task};
@@ -26,8 +26,8 @@ impl OpenCADStudio {
pub(in crate::app) fn mlstyle_mut(
&mut self,
tab: usize,
) -> Option<&mut acadrust::objects::MLineStyle> {
use acadrust::objects::ObjectType;
) -> Option<&mut codec::objects::MLineStyle> {
use codec::objects::ObjectType;
let name = self.mlstyle_selected.clone();
self.tabs[tab]
.scene
@@ -41,7 +41,7 @@ impl OpenCADStudio {
}
pub(in crate::app) fn load_mlstyle_bufs(&mut self, tab: usize) {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let name = self.mlstyle_selected.clone();
let Some((description, start_angle, end_angle, fill_color, elements)) = self.tabs[tab]
.scene
@@ -94,22 +94,22 @@ impl OpenCADStudio {
style.end_angle = value.to_radians();
}
if let Some(value) = fill_color {
style.fill_color = acadrust::types::Color::from_index(value);
style.fill_color = codec::types::Color::from_index(value);
}
for (element, values) in style.elements.iter_mut().zip(elements) {
if let Ok(value) = values[0].trim().parse::<f64>() {
element.offset = value;
}
if let Ok(value) = values[1].trim().parse::<i16>() {
element.color = acadrust::types::Color::from_index(value);
element.color = codec::types::Color::from_index(value);
}
element.linetype = values[2].clone();
}
}
}
pub(in crate::app) fn tablestyle_mut(&mut self, tab: usize) -> Option<&mut acadrust::objects::TableStyle> {
use acadrust::objects::ObjectType;
pub(in crate::app) fn tablestyle_mut(&mut self, tab: usize) -> Option<&mut codec::objects::TableStyle> {
use codec::objects::ObjectType;
let name = self.tablestyle_selected.clone();
self.tabs[tab]
.scene
@@ -125,9 +125,9 @@ impl OpenCADStudio {
/// Mutable access to a table style's cell style by row (0=Data,1=Header,2=Title).
pub(in crate::app) fn ts_cell_of(
s: &mut acadrust::objects::TableStyle,
s: &mut codec::objects::TableStyle,
row: u8,
) -> Option<&mut acadrust::objects::RowCellStyle> {
) -> Option<&mut codec::objects::RowCellStyle> {
match row {
0 => Some(&mut s.data_row_style),
1 => Some(&mut s.header_row_style),
@@ -140,9 +140,9 @@ impl OpenCADStudio {
/// (0=left 1=right 2=top 3=bottom 4=horizontal-inside 5=vertical-inside).
pub(in crate::app) fn ts_border_of(
c: &mut acadrust::objects::RowCellStyle,
c: &mut codec::objects::RowCellStyle,
border: u8,
) -> Option<&mut acadrust::objects::TableCellBorder> {
) -> Option<&mut codec::objects::TableCellBorder> {
match border {
0 => Some(&mut c.left_border),
1 => Some(&mut c.right_border),
@@ -157,7 +157,7 @@ impl OpenCADStudio {
/// Populate margin + per-cell edit buffers from the selected table style.
pub(in crate::app) fn load_tablestyle_bufs(&mut self, tab: usize) {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let name = self.tablestyle_selected.clone();
let Some(s) = self.tabs[tab]
.scene
@@ -212,8 +212,8 @@ impl OpenCADStudio {
/// Mutable access to the currently selected multileader style.
pub(in crate::app) fn mleaderstyle_mut(&mut self, tab: usize) -> Option<&mut acadrust::objects::MultiLeaderStyle> {
use acadrust::objects::ObjectType;
pub(in crate::app) fn mleaderstyle_mut(&mut self, tab: usize) -> Option<&mut codec::objects::MultiLeaderStyle> {
use codec::objects::ObjectType;
let name = self.mleaderstyle_selected.clone();
self.tabs[tab]
.scene
@@ -229,7 +229,7 @@ impl OpenCADStudio {
/// Populate all edit buffers from the currently selected multileader style.
pub(in crate::app) fn load_mleaderstyle_bufs(&mut self, tab: usize) {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let name = self.mleaderstyle_selected.clone();
let Some(s) = self.tabs[tab]
.scene
@@ -364,7 +364,7 @@ impl OpenCADStudio {
.text_styles
.get(&self.ds_dimtxsty)
.map(|style| style.handle)
.unwrap_or(acadrust::types::Handle::NULL);
.unwrap_or(codec::types::Handle::NULL);
let Some(ds) = doc.dim_styles.get_mut(&self.dimstyle_selected) else {
return;
};
@@ -466,12 +466,12 @@ impl OpenCADStudio {
// pictures are stale. Drop them and let each be drawn again. Without
// this an edit here would move the numbers and leave the drawing alone.
let edited = self.dimstyle_selected.clone();
let stale: Vec<acadrust::Handle> = self.tabs[tab]
let stale: Vec<codec::Handle> = self.tabs[tab]
.scene
.document
.entities()
.filter_map(|entity| match entity {
acadrust::EntityType::Dimension(dim)
codec::EntityType::Dimension(dim)
if dim.base().style_name.eq_ignore_ascii_case(&edited) =>
{
Some(entity.common().handle)
@@ -480,6 +480,11 @@ impl OpenCADStudio {
})
.collect();
for handle in stale {
if let Some(codec::EntityType::Dimension(dimension)) =
self.tabs[tab].scene.document.get_entity_mut(handle)
{
crate::entities::dimension::reset_automatic_text_position(dimension.base_mut());
}
self.tabs[tab].scene.invalidate_dim_block_recorded(handle);
}
@@ -699,10 +704,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
c.text_height = h;
}
if let Some(v) = tc {
c.text_color = acadrust::types::Color::from_index(v);
c.text_color = codec::types::Color::from_index(v);
}
if let Some(v) = fc {
c.fill_color = acadrust::types::Color::from_index(v);
c.fill_color = codec::types::Color::from_index(v);
}
if let Some(v) = dtype {
c.data_type = v;
@@ -714,10 +719,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
for (b, (lw, color, spacing)) in border_vals.into_iter().enumerate() {
if let Some(bd) = Self::ts_border_of(c, b as u8) {
if let Some(v) = lw {
bd.line_weight = acadrust::types::LineWeight::from_value(v);
bd.line_weight = codec::types::LineWeight::from_value(v);
}
if let Some(v) = color {
bd.color = acadrust::types::Color::from_index(v);
bd.color = codec::types::Color::from_index(v);
}
if let Some(v) = spacing {
bd.double_line_spacing = v;
@@ -729,7 +734,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
pub(super) fn on_ml_style_dialog_open(&mut self) -> Task<Message> {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let i = self.active_tab;
let cur = self.tabs[i].scene.document.header.multiline_style.clone();
let exists = self.tabs[i]
@@ -762,7 +767,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
pub(super) fn on_mleader_style_dialog_open(&mut self) -> Task<Message> {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let i = self.active_tab;
let cur = self.tabs[i].active_mleader_style.clone();
let exists = self.tabs[i]
@@ -795,7 +800,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
pub(super) fn on_mleader_style_dialog_set_current(&mut self) -> Task<Message> {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let i = self.active_tab;
let name = self.mleaderstyle_selected.clone();
let exists = self.tabs[i]
@@ -848,7 +853,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
pub(super) fn on_mleader_style_set_enum(&mut self, field: &'static str, value: String) -> Task<Message> {
use acadrust::objects::{
use codec::objects::{
BlockContentConnectionType, LeaderContentType, LeaderDrawOrderType,
MultiLeaderDrawOrderType, MultiLeaderPathType, TextAlignmentType,
TextAngleType, TextAttachmentDirectionType, TextAttachmentType,
@@ -946,7 +951,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
pub(super) fn on_mleader_style_set_handle(&mut self, field: &'static str, value: String) -> Task<Message> {
let i = self.active_tab;
let doc = &self.tabs[i].scene.document;
let handle: Option<acadrust::types::Handle> = if value == "None"
let handle: Option<codec::types::Handle> = if value == "None"
|| value == "ByBlock"
|| value == "Closed filled"
{
@@ -1038,17 +1043,17 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
s.default_text = dt;
if let Some(v) = lc {
s.line_color = acadrust::types::Color::from_index(v);
s.line_color = codec::types::Color::from_index(v);
}
if let Some(v) = tc {
s.text_color = acadrust::types::Color::from_index(v);
s.text_color = codec::types::Color::from_index(v);
}
s.description = desc;
if let Some(v) = align {
s.align_space = v;
}
if let Some(v) = bclr {
s.block_content_color = acadrust::types::Color::from_index(v);
s.block_content_color = codec::types::Color::from_index(v);
}
if let Some(v) = brot {
s.block_content_rotation = v.to_radians();
@@ -1107,7 +1112,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Task::none()
}
pub(super) fn on_color_window_pick(&mut self, color: acadrust::types::Color) -> Task<Message> {
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
if matches!(
self.color_pick_target.as_ref().map(|(target, _)| target),
Some(crate::app::ColorPickTarget::PlotStyle)
@@ -1115,9 +1120,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
self.color_pick_target = None;
let rgb = match color {
acadrust::types::Color::Rgb { r, g, b } => Some((r, g, b)),
acadrust::types::Color::Index(index) => {
acadrust::types::aci_table::aci_to_rgb(index)
codec::types::Color::Rgb { r, g, b } => Some((r, g, b)),
codec::types::Color::Index(index) => {
codec::types::aci_table::aci_to_rgb(index)
}
_ => None,
};
@@ -1167,9 +1172,15 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Some(crate::app::ColorPickTarget::Ribbon) => {
Some(Message::RibbonColorChanged(color))
}
Some(crate::app::ColorPickTarget::Layer(idx)) => {
self.tabs[self.active_tab].layers.selected = Some(idx);
Some(Message::LayerColorSet(color))
Some(crate::app::ColorPickTarget::Layer(name)) => {
let i = self.active_tab;
match self.layer_panel_index(i, &name) {
Some(idx) => {
self.tabs[i].layers.selected = Some(idx);
Some(Message::LayerColorSet(color))
}
None => None,
}
}
Some(crate::app::ColorPickTarget::LayerState(idx)) => {
Some(Message::LayerStateEditorLayerColor(idx, color))
@@ -1193,28 +1204,25 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
);
let doc = &self.tabs[i].scene.document;
let handle = if value == "Default" || value == "ByBlock" {
acadrust::types::Handle::NULL
codec::types::Handle::NULL
} else if is_lt {
doc.line_types
.iter()
.find(|lt| lt.name == value)
.map(|lt| lt.handle)
.unwrap_or(acadrust::types::Handle::NULL)
.unwrap_or(codec::types::Handle::NULL)
} else {
doc.block_records
.iter()
.find(|b| b.name == value)
.map(|b| b.handle)
.unwrap_or(acadrust::types::Handle::NULL)
.unwrap_or(codec::types::Handle::NULL)
};
// Staged: persists on Apply.
if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&name) {
match field {
"dimblk" => ds.dimblk = handle,
"dimblk1" => {
ds.dimblk1 = handle;
ds.dimblk2 = handle;
}
"dimblk1" => ds.dimblk1 = handle,
"dimblk2" => ds.dimblk2 = handle,
"dimldrblk" => ds.dimldrblk = handle,
"dimltex_handle" => ds.dimltex_handle = handle,
@@ -1226,3 +1234,72 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Task::none()
}
}
#[cfg(test)]
mod ds_set_handle_tests {
// #41: picking the first arrowhead must not touch the second.
use crate::app::OpenCADStudio;
fn app_with_arrow_blocks() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
for name in ["ARROW_A", "ARROW_B"] {
let mut br = codec::tables::BlockRecord::new(name);
br.handle = app.tabs[i].scene.document.allocate_handle();
app.tabs[i]
.scene
.document
.block_records
.add(br)
.unwrap();
}
let first = app.tabs[i]
.scene
.document
.dim_styles
.iter()
.next()
.map(|s| s.name.clone())
.expect("fresh doc ships a dimstyle");
app.dimstyle_selected = first;
app
}
#[test]
fn dimblk1_pick_leaves_dimblk2_alone() {
let mut app = app_with_arrow_blocks();
let i = app.active_tab;
let name = app.dimstyle_selected.clone();
let hb = app.tabs[i]
.scene
.document
.block_records
.iter()
.find(|b| b.name == "ARROW_B")
.map(|b| b.handle)
.unwrap();
app.tabs[i]
.scene
.document
.dim_styles
.get_mut(&name)
.unwrap()
.dimblk2 = hb;
let _ = app.on_ds_set_handle("dimblk1", "ARROW_A".to_string());
let ds = app.tabs[i].scene.document.dim_styles.get(&name).unwrap();
let ha = app.tabs[i]
.scene
.document
.block_records
.iter()
.find(|b| b.name == "ARROW_A")
.map(|b| b.handle)
.unwrap();
assert_eq!(ds.dimblk1, ha, "first arrowhead takes the pick");
assert_eq!(
ds.dimblk2, hb,
"picking the first arrowhead must not touch the second"
);
}
}
+735 -289
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -5,7 +5,7 @@
use iced::Point;
use acadrust::types::Handle;
use codec::types::Handle;
use crate::app::OpenCADStudio;
use crate::scene::viewport_ref::{AcceptedSnap, ViewportFrame};
@@ -104,12 +104,12 @@ impl OpenCADStudio {
.scene
.document
.get_entity(source.source.handle),
Some(acadrust::EntityType::Insert(_))
Some(codec::EntityType::Insert(_))
) {
return true;
}
if source.snap_type == SnapType::Insertion {
let Some(acadrust::EntityType::Insert(insert)) = self.tabs[tab]
let Some(codec::EntityType::Insert(insert)) = self.tabs[tab]
.scene
.document
.get_entity(source.source.handle)
@@ -234,7 +234,7 @@ impl OpenCADStudio {
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.as_dvec3()).as_vec3());
saved_from.map(|p| frame.paper_to_model(p));
let hit = self.snapper.snap(
model_cursor,
local,