forked from 1b-agent/opencadstudio-upstream
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:
625 files changed
+104397
-28053
No files matched your search
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user