forked from 1b-agent/opencadstudio-upstream
fix(sheetset): read drawings off the UI thread
The wizard's folder scan, Import Layout as Sheet, the template layout lists and the drawing cache behind Model Views, Place on Sheet, callout / label blocks and block previews parsed whole DWGs on the UI thread. They now go through the shared `background_task` worker: a cache miss reads the drawing and handles the message that needed it again. Block previews lend the cached drawing to the preview scene instead of cloning it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40847d72a1
commit
3a5c1dbb7d
4 files changed
+305
-153
No files matched your search
+255
-139
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
use crate::app::{Message, OpenCADStudio};
|
use crate::app::{Message, OpenCADStudio};
|
||||||
use crate::ui::window::sheet_set::{
|
use crate::ui::window::sheet_set::{
|
||||||
BlockList, Category, FieldId, Form, FormKind, FoundLayout, ImportRow, MenuAction, Placing, PropRow, Properties,
|
BlockList, Category, FieldId, Form, FormKind, FoundLayout, ImportRead, ImportRow, MenuAction, Placing, PropRow,
|
||||||
RowKey, SelectBlock, SheetSetMsg, SheetStatus, SsDialog, TemplatePick, ViewAction, Wizard, WizardMsg,
|
Properties, RowKey, SelectBlock, SheetSetMsg, SheetStatus, SsDialog, TemplatePick, ViewAction, Wizard, WizardMsg,
|
||||||
};
|
};
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||||
use codec::entities::Entity as _;
|
use codec::entities::Entity as _;
|
||||||
@@ -300,13 +300,40 @@ fn prompts_for_template(db: &SheetSetDatabase, component: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A drawing's paper-space layouts and sheet link, for Import (run on a
|
||||||
|
/// worker thread).
|
||||||
|
fn read_import(path: &Path) -> ImportRead {
|
||||||
|
let doc = crate::io::load_file(path).map_err(|e| e.to_string())?;
|
||||||
|
Ok((paper_layouts(&doc), doc.sheet_set_data()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the drawings picked for Import off the UI thread.
|
||||||
|
fn read_imports(paths: Vec<PathBuf>, new_form: bool) -> Task<Message> {
|
||||||
|
crate::app::update::background_task(
|
||||||
|
crate::t!("Import Layout as Sheet"),
|
||||||
|
move || {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| {
|
||||||
|
let read = read_import(&p);
|
||||||
|
(p, read)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
move |read| Message::SheetSet(SheetSetMsg::ImportRead(new_form, read)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// The layouts of a drawing that can become sheets, each with the sheet set
|
/// The layouts of a drawing that can become sheets, each with the sheet set
|
||||||
/// it already belongs to: the drawing's sheet link, or this set.
|
/// it already belongs to: the drawing's sheet link, or this set.
|
||||||
fn import_rows(path: &Path, db: &SheetSetDatabase) -> Result<Vec<ImportRow>, String> {
|
fn import_rows(
|
||||||
let doc = crate::io::load_file(path).map_err(|e| e.to_string())?;
|
path: &Path,
|
||||||
|
layouts: Vec<(String, String)>,
|
||||||
|
link: Option<&SheetSetData>,
|
||||||
|
db: &SheetSetDatabase,
|
||||||
|
) -> Vec<ImportRow> {
|
||||||
let drawing = path.to_string_lossy().to_string();
|
let drawing = path.to_string_lossy().to_string();
|
||||||
let link = doc.sheet_set_data();
|
layouts
|
||||||
Ok(paper_layouts(&doc)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(layout, handle)| {
|
.map(|(layout, handle)| {
|
||||||
let mine = db.sheet_for(&drawing, Some(&layout)).is_some_and(|s| {
|
let mine = db.sheet_for(&drawing, Some(&layout)).is_some_and(|s| {
|
||||||
@@ -315,14 +342,43 @@ fn import_rows(path: &Path, db: &SheetSetDatabase) -> Result<Vec<ImportRow>, Str
|
|||||||
// A layout of this set is taken; one the drawing's sheet link
|
// A layout of this set is taken; one the drawing's sheet link
|
||||||
// names for another set is only warned about (still importable).
|
// names for another set is only warned about (still importable).
|
||||||
let warn = !mine
|
let warn = !mine
|
||||||
&& link.as_ref().is_some_and(|l| {
|
&& link.is_some_and(|l| {
|
||||||
l.layout_name.eq_ignore_ascii_case(&layout)
|
l.layout_name.eq_ignore_ascii_case(&layout)
|
||||||
&& !l.sheet_set_file_name.is_empty()
|
&& !l.sheet_set_file_name.is_empty()
|
||||||
&& db.path.as_deref().is_none_or(|p| ss::path_key(p) != ss::path_key(&l.sheet_set_file_name))
|
&& db.path.as_deref().is_none_or(|p| ss::path_key(p) != ss::path_key(&l.sheet_set_file_name))
|
||||||
});
|
});
|
||||||
ImportRow { drawing: drawing.clone(), layout, handle, on: !mine, taken: mine, warn }
|
ImportRow { drawing: drawing.clone(), layout, handle, on: !mine, taken: mine, warn }
|
||||||
})
|
})
|
||||||
.collect())
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layouts of the drawings in a wizard folder (run on a worker thread).
|
||||||
|
fn scan_folder(path: &Path) -> Vec<FoundLayout> {
|
||||||
|
let folder = path.to_string_lossy().to_string();
|
||||||
|
let mut files: Vec<PathBuf> = std::fs::read_dir(path)
|
||||||
|
.map(|rd| {
|
||||||
|
rd.filter_map(|e| e.ok().map(|e| e.path()))
|
||||||
|
.filter(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("dwg")))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
files.sort();
|
||||||
|
let mut found = Vec::new();
|
||||||
|
for file in files {
|
||||||
|
let Ok(doc) = crate::io::load_file(&file) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for (layout, handle) in paper_layouts(&doc) {
|
||||||
|
found.push(FoundLayout {
|
||||||
|
folder: folder.clone(),
|
||||||
|
file: file.to_string_lossy().to_string(),
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
on: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rename & Renumber: layout and file names follow the options.
|
/// Rename & Renumber: layout and file names follow the options.
|
||||||
@@ -752,7 +808,17 @@ impl OpenCADStudio {
|
|||||||
|p| Message::SheetSet(SheetSetMsg::ImportAdd(p)),
|
|p| Message::SheetSet(SheetSetMsg::ImportAdd(p)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SheetSetMsg::ImportAdd(paths) => self.import_add(paths),
|
SheetSetMsg::ImportAdd(paths) if !paths.is_empty() => return read_imports(paths, false),
|
||||||
|
SheetSetMsg::ImportAdd(_) => {}
|
||||||
|
SheetSetMsg::ImportRead(new_form, read) => self.import_read(new_form, read),
|
||||||
|
SheetSetMsg::FolderScanned(path, found) => self.folder_scanned(path, found),
|
||||||
|
SheetSetMsg::DrawingRead(path, read, then) => match read.0 {
|
||||||
|
Ok((modified, doc)) => {
|
||||||
|
self.sheet_set.drawings.insert(&path, modified, doc);
|
||||||
|
return self.on_sheet_set(*then);
|
||||||
|
}
|
||||||
|
Err(e) => self.command_line.push_error(&format!("{path}: {e}")),
|
||||||
|
},
|
||||||
SheetSetMsg::Previous => self.rename_step(false),
|
SheetSetMsg::Previous => self.rename_step(false),
|
||||||
SheetSetMsg::Next => self.rename_step(true),
|
SheetSetMsg::Next => self.rename_step(true),
|
||||||
SheetSetMsg::ViewsByCategory(v) => self.sheet_set.by_category = v,
|
SheetSetMsg::ViewsByCategory(v) => self.sheet_set.by_category = v,
|
||||||
@@ -778,10 +844,14 @@ impl OpenCADStudio {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
// A whole drawing taken as a block shows its model space.
|
// A whole drawing taken as a block shows its model space.
|
||||||
let preview = target.and_then(|(file, name)| {
|
let mut preview = None;
|
||||||
let whole = self.sheet_set.drawings.get(&file).is_some_and(|d| d.block_records.get(&name).is_none());
|
if let Some((file, name)) = target {
|
||||||
self.block_preview(&file, if whole { "" } else { &name })
|
let whole = match self.drawing_or_read(&file, SheetSetMsg::BlockListSelect(k)) {
|
||||||
});
|
Ok(doc) => doc.block_records.get(&name).is_none(),
|
||||||
|
Err(task) => return task,
|
||||||
|
};
|
||||||
|
preview = self.block_preview(&file, if whole { "" } else { &name });
|
||||||
|
}
|
||||||
if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.as_mut() {
|
if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.as_mut() {
|
||||||
b.selected = Some(k);
|
b.selected = Some(k);
|
||||||
b.preview = preview;
|
b.preview = preview;
|
||||||
@@ -820,7 +890,7 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
SheetSetMsg::SelectBlockPicked(None) => {}
|
SheetSetMsg::SelectBlockPicked(None) => {}
|
||||||
SheetSetMsg::SelectBlockPicked(Some(path)) => self.select_block_file(path),
|
SheetSetMsg::SelectBlockPicked(Some(path)) => return self.select_block_file(path),
|
||||||
SheetSetMsg::SelectBlockWhole(v) => {
|
SheetSetMsg::SelectBlockWhole(v) => {
|
||||||
let file = match self.sheet_set.dialog.as_ref() {
|
let file = match self.sheet_set.dialog.as_ref() {
|
||||||
Some(SsDialog::SelectBlock(b)) => b.file.clone(),
|
Some(SsDialog::SelectBlock(b)) => b.file.clone(),
|
||||||
@@ -847,17 +917,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SheetSetMsg::ToggleDrawing(path) => {
|
SheetSetMsg::ToggleDrawing(path) => {
|
||||||
let key = ss::path_key(&path);
|
if !self.sheet_set.open_drawings.remove(&ss::path_key(&path)) {
|
||||||
if !self.sheet_set.open_drawings.remove(&key) {
|
return self.on_sheet_set(SheetSetMsg::SeeViews(path));
|
||||||
self.load_model_views(&path);
|
|
||||||
self.sheet_set.open_drawings.insert(key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SheetSetMsg::SeeViews(path) => {
|
SheetSetMsg::SeeViews(path) => {
|
||||||
self.load_model_views(&path);
|
if let Err(task) = self.load_model_views(&path) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
self.sheet_set.open_drawings.insert(ss::path_key(&path));
|
self.sheet_set.open_drawings.insert(ss::path_key(&path));
|
||||||
}
|
}
|
||||||
SheetSetMsg::PlaceOnSheet(path, view) => self.start_place_view(path, view),
|
SheetSetMsg::PlaceOnSheet(path, view) => return self.start_place_view(path, view),
|
||||||
SheetSetMsg::ViewMenu(id, action) => {
|
SheetSetMsg::ViewMenu(id, action) => {
|
||||||
self.sheet_set.submenu = None;
|
self.sheet_set.submenu = None;
|
||||||
return self.view_menu(id, action);
|
return self.view_menu(id, action);
|
||||||
@@ -911,10 +981,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SheetSetMsg::OpenPicked(None) | SheetSetMsg::ImportPicked(None) | SheetSetMsg::TemplatePicked(None) => {}
|
SheetSetMsg::OpenPicked(None) | SheetSetMsg::ImportPicked(None) | SheetSetMsg::TemplatePicked(None) => {}
|
||||||
SheetSetMsg::ImportPicked(Some(path)) => self.import_layout_form(path),
|
SheetSetMsg::ImportPicked(Some(path)) => return read_imports(vec![path], true),
|
||||||
SheetSetMsg::FolderPicked(field, Some(path)) => self.dialog_folder(field, path),
|
SheetSetMsg::FolderPicked(field, Some(path)) => return self.dialog_folder(field, path),
|
||||||
SheetSetMsg::FolderPicked(_, None) => {}
|
SheetSetMsg::FolderPicked(_, None) => {}
|
||||||
SheetSetMsg::TemplatePicked(Some(path)) => self.properties_template(path),
|
SheetSetMsg::TemplatePicked(Some(path)) => return self.properties_template(path),
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
SheetSetMsg::Browse(field) => {
|
SheetSetMsg::Browse(field) => {
|
||||||
return Task::perform(
|
return Task::perform(
|
||||||
@@ -1116,7 +1186,14 @@ impl OpenCADStudio {
|
|||||||
// A subset that prompts for its template asks for it first.
|
// A subset that prompts for its template asks for it first.
|
||||||
if prompts_for_template(db, &id) {
|
if prompts_for_template(db, &id) {
|
||||||
let file = template.map(|t| t.file_name).unwrap_or_default();
|
let file = template.map(|t| t.file_name).unwrap_or_default();
|
||||||
let layouts = crate::io::load_file(Path::new(&file)).map(|d| paper_layouts(&d).into_iter().map(|(n, _)| n).collect()).unwrap_or_default();
|
let layouts = if Path::new(&file).is_file() {
|
||||||
|
match self.drawing_or_read(&file, SheetSetMsg::Menu(id.clone(), MenuAction::NewSheet)) {
|
||||||
|
Ok(doc) => paper_layouts(&doc).into_iter().map(|(n, _)| n).collect(),
|
||||||
|
Err(task) => return task,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
self.sheet_set.dialog = Some(SsDialog::Template(TemplatePick { form, file, layouts, layout: 0 }));
|
self.sheet_set.dialog = Some(SsDialog::Template(TemplatePick { form, file, layouts, layout: 0 }));
|
||||||
} else {
|
} else {
|
||||||
self.sheet_set.dialog = Some(SsDialog::Form(form));
|
self.sheet_set.dialog = Some(SsDialog::Form(form));
|
||||||
@@ -1352,46 +1429,42 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dialog_folder(&mut self, field: FieldId, path: PathBuf) {
|
fn dialog_folder(&mut self, field: FieldId, path: PathBuf) -> Task<Message> {
|
||||||
let folder = path.to_string_lossy().to_string();
|
|
||||||
if field == FieldId::AddFolder {
|
if field == FieldId::AddFolder {
|
||||||
self.wizard_add_folder(path);
|
return self.wizard_add_folder(path);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
self.dialog_input(field, folder);
|
self.dialog_input(field, path.to_string_lossy().to_string());
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wizard_add_folder(&mut self, path: PathBuf) {
|
/// The wizard's Browse for existing drawings: the folder's drawings are
|
||||||
|
/// read off the UI thread.
|
||||||
|
fn wizard_add_folder(&mut self, path: PathBuf) -> Task<Message> {
|
||||||
|
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_ref() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if w.folders.contains(&path.to_string_lossy().to_string()) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
crate::app::update::background_task(
|
||||||
|
crate::t!("Sheet Set Manager"),
|
||||||
|
move || {
|
||||||
|
let found = scan_folder(&path);
|
||||||
|
(path, found)
|
||||||
|
},
|
||||||
|
|(path, found)| Message::SheetSet(SheetSetMsg::FolderScanned(path, found)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn folder_scanned(&mut self, path: PathBuf, found: Vec<FoundLayout>) {
|
||||||
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_mut() else {
|
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let folder = path.to_string_lossy().to_string();
|
let folder = path.to_string_lossy().to_string();
|
||||||
if w.folders.contains(&folder) {
|
if !w.folders.contains(&folder) {
|
||||||
return;
|
w.layouts.extend(found);
|
||||||
|
w.folders.push(folder);
|
||||||
}
|
}
|
||||||
let mut files: Vec<PathBuf> = std::fs::read_dir(&path)
|
|
||||||
.map(|rd| {
|
|
||||||
rd.filter_map(|e| e.ok().map(|e| e.path()))
|
|
||||||
.filter(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("dwg")))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
files.sort();
|
|
||||||
for file in files {
|
|
||||||
let Ok(doc) = crate::io::load_file(&file) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for (layout, handle) in paper_layouts(&doc) {
|
|
||||||
w.layouts.push(FoundLayout {
|
|
||||||
folder: folder.clone(),
|
|
||||||
file: file.to_string_lossy().to_string(),
|
|
||||||
layout,
|
|
||||||
handle,
|
|
||||||
on: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.folders.push(folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_wizard(&mut self, m: WizardMsg) {
|
fn on_wizard(&mut self, m: WizardMsg) {
|
||||||
@@ -1429,18 +1502,42 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_layout_form(&mut self, path: PathBuf) {
|
/// The drawings picked for Import, read: a new Import dialog for the
|
||||||
let (Some(set), Some(parent)) = (self.sheet_set.current, self.sheet_set_import_parent.take()) else {
|
/// subset whose menu asked, or the open dialog's rows replaced.
|
||||||
|
fn import_read(&mut self, new_form: bool, read: Vec<(PathBuf, ImportRead)>) {
|
||||||
|
let set = match self.sheet_set.dialog.as_ref() {
|
||||||
|
_ if new_form => self.sheet_set.current,
|
||||||
|
Some(SsDialog::Form(f)) => Some(f.set),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(db) = set.and_then(|k| self.sheet_set.sets.get(k)) else { return };
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for (path, r) in read {
|
||||||
|
match r {
|
||||||
|
Ok((layouts, link)) => rows.extend(import_rows(&path, layouts, link.as_ref(), db)),
|
||||||
|
Err(e) => errors.push(format!("{}: {e}", path.display())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let failed = !errors.is_empty();
|
||||||
|
for e in errors {
|
||||||
|
self.command_line.push_error(&e);
|
||||||
|
}
|
||||||
|
if !new_form {
|
||||||
|
// The drawings picked replace the list.
|
||||||
|
if let Some(SsDialog::Form(f)) = self.sheet_set.dialog.as_mut() {
|
||||||
|
if !rows.is_empty() {
|
||||||
|
f.rows = rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (Some(set), Some(parent)) = (set, self.sheet_set_import_parent.take()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(db) = self.sheet_set.db() else { return };
|
if failed {
|
||||||
let rows = match import_rows(&path, db) {
|
return;
|
||||||
Ok(rows) => rows,
|
}
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(&format!("{}: {e}", path.display()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.sheet_set.dialog = Some(SsDialog::Form(Form {
|
self.sheet_set.dialog = Some(SsDialog::Form(Form {
|
||||||
kind: FormKind::ImportLayout,
|
kind: FormKind::ImportLayout,
|
||||||
set,
|
set,
|
||||||
@@ -1452,25 +1549,6 @@ impl OpenCADStudio {
|
|||||||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Browse for Drawings of the open Import dialog: add their layouts.
|
|
||||||
fn import_add(&mut self, paths: Vec<PathBuf>) {
|
|
||||||
let Some(SsDialog::Form(f)) = self.sheet_set.dialog.as_ref() else { return };
|
|
||||||
let Some(db) = self.sheet_set.sets.get(f.set) else { return };
|
|
||||||
let mut added = Vec::new();
|
|
||||||
for p in paths {
|
|
||||||
match import_rows(&p, db) {
|
|
||||||
Ok(rows) => added.extend(rows),
|
|
||||||
Err(e) => self.command_line.push_error(&format!("{}: {e}", p.display())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The drawings picked replace the list.
|
|
||||||
if let Some(SsDialog::Form(f)) = self.sheet_set.dialog.as_mut() {
|
|
||||||
if !added.is_empty() {
|
|
||||||
f.rows = added;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rename & Renumber: number and title into the set, and the layout and
|
/// Rename & Renumber: number and title into the set, and the layout and
|
||||||
/// drawing file renamed when they changed.
|
/// drawing file renamed when they changed.
|
||||||
fn apply_rename(&mut self, f: &Form) -> Result<(), String> {
|
fn apply_rename(&mut self, f: &Form) -> Result<(), String> {
|
||||||
@@ -1658,16 +1736,38 @@ impl OpenCADStudio {
|
|||||||
self.save_current_sheet_set();
|
self.save_current_sheet_set();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A block preview (`name` empty: the drawing's model space), cached by
|
/// A drawing read and cached, or the task that reads it off the UI thread
|
||||||
/// `file|name`; returns its key.
|
/// and then handles `then` again.
|
||||||
|
fn drawing_or_read(
|
||||||
|
&mut self,
|
||||||
|
path: &str,
|
||||||
|
then: SheetSetMsg,
|
||||||
|
) -> Result<std::sync::Arc<codec::CadDocument>, Task<Message>> {
|
||||||
|
if let Some(doc) = self.sheet_set.drawings.get(path) {
|
||||||
|
return Ok(doc);
|
||||||
|
}
|
||||||
|
let path = path.to_string();
|
||||||
|
Err(crate::app::update::background_task(
|
||||||
|
crate::t!("Sheet Set Manager"),
|
||||||
|
{
|
||||||
|
let path = path.clone();
|
||||||
|
move || crate::ui::window::sheet_set::read_drawing(&path)
|
||||||
|
},
|
||||||
|
move |read| Message::SheetSet(SheetSetMsg::DrawingRead(path, read, Box::new(then))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block preview (`name` empty: the drawing's model space) of a cached
|
||||||
|
/// drawing, cached by `file|name`; returns its key.
|
||||||
fn block_preview(&mut self, file: &str, name: &str) -> Option<String> {
|
fn block_preview(&mut self, file: &str, name: &str) -> Option<String> {
|
||||||
let key = format!("{}|{}", ss::path_key(file), name);
|
let key = format!("{}|{}", ss::path_key(file), name);
|
||||||
if self.sheet_set.previews.contains_key(&key) {
|
if self.sheet_set.previews.contains_key(&key) {
|
||||||
return Some(key);
|
return Some(key);
|
||||||
}
|
}
|
||||||
let doc = self.sheet_set.drawings.get(file)?;
|
// The cached drawing is lent to a scene for tessellation, not copied.
|
||||||
|
let (modified, doc) = self.sheet_set.drawings.take(file)?;
|
||||||
let mut scene = crate::scene::Scene::new();
|
let mut scene = crate::scene::Scene::new();
|
||||||
scene.document = (*doc).clone();
|
scene.document = std::sync::Arc::try_unwrap(doc).unwrap_or_else(|doc| (*doc).clone());
|
||||||
let wires = if name.is_empty() {
|
let wires = if name.is_empty() {
|
||||||
let model = scene.document.header.model_space_block_handle;
|
let model = scene.document.header.model_space_block_handle;
|
||||||
let entities: Vec<codec::EntityType> = scene
|
let entities: Vec<codec::EntityType> = scene
|
||||||
@@ -1682,6 +1782,8 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
scene.block_preview_wires(name)
|
scene.block_preview_wires(name)
|
||||||
};
|
};
|
||||||
|
let doc = std::mem::take(&mut scene.document);
|
||||||
|
self.sheet_set.drawings.insert(file, modified, std::sync::Arc::new(doc));
|
||||||
self.sheet_set.previews.insert(key.clone(), wires);
|
self.sheet_set.previews.insert(key.clone(), wires);
|
||||||
Some(key)
|
Some(key)
|
||||||
}
|
}
|
||||||
@@ -1697,19 +1799,17 @@ impl OpenCADStudio {
|
|||||||
Some((db.resolve_file(b), b.prop("Name").unwrap_or("").to_string()))
|
Some((db.resolve_file(b), b.prop("Name").unwrap_or("").to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Named model views of a drawing (read once, when its row is opened).
|
/// Named model views of a drawing (read once, when its row is opened);
|
||||||
fn load_model_views(&mut self, path: &str) {
|
/// `Err` reads the drawing first.
|
||||||
|
fn load_model_views(&mut self, path: &str) -> Result<(), Task<Message>> {
|
||||||
let key = ss::path_key(path);
|
let key = ss::path_key(path);
|
||||||
if self.sheet_set.model_views.contains_key(&key) {
|
if self.sheet_set.model_views.contains_key(&key) {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
let views = self
|
let doc = self.drawing_or_read(path, SheetSetMsg::SeeViews(path.to_string()))?;
|
||||||
.sheet_set
|
let views = doc.views.iter().filter(|v| !v.paper_space).map(|v| v.name.clone()).collect();
|
||||||
.drawings
|
|
||||||
.get(path)
|
|
||||||
.map(|d| d.views.iter().filter(|v| !v.paper_space).map(|v| v.name.clone()).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
self.sheet_set.model_views.insert(key, views);
|
self.sheet_set.model_views.insert(key, views);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current set's sheet shown by the active tab at its current layout.
|
/// The current set's sheet shown by the active tab at its current layout.
|
||||||
@@ -1748,28 +1848,38 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Place on Sheet: only on a sheet of this set open at its layout.
|
/// Place on Sheet: only on a sheet of this set open at its layout.
|
||||||
fn start_place_view(&mut self, drawing: String, view: Option<String>) {
|
/// The model drawing and the set's view label block drawing are read
|
||||||
|
/// first.
|
||||||
|
fn start_place_view(&mut self, drawing: String, view: Option<String>) -> Task<Message> {
|
||||||
if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") {
|
if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") {
|
||||||
self.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.").as_ref());
|
crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.")
|
||||||
return;
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
}
|
}
|
||||||
let Some((set, sheet)) = self.current_sheet() else {
|
let Some((set, sheet)) = self.current_sheet() else {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("Open a sheet of this sheet set at its layout to place a view on it.").as_ref());
|
.push_error(crate::t!("Open a sheet of this sheet set at its layout to place a view on it.").as_ref());
|
||||||
return;
|
return Task::none();
|
||||||
|
};
|
||||||
|
let again = || SheetSetMsg::PlaceOnSheet(drawing.clone(), view.clone());
|
||||||
|
let label = self.sheet_set.db().and_then(|db| db.sheet_set().named("DefLabelBlk").map(|b| db.resolve_file(b)));
|
||||||
|
if let Some(label) = label.filter(|l| Path::new(l).is_file()) {
|
||||||
|
if let Err(task) = self.drawing_or_read(&label, again()) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let size = match self.drawing_or_read(&drawing, again()) {
|
||||||
|
Ok(doc) => model_view(&doc, view.as_deref()).map(|v| (v.width, v.height)),
|
||||||
|
Err(task) => return task,
|
||||||
};
|
};
|
||||||
let size = self
|
|
||||||
.sheet_set
|
|
||||||
.drawings
|
|
||||||
.get(&drawing)
|
|
||||||
.and_then(|d| model_view(&d, view.as_deref()))
|
|
||||||
.map(|v| (v.width, v.height));
|
|
||||||
let Some(size) = size else {
|
let Some(size) = size else {
|
||||||
self.command_line.push_error(crate::tf!("{}: no view to place.", drawing).as_ref());
|
self.command_line.push_error(crate::tf!("{}: no view to place.", drawing).as_ref());
|
||||||
return;
|
return Task::none();
|
||||||
};
|
};
|
||||||
self.start_placing(Placing::View { set, sheet, drawing, view }, size);
|
self.start_placing(Placing::View { set, sheet, drawing, view }, size);
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sheet view's commands.
|
/// A sheet view's commands.
|
||||||
@@ -1812,17 +1922,22 @@ impl OpenCADStudio {
|
|||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("Open the view's sheet at its layout to place a block on it.").as_ref());
|
.push_error(crate::t!("Open the view's sheet at its layout to place a block on it.").as_ref());
|
||||||
}
|
}
|
||||||
ViewAction::Callout(block) => {
|
ViewAction::Callout(_) | ViewAction::Label => {
|
||||||
let Some(b) = db.find(&block) else { return Task::none() };
|
let b = match &action {
|
||||||
let (file, name) = (db.resolve_file(b), b.prop("Name").unwrap_or("").to_string());
|
ViewAction::Callout(block) => db.find(block),
|
||||||
self.start_placing(Placing::Block { set, sheet, view: id, file, name }, (0.0, 0.0));
|
_ => db.sheet_set().named("DefLabelBlk"),
|
||||||
}
|
};
|
||||||
ViewAction::Label => {
|
let Some(b) = b else {
|
||||||
let Some(b) = db.sheet_set().named("DefLabelBlk") else {
|
if action == ViewAction::Label {
|
||||||
self.command_line.push_error(crate::t!("The sheet set has no label block for views.").as_ref());
|
self.command_line.push_error(crate::t!("The sheet set has no label block for views.").as_ref());
|
||||||
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let (file, name) = (db.resolve_file(b), b.prop("Name").unwrap_or("").to_string());
|
let (file, name) = (db.resolve_file(b), b.prop("Name").unwrap_or("").to_string());
|
||||||
|
// The block's drawing is read first.
|
||||||
|
if let Err(task) = self.drawing_or_read(&file, SheetSetMsg::ViewMenu(id.clone(), action)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
self.start_placing(Placing::Block { set, sheet, view: id, file, name }, (0.0, 0.0));
|
self.start_placing(Placing::Block { set, sheet, view: id, file, name }, (0.0, 0.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1963,24 +2078,20 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Select Block: read a drawing's named blocks.
|
/// Select Block: read a drawing's named blocks.
|
||||||
fn select_block_file(&mut self, path: PathBuf) {
|
fn select_block_file(&mut self, path: PathBuf) -> Task<Message> {
|
||||||
let file = path.to_string_lossy().to_string();
|
let file = path.to_string_lossy().to_string();
|
||||||
let names: Vec<(String, String, bool)> = match self.sheet_set.drawings.get(&file).ok_or_else(|| crate::t!("cannot be read").into_owned()) {
|
let doc = match self.drawing_or_read(&file, SheetSetMsg::SelectBlockPicked(Some(path))) {
|
||||||
Ok(doc) => {
|
Ok(doc) => doc,
|
||||||
let mut n: Vec<(String, String, bool)> = doc
|
Err(task) => return task,
|
||||||
.block_records
|
|
||||||
.iter()
|
|
||||||
.filter(|b| !b.name.starts_with('*') && !b.is_layout())
|
|
||||||
.map(|b| (b.name.clone(), format!("{:X}", b.handle.value()), false))
|
|
||||||
.collect();
|
|
||||||
n.sort();
|
|
||||||
n
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(&format!("{}: {e}", path.display()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let mut names: Vec<(String, String, bool)> = doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.filter(|b| !b.name.starts_with('*') && !b.is_layout())
|
||||||
|
.map(|b| (b.name.clone(), format!("{:X}", b.handle.value()), false))
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
drop(doc);
|
||||||
// The whole drawing is the first preview.
|
// The whole drawing is the first preview.
|
||||||
let preview = self.block_preview(&file, "");
|
let preview = self.block_preview(&file, "");
|
||||||
if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() {
|
if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() {
|
||||||
@@ -1989,6 +2100,7 @@ impl OpenCADStudio {
|
|||||||
b.preview = preview;
|
b.preview = preview;
|
||||||
b.error = None;
|
b.error = None;
|
||||||
}
|
}
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model Views: each location folder of the current set and its drawings.
|
/// Model Views: each location folder of the current set and its drawings.
|
||||||
@@ -2057,20 +2169,23 @@ impl OpenCADStudio {
|
|||||||
self.sheet_set.status = status;
|
self.sheet_set.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties_template(&mut self, path: PathBuf) {
|
fn properties_template(&mut self, path: PathBuf) -> Task<Message> {
|
||||||
let layouts = crate::io::load_file(&path).map(|d| paper_layouts(&d)).unwrap_or_default();
|
let file = path.to_string_lossy().to_string();
|
||||||
|
let layouts = match self.drawing_or_read(&file, SheetSetMsg::TemplatePicked(Some(path))) {
|
||||||
|
Ok(doc) => paper_layouts(&doc),
|
||||||
|
Err(task) => return task,
|
||||||
|
};
|
||||||
if let Some(SsDialog::Template(t)) = self.sheet_set.dialog.as_mut() {
|
if let Some(SsDialog::Template(t)) = self.sheet_set.dialog.as_mut() {
|
||||||
t.file = path.to_string_lossy().to_string();
|
t.file = file;
|
||||||
t.layouts = layouts.into_iter().map(|(n, _)| n).collect();
|
t.layouts = layouts.into_iter().map(|(n, _)| n).collect();
|
||||||
t.layout = 0;
|
t.layout = 0;
|
||||||
return;
|
return Task::none();
|
||||||
}
|
}
|
||||||
let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() else {
|
let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() else {
|
||||||
return;
|
return Task::none();
|
||||||
};
|
};
|
||||||
match layouts.first() {
|
match layouts.first() {
|
||||||
Some((name, _)) => {
|
Some((name, _)) => {
|
||||||
let file = path.to_string_lossy().to_string();
|
|
||||||
if let Some(row) = p.rows.iter_mut().find(|r| r.key == RowKey::Template) {
|
if let Some(row) = p.rows.iter_mut().find(|r| r.key == RowKey::Template) {
|
||||||
row.value = format!("{name} ({file})");
|
row.value = format!("{name} ({file})");
|
||||||
}
|
}
|
||||||
@@ -2079,6 +2194,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
None => p.error = Some(crate::t!("The drawing has no layout to use as a sheet template.").into_owned()),
|
None => p.error = Some(crate::t!("The drawing has no layout to use as a sheet template.").into_owned()),
|
||||||
}
|
}
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sheet_set_dialog_ok(&mut self) -> Task<Message> {
|
fn sheet_set_dialog_ok(&mut self) -> Task<Message> {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ fn write_export(path: &std::path::Path, bytes: &[u8]) -> Result<(), String> {
|
|||||||
/// reaches the UI thread: it is caught on the worker and re-surfaced as
|
/// reaches the UI thread: it is caught on the worker and re-surfaced as
|
||||||
/// [`Message::BackgroundTaskFailed`] so a crashing export or import reports
|
/// [`Message::BackgroundTaskFailed`] so a crashing export or import reports
|
||||||
/// on the command line instead of killing the application.
|
/// on the command line instead of killing the application.
|
||||||
pub(super) fn background_task<T, F, M>(context: impl Into<String>, work: F, map: M) -> Task<Message>
|
pub(in crate::app) fn background_task<T, F, M>(context: impl Into<String>, work: F, map: M) -> Task<Message>
|
||||||
where
|
where
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
F: FnOnce() -> T + Send + 'static,
|
F: FnOnce() -> T + Send + 'static,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ mod blocks_palette;
|
|||||||
mod dialog;
|
mod dialog;
|
||||||
mod dynamic;
|
mod dynamic;
|
||||||
mod file;
|
mod file;
|
||||||
|
pub(in crate::app) use file::background_task;
|
||||||
mod page_setup_import;
|
mod page_setup_import;
|
||||||
mod style;
|
mod style;
|
||||||
pub(in crate::app) mod util;
|
pub(in crate::app) mod util;
|
||||||
|
|||||||
+48
-13
@@ -91,9 +91,11 @@ pub struct SheetSetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Drawings read from disk with their modification time; a changed file is
|
/// Drawings read from disk with their modification time; a changed file is
|
||||||
/// read again.
|
/// read again (off the UI thread, see [`read_drawing`]).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DrawingCache(pub std::collections::HashMap<String, (std::time::SystemTime, std::sync::Arc<codec::CadDocument>)>);
|
pub struct DrawingCache(
|
||||||
|
pub std::collections::HashMap<String, (std::time::SystemTime, std::sync::Arc<codec::CadDocument>)>,
|
||||||
|
);
|
||||||
|
|
||||||
impl std::fmt::Debug for DrawingCache {
|
impl std::fmt::Debug for DrawingCache {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -102,21 +104,47 @@ impl std::fmt::Debug for DrawingCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DrawingCache {
|
impl DrawingCache {
|
||||||
/// The drawing at `path`, read again when its modification time changed.
|
/// The drawing at `path` when it was read and is unchanged since.
|
||||||
pub fn get(&mut self, path: &str) -> Option<std::sync::Arc<codec::CadDocument>> {
|
pub fn get(&self, path: &str) -> Option<std::sync::Arc<codec::CadDocument>> {
|
||||||
let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
|
let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
|
||||||
let key = ss::path_key(path);
|
let (t, doc) = self.0.get(&ss::path_key(path))?;
|
||||||
if let Some((t, doc)) = self.0.get(&key) {
|
(*t == modified).then(|| doc.clone())
|
||||||
if *t == modified {
|
}
|
||||||
return Some(doc.clone());
|
|
||||||
}
|
pub fn insert(&mut self, path: &str, modified: std::time::SystemTime, doc: std::sync::Arc<codec::CadDocument>) {
|
||||||
}
|
self.0.insert(ss::path_key(path), (modified, doc));
|
||||||
let doc = std::sync::Arc::new(crate::io::load_file(std::path::Path::new(path)).ok()?);
|
}
|
||||||
self.0.insert(key, (modified, doc.clone()));
|
|
||||||
Some(doc)
|
/// Take the drawing at `path` out (to lend it to a scene); put it back
|
||||||
|
/// with [`DrawingCache::insert`].
|
||||||
|
pub fn take(&mut self, path: &str) -> Option<(std::time::SystemTime, std::sync::Arc<codec::CadDocument>)> {
|
||||||
|
self.0.remove(&ss::path_key(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A drawing read for the cache (its modification time and document).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ReadDrawing(pub Result<(std::time::SystemTime, std::sync::Arc<codec::CadDocument>), String>);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ReadDrawing {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(if self.0.is_ok() { "ReadDrawing(Ok)" } else { "ReadDrawing(Err)" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the drawing at `path` (run on a worker thread).
|
||||||
|
pub fn read_drawing(path: &str) -> ReadDrawing {
|
||||||
|
let modified = std::fs::metadata(path).and_then(|m| m.modified()).map_err(|e| e.to_string());
|
||||||
|
ReadDrawing(modified.and_then(|t| {
|
||||||
|
let doc = crate::io::load_file(std::path::Path::new(path)).map_err(|e| e.to_string())?;
|
||||||
|
Ok((t, std::sync::Arc::new(doc)))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The paper-space layouts (name, handle) and the sheet link of a drawing
|
||||||
|
/// picked for Import Layout as Sheet, or why it could not be read.
|
||||||
|
pub type ImportRead = Result<(Vec<(String, String)>, Option<codec::sheet_set::SheetSetData>), String>;
|
||||||
|
|
||||||
/// A row menu item: an entry, a separator, or a fly-out submenu (label, key, items).
|
/// A row menu item: an entry, a separator, or a fly-out submenu (label, key, items).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Mi {
|
pub enum Mi {
|
||||||
@@ -193,6 +221,13 @@ pub enum SheetSetMsg {
|
|||||||
/// Import: Browse for Drawings and the drawings picked.
|
/// Import: Browse for Drawings and the drawings picked.
|
||||||
BrowseDrawings,
|
BrowseDrawings,
|
||||||
ImportAdd(Vec<std::path::PathBuf>),
|
ImportAdd(Vec<std::path::PathBuf>),
|
||||||
|
/// The drawings picked for import, read: into a new Import dialog
|
||||||
|
/// (`true`) or replacing the open dialog's rows.
|
||||||
|
ImportRead(bool, Vec<(std::path::PathBuf, ImportRead)>),
|
||||||
|
/// A drawing read for the cache, then the message that needed it.
|
||||||
|
DrawingRead(String, ReadDrawing, Box<SheetSetMsg>),
|
||||||
|
/// The wizard's folder of existing drawings, scanned for layouts.
|
||||||
|
FolderScanned(std::path::PathBuf, Vec<FoundLayout>),
|
||||||
/// Rename & Renumber: apply and go to the previous / next sheet.
|
/// Rename & Renumber: apply and go to the previous / next sheet.
|
||||||
Previous,
|
Previous,
|
||||||
Next,
|
Next,
|
||||||
|
|||||||
Reference in new issue
Block a user