fix(save): preserve 3D view and thumbnails
Persist camera, render state, and solid fallback geometry across saves. Generate previews from a clean live viewport and refresh recent-file thumbnails immediately.
This commit is contained in:
parent
c4515cbbc1
commit
e94cbedaa2
15 changed files with 652 additions and 623 deletions
|
|
@ -195,10 +195,6 @@ pub(super) struct DocumentTab {
|
||||||
pub(super) active_mleader_style: String,
|
pub(super) active_mleader_style: String,
|
||||||
/// Last camera_generation value written back to the document.
|
/// Last camera_generation value written back to the document.
|
||||||
pub(super) last_synced_camera_gen: u64,
|
pub(super) last_synced_camera_gen: u64,
|
||||||
/// Render-state key of `scene.document.preview`. Matching saves reuse the
|
|
||||||
/// encoded DWG thumbnail instead of rescanning every resident wire.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub(super) thumbnail_cache_key: Option<super::ThumbnailCacheKey>,
|
|
||||||
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
|
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
|
||||||
/// Cannot be closed; the viewport area renders a welcome page instead
|
/// Cannot be closed; the viewport area renders a welcome page instead
|
||||||
/// of the model-space shader. The scene is still constructed so the
|
/// of the model-space shader. The scene is still constructed so the
|
||||||
|
|
@ -486,8 +482,6 @@ impl DocumentTab {
|
||||||
active_block_edit: None,
|
active_block_edit: None,
|
||||||
active_mleader_style: "Standard".to_string(),
|
active_mleader_style: "Standard".to_string(),
|
||||||
last_synced_camera_gen: 0,
|
last_synced_camera_gen: 0,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
thumbnail_cache_key: None,
|
|
||||||
is_start: false,
|
is_start: false,
|
||||||
pan_mode: false,
|
pan_mode: false,
|
||||||
orbit_mode: false,
|
orbit_mode: false,
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,12 @@ pub(super) struct OpenCADStudio {
|
||||||
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
||||||
/// OS window Id of the primary application window.
|
/// OS window Id of the primary application window.
|
||||||
main_window: Option<window::Id>,
|
main_window: Option<window::Id>,
|
||||||
|
/// Hides drawing UI overlays for one thumbnail capture frame.
|
||||||
|
thumbnail_capture_clean: bool,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pending_native_thumbnail_save: Option<PendingNativeThumbnailSave>,
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pending_web_thumbnail_save: Option<PendingWebThumbnailSave>,
|
||||||
// ── Floating panel windows ────────────────────────────────────────────
|
// ── Floating panel windows ────────────────────────────────────────────
|
||||||
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
||||||
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
||||||
|
|
@ -1131,6 +1137,28 @@ pub(super) enum SaveContinuation {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(super) struct PendingNativeThumbnailSave {
|
||||||
|
tab_id: u64,
|
||||||
|
path: PathBuf,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
purpose: SavePurpose,
|
||||||
|
continuation: SaveContinuation,
|
||||||
|
set_current_path: bool,
|
||||||
|
check_external_change: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub(super) struct PendingWebThumbnailSave {
|
||||||
|
tab_id: u64,
|
||||||
|
filename: String,
|
||||||
|
ext: String,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(super) struct PendingSaveFailure {
|
pub(super) struct PendingSaveFailure {
|
||||||
|
|
@ -1154,16 +1182,6 @@ pub(super) struct PendingExternalChange {
|
||||||
set_current_path: bool,
|
set_current_path: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub(super) struct ThumbnailCacheKey {
|
|
||||||
epoch: u64,
|
|
||||||
camera_generation: u64,
|
|
||||||
bg_color: [u32; 4],
|
|
||||||
png: bool,
|
|
||||||
viewport: [u32; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct SaveOutcome {
|
pub struct SaveOutcome {
|
||||||
|
|
@ -1178,7 +1196,6 @@ pub struct SaveOutcome {
|
||||||
set_current_path: bool,
|
set_current_path: bool,
|
||||||
purpose: SavePurpose,
|
purpose: SavePurpose,
|
||||||
continuation: SaveContinuation,
|
continuation: SaveContinuation,
|
||||||
thumbnail_key: Option<ThumbnailCacheKey>,
|
|
||||||
refreshed_preview: Option<Option<acadrust::Preview>>,
|
refreshed_preview: Option<Option<acadrust::Preview>>,
|
||||||
result: Result<(), crate::io::SaveFailure>,
|
result: Result<(), crate::io::SaveFailure>,
|
||||||
}
|
}
|
||||||
|
|
@ -1915,9 +1932,23 @@ pub enum Message {
|
||||||
AecDropBack,
|
AecDropBack,
|
||||||
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
||||||
AutoSave,
|
AutoSave,
|
||||||
|
/// A clean viewport frame is ready for thumbnail capture.
|
||||||
|
ThumbnailCaptureFrame,
|
||||||
|
/// Restore drawing UI after the compositor screenshot is captured.
|
||||||
|
ThumbnailCaptureFinished,
|
||||||
/// Native background save/autosave completed.
|
/// Native background save/autosave completed.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
SaveFinished(SaveOutcome),
|
SaveFinished(SaveOutcome),
|
||||||
|
/// Web viewport capture completed; serialize and download the drawing.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
WebSaveScreenshot {
|
||||||
|
tab_id: u64,
|
||||||
|
filename: String,
|
||||||
|
ext: String,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
bounds: Option<iced::Rectangle>,
|
||||||
|
screenshot: Option<iced::window::Screenshot>,
|
||||||
|
},
|
||||||
/// Retry the failed save after the other application releases the file.
|
/// Retry the failed save after the other application releases the file.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
SaveFileInUseRetry,
|
SaveFileInUseRetry,
|
||||||
|
|
@ -3161,6 +3192,11 @@ impl OpenCADStudio {
|
||||||
show_layout_tabs: true,
|
show_layout_tabs: true,
|
||||||
last_point: None,
|
last_point: None,
|
||||||
main_window: None,
|
main_window: None,
|
||||||
|
thumbnail_capture_clean: false,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pending_native_thumbnail_save: None,
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pending_web_thumbnail_save: None,
|
||||||
color_pick_target: None,
|
color_pick_target: None,
|
||||||
color_picker_tab: ColorPickerTab::Index,
|
color_picker_tab: ColorPickerTab::Index,
|
||||||
recent_colors: Vec::new(),
|
recent_colors: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -237,36 +237,72 @@ fn plot_scene_content(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// Before a save, give every cached solid that still has no ACIS
|
/// Persist exact ACIS bodies and kernel-derived edge caches before saving.
|
||||||
/// geometry (EXTRUDE/REVOLVE/SWEEP/LOFT/boolean results) an exact modeler
|
fn sync_solid_models_for_save(&mut self, i: usize) {
|
||||||
/// body derived from its B-rep, so the written DWG/DXF carries real
|
|
||||||
/// 3-D geometry other CAD apps can open instead of an empty data stream.
|
|
||||||
/// A body holding something the kernel has no ACIS record form for is
|
|
||||||
/// left untouched rather than written out half-complete.
|
|
||||||
fn sync_solid_models_to_acis(&mut self, i: usize) {
|
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
let targets: Vec<acadrust::Handle> = scene
|
let targets: Vec<(acadrust::Handle, bool, bool)> = scene
|
||||||
.solid_models
|
.document
|
||||||
.keys()
|
.entities()
|
||||||
.copied()
|
.filter_map(|entity| {
|
||||||
.filter(|h| {
|
let EntityType::Solid3D(solid) = entity else {
|
||||||
matches!(
|
return None;
|
||||||
scene.document.get_entity(*h),
|
};
|
||||||
Some(EntityType::Solid3D(s)) if !s.acis_data.has_data()
|
let h = solid.common.handle;
|
||||||
)
|
let needs_acis = !solid.acis_data.has_data();
|
||||||
|
let needs_wires = solid.wires.is_empty();
|
||||||
|
(needs_acis || needs_wires).then_some((h, needs_acis, needs_wires))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for h in targets {
|
for (h, needs_acis, needs_wires) in targets {
|
||||||
// Build the SAT while borrowing solid_models; the returned document
|
let body = scene.solid_models.get(&h);
|
||||||
// is owned, so the borrow ends before we mutate the entity.
|
let sat = needs_acis
|
||||||
let sat = scene
|
.then(|| {
|
||||||
.solid_models
|
body.and_then(crate::scene::convert::acis_export::planar_solid_to_sat)
|
||||||
.get(&h)
|
})
|
||||||
.and_then(crate::scene::convert::acis_export::planar_solid_to_sat);
|
.flatten();
|
||||||
if let Some(sat) = sat {
|
let wires = needs_wires.then(|| {
|
||||||
if let Some(EntityType::Solid3D(s)) = scene.document.get_entity_mut(h) {
|
if let Some(body) = body {
|
||||||
s.set_sat_document(&sat);
|
return crate::scene::model::solid_model::edge_wires(body);
|
||||||
|
}
|
||||||
|
let Some(mesh) = scene.meshes.get(&h).or_else(|| scene.block_meshes.get(&h)) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
mesh.edge_verts
|
||||||
|
.chunks_exact(2)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, points)| {
|
||||||
|
let first_low = mesh
|
||||||
|
.edge_verts_low
|
||||||
|
.get(index * 2)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or([0.0; 3]);
|
||||||
|
let second_low = mesh
|
||||||
|
.edge_verts_low
|
||||||
|
.get(index * 2 + 1)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or([0.0; 3]);
|
||||||
|
acadrust::entities::Wire::from_points(vec![
|
||||||
|
acadrust::types::Vector3::new(
|
||||||
|
points[0][0] as f64 + first_low[0] as f64,
|
||||||
|
points[0][1] as f64 + first_low[1] as f64,
|
||||||
|
points[0][2] as f64 + first_low[2] as f64,
|
||||||
|
),
|
||||||
|
acadrust::types::Vector3::new(
|
||||||
|
points[1][0] as f64 + second_low[0] as f64,
|
||||||
|
points[1][1] as f64 + second_low[1] as f64,
|
||||||
|
points[1][2] as f64 + second_low[2] as f64,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
if let Some(EntityType::Solid3D(solid)) = scene.document.get_entity_mut(h) {
|
||||||
|
if let Some(sat) = sat {
|
||||||
|
solid.set_sat_document(&sat);
|
||||||
|
}
|
||||||
|
if let Some(wires) = wires.filter(|wires| !wires.is_empty()) {
|
||||||
|
solid.wires = wires;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1408,14 +1444,23 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
move |result| Message::ObjImportFinished(tab_id, path, result),)
|
move |result| Message::ObjImportFinished(tab_id, path, result),)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_view_state_for_save(&mut self, i: usize) {
|
||||||
|
self.sync_vport_display(i);
|
||||||
|
if self.tabs[i].active_block_edit.is_none() {
|
||||||
|
self.tabs[i].scene.sync_camera_to_document();
|
||||||
|
self.tabs[i].last_synced_camera_gen =
|
||||||
|
self.tabs[i].scene.camera_generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
||||||
self.sync_vport_display(i);
|
self.sync_view_state_for_save(i);
|
||||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||||
self.stamp_header_sysvars(i);
|
self.stamp_header_sysvars(i);
|
||||||
self.tabs[i].scene.document.header.user_real1 =
|
self.tabs[i].scene.document.header.user_real1 =
|
||||||
self.tabs[i].scene.annotation_scale as f64;
|
self.tabs[i].scene.annotation_scale as f64;
|
||||||
self.sync_solid_models_to_acis(i);
|
self.sync_solid_models_for_save(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -1470,16 +1515,120 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
fn stamp_thumbnail(&mut self, i: usize, version: acadrust::DxfVersion) {
|
pub(super) fn on_thumbnail_capture_frame(&mut self) -> Task<Message> {
|
||||||
let scene = &self.tabs[i].scene;
|
let Some(pending) = self.pending_web_thumbnail_save.take() else {
|
||||||
let preview = crate::io::thumbnail::from_snapshot(
|
return Task::none();
|
||||||
&scene.entity_wires(),
|
};
|
||||||
&scene.camera.borrow(),
|
iced::window::latest()
|
||||||
scene.bg_color,
|
.then(|window| match window {
|
||||||
version >= acadrust::DxfVersion::AC1027,
|
Some(window) => iced::window::screenshot(window).map(Some),
|
||||||
self.vp_size,
|
None => Task::done(None),
|
||||||
);
|
})
|
||||||
self.tabs[i].scene.document.preview = preview;
|
.map(move |screenshot| Message::WebSaveScreenshot {
|
||||||
|
tab_id: pending.tab_id,
|
||||||
|
filename: pending.filename.clone(),
|
||||||
|
ext: pending.ext.clone(),
|
||||||
|
version: pending.version,
|
||||||
|
bounds: Some(pending.bounds),
|
||||||
|
screenshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(super) fn on_thumbnail_capture_frame(&mut self) -> Task<Message> {
|
||||||
|
let Some(pending) = self.pending_native_thumbnail_save.take() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == pending.tab_id) else {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
self.queue_native_save(
|
||||||
|
i,
|
||||||
|
pending.path,
|
||||||
|
pending.version,
|
||||||
|
pending.purpose,
|
||||||
|
pending.continuation,
|
||||||
|
pending.set_current_path,
|
||||||
|
pending.check_external_change,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub(super) fn on_web_save_screenshot(
|
||||||
|
&mut self,
|
||||||
|
tab_id: u64,
|
||||||
|
filename: String,
|
||||||
|
ext: String,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
bounds: Option<iced::Rectangle>,
|
||||||
|
screenshot: Option<iced::window::Screenshot>,
|
||||||
|
) -> Task<Message> {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let preview = screenshot.as_ref().and_then(|screenshot| {
|
||||||
|
bounds.and_then(|bounds| {
|
||||||
|
crate::io::thumbnail::from_screenshot(
|
||||||
|
screenshot,
|
||||||
|
bounds,
|
||||||
|
version >= acadrust::DxfVersion::AC1027,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if let Some(preview) = preview {
|
||||||
|
self.tabs[i].scene.document.preview = Some(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recent_task = Task::none();
|
||||||
|
let saved = match crate::io::save_to_bytes(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&ext,
|
||||||
|
version,
|
||||||
|
) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
crate::sys::download_bytes(&filename, &bytes);
|
||||||
|
let cache_name = std::path::Path::new(&filename)
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| filename.clone());
|
||||||
|
let path = std::path::PathBuf::from(cache_name);
|
||||||
|
self.tabs[i].current_path = Some(path.clone());
|
||||||
|
self.tabs[i].scene.document.version = version;
|
||||||
|
self.tabs[i].dirty = false;
|
||||||
|
self.tabs[i].recovery_save_as_required = false;
|
||||||
|
recent_task = Task::perform(
|
||||||
|
async move {
|
||||||
|
crate::io::web_recent::store(&path.to_string_lossy(), &bytes)
|
||||||
|
.await
|
||||||
|
.map(|_| path)
|
||||||
|
},
|
||||||
|
Message::WebRecentStored,
|
||||||
|
);
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::tf!("Saved: {filename}").as_ref());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::tf!("Save failed: {error}").as_ref());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.save_dialog_for_unsaved {
|
||||||
|
if saved {
|
||||||
|
if let Some(crate::app::PendingClose::Tab(index)) = self.pending_close.take() {
|
||||||
|
let continuation = self.update(Message::TabClose(index));
|
||||||
|
let rest = self.continue_tab_close_queue();
|
||||||
|
return Task::batch([recent_task, continuation, rest]);
|
||||||
|
}
|
||||||
|
} else if self.pending_close.is_some() {
|
||||||
|
let retry = self.open_unsaved_dialog_window();
|
||||||
|
return Task::batch([recent_task, retry]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recent_task
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -1494,11 +1643,27 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
check_external_change: bool,
|
check_external_change: bool,
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
let tab_id = self.tabs[i].id;
|
let tab_id = self.tabs[i].id;
|
||||||
|
if self
|
||||||
|
.pending_native_thumbnail_save
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|pending| pending.tab_id == tab_id)
|
||||||
|
{
|
||||||
|
if purpose != crate::app::SavePurpose::Autosave {
|
||||||
|
self.command_line
|
||||||
|
.push_info(crate::t!("Save already running for this drawing.").as_ref());
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let capture_ready =
|
||||||
|
self.thumbnail_capture_clean && self.pending_native_thumbnail_save.is_none();
|
||||||
if self.active_save_jobs.contains_key(&tab_id) {
|
if self.active_save_jobs.contains_key(&tab_id) {
|
||||||
if purpose != crate::app::SavePurpose::Autosave {
|
if purpose != crate::app::SavePurpose::Autosave {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(crate::t!("Save already running for this drawing.").as_ref());
|
.push_info(crate::t!("Save already running for this drawing.").as_ref());
|
||||||
}
|
}
|
||||||
|
if capture_ready {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let destination_is_current = self.tabs[i]
|
let destination_is_current = self.tabs[i]
|
||||||
|
|
@ -1516,6 +1681,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.active_tab = i;
|
self.active_tab = i;
|
||||||
self.save_dialog_for_unsaved =
|
self.save_dialog_for_unsaved =
|
||||||
continuation != crate::app::SaveContinuation::None;
|
continuation != crate::app::SaveContinuation::None;
|
||||||
|
if capture_ready {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
}
|
||||||
return self.open_save_dialog_window(i);
|
return self.open_save_dialog_window(i);
|
||||||
}
|
}
|
||||||
if purpose != crate::app::SavePurpose::Autosave
|
if purpose != crate::app::SavePurpose::Autosave
|
||||||
|
|
@ -1540,6 +1708,33 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
});
|
});
|
||||||
self.restore_failed_save_continuation(continuation, i);
|
self.restore_failed_save_continuation(continuation, i);
|
||||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||||
|
if capture_ready {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
if purpose != crate::app::SavePurpose::Autosave
|
||||||
|
&& i == self.active_tab
|
||||||
|
&& !self.thumbnail_capture_clean
|
||||||
|
&& self.main_window.is_some()
|
||||||
|
&& crate::ui::wrap_bar::dropdown_bounds(
|
||||||
|
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.pending_native_thumbnail_save = Some(
|
||||||
|
crate::app::PendingNativeThumbnailSave {
|
||||||
|
tab_id,
|
||||||
|
path,
|
||||||
|
version,
|
||||||
|
purpose,
|
||||||
|
continuation,
|
||||||
|
set_current_path,
|
||||||
|
check_external_change,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.thumbnail_capture_clean = true;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1566,6 +1761,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
});
|
});
|
||||||
self.restore_failed_save_continuation(continuation, i);
|
self.restore_failed_save_continuation(continuation, i);
|
||||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||||
|
if capture_ready {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||||
|
|
@ -1579,32 +1777,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let epoch = self.tabs[i].scene.geometry_epoch;
|
let epoch = self.tabs[i].scene.geometry_epoch;
|
||||||
let revision = self.tabs[i].edit_revision;
|
let revision = self.tabs[i].edit_revision;
|
||||||
let camera_generation = self.tabs[i].scene.camera_generation;
|
let camera_generation = self.tabs[i].scene.camera_generation;
|
||||||
let thumbnail_key = (purpose != crate::app::SavePurpose::Autosave).then(|| {
|
let thumbnail = (purpose != crate::app::SavePurpose::Autosave && i == self.active_tab)
|
||||||
let scene = &self.tabs[i].scene;
|
.then_some(version >= acadrust::DxfVersion::AC1027);
|
||||||
crate::app::ThumbnailCacheKey {
|
let capture_bounds = thumbnail.and_then(|_| {
|
||||||
epoch,
|
crate::ui::wrap_bar::dropdown_bounds(
|
||||||
camera_generation,
|
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
|
||||||
bg_color: scene.bg_color.map(f32::to_bits),
|
)
|
||||||
png: version >= acadrust::DxfVersion::AC1027,
|
|
||||||
viewport: [self.vp_size.0.to_bits(), self.vp_size.1.to_bits()],
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
let thumbnail = if thumbnail_key == self.tabs[i].thumbnail_cache_key {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
thumbnail_key.map(|key| {
|
|
||||||
let scene = &self.tabs[i].scene;
|
|
||||||
(
|
|
||||||
scene.entity_wires(),
|
|
||||||
scene.camera.borrow().clone(),
|
|
||||||
scene.bg_color,
|
|
||||||
key.png,
|
|
||||||
self.vp_size,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let clone_started = iced::time::Instant::now();
|
let clone_started = iced::time::Instant::now();
|
||||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
let snapshot = self.tabs[i].scene.document.clone();
|
||||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if crate::perf::enabled() {
|
if crate::perf::enabled() {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
|
|
@ -1650,6 +1831,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let (expected_fingerprint, verify_reader) = match verification {
|
let (expected_fingerprint, verify_reader) = match verification {
|
||||||
Ok(verification) => verification,
|
Ok(verification) => verification,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
if capture_ready {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
}
|
||||||
return Task::perform(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
crate::app::SaveOutcome {
|
crate::app::SaveOutcome {
|
||||||
|
|
@ -1664,7 +1848,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
set_current_path,
|
set_current_path,
|
||||||
purpose,
|
purpose,
|
||||||
continuation,
|
continuation,
|
||||||
thumbnail_key,
|
|
||||||
refreshed_preview: None,
|
refreshed_preview: None,
|
||||||
result: Err(error),
|
result: Err(error),
|
||||||
}
|
}
|
||||||
|
|
@ -1674,65 +1857,97 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let worker_path = path.clone();
|
let worker_path = path.clone();
|
||||||
|
let capture_window = thumbnail.and(capture_bounds).and(self.main_window);
|
||||||
Task::perform(
|
let mut work = Some((
|
||||||
async move {
|
snapshot,
|
||||||
let (result, refreshed_preview) = std::thread::spawn(move || {
|
thumbnail,
|
||||||
let mut refreshed_preview = None;
|
capture_bounds,
|
||||||
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
worker_path,
|
||||||
let started = iced::time::Instant::now();
|
expected_fingerprint,
|
||||||
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
verify_reader,
|
||||||
&wires,
|
path,
|
||||||
&camera,
|
previous_autosave,
|
||||||
bg_color,
|
));
|
||||||
png,
|
let mut run_save = move |screenshot: Option<iced::window::Screenshot>| {
|
||||||
viewport,
|
let (
|
||||||
);
|
mut snapshot,
|
||||||
if crate::perf::enabled() {
|
thumbnail,
|
||||||
crate::perf_record!(
|
capture_bounds,
|
||||||
"[perf] save-thumbnail {:.1}ms wires={}",
|
worker_path,
|
||||||
started.elapsed().as_secs_f64() * 1000.0,
|
expected_fingerprint,
|
||||||
wires.len(),
|
verify_reader,
|
||||||
);
|
path,
|
||||||
|
previous_autosave,
|
||||||
|
) = work.take().expect("save capture produced more than one result");
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let (result, refreshed_preview) = std::thread::spawn(move || {
|
||||||
|
let mut refreshed_preview = None;
|
||||||
|
if let Some(png) = thumbnail {
|
||||||
|
let started = iced::time::Instant::now();
|
||||||
|
if let Some(preview) = screenshot.as_ref().and_then(|screenshot| {
|
||||||
|
capture_bounds.and_then(|bounds| {
|
||||||
|
crate::io::thumbnail::from_screenshot(screenshot, bounds, png)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
snapshot.preview = Some(preview);
|
||||||
|
refreshed_preview = Some(snapshot.preview.clone());
|
||||||
|
}
|
||||||
|
if crate::perf::enabled() {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] save-thumbnail {:.1}ms",
|
||||||
|
started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
refreshed_preview = Some(snapshot.preview.clone());
|
let result = crate::io::save_owned_as_version_atomic(
|
||||||
}
|
snapshot,
|
||||||
let result = crate::io::save_owned_as_version_atomic(
|
&worker_path,
|
||||||
snapshot,
|
version,
|
||||||
&worker_path,
|
backup,
|
||||||
|
expected_fingerprint,
|
||||||
|
verify_reader,
|
||||||
|
);
|
||||||
|
(result, refreshed_preview)
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
(
|
||||||
|
Err(crate::io::SaveFailure::other("save worker panicked")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
crate::app::SaveOutcome {
|
||||||
|
job_id,
|
||||||
|
tab_id,
|
||||||
|
epoch,
|
||||||
|
revision,
|
||||||
|
camera_generation,
|
||||||
|
path,
|
||||||
version,
|
version,
|
||||||
backup,
|
previous_autosave,
|
||||||
expected_fingerprint,
|
set_current_path,
|
||||||
verify_reader,
|
purpose,
|
||||||
);
|
continuation,
|
||||||
(result, refreshed_preview)
|
refreshed_preview,
|
||||||
})
|
result,
|
||||||
.join()
|
}
|
||||||
.unwrap_or_else(|_| {
|
},
|
||||||
(
|
Message::SaveFinished,
|
||||||
Err(crate::io::SaveFailure::other("save worker panicked")),
|
)
|
||||||
None,
|
};
|
||||||
)
|
match capture_window {
|
||||||
});
|
Some(window) => iced::window::screenshot(window).map(Some).then(move |screenshot| {
|
||||||
crate::app::SaveOutcome {
|
Task::batch([
|
||||||
job_id,
|
Task::done(Message::ThumbnailCaptureFinished),
|
||||||
tab_id,
|
run_save(screenshot),
|
||||||
epoch,
|
])
|
||||||
revision,
|
}),
|
||||||
camera_generation,
|
None => {
|
||||||
path,
|
self.thumbnail_capture_clean = false;
|
||||||
version,
|
run_save(None)
|
||||||
previous_autosave,
|
}
|
||||||
set_current_path,
|
}
|
||||||
purpose,
|
|
||||||
continuation,
|
|
||||||
thumbnail_key,
|
|
||||||
refreshed_preview,
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::SaveFinished,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -1836,7 +2051,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
if let Some(preview) = outcome.refreshed_preview {
|
if let Some(preview) = outcome.refreshed_preview {
|
||||||
self.tabs[i].scene.document.preview = preview;
|
self.tabs[i].scene.document.preview = preview;
|
||||||
}
|
}
|
||||||
self.tabs[i].thumbnail_cache_key = outcome.thumbnail_key;
|
|
||||||
}
|
}
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
match outcome.purpose {
|
match outcome.purpose {
|
||||||
|
|
@ -1850,10 +2064,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_none_or(|current| {
|
.is_none_or(|current| {
|
||||||
!native_paths_match(current, &outcome.path)
|
!native_paths_match(current, &outcome.path)
|
||||||
});
|
});
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Saved: {}", outcome.path.display()).as_ref());
|
.push_output(crate::tf!("Saved: {}", outcome.path.display()).as_ref());
|
||||||
self.recent_thumbs.remove(&outcome.path);
|
|
||||||
if let Some(previous) = outcome.previous_autosave {
|
if let Some(previous) = outcome.previous_autosave {
|
||||||
if previous != outcome.path {
|
if previous != outcome.path {
|
||||||
let _ = std::fs::remove_file(previous);
|
let _ = std::fs::remove_file(previous);
|
||||||
|
|
@ -1865,8 +2078,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
if outcome.purpose == crate::app::SavePurpose::SaveAs {
|
if outcome.purpose == crate::app::SavePurpose::SaveAs {
|
||||||
self.tabs[i].recovery_save_as_required = false;
|
self.tabs[i].recovery_save_as_required = false;
|
||||||
}
|
}
|
||||||
tasks.push(self.push_recent(outcome.path.clone()));
|
|
||||||
}
|
}
|
||||||
|
tasks.push(self.push_recent(outcome.path.clone()));
|
||||||
self.refresh_native_edit_guard_after_save(
|
self.refresh_native_edit_guard_after_save(
|
||||||
i,
|
i,
|
||||||
&outcome.path,
|
&outcome.path,
|
||||||
|
|
@ -2064,7 +2277,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
// once after the destination/version is known.
|
// once after the destination/version is known.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
self.sync_vport_display(i);
|
self.sync_view_state_for_save(i);
|
||||||
self.stamp_header_sysvars(i);
|
self.stamp_header_sysvars(i);
|
||||||
}
|
}
|
||||||
// Native: save straight to the known path. Web has no path
|
// Native: save straight to the known path. Web has no path
|
||||||
|
|
@ -2199,62 +2412,38 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let close = self.close_save_dialog_window();
|
let close = self.close_save_dialog_window();
|
||||||
|
self.sync_view_state_for_save(i);
|
||||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||||
self.stamp_header_sysvars(i);
|
self.stamp_header_sysvars(i);
|
||||||
self.sync_solid_models_to_acis(i);
|
self.sync_solid_models_for_save(i);
|
||||||
self.stamp_thumbnail(i, version);
|
let tab_id = self.tabs[i].id;
|
||||||
let mut recent_task = Task::none();
|
let bounds = crate::ui::wrap_bar::dropdown_bounds(
|
||||||
let saved = match crate::io::save_to_bytes(
|
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
|
||||||
&self.tabs[i].scene.document,
|
);
|
||||||
ext,
|
let Some(bounds) = bounds else {
|
||||||
version,
|
return Task::batch([
|
||||||
) {
|
close,
|
||||||
Ok(bytes) => {
|
Task::done(Message::WebSaveScreenshot {
|
||||||
crate::sys::download_bytes(&filename, &bytes);
|
tab_id,
|
||||||
let cache_name = std::path::Path::new(&filename)
|
filename,
|
||||||
.file_name()
|
ext: ext.to_string(),
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
version,
|
||||||
.unwrap_or_else(|| filename.clone());
|
bounds: None,
|
||||||
let path = std::path::PathBuf::from(cache_name);
|
screenshot: None,
|
||||||
self.tabs[i].current_path = Some(path.clone());
|
}),
|
||||||
self.tabs[i].scene.document.version = version;
|
]);
|
||||||
self.tabs[i].dirty = false;
|
|
||||||
self.tabs[i].recovery_save_as_required = false;
|
|
||||||
recent_task = Task::perform(
|
|
||||||
async move {
|
|
||||||
crate::io::web_recent::store(
|
|
||||||
&path.to_string_lossy(),
|
|
||||||
&bytes,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| path)
|
|
||||||
},
|
|
||||||
Message::WebRecentStored,
|
|
||||||
);
|
|
||||||
self.command_line.push_output(crate::tf!("Saved: {filename}").as_ref());
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(crate::tf!("Save failed: {e}").as_ref());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// Continue a pending tab close.
|
self.pending_web_thumbnail_save = Some(
|
||||||
if self.save_dialog_for_unsaved {
|
crate::app::PendingWebThumbnailSave {
|
||||||
if saved {
|
tab_id,
|
||||||
if let Some(crate::app::PendingClose::Tab(idx)) =
|
filename,
|
||||||
self.pending_close.take()
|
ext: ext.to_string(),
|
||||||
{
|
version,
|
||||||
let cont = self.update(Message::TabClose(idx));
|
bounds,
|
||||||
let rest = self.continue_tab_close_queue();
|
},
|
||||||
return Task::batch([close, recent_task, cont, rest]);
|
);
|
||||||
}
|
self.thumbnail_capture_clean = true;
|
||||||
} else if self.pending_close.is_some() {
|
close
|
||||||
let retry = self.open_unsaved_dialog_window();
|
|
||||||
return Task::batch([close, recent_task, retry]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Task::batch([close, recent_task])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2365,6 +2554,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
self.prepare_native_save(i);
|
||||||
let version = self.tabs[i].scene.document.version;
|
let version = self.tabs[i].scene.document.version;
|
||||||
let target = self.autosave_target(i);
|
let target = self.autosave_target(i);
|
||||||
tasks.push(self.queue_native_save(
|
tasks.push(self.queue_native_save(
|
||||||
|
|
|
||||||
|
|
@ -6320,9 +6320,33 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::AutoSave => self.on_autosave(),
|
Message::AutoSave => self.on_autosave(),
|
||||||
|
|
||||||
|
Message::ThumbnailCaptureFrame => self.on_thumbnail_capture_frame(),
|
||||||
|
|
||||||
|
Message::ThumbnailCaptureFinished => {
|
||||||
|
self.thumbnail_capture_clean = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
|
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Message::WebSaveScreenshot {
|
||||||
|
tab_id,
|
||||||
|
filename,
|
||||||
|
ext,
|
||||||
|
version,
|
||||||
|
bounds,
|
||||||
|
screenshot,
|
||||||
|
} => self.on_web_save_screenshot(
|
||||||
|
tab_id,
|
||||||
|
filename,
|
||||||
|
ext,
|
||||||
|
version,
|
||||||
|
bounds,
|
||||||
|
screenshot,
|
||||||
|
),
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
|
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -809,6 +809,7 @@ impl OpenCADStudio {
|
||||||
// render mode; the model-layout tab style is untouched.
|
// render mode; the model-layout tab style is untouched.
|
||||||
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Viewport visual style: {label}").as_ref());
|
.push_output(crate::tf!("Viewport visual style: {label}").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
|
|
@ -827,6 +828,7 @@ impl OpenCADStudio {
|
||||||
// Re-upload face3d fills on the next frame — the render
|
// Re-upload face3d fills on the next frame — the render
|
||||||
// pipeline keys its upload cache off `geometry_epoch`.
|
// pipeline keys its upload cache off `geometry_epoch`.
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Visual style: {label}").as_ref());
|
.push_output(crate::tf!("Visual style: {label}").as_ref());
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
|
||||||
// the `view::` path as before the split.
|
// the `view::` path as before the split.
|
||||||
pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
|
pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
|
||||||
|
|
||||||
|
pub(in crate::app) const VIEWPORT_CAPTURE_BOUNDS_ID: &str = "viewport-capture-bounds";
|
||||||
|
|
||||||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
||||||
/// The desk shown around the sheet, as the widget wants it. One definition,
|
/// The desk shown around the sheet, as the widget wants it. One definition,
|
||||||
/// shared with the renderer that clears the sheet viewport to the same thing.
|
/// shared with the renderer that clears the sheet viewport to the same thing.
|
||||||
|
|
@ -184,6 +186,7 @@ impl OpenCADStudio {
|
||||||
pub fn view_main(&self) -> Element<'_, Message> {
|
pub fn view_main(&self) -> Element<'_, Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let tab = &self.tabs[i];
|
let tab = &self.tabs[i];
|
||||||
|
let thumbnail_capture_clean = self.thumbnail_capture_clean;
|
||||||
let theme_text = self.active_theme.palette().background.base.text;
|
let theme_text = self.active_theme.palette().background.base.text;
|
||||||
let viewcube_text_color = [
|
let viewcube_text_color = [
|
||||||
theme_text.r,
|
theme_text.r,
|
||||||
|
|
@ -229,8 +232,10 @@ impl OpenCADStudio {
|
||||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||||
tab.scene.active_model_tile_bounds(vw, vh).width
|
tab.scene.active_model_tile_bounds(vw, vh).width
|
||||||
};
|
};
|
||||||
let viewcube_visible =
|
let viewcube_visible = self.show_viewcube
|
||||||
self.show_viewcube && !tab.is_start && viewcube_has_room(render_bar_w, active_vp_w);
|
&& !thumbnail_capture_clean
|
||||||
|
&& !tab.is_start
|
||||||
|
&& viewcube_has_room(render_bar_w, active_vp_w);
|
||||||
// Start tab: render welcome page in place of the viewport.
|
// Start tab: render welcome page in place of the viewport.
|
||||||
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
||||||
// returned here also flags the rest of `view` to skip drawing-only
|
// returned here also flags the rest of `view` to skip drawing-only
|
||||||
|
|
@ -258,6 +263,7 @@ impl OpenCADStudio {
|
||||||
shader(ViewportPane::model(
|
shader(ViewportPane::model(
|
||||||
&tab.scene,
|
&tab.scene,
|
||||||
viewcube_visible,
|
viewcube_visible,
|
||||||
|
!thumbnail_capture_clean,
|
||||||
viewport_render_mode,
|
viewport_render_mode,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
))
|
))
|
||||||
|
|
@ -276,6 +282,7 @@ impl OpenCADStudio {
|
||||||
// mouse_areas' hover state and drops their move events).
|
// mouse_areas' hover state and drops their move events).
|
||||||
let scene = &tab.scene;
|
let scene = &tab.scene;
|
||||||
let show_viewcube = viewcube_visible;
|
let show_viewcube = viewcube_visible;
|
||||||
|
let show_interaction = !thumbnail_capture_clean;
|
||||||
let render_mode = viewport_render_mode;
|
let render_mode = viewport_render_mode;
|
||||||
let size_probe: Element<'_, Message> = responsive(move |size| {
|
let size_probe: Element<'_, Message> = responsive(move |size| {
|
||||||
{
|
{
|
||||||
|
|
@ -293,6 +300,7 @@ impl OpenCADStudio {
|
||||||
shader(ViewportPane::for_pane(
|
shader(ViewportPane::for_pane(
|
||||||
scene,
|
scene,
|
||||||
show_viewcube,
|
show_viewcube,
|
||||||
|
show_interaction,
|
||||||
render_mode,
|
render_mode,
|
||||||
idx,
|
idx,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
|
@ -868,6 +876,20 @@ impl OpenCADStudio {
|
||||||
.height(Fill)]
|
.height(Fill)]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
|
} else if thumbnail_capture_clean {
|
||||||
|
let capture_bg = if is_paper { PAPER_SPACE_BACKGROUND } else { bg_color };
|
||||||
|
stack![
|
||||||
|
container(Space::new())
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(capture_bg)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
|
viewport_3d,
|
||||||
|
]
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
} else if is_paper {
|
} else if is_paper {
|
||||||
// Keep the grid above the opaque GPU sheet and below interaction UI.
|
// Keep the grid above the opaque GPU sheet and below interaction UI.
|
||||||
stack![
|
stack![
|
||||||
|
|
@ -902,6 +924,7 @@ impl OpenCADStudio {
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !thumbnail_capture_clean {
|
||||||
// Per-pane input pane_grid goes ABOVE the crosshair overlay so it
|
// Per-pane input pane_grid goes ABOVE the crosshair overlay so it
|
||||||
// receives mouse events (the overlay's `Hidden` cursor would otherwise
|
// receives mouse events (the overlay's `Hidden` cursor would otherwise
|
||||||
// starve any layer beneath it). The controls bar is pushed on top of it.
|
// starve any layer beneath it). The controls bar is pushed on top of it.
|
||||||
|
|
@ -1481,6 +1504,7 @@ impl OpenCADStudio {
|
||||||
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
|
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Docked side panels (Properties, block palette, future palettes) live
|
// Docked side panels (Properties, block palette, future palettes) live
|
||||||
// in an ordered vertical stack on the left/right edge of the drawing
|
// in an ordered vertical stack on the left/right edge of the drawing
|
||||||
|
|
@ -1535,7 +1559,13 @@ impl OpenCADStudio {
|
||||||
if let Some(e) = left_edge {
|
if let Some(e) = left_edge {
|
||||||
parts.push(e);
|
parts.push(e);
|
||||||
}
|
}
|
||||||
parts.push(viewport_stack.into());
|
parts.push(
|
||||||
|
crate::ui::wrap_bar::PosReport::new(
|
||||||
|
VIEWPORT_CAPTURE_BOUNDS_ID,
|
||||||
|
viewport_stack,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
if let Some(e) = right_edge {
|
if let Some(e) = right_edge {
|
||||||
parts.push(e);
|
parts.push(e);
|
||||||
}
|
}
|
||||||
|
|
@ -1712,7 +1742,9 @@ impl OpenCADStudio {
|
||||||
&self.history_content,
|
&self.history_content,
|
||||||
self.win_size.1,
|
self.win_size.1,
|
||||||
);
|
);
|
||||||
let center_stack: Element<'_, Message> = if tab.is_start {
|
let center_stack: Element<'_, Message> = if thumbnail_capture_clean {
|
||||||
|
workspace
|
||||||
|
} else if tab.is_start {
|
||||||
column![
|
column![
|
||||||
workspace,
|
workspace,
|
||||||
iced::widget::container(command_line)
|
iced::widget::container(command_line)
|
||||||
|
|
@ -2096,6 +2128,11 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
};
|
};
|
||||||
|
let thumbnail_capture = if self.thumbnail_capture_clean {
|
||||||
|
window::frames().map(|_| Message::ThumbnailCaptureFrame)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
};
|
||||||
// Blink the MText preview caret while the editor is open.
|
// Blink the MText preview caret while the editor is open.
|
||||||
let caret_blink = if self.mtext_editor.is_some() {
|
let caret_blink = if self.mtext_editor.is_some() {
|
||||||
iced::time::every(std::time::Duration::from_millis(530))
|
iced::time::every(std::time::Duration::from_millis(530))
|
||||||
|
|
@ -2154,6 +2191,7 @@ impl OpenCADStudio {
|
||||||
grip_dwell,
|
grip_dwell,
|
||||||
hover_dwell,
|
hover_dwell,
|
||||||
nav_settle,
|
nav_settle,
|
||||||
|
thumbnail_capture,
|
||||||
caret_blink,
|
caret_blink,
|
||||||
web_fonts,
|
web_fonts,
|
||||||
autosave,
|
autosave,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
//! DWG preview thumbnails.
|
//! DWG preview thumbnails.
|
||||||
//!
|
//!
|
||||||
//! - [`from_snapshot`] rasterizes the drawing exactly as it is framed on screen —
|
//! - [`from_screenshot`] crops the visible drawing area into a small
|
||||||
//! the live camera's pan / zoom / rotation, only the currently visible region,
|
//! [`acadrust::Preview`] embedded on save.
|
||||||
//! not the whole extent — into a small [`acadrust::Preview`] embedded on save
|
|
||||||
//! so OCS drawings show a thumbnail in file browsers and other CAD apps.
|
|
||||||
//! - [`read_handle`] / [`extract_to_png`] read a DWG's *embedded* preview back
|
//! - [`read_handle`] / [`extract_to_png`] read a DWG's *embedded* preview back
|
||||||
//! for the Start page and the OS file-manager thumbnailer. Extraction lives in
|
//! for the Start page and the OS file-manager thumbnailer. Extraction lives in
|
||||||
//! the shared [`dwg_thumbnailer`] core crate (also used by the Windows/macOS
|
//! the shared [`dwg_thumbnailer`] core crate (also used by the Windows/macOS
|
||||||
|
|
@ -11,67 +9,55 @@
|
||||||
|
|
||||||
use acadrust::{Preview, PreviewFormat};
|
use acadrust::{Preview, PreviewFormat};
|
||||||
use iced::Rectangle;
|
use iced::Rectangle;
|
||||||
use image::{ImageFormat, Rgb, RgbImage};
|
use image::{ImageFormat, RgbImage};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::scene::WireModel;
|
/// Build a preview from the visible drawing area of an Iced window screenshot.
|
||||||
use crate::scene::view::camera::Camera;
|
pub fn from_screenshot(
|
||||||
|
screenshot: &iced::window::Screenshot,
|
||||||
|
bounds: Rectangle,
|
||||||
|
png: bool,
|
||||||
|
) -> Option<Preview> {
|
||||||
|
let scale = screenshot.scale_factor;
|
||||||
|
if !scale.is_finite() || scale <= 0.0 || bounds.width <= 0.0 || bounds.height <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = screenshot.size.width as f32;
|
||||||
|
let height = screenshot.size.height as f32;
|
||||||
|
let left = (bounds.x * scale).floor().clamp(0.0, width) as u32;
|
||||||
|
let top = (bounds.y * scale).floor().clamp(0.0, height) as u32;
|
||||||
|
let right = ((bounds.x + bounds.width) * scale)
|
||||||
|
.ceil()
|
||||||
|
.clamp(0.0, width) as u32;
|
||||||
|
let bottom = ((bounds.y + bounds.height) * scale)
|
||||||
|
.ceil()
|
||||||
|
.clamp(0.0, height) as u32;
|
||||||
|
if right <= left || bottom <= top {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cropped = screenshot
|
||||||
|
.crop(Rectangle {
|
||||||
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width: right - left,
|
||||||
|
height: bottom - top,
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
let rgba = image::RgbaImage::from_raw(
|
||||||
|
cropped.size.width,
|
||||||
|
cropped.size.height,
|
||||||
|
cropped.rgba.to_vec(),
|
||||||
|
)?;
|
||||||
|
let (cw, ch) = canvas_dims(cropped.size.width as f64 / cropped.size.height as f64);
|
||||||
|
let resized = image::imageops::resize(&rgba, cw, ch, image::imageops::FilterType::Triangle);
|
||||||
|
encode(image::DynamicImage::ImageRgba8(resized).into_rgb8(), png)
|
||||||
|
}
|
||||||
|
|
||||||
/// Longest edge of the generated thumbnail, in pixels.
|
/// Longest edge of the generated thumbnail, in pixels.
|
||||||
const MAX_DIM: u32 = 256;
|
const MAX_DIM: u32 = 256;
|
||||||
|
|
||||||
/// Build a preview matching what is currently on screen: the drawing's wires
|
|
||||||
/// projected through the live camera into a `viewport`-aspect canvas, so the
|
|
||||||
/// thumbnail is the visible framing (pan / zoom / rotation, only the on-screen
|
|
||||||
/// region), not the whole extent. `viewport` is the model pane's pixel size.
|
|
||||||
/// `None` when the drawing is empty (clears any stale preview).
|
|
||||||
///
|
|
||||||
/// `png` picks the encoding: a line drawing on a flat background is almost all
|
|
||||||
/// one colour, so a **PNG** collapses to a few KB where the uncompressed
|
|
||||||
/// **BMP/DIB** stays ~180 KB at 256². PNG previews are only valid from R2013
|
|
||||||
/// (AC1027) on, so the caller passes `false` for older targets → BMP/DIB.
|
|
||||||
///
|
|
||||||
/// Native background saves retain
|
|
||||||
/// the resident wire `Arc` and a small camera copy, then run the point scan and
|
|
||||||
/// image encoding on the save worker instead of blocking the UI thread.
|
|
||||||
pub fn from_snapshot(
|
|
||||||
wires: &[WireModel],
|
|
||||||
camera: &Camera,
|
|
||||||
bg_color: [f32; 4],
|
|
||||||
png: bool,
|
|
||||||
viewport: (f32, f32),
|
|
||||||
) -> Option<Preview> {
|
|
||||||
if wires.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (vw, vh) = viewport;
|
|
||||||
if !(vw > 0.0 && vh > 0.0) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canvas keeps the viewport's aspect so the framing is undistorted; longest
|
|
||||||
// edge = MAX_DIM. Projecting with the canvas rectangle as the camera bounds
|
|
||||||
// makes `project` return pixel coordinates already in canvas space.
|
|
||||||
let (cw, ch) = canvas_dims((vw / vh) as f64);
|
|
||||||
let bounds = Rectangle {
|
|
||||||
x: 0.0,
|
|
||||||
y: 0.0,
|
|
||||||
width: cw as f32,
|
|
||||||
height: ch as f32,
|
|
||||||
};
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
rasterize_snapshot_parallel(wires, camera, bounds, cw, ch, bg_color, png)
|
|
||||||
}
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
rasterize(wires, cw, ch, bg_color, png, |x, y, z| {
|
|
||||||
camera.project(glam::DVec3::new(x, y, z), bounds)
|
|
||||||
.map(|s| (s.x.round() as i32, s.y.round() as i32))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Canvas dimensions for an aspect ratio, longest edge = [`MAX_DIM`].
|
/// Canvas dimensions for an aspect ratio, longest edge = [`MAX_DIM`].
|
||||||
fn canvas_dims(aspect: f64) -> (u32, u32) {
|
fn canvas_dims(aspect: f64) -> (u32, u32) {
|
||||||
if aspect >= 1.0 {
|
if aspect >= 1.0 {
|
||||||
|
|
@ -81,184 +67,7 @@ fn canvas_dims(aspect: f64) -> (u32, u32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
/// Encode PNG for R2013+; older targets receive a BMP/DIB.
|
||||||
fn rasterize_snapshot_parallel(
|
|
||||||
wires: &[WireModel],
|
|
||||||
camera: &Camera,
|
|
||||||
bounds: Rectangle,
|
|
||||||
cw: u32,
|
|
||||||
ch: u32,
|
|
||||||
bg: [f32; 4],
|
|
||||||
png: bool,
|
|
||||||
) -> Option<Preview> {
|
|
||||||
use crate::par::prelude::*;
|
|
||||||
|
|
||||||
let total_segments: usize = wires
|
|
||||||
.iter()
|
|
||||||
.map(|wire| wire.points.len().saturating_sub(1))
|
|
||||||
.sum();
|
|
||||||
if total_segments < 100_000 {
|
|
||||||
return rasterize(wires, cw, ch, bg, png, |x, y, z| {
|
|
||||||
camera.project(glam::DVec3::new(x, y, z), bounds)
|
|
||||||
.map(|point| (point.x.round() as i32, point.y.round() as i32))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let task_count = (rayon::current_num_threads().max(1) * 2).min(total_segments);
|
|
||||||
let target_work = total_segments.div_ceil(task_count).max(1);
|
|
||||||
let mut tasks: Vec<Vec<(usize, usize, usize)>> = Vec::with_capacity(task_count);
|
|
||||||
let mut task = Vec::new();
|
|
||||||
let mut task_work = 0usize;
|
|
||||||
for (wire_index, wire) in wires.iter().enumerate() {
|
|
||||||
let segment_count = wire.points.len().saturating_sub(1);
|
|
||||||
let mut start = 0usize;
|
|
||||||
while start < segment_count {
|
|
||||||
let take = (target_work - task_work).min(segment_count - start);
|
|
||||||
task.push((wire_index, start, start + take));
|
|
||||||
task_work += take;
|
|
||||||
start += take;
|
|
||||||
if task_work == target_work {
|
|
||||||
tasks.push(std::mem::take(&mut task));
|
|
||||||
task_work = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !task.is_empty() {
|
|
||||||
tasks.push(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
let layers: Vec<Vec<u32>> = tasks
|
|
||||||
.par_iter()
|
|
||||||
.map(|task| {
|
|
||||||
let mut pixels = vec![u32::MAX; (cw * ch) as usize];
|
|
||||||
for &(wire_index, start, end) in task {
|
|
||||||
let wire = &wires[wire_index];
|
|
||||||
let color = to_rgb(wire.color);
|
|
||||||
let packed =
|
|
||||||
color[0] as u32 | (color[1] as u32) << 8 | (color[2] as u32) << 16;
|
|
||||||
let mut previous = projected_wire_point(wire, start, camera, bounds);
|
|
||||||
for index in start + 1..=end {
|
|
||||||
let current = projected_wire_point(wire, index, camera, bounds);
|
|
||||||
if let (Some(a), Some(b)) = (previous, current) {
|
|
||||||
draw_line_layer(&mut pixels, cw, ch, a, b, packed);
|
|
||||||
}
|
|
||||||
previous = current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pixels
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut image = RgbImage::from_pixel(cw, ch, Rgb(to_rgb(bg)));
|
|
||||||
for layer in layers {
|
|
||||||
for (pixel, packed) in image.pixels_mut().zip(layer) {
|
|
||||||
if packed != u32::MAX {
|
|
||||||
*pixel = Rgb([
|
|
||||||
packed as u8,
|
|
||||||
(packed >> 8) as u8,
|
|
||||||
(packed >> 16) as u8,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encode(image, png)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn projected_wire_point(
|
|
||||||
wire: &WireModel,
|
|
||||||
index: usize,
|
|
||||||
camera: &Camera,
|
|
||||||
bounds: Rectangle,
|
|
||||||
) -> Option<(i32, i32)> {
|
|
||||||
let point = wire.points.get(index)?;
|
|
||||||
if !point[0].is_finite() || !point[1].is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (x, y, z) = abs_xyz(wire, index, point);
|
|
||||||
camera
|
|
||||||
.project(glam::DVec3::new(x, y, z), bounds)
|
|
||||||
.map(|screen| (screen.x.round() as i32, screen.y.round() as i32))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn draw_line_layer(
|
|
||||||
pixels: &mut [u32],
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
(x0, y0): (i32, i32),
|
|
||||||
(x1, y1): (i32, i32),
|
|
||||||
color: u32,
|
|
||||||
) {
|
|
||||||
let (width, height) = (width as i32, height as i32);
|
|
||||||
let Some(((mut x0, mut y0), (x1, y1))) =
|
|
||||||
clip_line_to_rect((x0, y0), (x1, y1), width, height)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let dx = (x1 - x0).abs();
|
|
||||||
let dy = -(y1 - y0).abs();
|
|
||||||
let sx = if x0 < x1 { 1 } else { -1 };
|
|
||||||
let sy = if y0 < y1 { 1 } else { -1 };
|
|
||||||
let mut error = dx + dy;
|
|
||||||
loop {
|
|
||||||
if x0 >= 0 && x0 < width && y0 >= 0 && y0 < height {
|
|
||||||
pixels[y0 as usize * width as usize + x0 as usize] = color;
|
|
||||||
}
|
|
||||||
if x0 == x1 && y0 == y1 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let twice = 2 * error;
|
|
||||||
if twice >= dy {
|
|
||||||
error += dy;
|
|
||||||
x0 += sx;
|
|
||||||
}
|
|
||||||
if twice <= dx {
|
|
||||||
error += dx;
|
|
||||||
y0 += sy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rasterize `wires` onto a `bg`-filled `cw`×`ch` canvas, placing each vertex
|
|
||||||
/// with `project` (world XYZ → canvas pixel, `None` = not projectable), and
|
|
||||||
/// encode the result. A `None` from `project` breaks the polyline run, as does a
|
|
||||||
/// NaN separator, so off-screen / clipped segments simply don't draw.
|
|
||||||
fn rasterize(
|
|
||||||
wires: &[WireModel],
|
|
||||||
cw: u32,
|
|
||||||
ch: u32,
|
|
||||||
bg: [f32; 4],
|
|
||||||
png: bool,
|
|
||||||
project: impl Fn(f64, f64, f64) -> Option<(i32, i32)>,
|
|
||||||
) -> Option<Preview> {
|
|
||||||
let mut img = RgbImage::from_pixel(cw, ch, Rgb(to_rgb(bg)));
|
|
||||||
for w in wires {
|
|
||||||
let col = Rgb(to_rgb(w.color));
|
|
||||||
let mut prev: Option<(i32, i32)> = None;
|
|
||||||
for (i, p) in w.points.iter().enumerate() {
|
|
||||||
if !p[0].is_finite() || !p[1].is_finite() {
|
|
||||||
prev = None; // NaN separator breaks the run
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let (x, y, z) = abs_xyz(w, i, p);
|
|
||||||
let cur = project(x, y, z);
|
|
||||||
if let (Some(a), Some(b)) = (prev, cur) {
|
|
||||||
draw_line(&mut img, a, b, col);
|
|
||||||
}
|
|
||||||
prev = cur;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encode(img, png)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode the canvas. PNG for R2013+ targets (few KB); else a BMP → DIB (no
|
|
||||||
/// 14-byte BITMAPFILEHEADER, which the DWG preview container doesn't carry).
|
|
||||||
/// The BMP is an 8-bit **RLE8**-compressed DIB — a line drawing on a flat
|
|
||||||
/// background is a few distinct colours with long single-colour runs, so it
|
|
||||||
/// collapses from the ~180 KB of a 24-bit DIB to a handful of KB. A view with
|
|
||||||
/// more than 256 distinct colours (rare) can't be palettised, so it falls back
|
|
||||||
/// to the 24-bit uncompressed DIB.
|
|
||||||
fn encode(img: RgbImage, png: bool) -> Option<Preview> {
|
fn encode(img: RgbImage, png: bool) -> Option<Preview> {
|
||||||
if png {
|
if png {
|
||||||
let mut buf = Cursor::new(Vec::new());
|
let mut buf = Cursor::new(Vec::new());
|
||||||
|
|
@ -347,25 +156,6 @@ fn bmp24_dib(img: &RgbImage) -> Option<Vec<u8>> {
|
||||||
(bmp.len() > 14).then(|| bmp[14..].to_vec())
|
(bmp.len() > 14).then(|| bmp[14..].to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Absolute world XYZ of vertex `i`, reconstructing the double-single residual.
|
|
||||||
#[inline]
|
|
||||||
fn abs_xyz(w: &WireModel, i: usize, p: &[f32; 3]) -> (f64, f64, f64) {
|
|
||||||
let (lx, ly, lz) = w
|
|
||||||
.points_low
|
|
||||||
.get(i)
|
|
||||||
.map_or((0.0, 0.0, 0.0), |l| (l[0] as f64, l[1] as f64, l[2] as f64));
|
|
||||||
(p[0] as f64 + lx, p[1] as f64 + ly, p[2] as f64 + lz)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn to_rgb(c: [f32; 4]) -> [u8; 3] {
|
|
||||||
[
|
|
||||||
(c[0].clamp(0.0, 1.0) * 255.0).round() as u8,
|
|
||||||
(c[1].clamp(0.0, 1.0) * 255.0).round() as u8,
|
|
||||||
(c[2].clamp(0.0, 1.0) * 255.0).round() as u8,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a DWG's embedded preview and write it as a PNG at `output`, scaled so
|
/// Read a DWG's embedded preview and write it as a PNG at `output`, scaled so
|
||||||
/// its longest edge is at most `size`. Returns `false` on any failure (no
|
/// its longest edge is at most `size`. Returns `false` on any failure (no
|
||||||
/// preview, undecodable, write error) so the OS thumbnailer falls back to a
|
/// preview, undecodable, write error) so the OS thumbnailer falls back to a
|
||||||
|
|
@ -399,14 +189,6 @@ pub fn read_handle(path: &std::path::Path) -> Option<iced::widget::image::Handle
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn wire(pts: &[[f32; 2]], color: [f32; 4]) -> WireModel {
|
|
||||||
WireModel {
|
|
||||||
points: pts.iter().map(|&[x, y]| [x, y, 0.0]).collect(),
|
|
||||||
color,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepend a `BITMAPFILEHEADER` so `image` can decode the DIB. Mirrors the
|
/// Prepend a `BITMAPFILEHEADER` so `image` can decode the DIB. Mirrors the
|
||||||
/// palette-aware offset the shared `dwg_thumbnailer::dib_to_bmp` computes.
|
/// palette-aware offset the shared `dwg_thumbnailer::dib_to_bmp` computes.
|
||||||
fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
|
fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
|
||||||
|
|
@ -429,35 +211,13 @@ mod tests {
|
||||||
assert_eq!(canvas_dims(0.5), (MAX_DIM / 2, MAX_DIM)); // tall
|
assert_eq!(canvas_dims(0.5), (MAX_DIM / 2, MAX_DIM)); // tall
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rasterize_draws_a_valid_non_blank_dib() {
|
|
||||||
let bg = [0.1, 0.1, 0.1, 1.0];
|
|
||||||
// A closed square (connected polyline) in a distinct colour.
|
|
||||||
let sq = wire(
|
|
||||||
&[[10.0, 10.0], [90.0, 10.0], [90.0, 90.0], [10.0, 90.0], [10.0, 10.0]],
|
|
||||||
[1.0, 0.0, 0.0, 1.0],
|
|
||||||
);
|
|
||||||
// Trivial projector: world XY straight to pixels (Y flipped), z ignored.
|
|
||||||
let p = rasterize(&[sq], MAX_DIM, MAX_DIM, bg, false, |x, y, _| {
|
|
||||||
Some((x.round() as i32, (MAX_DIM as f64 - y).round() as i32))
|
|
||||||
})
|
|
||||||
.expect("some preview");
|
|
||||||
assert_eq!(p.format, PreviewFormat::Bmp);
|
|
||||||
// DIB starts with a 40-byte BITMAPINFOHEADER.
|
|
||||||
assert_eq!(&p.data[0..4], &40u32.to_le_bytes());
|
|
||||||
let img = image::load_from_memory(&dib_to_bmp(&p.data)).expect("decodes").to_rgb8();
|
|
||||||
assert_eq!((img.width(), img.height()), (MAX_DIM, MAX_DIM));
|
|
||||||
let bg_px = to_rgb(bg);
|
|
||||||
assert!(img.pixels().any(|px| px.0 != bg_px), "nothing drawn");
|
|
||||||
assert!(img.pixels().any(|px| px.0[0] > 128 && px.0[1] < 64), "square not red");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rle8_bmp_is_8bit_compressed_and_round_trips() {
|
fn rle8_bmp_is_8bit_compressed_and_round_trips() {
|
||||||
let bg = [1.0, 1.0, 1.0, 1.0];
|
let mut image = RgbImage::from_pixel(MAX_DIM, MAX_DIM, image::Rgb([255, 255, 255]));
|
||||||
let sq = wire(&[[10.0, 10.0], [90.0, 90.0]], [0.0, 0.0, 0.0, 1.0]);
|
for i in 10..90 {
|
||||||
let proj = |x: f64, y: f64, _z: f64| Some((x.round() as i32, y.round() as i32));
|
image.put_pixel(i, i, image::Rgb([0, 0, 0]));
|
||||||
let bmp = rasterize(&[sq], MAX_DIM, MAX_DIM, bg, false, proj).unwrap();
|
}
|
||||||
|
let bmp = encode(image, false).unwrap();
|
||||||
assert_eq!(bmp.format, PreviewFormat::Bmp);
|
assert_eq!(bmp.format, PreviewFormat::Bmp);
|
||||||
// 8-bit, BI_RLE8.
|
// 8-bit, BI_RLE8.
|
||||||
assert_eq!(u16::from_le_bytes([bmp.data[14], bmp.data[15]]), 8, "bitcount");
|
assert_eq!(u16::from_le_bytes([bmp.data[14], bmp.data[15]]), 8, "bitcount");
|
||||||
|
|
@ -476,90 +236,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn png_preview_decodes() {
|
fn png_preview_decodes() {
|
||||||
let bg = [1.0, 1.0, 1.0, 1.0];
|
let image = RgbImage::from_pixel(MAX_DIM, MAX_DIM, image::Rgb([255, 255, 255]));
|
||||||
let sq = wire(&[[10.0, 10.0], [90.0, 90.0]], [0.0, 0.0, 0.0, 1.0]);
|
let p = encode(image, true).unwrap();
|
||||||
let p = rasterize(&[sq], MAX_DIM, MAX_DIM, bg, true, |x, y, _| {
|
|
||||||
Some((x.round() as i32, y.round() as i32))
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(p.format, PreviewFormat::Png);
|
assert_eq!(p.format, PreviewFormat::Png);
|
||||||
let img = image::load_from_memory_with_format(&p.data, ImageFormat::Png).expect("png decodes");
|
let img = image::load_from_memory_with_format(&p.data, ImageFormat::Png).expect("png decodes");
|
||||||
assert_eq!((img.width(), img.height()), (MAX_DIM, MAX_DIM));
|
assert_eq!((img.width(), img.height()), (MAX_DIM, MAX_DIM));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bresenham line, clipped to the image bounds.
|
|
||||||
fn draw_line(img: &mut RgbImage, (x0, y0): (i32, i32), (x1, y1): (i32, i32), col: Rgb<u8>) {
|
|
||||||
let (w, h) = (img.width() as i32, img.height() as i32);
|
|
||||||
let Some(((mut x0, mut y0), (x1, y1))) =
|
|
||||||
clip_line_to_rect((x0, y0), (x1, y1), w, h)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let dx = (x1 - x0).abs();
|
|
||||||
let dy = -(y1 - y0).abs();
|
|
||||||
let sx = if x0 < x1 { 1 } else { -1 };
|
|
||||||
let sy = if y0 < y1 { 1 } else { -1 };
|
|
||||||
let mut err = dx + dy;
|
|
||||||
loop {
|
|
||||||
if x0 >= 0 && x0 < w && y0 >= 0 && y0 < h {
|
|
||||||
img.put_pixel(x0 as u32, y0 as u32, col);
|
|
||||||
}
|
|
||||||
if x0 == x1 && y0 == y1 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let e2 = 2 * err;
|
|
||||||
if e2 >= dy {
|
|
||||||
err += dy;
|
|
||||||
x0 += sx;
|
|
||||||
}
|
|
||||||
if e2 <= dx {
|
|
||||||
err += dx;
|
|
||||||
y0 += sy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clip_line_to_rect(
|
|
||||||
(x0, y0): (i32, i32),
|
|
||||||
(x1, y1): (i32, i32),
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
) -> Option<((i32, i32), (i32, i32))> {
|
|
||||||
if width <= 0 || height <= 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (x0, y0, x1, y1) = (x0 as f64, y0 as f64, x1 as f64, y1 as f64);
|
|
||||||
let (dx, dy) = (x1 - x0, y1 - y0);
|
|
||||||
let mut enter = 0.0f64;
|
|
||||||
let mut leave = 1.0f64;
|
|
||||||
for (p, q) in [
|
|
||||||
(-dx, x0),
|
|
||||||
(dx, width.saturating_sub(1) as f64 - x0),
|
|
||||||
(-dy, y0),
|
|
||||||
(dy, height.saturating_sub(1) as f64 - y0),
|
|
||||||
] {
|
|
||||||
if p == 0.0 {
|
|
||||||
if q < 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let ratio = q / p;
|
|
||||||
if p < 0.0 {
|
|
||||||
enter = enter.max(ratio);
|
|
||||||
} else {
|
|
||||||
leave = leave.min(ratio);
|
|
||||||
}
|
|
||||||
if enter > leave {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let point = |t: f64| {
|
|
||||||
(
|
|
||||||
(x0 + t * dx).round().clamp(0.0, width.saturating_sub(1) as f64) as i32,
|
|
||||||
(y0 + t * dy).round().clamp(0.0, height.saturating_sub(1) as f64) as i32,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
Some((point(enter), point(leave)))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,9 @@ impl Scene {
|
||||||
self.camera_generation = saved_generation;
|
self.camera_generation = saved_generation;
|
||||||
|
|
||||||
if let Some((min, max)) = refreshed {
|
if let Some((min, max)) = refreshed {
|
||||||
self.camera.borrow_mut().fit_depth_to_bounds(min, max);
|
self.camera
|
||||||
|
.borrow_mut()
|
||||||
|
.fit_depth_to_bounds_f64(min, max);
|
||||||
self.projection_bounds_epoch.set(self.geometry_epoch);
|
self.projection_bounds_epoch.set(self.geometry_epoch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -672,11 +674,7 @@ impl Scene {
|
||||||
// The saved-view convention uses a 24 mm vertical aperture. This is
|
// The saved-view convention uses a 24 mm vertical aperture. This is
|
||||||
// the inverse of the projection path's documented
|
// the inverse of the projection path's documented
|
||||||
// `distance = view_height * lens / 24` relation.
|
// `distance = view_height * lens / 24` relation.
|
||||||
let fov_y = if perspective {
|
let fov_y = 2.0 * (12.0 / lens_length).atan();
|
||||||
2.0 * (12.0 / lens_length).atan()
|
|
||||||
} else {
|
|
||||||
45.0_f32.to_radians()
|
|
||||||
};
|
|
||||||
let distance = ((view_height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001);
|
let distance = ((view_height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001);
|
||||||
Some(Camera {
|
Some(Camera {
|
||||||
target,
|
target,
|
||||||
|
|
@ -729,9 +727,9 @@ impl Scene {
|
||||||
let view_dir = cam.rotation * glam::Vec3::Z;
|
let view_dir = cam.rotation * glam::Vec3::Z;
|
||||||
let view_height = cam.ortho_size() * 2.0;
|
let view_height = cam.ortho_size() * 2.0;
|
||||||
let target_wcs = acadrust::types::Vector3 {
|
let target_wcs = acadrust::types::Vector3 {
|
||||||
x: (cam.target.x as f64) + [0.0_f64; 3][0],
|
x: cam.target.x,
|
||||||
y: (cam.target.y as f64) + [0.0_f64; 3][1],
|
y: cam.target.y,
|
||||||
z: (cam.target.z as f64) + [0.0_f64; 3][2],
|
z: cam.target.z,
|
||||||
};
|
};
|
||||||
entry.lower_left = lower_left;
|
entry.lower_left = lower_left;
|
||||||
entry.upper_right = upper_right;
|
entry.upper_right = upper_right;
|
||||||
|
|
@ -1186,9 +1184,11 @@ impl Scene {
|
||||||
if !ok {
|
if !ok {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let min = glam::Vec3::new(lo.x as f32, lo.y as f32, lo.z as f32);
|
let min = glam::DVec3::new(lo.x, lo.y, lo.z);
|
||||||
let max = glam::Vec3::new(hi.x as f32, hi.y as f32, hi.z as f32);
|
let max = glam::DVec3::new(hi.x, hi.y, hi.z);
|
||||||
self.camera.borrow_mut().fit_depth_to_bounds(min, max);
|
self.camera
|
||||||
|
.borrow_mut()
|
||||||
|
.fit_depth_to_bounds_f64(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fit_paper_space_extents(&mut self) {
|
fn fit_paper_space_extents(&mut self) {
|
||||||
|
|
@ -1557,7 +1557,7 @@ impl Scene {
|
||||||
if index == active {
|
if index == active {
|
||||||
tile.camera = live_camera.clone();
|
tile.camera = live_camera.clone();
|
||||||
} else {
|
} else {
|
||||||
tile.camera.fit_to_bounds(min, max, aspect);
|
tile.camera.fit_to_bounds_f64(min, max, aspect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(aspect) = aspects.get(active) {
|
if let Some(aspect) = aspects.get(active) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ pub(crate) fn body_transform(
|
||||||
values.extend_from_slice(&components[..len]);
|
values.extend_from_slice(&components[..len]);
|
||||||
} else if let Some(value) = token.as_float() {
|
} else if let Some(value) = token.as_float() {
|
||||||
values.push(value);
|
values.push(value);
|
||||||
|
} else if let Some(value) = token.as_integer() {
|
||||||
|
values.push(value as f64);
|
||||||
} else if let Some(text) = token.as_string() {
|
} else if let Some(text) = token.as_string() {
|
||||||
for word in text.split_ascii_whitespace() {
|
for word in text.split_ascii_whitespace() {
|
||||||
let Ok(value) = word.parse::<f64>() else {
|
let Ok(value) = word.parse::<f64>() else {
|
||||||
|
|
|
||||||
|
|
@ -924,6 +924,7 @@ impl Scene {
|
||||||
annotation_scale_handle: Option<Handle>,
|
annotation_scale_handle: Option<Handle>,
|
||||||
all_visible: bool,
|
all_visible: bool,
|
||||||
viewport: Option<Handle>,
|
viewport: Option<Handle>,
|
||||||
|
tint_selected: bool,
|
||||||
) -> Vec<HatchModel> {
|
) -> Vec<HatchModel> {
|
||||||
let layer_hidden = |layer: &str| {
|
let layer_hidden = |layer: &str| {
|
||||||
self.document
|
self.document
|
||||||
|
|
@ -1098,7 +1099,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.selected.contains(&handle) {
|
if tint_selected && self.selected.contains(&handle) {
|
||||||
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
||||||
}
|
}
|
||||||
let d = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]);
|
let d = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]);
|
||||||
|
|
@ -1115,7 +1116,7 @@ impl Scene {
|
||||||
models.extend(self.instanced_hatch_models(
|
models.extend(self.instanced_hatch_models(
|
||||||
target_block,
|
target_block,
|
||||||
hatch_bg,
|
hatch_bg,
|
||||||
true,
|
tint_selected,
|
||||||
frozen,
|
frozen,
|
||||||
annotation_scale_handle,
|
annotation_scale_handle,
|
||||||
all_visible,
|
all_visible,
|
||||||
|
|
|
||||||
|
|
@ -5674,14 +5674,19 @@ impl Scene {
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(super) fn hatch_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
pub(super) fn hatch_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
||||||
|
self.hatch_models_arc_for_view(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hatch_models_arc_for_view(&self, tint_selected: bool) -> Arc<Vec<HatchModel>> {
|
||||||
// Hatch models bake the selection tint (issue #71), so they depend on
|
// Hatch models bake the selection tint (issue #71), so they depend on
|
||||||
// the *selected set* — but NOT on hover. Keying on `selection_generation`
|
// the *selected set* — but NOT on hover. Keying on `selection_generation`
|
||||||
// (which also bumps on every hover) made each hover-over a new entity
|
// (which also bumps on every hover) made each hover-over a new entity
|
||||||
// rebuild every hatch model: an O(N-hatch) stutter on hatch-heavy
|
// rebuild every hatch model: an O(N-hatch) stutter on hatch-heavy
|
||||||
// drawings. Key on a signature of `selected` instead, so hover (which
|
// drawings. Key on a signature of `selected` instead, so hover (which
|
||||||
// never changes `selected`) keeps the cache warm.
|
// never changes `selected`) keeps the cache warm.
|
||||||
let sel_sig = self.selected_hatch_sig();
|
let sel_sig = if tint_selected { self.selected_hatch_sig() } else { 0 };
|
||||||
let target_block = self.content_render_block_handle();
|
let target_block = self.content_render_block_handle();
|
||||||
let key = (target_block, self.current_layout.clone());
|
let key = (target_block, self.current_layout.clone());
|
||||||
{
|
{
|
||||||
|
|
@ -5728,6 +5733,7 @@ impl Scene {
|
||||||
scale,
|
scale,
|
||||||
self.annotation_all_visible(),
|
self.annotation_all_visible(),
|
||||||
None,
|
None,
|
||||||
|
tint_selected,
|
||||||
));
|
));
|
||||||
self.hatch_cache.borrow_mut().insert(
|
self.hatch_cache.borrow_mut().insert(
|
||||||
key,
|
key,
|
||||||
|
|
@ -6324,9 +6330,10 @@ impl Scene {
|
||||||
&self,
|
&self,
|
||||||
viewport: Handle,
|
viewport: Handle,
|
||||||
frozen: &HashSet<Handle>,
|
frozen: &HashSet<Handle>,
|
||||||
|
tint_selected: bool,
|
||||||
) -> Arc<Vec<HatchModel>> {
|
) -> Arc<Vec<HatchModel>> {
|
||||||
if viewport.is_null() && frozen.is_empty() {
|
if viewport.is_null() && frozen.is_empty() {
|
||||||
return self.hatch_models_arc();
|
return self.hatch_models_arc_for_view(tint_selected);
|
||||||
}
|
}
|
||||||
let target_block = self.content_render_block_handle();
|
let target_block = self.content_render_block_handle();
|
||||||
let scale = self.viewport_scale_handle(viewport);
|
let scale = self.viewport_scale_handle(viewport);
|
||||||
|
|
@ -6336,7 +6343,7 @@ impl Scene {
|
||||||
^ self.viewport_style_key(Some(viewport)).rotate_left(23);
|
^ self.viewport_style_key(Some(viewport)).rotate_left(23);
|
||||||
let sig = Self::frozen_layers_sig(frozen) ^ context_sig;
|
let sig = Self::frozen_layers_sig(frozen) ^ context_sig;
|
||||||
let key = (target_block, self.current_layout.clone(), sig);
|
let key = (target_block, self.current_layout.clone(), sig);
|
||||||
let sel = self.selected_hatch_sig();
|
let sel = if tint_selected { self.selected_hatch_sig() } else { 0 };
|
||||||
if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&key) {
|
if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&key) {
|
||||||
if *e == self.geometry_epoch && *s == sel {
|
if *e == self.geometry_epoch && *s == sel {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
|
|
@ -6348,6 +6355,7 @@ impl Scene {
|
||||||
scale,
|
scale,
|
||||||
all_visible,
|
all_visible,
|
||||||
Some(viewport),
|
Some(viewport),
|
||||||
|
tint_selected,
|
||||||
));
|
));
|
||||||
self.frozen_hatch_cache
|
self.frozen_hatch_cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,18 @@ impl Scene {
|
||||||
Arc<Vec<HatchModel>>,
|
Arc<Vec<HatchModel>>,
|
||||||
Arc<Vec<ImageModel>>,
|
Arc<Vec<ImageModel>>,
|
||||||
) {
|
) {
|
||||||
let selected = self.selected_hatch_sig();
|
self.paper_sheet_render_models_for_view(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn paper_sheet_render_models_for_view(
|
||||||
|
&self,
|
||||||
|
tint_selected: bool,
|
||||||
|
) -> (
|
||||||
|
Arc<Vec<HatchModel>>,
|
||||||
|
Arc<Vec<HatchModel>>,
|
||||||
|
Arc<Vec<ImageModel>>,
|
||||||
|
) {
|
||||||
|
let selected = if tint_selected { self.selected_hatch_sig() } else { 0 };
|
||||||
let reuse = {
|
let reuse = {
|
||||||
let cache = self.paper_sheet_render_cache.borrow();
|
let cache = self.paper_sheet_render_cache.borrow();
|
||||||
if let Some(cache) = cache.get(&self.current_layout) {
|
if let Some(cache) = cache.get(&self.current_layout) {
|
||||||
|
|
@ -259,7 +270,11 @@ impl Scene {
|
||||||
if let Some(sheet) = self.paper_sheet_fill() {
|
if let Some(sheet) = self.paper_sheet_fill() {
|
||||||
hatches.push(sheet);
|
hatches.push(sheet);
|
||||||
}
|
}
|
||||||
hatches.extend(self.paper_canvas_hatches().iter().cloned());
|
hatches.extend(
|
||||||
|
self.paper_canvas_hatches(tint_selected)
|
||||||
|
.iter()
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
let hatches = Arc::new(hatches);
|
let hatches = Arc::new(hatches);
|
||||||
let wipeouts = self.paper_canvas_wipeouts();
|
let wipeouts = self.paper_canvas_wipeouts();
|
||||||
let images = self.paper_sheet_images();
|
let images = self.paper_sheet_images();
|
||||||
|
|
@ -381,7 +396,7 @@ impl Scene {
|
||||||
/// entity handle) rather than the already-flattened arc — the
|
/// entity handle) rather than the already-flattened arc — the
|
||||||
/// flattened arc carries pattern names, not handles, so filtering
|
/// flattened arc carries pattern names, not handles, so filtering
|
||||||
/// there is unreliable.
|
/// there is unreliable.
|
||||||
pub fn paper_canvas_hatches(&self) -> Arc<Vec<HatchModel>> {
|
fn paper_canvas_hatches(&self, tint_selected: bool) -> Arc<Vec<HatchModel>> {
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let layer_hidden = |layer: &str| {
|
let layer_hidden = |layer: &str| {
|
||||||
self.document
|
self.document
|
||||||
|
|
@ -455,7 +470,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.selected.contains(&handle) {
|
if tint_selected && self.selected.contains(&handle) {
|
||||||
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
||||||
}
|
}
|
||||||
models.push(m);
|
models.push(m);
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ pub struct Camera {
|
||||||
/// that span, clipping the drawing (#473). Re-projecting the box each frame
|
/// that span, clipping the drawing (#473). Re-projecting the box each frame
|
||||||
/// keeps near/far exactly as deep as the current view needs. `None` falls
|
/// keeps near/far exactly as deep as the current view needs. `None` falls
|
||||||
/// back to `depth_half_range`.
|
/// back to `depth_half_range`.
|
||||||
pub model_bounds: Option<(Vec3, Vec3)>,
|
pub model_bounds: Option<(DVec3, DVec3)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Camera {
|
impl Default for Camera {
|
||||||
|
|
@ -435,25 +435,24 @@ impl Camera {
|
||||||
|
|
||||||
if let Some((min, max)) = self.model_bounds {
|
if let Some((min, max)) = self.model_bounds {
|
||||||
let corners = [
|
let corners = [
|
||||||
Vec3::new(min.x, min.y, min.z),
|
DVec3::new(min.x, min.y, min.z),
|
||||||
Vec3::new(max.x, min.y, min.z),
|
DVec3::new(max.x, min.y, min.z),
|
||||||
Vec3::new(min.x, max.y, min.z),
|
DVec3::new(min.x, max.y, min.z),
|
||||||
Vec3::new(max.x, max.y, min.z),
|
DVec3::new(max.x, max.y, min.z),
|
||||||
Vec3::new(min.x, min.y, max.z),
|
DVec3::new(min.x, min.y, max.z),
|
||||||
Vec3::new(max.x, min.y, max.z),
|
DVec3::new(max.x, min.y, max.z),
|
||||||
Vec3::new(min.x, max.y, max.z),
|
DVec3::new(min.x, max.y, max.z),
|
||||||
Vec3::new(max.x, max.y, max.z),
|
DVec3::new(max.x, max.y, max.z),
|
||||||
];
|
];
|
||||||
let mut new_min = Vec3::splat(f32::INFINITY);
|
let mut new_min = DVec3::splat(f64::INFINITY);
|
||||||
let mut new_max = Vec3::splat(f32::NEG_INFINITY);
|
let mut new_max = DVec3::splat(f64::NEG_INFINITY);
|
||||||
for corner in corners {
|
for corner in corners {
|
||||||
let transformed = transform.apply(acadrust::types::Vector3::new(
|
let transformed = transform.apply(acadrust::types::Vector3::new(
|
||||||
corner.x as f64,
|
corner.x,
|
||||||
corner.y as f64,
|
corner.y,
|
||||||
corner.z as f64,
|
corner.z,
|
||||||
));
|
));
|
||||||
let transformed =
|
let transformed = DVec3::new(transformed.x, transformed.y, transformed.z);
|
||||||
Vec3::new(transformed.x as f32, transformed.y as f32, transformed.z as f32);
|
|
||||||
new_min = new_min.min(transformed);
|
new_min = new_min.min(transformed);
|
||||||
new_max = new_max.max(transformed);
|
new_max = new_max.max(transformed);
|
||||||
}
|
}
|
||||||
|
|
@ -578,16 +577,16 @@ impl Camera {
|
||||||
/// current target: `x`/`y` span the screen plane, `z` runs along the eye
|
/// current target: `x`/`y` span the screen plane, `z` runs along the eye
|
||||||
/// direction. The offset is taken in f64 before the cast, so a corner at
|
/// direction. The offset is taken in f64 before the cast, so a corner at
|
||||||
/// UTM scale doesn't lose the difference to cancellation.
|
/// UTM scale doesn't lose the difference to cancellation.
|
||||||
fn bounds_in_view(&self, min: Vec3, max: Vec3) -> [Vec3; 8] {
|
fn bounds_in_view(&self, min: DVec3, max: DVec3) -> [Vec3; 8] {
|
||||||
let inv = self.rotation.inverse();
|
let inv = self.rotation.inverse();
|
||||||
let mut out = [Vec3::ZERO; 8];
|
let mut out = [Vec3::ZERO; 8];
|
||||||
for (i, slot) in out.iter_mut().enumerate() {
|
for (i, slot) in out.iter_mut().enumerate() {
|
||||||
let corner = Vec3::new(
|
let corner = DVec3::new(
|
||||||
if i & 1 == 0 { min.x } else { max.x },
|
if i & 1 == 0 { min.x } else { max.x },
|
||||||
if i & 2 == 0 { min.y } else { max.y },
|
if i & 2 == 0 { min.y } else { max.y },
|
||||||
if i & 4 == 0 { min.z } else { max.z },
|
if i & 4 == 0 { min.z } else { max.z },
|
||||||
);
|
);
|
||||||
*slot = inv * (corner.as_dvec3() - self.target).as_vec3();
|
*slot = inv * (corner - self.target).as_vec3();
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
@ -595,7 +594,7 @@ impl Camera {
|
||||||
/// Half-extent of `min..max` along the current eye direction (with the
|
/// Half-extent of `min..max` along the current eye direction (with the
|
||||||
/// same 5% margin `ortho_depth_range` needs), measured from the target.
|
/// same 5% margin `ortho_depth_range` needs), measured from the target.
|
||||||
/// This is exactly what `distance ± r` has to contain to avoid clipping.
|
/// This is exactly what `distance ± r` has to contain to avoid clipping.
|
||||||
fn depth_extent_in_view(&self, min: Vec3, max: Vec3) -> f32 {
|
fn depth_extent_in_view(&self, min: DVec3, max: DVec3) -> f32 {
|
||||||
let depth_r = self
|
let depth_r = self
|
||||||
.bounds_in_view(min, max)
|
.bounds_in_view(min, max)
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -613,7 +612,11 @@ impl Camera {
|
||||||
/// 800 km below its plane zoomed out to 800 km and became a dot. The two
|
/// 800 km below its plane zoomed out to 800 km and became a dot. The two
|
||||||
/// agree on a flat drawing, which is why it went unnoticed.
|
/// agree on a flat drawing, which is why it went unnoticed.
|
||||||
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3, aspect: f32) {
|
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3, aspect: f32) {
|
||||||
self.target = ((min + max) * 0.5).as_dvec3();
|
self.fit_to_bounds_f64(min.as_dvec3(), max.as_dvec3(), aspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fit_to_bounds_f64(&mut self, min: DVec3, max: DVec3, aspect: f32) {
|
||||||
|
self.target = (min + max) * 0.5;
|
||||||
let corners = self.bounds_in_view(min, max);
|
let corners = self.bounds_in_view(min, max);
|
||||||
// Half-height the view needs so the box fits BOTH axes at the given
|
// Half-height the view needs so the box fits BOTH axes at the given
|
||||||
// viewport aspect. The old circumscribed-radius rule fitted the box's
|
// viewport aspect. The old circumscribed-radius rule fitted the box's
|
||||||
|
|
@ -628,7 +631,7 @@ impl Camera {
|
||||||
// that to get the distance which just contains `half_h`, plus a small
|
// that to get the distance which just contains `half_h`, plus a small
|
||||||
// border margin.
|
// border margin.
|
||||||
self.distance = (half_h / (self.fov_y * 0.5).tan() * 1.1).max(1e-3);
|
self.distance = (half_h / (self.fov_y * 0.5).tan() * 1.1).max(1e-3);
|
||||||
self.fit_depth_to_bounds(min, max);
|
self.fit_depth_to_bounds_f64(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Size only the near/far span to `min..max`, leaving the pose alone.
|
/// Size only the near/far span to `min..max`, leaving the pose alone.
|
||||||
|
|
@ -636,7 +639,7 @@ impl Camera {
|
||||||
/// Split out of [`fit_to_bounds`] for the camera restored from a file's
|
/// Split out of [`fit_to_bounds`] for the camera restored from a file's
|
||||||
/// saved view: that pose must not move, but its depth range still has to
|
/// saved view: that pose must not move, but its depth range still has to
|
||||||
/// cover the model or geometry outside it is silently clipped away.
|
/// cover the model or geometry outside it is silently clipped away.
|
||||||
pub fn fit_depth_to_bounds(&mut self, min: Vec3, max: Vec3) {
|
pub fn fit_depth_to_bounds_f64(&mut self, min: DVec3, max: DVec3) {
|
||||||
// Cache the box so `ortho_depth_range` re-derives the near/far depth for
|
// Cache the box so `ortho_depth_range` re-derives the near/far depth for
|
||||||
// the live eye direction every frame — orbiting off the fitted pose then
|
// the live eye direction every frame — orbiting off the fitted pose then
|
||||||
// never clips the drawing (#473). Keeping it tied to the model (not to
|
// never clips the drawing (#473). Keeping it tied to the model (not to
|
||||||
|
|
@ -648,7 +651,7 @@ impl Camera {
|
||||||
self.depth_half_range = self.depth_extent_in_view(min, max);
|
self.depth_half_range = self.depth_extent_in_view(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fitted_model_bounds(&self) -> Option<(Vec3, Vec3)> {
|
pub(crate) fn fitted_model_bounds(&self) -> Option<(DVec3, DVec3)> {
|
||||||
self.model_bounds
|
self.model_bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3174,6 +3174,7 @@ impl Scene {
|
||||||
model_render_mode: acadrust::entities::ViewportRenderMode,
|
model_render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
_hover_region: Option<usize>,
|
_hover_region: Option<usize>,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
|
show_interaction: bool,
|
||||||
viewcube_text_color: [f32; 4],
|
viewcube_text_color: [f32; 4],
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
let nav_build_started = iced::time::Instant::now();
|
let nav_build_started = iced::time::Instant::now();
|
||||||
|
|
@ -3181,7 +3182,7 @@ impl Scene {
|
||||||
// Hover comes from the scene cell driven by the app-level
|
// Hover comes from the scene cell driven by the app-level
|
||||||
// `CursorMoved` handler — the cube overlay sits above the shader
|
// `CursorMoved` handler — the cube overlay sits above the shader
|
||||||
// and would otherwise mask the move event from `Program::update`.
|
// and would otherwise mask the move event from `Program::update`.
|
||||||
let hover_region = self.viewcube_hover.get();
|
let hover_region = show_interaction.then(|| self.viewcube_hover.get()).flatten();
|
||||||
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
||||||
if bounds.height > 0.0 {
|
if bounds.height > 0.0 {
|
||||||
self.set_render_aspect(bounds.width / bounds.height);
|
self.set_render_aspect(bounds.width / bounds.height);
|
||||||
|
|
@ -3197,7 +3198,14 @@ impl Scene {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|inst| {
|
.filter_map(|inst| {
|
||||||
let force = self.refresh_consume(self.instance_id_for(inst));
|
let force = self.refresh_consume(self.instance_id_for(inst));
|
||||||
self.viewport_data_for(inst, canvas, hover_region, show_viewcube, force)
|
self.viewport_data_for(
|
||||||
|
inst,
|
||||||
|
canvas,
|
||||||
|
hover_region,
|
||||||
|
show_viewcube,
|
||||||
|
show_interaction,
|
||||||
|
force,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// Empty viewports → blit nothing; the container background (model bg
|
// Empty viewports → blit nothing; the container background (model bg
|
||||||
|
|
@ -3226,9 +3234,10 @@ impl Scene {
|
||||||
tile_idx: usize,
|
tile_idx: usize,
|
||||||
model_render_mode: acadrust::entities::ViewportRenderMode,
|
model_render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
|
show_interaction: bool,
|
||||||
viewcube_text_color: [f32; 4],
|
viewcube_text_color: [f32; 4],
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
let hover_region = self.viewcube_hover.get();
|
let hover_region = show_interaction.then(|| self.viewcube_hover.get()).flatten();
|
||||||
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
|
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
|
||||||
let bg_color = [0.0, 0.0, 0.0, 0.0];
|
let bg_color = [0.0, 0.0, 0.0, 0.0];
|
||||||
let tiles = self.model_tiles.borrow();
|
let tiles = self.model_tiles.borrow();
|
||||||
|
|
@ -3279,7 +3288,14 @@ impl Scene {
|
||||||
};
|
};
|
||||||
let force = self.refresh_consume(self.instance_id_for(&inst));
|
let force = self.refresh_consume(self.instance_id_for(&inst));
|
||||||
let viewports = self
|
let viewports = self
|
||||||
.viewport_data_for(&inst, canvas, hover_region, show_viewcube, force)
|
.viewport_data_for(
|
||||||
|
&inst,
|
||||||
|
canvas,
|
||||||
|
hover_region,
|
||||||
|
show_viewcube,
|
||||||
|
show_interaction,
|
||||||
|
force,
|
||||||
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
let perf_nav = perf_nav.map(|mut sample| {
|
let perf_nav = perf_nav.map(|mut sample| {
|
||||||
|
|
@ -3304,6 +3320,7 @@ impl Scene {
|
||||||
canvas: (f32, f32),
|
canvas: (f32, f32),
|
||||||
hover_region: Option<usize>,
|
hover_region: Option<usize>,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
|
show_interaction: bool,
|
||||||
force_rasterize: bool,
|
force_rasterize: bool,
|
||||||
) -> Option<ViewportData> {
|
) -> Option<ViewportData> {
|
||||||
let display = self.viewport_display_settings(inst);
|
let display = self.viewport_display_settings(inst);
|
||||||
|
|
@ -3443,7 +3460,7 @@ impl Scene {
|
||||||
// paper layout, model-space overlays go to all content viewports while
|
// paper layout, model-space overlays go to all content viewports while
|
||||||
// paper-space overlays stay on the sheet. This also keeps model-space
|
// paper-space overlays stay on the sheet. This also keeps model-space
|
||||||
// coordinates out of the full-canvas sheet pass (#540).
|
// coordinates out of the full-canvas sheet pass (#540).
|
||||||
let show_live_overlay = if self.current_layout == "Model" {
|
let show_live_overlay = show_interaction && if self.current_layout == "Model" {
|
||||||
true
|
true
|
||||||
} else if self.active_viewport.is_some() {
|
} else if self.active_viewport.is_some() {
|
||||||
!inst.paper_sheet
|
!inst.paper_sheet
|
||||||
|
|
@ -3592,11 +3609,12 @@ impl Scene {
|
||||||
// paper area. Content viewport model builders are block-filtered too;
|
// paper area. Content viewport model builders are block-filtered too;
|
||||||
// the scissor only clips their already-correct Model Space set.
|
// the scissor only clips their already-correct Model Space set.
|
||||||
let (hatches, wipeout_hatches, paper_images) = if inst.paper_sheet {
|
let (hatches, wipeout_hatches, paper_images) = if inst.paper_sheet {
|
||||||
let (hatches, wipeouts, images) = self.paper_sheet_render_models();
|
let (hatches, wipeouts, images) =
|
||||||
|
self.paper_sheet_render_models_for_view(show_interaction);
|
||||||
(hatches, wipeouts, Some(images))
|
(hatches, wipeouts, Some(images))
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.hatch_models_for_viewport(inst.handle, &vp_frozen),
|
self.hatch_models_for_viewport(inst.handle, &vp_frozen, show_interaction),
|
||||||
self.wipeout_models_for_viewport(inst.handle, &vp_frozen),
|
self.wipeout_models_for_viewport(inst.handle, &vp_frozen),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
@ -3750,10 +3768,21 @@ impl Scene {
|
||||||
camera_generation: self.camera_generation,
|
camera_generation: self.camera_generation,
|
||||||
wire_content_id,
|
wire_content_id,
|
||||||
wire_patch,
|
wire_patch,
|
||||||
selected_handles: Arc::new(self.selected.iter().copied().collect()),
|
selected_handles: Arc::new(if show_interaction {
|
||||||
hover_handles: Arc::new(self.hover_highlight_handles()),
|
self.selected.iter().copied().collect()
|
||||||
selection_generation: self.selection_generation,
|
} else {
|
||||||
selected_sig: self.selected_set_sig(),
|
rustc_hash::FxHashSet::default()
|
||||||
|
}),
|
||||||
|
hover_handles: Arc::new(if show_interaction {
|
||||||
|
self.hover_highlight_handles()
|
||||||
|
} else {
|
||||||
|
rustc_hash::FxHashSet::default()
|
||||||
|
}),
|
||||||
|
selection_generation: self
|
||||||
|
.selection_generation
|
||||||
|
.wrapping_mul(2)
|
||||||
|
.wrapping_add(u64::from(!show_interaction)),
|
||||||
|
selected_sig: if show_interaction { self.selected_set_sig() } else { 0 },
|
||||||
screen_rect,
|
screen_rect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use iced::{mouse, Event, Rectangle};
|
||||||
pub struct ViewportPane<'a> {
|
pub struct ViewportPane<'a> {
|
||||||
pub scene: &'a Scene,
|
pub scene: &'a Scene,
|
||||||
pub show_viewcube: bool,
|
pub show_viewcube: bool,
|
||||||
|
pub show_interaction: bool,
|
||||||
/// Render mode applied to the active Model tile or active paper viewport.
|
/// Render mode applied to the active Model tile or active paper viewport.
|
||||||
/// The app may supply a temporary gallery-hover mode; inactive viewports
|
/// The app may supply a temporary gallery-hover mode; inactive viewports
|
||||||
/// keep the values stored on their own tile/entity.
|
/// keep the values stored on their own tile/entity.
|
||||||
|
|
@ -33,12 +34,14 @@ impl<'a> ViewportPane<'a> {
|
||||||
pub fn model(
|
pub fn model(
|
||||||
scene: &'a Scene,
|
scene: &'a Scene,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
|
show_interaction: bool,
|
||||||
render_mode: acadrust::entities::ViewportRenderMode,
|
render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
viewcube_text_color: [f32; 4],
|
viewcube_text_color: [f32; 4],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
scene,
|
scene,
|
||||||
show_viewcube,
|
show_viewcube,
|
||||||
|
show_interaction,
|
||||||
render_mode,
|
render_mode,
|
||||||
pane: None,
|
pane: None,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
|
@ -50,6 +53,7 @@ impl<'a> ViewportPane<'a> {
|
||||||
pub fn for_pane(
|
pub fn for_pane(
|
||||||
scene: &'a Scene,
|
scene: &'a Scene,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
|
show_interaction: bool,
|
||||||
render_mode: acadrust::entities::ViewportRenderMode,
|
render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
tile_idx: usize,
|
tile_idx: usize,
|
||||||
viewcube_text_color: [f32; 4],
|
viewcube_text_color: [f32; 4],
|
||||||
|
|
@ -57,6 +61,7 @@ impl<'a> ViewportPane<'a> {
|
||||||
Self {
|
Self {
|
||||||
scene,
|
scene,
|
||||||
show_viewcube,
|
show_viewcube,
|
||||||
|
show_interaction,
|
||||||
render_mode,
|
render_mode,
|
||||||
pane: Some(tile_idx),
|
pane: Some(tile_idx),
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
|
@ -82,6 +87,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
idx,
|
idx,
|
||||||
self.render_mode,
|
self.render_mode,
|
||||||
self.show_viewcube,
|
self.show_viewcube,
|
||||||
|
self.show_interaction,
|
||||||
self.viewcube_text_color,
|
self.viewcube_text_color,
|
||||||
),
|
),
|
||||||
None => self.scene.build_viewports(
|
None => self.scene.build_viewports(
|
||||||
|
|
@ -89,6 +95,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
self.render_mode,
|
self.render_mode,
|
||||||
state.hover_region,
|
state.hover_region,
|
||||||
self.show_viewcube,
|
self.show_viewcube,
|
||||||
|
self.show_interaction,
|
||||||
self.viewcube_text_color,
|
self.viewcube_text_color,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue