forked from 1b-agent/opencadstudio-upstream
feat(thumbnail): embed a DWG preview on save, show it on the Start page
Generate a small wire-raster thumbnail when saving a DWG and embed it via acadrust's preview API, so OCS drawings show a thumbnail in file browsers and other CAD applications. Read the embedded preview back cheaply (file header + preview bytes only, no full parse) to show a thumbnail per file in the Start page's Recent Documents panel. - io/thumbnail.rs: CPU rasterizer (current-layout wires -> BMP DIB) plus a preview-only reader/decoder (BMP/PNG -> iced handle) for the Start page. - Embedded on save via stamp_thumbnail on both save paths. - recent_thumbs cache filled on config load / recents change (never from a view); a thumbnail is drawn beside each recent file. Requires acadrust at 3111694 (preview read/write), already pinned in Cargo.lock. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
d38fcda07c
commit
5c4ba97c5f
7 files changed
+326
-10
No files matched your search
@@ -227,6 +227,11 @@ pub(super) struct OpenCADStudio {
|
||||
ribbon: Ribbon,
|
||||
/// Recently opened files, newest first — backs the Start page panel.
|
||||
recent_files: Vec<std::path::PathBuf>,
|
||||
/// Decoded DWG preview thumbnails for the Start page, keyed by path.
|
||||
/// `None` = the file has no readable preview (DXF, missing, WMF). Filled
|
||||
/// lazily by `refresh_recent_thumbs`; never read from a `view` call.
|
||||
recent_thumbs:
|
||||
std::collections::HashMap<std::path::PathBuf, Option<iced::widget::image::Handle>>,
|
||||
/// How many recent files to keep (user-set on the Start page, persisted).
|
||||
recent_limit: usize,
|
||||
/// Live text of the recent-limit input box (may differ from `recent_limit`
|
||||
@@ -2090,6 +2095,7 @@ impl OpenCADStudio {
|
||||
// Populated from the consolidated config after construction
|
||||
// (`apply_config`); default empty here.
|
||||
recent_files: Vec::new(),
|
||||
recent_thumbs: std::collections::HashMap::new(),
|
||||
recent_limit: recent::RECENT_DEFAULT,
|
||||
recent_limit_input: recent::RECENT_DEFAULT.to_string(),
|
||||
command_line: CommandLine::new(),
|
||||
|
||||
@@ -17,9 +17,21 @@ impl OpenCADStudio {
|
||||
self.recent_files.retain(|r| r != &path);
|
||||
self.recent_files.insert(0, path);
|
||||
self.recent_files.truncate(self.recent_limit);
|
||||
self.refresh_recent_thumbs();
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
/// Decode any not-yet-cached DWG preview thumbnails for the current recents
|
||||
/// (cheap preview-only reads). Cached per path; safe to call repeatedly.
|
||||
/// Call when the recents list changes — never from a `view`.
|
||||
pub(super) fn refresh_recent_thumbs(&mut self) {
|
||||
for path in self.recent_files.clone() {
|
||||
self.recent_thumbs
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| crate::io::thumbnail::read_handle(&path));
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop a path from the recents list (manual removal from the Start page).
|
||||
pub(super) fn remove_recent(&mut self, path: &Path) {
|
||||
self.recent_files.retain(|r| r.as_path() != path);
|
||||
|
||||
@@ -296,6 +296,7 @@ impl OpenCADStudio {
|
||||
.clamp(crate::app::recent::RECENT_MIN, crate::app::recent::RECENT_MAX);
|
||||
self.recent_files.truncate(self.recent_limit);
|
||||
self.recent_limit_input = self.recent_limit.to_string();
|
||||
self.refresh_recent_thumbs();
|
||||
self.statusbar_config = cfg.statusbar;
|
||||
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
|
||||
self.plot_dialog = cfg.plot;
|
||||
@@ -670,6 +671,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Rasterize the current drawing into the document's DWG preview image, so
|
||||
/// a saved file shows a thumbnail in file browsers and other CAD apps.
|
||||
/// A degenerate/empty drawing clears any stale preview.
|
||||
pub(super) fn stamp_thumbnail(&mut self, i: usize) {
|
||||
self.tabs[i].scene.document.preview =
|
||||
crate::io::thumbnail::from_scene(&self.tabs[i].scene);
|
||||
// The on-disk thumbnail will change — drop the cached Start-page handle
|
||||
// so it re-reads the updated file on the next refresh.
|
||||
if let Some(p) = self.tabs[i].current_path.clone() {
|
||||
self.recent_thumbs.remove(&p);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
||||
if self.read_only {
|
||||
self.command_line
|
||||
@@ -695,6 +709,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
crate::io::write_backup(&path);
|
||||
}
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
self.stamp_thumbnail(i);
|
||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
@@ -755,6 +770,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
// Persist Ortho / running OSNAP into the header (Save-As path).
|
||||
self.stamp_header_sysvars(i);
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
self.stamp_thumbnail(i);
|
||||
|
||||
// Native: write to the chosen path. Web: download the bytes
|
||||
// under the chosen name (no filesystem).
|
||||
|
||||
+38
-9
@@ -118,6 +118,7 @@ impl OpenCADStudio {
|
||||
start_page_view(
|
||||
&self.patrons,
|
||||
&self.recent_files,
|
||||
&self.recent_thumbs,
|
||||
self.recent_limit,
|
||||
&self.recent_limit_input,
|
||||
self.win_size.0,
|
||||
@@ -2187,6 +2188,10 @@ pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Mes
|
||||
pub(super) fn start_page_view<'a>(
|
||||
patrons: &'a [(String, i64)],
|
||||
recents: &'a [std::path::PathBuf],
|
||||
thumbs: &'a std::collections::HashMap<
|
||||
std::path::PathBuf,
|
||||
Option<iced::widget::image::Handle>,
|
||||
>,
|
||||
recent_limit: usize,
|
||||
recent_limit_input: &'a str,
|
||||
avail_w: f32,
|
||||
@@ -2540,7 +2545,7 @@ pub(super) fn start_page_view<'a>(
|
||||
let avail = (avail_w - 16.0).max(0.0); // minus the page's l/r padding
|
||||
let fits_all = avail >= recent_w + welcome_min + sup_w;
|
||||
|
||||
let recent = recent_files_panel(recents, recent_limit, recent_limit_input);
|
||||
let recent = recent_files_panel(recents, thumbs, recent_limit, recent_limit_input);
|
||||
let welcome = container(content).width(Fill).height(Fill);
|
||||
|
||||
// Right rail: Patreon supporters, fetched at boot. When the list is empty
|
||||
@@ -2719,6 +2724,10 @@ pub(super) fn start_page_view<'a>(
|
||||
// open — entries persist across sessions.
|
||||
pub(super) fn recent_files_panel<'a>(
|
||||
recents: &'a [std::path::PathBuf],
|
||||
thumbs: &'a std::collections::HashMap<
|
||||
std::path::PathBuf,
|
||||
Option<iced::widget::image::Handle>,
|
||||
>,
|
||||
limit: usize,
|
||||
limit_input: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -2779,17 +2788,37 @@ pub(super) fn recent_files_panel<'a>(
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Leading DWG preview thumbnail (fixed box keeps rows aligned even
|
||||
// when a file has no readable preview).
|
||||
let thumb: Element<'a, Message> = match thumbs.get(path).and_then(|o| o.as_ref()) {
|
||||
Some(h) => container(
|
||||
iced::widget::image(h.clone())
|
||||
.content_fit(iced::ContentFit::Contain)
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
)
|
||||
.width(46)
|
||||
.height(34)
|
||||
.into(),
|
||||
None => iced::widget::Space::new().width(46).height(34).into(),
|
||||
};
|
||||
|
||||
let path_for_open = path.clone();
|
||||
let open_btn = button(
|
||||
column![
|
||||
text(crate::ui::text_util::elide(&name, 32))
|
||||
.size(12)
|
||||
.color(TEXT),
|
||||
text(crate::ui::text_util::elide(&dir, 42))
|
||||
.size(10)
|
||||
.color(MUTED),
|
||||
row![
|
||||
thumb,
|
||||
column![
|
||||
text(crate::ui::text_util::elide(&name, 28))
|
||||
.size(12)
|
||||
.color(TEXT),
|
||||
text(crate::ui::text_util::elide(&dir, 38))
|
||||
.size(10)
|
||||
.color(MUTED),
|
||||
]
|
||||
.spacing(2),
|
||||
]
|
||||
.spacing(2),
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::OpenRecent(path_for_open))
|
||||
.padding([6, 12])
|
||||
|
||||
Reference in new issue
Block a user