Blocks palette: tabs, options, drag insertion and -INSERT

Rebuild the Blocks palette with Current Drawing / Recent / Favorites /
Libraries tabs, wildcard filter, six views, insertion options, click and
drop insertion and a block context menu. INSERT opens the palette, the
new -INSERT runs on the command line, the ribbon Insert button becomes a
block gallery, and BLOCKMRULIST, BLOCKREDEFINEMODE, BLOCKNAVIGATE,
BLOCKSTATE, EXPLMODE and INSNAME are added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-01 11:17:38 +03:00
1 parent 3dcebf0dc6
commit 0826101c5e
49 files changed
+3401 -573

No files matched your search

+603
View File
@@ -0,0 +1,603 @@
//! 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;
}
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 }
}
}
+41 -119
View File
@@ -2,7 +2,6 @@
#![allow(unused_imports)]
use super::util::*;
use crate::ui::window::block_palette::BlockPaletteMsg;
use super::{format_size, VIEWCUBE_HIT_SIZE};
use crate::app::helpers::{
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
@@ -373,21 +372,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: 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).
@@ -441,7 +460,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.
@@ -513,74 +538,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> {
@@ -754,57 +727,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.reset_command_start_state(i);
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
@@ -1626,7 +1548,7 @@ 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"));
}
+19
View File
@@ -734,6 +734,14 @@ impl OpenCADStudio {
grid_beyond_limits: self.grid_beyond_limits,
block_mru: self.block_mru.clone(),
block_freq: self.block_freq.clone(),
block_recent: self.block_palette.recent.clone(),
block_favorites: self.block_palette.favorites.clone(),
block_libraries: self.block_palette.libraries.clone(),
block_palette_view: self.block_palette.view as u8,
block_insert: self.block_palette.options.clone(),
block_mru_list: self.block_mru_list,
block_redefine_mode: self.block_redefine_mode,
block_navigate: self.block_navigate.clone(),
}
}
@@ -854,6 +862,15 @@ impl OpenCADStudio {
.take(200)
.map(|(k, v)| (k.clone(), *v))
.collect();
self.block_mru_list = s.block_mru_list.min(100);
self.block_redefine_mode = s.block_redefine_mode.min(2);
self.block_navigate = s.block_navigate.clone();
self.block_palette.recent = s.block_recent.clone();
self.block_palette.favorites = s.block_favorites.clone();
self.block_palette.libraries = s.block_libraries.clone();
self.block_palette.view = crate::ui::window::block_palette::ViewMode::from_u8(s.block_palette_view);
self.block_palette.set_options(s.block_insert.clone());
self.trim_recent_blocks();
// Push restored display defaults onto every drawing tab that exists now.
// Tabs created later pick them up at their construction site.
for idx in 0..self.tabs.len() {
@@ -863,6 +880,8 @@ impl OpenCADStudio {
}
pub(crate) fn record_block_insert(&mut self, name: &str) {
self.insname = name.to_string();
self.note_recent_block(name);
let key = name.to_ascii_uppercase();
*self.block_freq.entry(key).or_insert(0) += 1;
self.block_mru.retain(|n| !n.eq_ignore_ascii_case(name));
+4
View File
@@ -92,6 +92,7 @@ fn reorder_insertion_index(from: usize, to: usize, after: bool, len: usize) -> O
mod command;
mod context_menu;
mod blocks_palette;
mod dialog;
mod dynamic;
mod file;
@@ -5768,6 +5769,9 @@ impl OpenCADStudio {
if self.tabs[self.active_tab].is_start {
self.ribbon.close_dropdown();
} else {
if id == crate::modules::insert::insert_block::GALLERY_ID {
self.refresh_block_palette();
}
self.ribbon.toggle_dropdown(&id);
}
Task::none()
+5
View File
@@ -3356,6 +3356,7 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
let i = self.active_tab;
self.block_palette.pressed = None;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
@@ -3724,6 +3725,10 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
let i = self.active_tab;
// A block dragged from the Blocks palette lands here.
if let Some(item) = self.take_block_drop() {
return self.drop_block(item);
}
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);