perf: build hatch/image/mesh caches off the UI thread
Parsing + DerivedCaches construction now runs entirely in a dedicated OS thread inside open_path(). FileOpened handler only assigns the pre-built caches, keeping the UI thread free during file load. - Add build_derived_caches(): parallel hatch + mesh tessellation via rayon - Extend Message::FileOpened / open_path return type with DerivedCaches - Drop populate_*_from_document() calls from FileOpened handler
This commit is contained in:
parent
45f9f96c93
commit
2da3640b45
4 changed files with 172 additions and 51 deletions
|
|
@ -212,7 +212,7 @@ pub enum DsField {
|
|||
pub enum Message {
|
||||
Tick(Instant),
|
||||
OpenFile,
|
||||
FileOpened(Result<(String, PathBuf, CadDocument), String>),
|
||||
FileOpened(Result<(String, PathBuf, CadDocument, crate::scene::DerivedCaches), String>),
|
||||
SaveFile,
|
||||
SaveAs,
|
||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ impl H7CAD {
|
|||
|
||||
Message::OpenFile => Task::perform(crate::io::pick_and_open(), Message::FileOpened),
|
||||
|
||||
Message::FileOpened(Ok((name, path, doc))) => {
|
||||
Message::FileOpened(Ok((name, path, doc, caches))) => {
|
||||
let entity_count = doc.entities().count();
|
||||
self.command_line
|
||||
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
||||
|
|
@ -87,10 +87,12 @@ impl H7CAD {
|
|||
}
|
||||
}
|
||||
|
||||
self.tabs[i].scene.compute_and_set_world_offset();
|
||||
self.tabs[i].scene.populate_hatches_from_document();
|
||||
self.tabs[i].scene.populate_images_from_document();
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
// Caches were built on the background thread inside open_path().
|
||||
self.tabs[i].scene.world_offset = caches.world_offset;
|
||||
self.tabs[i].scene.local_extent_max = caches.local_extent_max;
|
||||
self.tabs[i].scene.hatches = caches.hatches;
|
||||
self.tabs[i].scene.images = caches.images;
|
||||
self.tabs[i].scene.meshes = caches.meshes;
|
||||
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
||||
self.tabs[i].scene.preview_wires = vec![];
|
||||
self.tabs[i].scene.current_layout = "Model".to_string();
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ pub mod xref;
|
|||
use acadrust::entities::{Dimension, EntityType};
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
use crate::scene::DerivedCaches;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// ── Open ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Show a file-open dialog and load the selected DWG or DXF file.
|
||||
/// Returns `(filename, path, document)` or an error string.
|
||||
pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument), String> {
|
||||
/// Returns `(filename, path, document, caches)` or an error string.
|
||||
pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let handle = rfd::AsyncFileDialog::new()
|
||||
.set_title("Open CAD file")
|
||||
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
||||
|
|
@ -40,13 +41,22 @@ pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument), String> {
|
|||
}
|
||||
|
||||
/// Load a CAD file from a known path (used by recent files).
|
||||
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument), String> {
|
||||
/// Parsing and cache building run on a dedicated OS thread so the async
|
||||
/// executor stays free for rendering during the load.
|
||||
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let doc = load_file(&path)?;
|
||||
Ok((name, path, doc))
|
||||
let path2 = path.clone();
|
||||
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
||||
let doc = load_file(&path2)?;
|
||||
let caches = crate::scene::build_derived_caches(&doc);
|
||||
Ok((doc, caches))
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "parser thread panicked".to_string())??;
|
||||
Ok((name, path, doc, caches))
|
||||
}
|
||||
|
||||
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
||||
|
|
|
|||
189
src/scene/mod.rs
189
src/scene/mod.rs
|
|
@ -60,6 +60,109 @@ use std::sync::Arc;
|
|||
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
|
||||
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
/// Pre-built entity caches returned by [`build_derived_caches`].
|
||||
/// Produced in the file-load background task so the UI thread only assigns.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DerivedCaches {
|
||||
pub world_offset: [f64; 3],
|
||||
pub local_extent_max: f32,
|
||||
pub hatches: HashMap<Handle, HatchModel>,
|
||||
pub images: HashMap<Handle, ImageModel>,
|
||||
pub meshes: HashMap<Handle, MeshModel>,
|
||||
}
|
||||
|
||||
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
||||
/// Intended to run on a background thread during file load.
|
||||
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
// world_offset
|
||||
let h = &doc.header;
|
||||
let (min, max) = (h.model_space_extents_min, h.model_space_extents_max);
|
||||
let valid = min.x < max.x && min.y < max.y;
|
||||
let (world_offset, local_extent_max) = if valid {
|
||||
let offset = [(min.x + max.x) * 0.5, (min.y + max.y) * 0.5, (min.z + max.z) * 0.5];
|
||||
let hw = ((max.x - min.x) * 0.5) as f32;
|
||||
let hh = ((max.y - min.y) * 0.5) as f32;
|
||||
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
|
||||
(offset, hw.max(hh).max(hz) * 10.0)
|
||||
} else {
|
||||
([0.0; 3], 1e9_f32)
|
||||
};
|
||||
|
||||
// model-space block handle (same logic as Scene::model_space_block_handle)
|
||||
let model_block = doc.objects.values().find_map(|obj| {
|
||||
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||
if l.name == "Model" && !l.block_record.is_null() { Some(l.block_record) } else { None }
|
||||
} else { None }
|
||||
}).unwrap_or_else(|| {
|
||||
doc.block_records.get("*Model_Space").map(|br| br.handle).unwrap_or(Handle::NULL)
|
||||
});
|
||||
|
||||
use rayon::prelude::*;
|
||||
|
||||
// hatches
|
||||
let hatch_entries: Vec<(Handle, EntityType)> = doc.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Hatch(h2) => Some((h2.common.handle, e.clone())),
|
||||
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let hatches: HashMap<Handle, HatchModel> = hatch_entries
|
||||
.into_par_iter()
|
||||
.filter_map(|(handle, kind)| {
|
||||
let owner = kind.common().owner_handle;
|
||||
let offset = if owner == model_block { world_offset } else { [0.0; 3] };
|
||||
let model = match &kind {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
Scene::hatch_model_from_dxf(dxf, color, offset)
|
||||
}
|
||||
EntityType::Solid(solid) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
Some(Scene::solid_hatch_model(solid, color, offset))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
model.map(|m| (handle, m))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// images
|
||||
let images: HashMap<Handle, ImageModel> = doc.entities()
|
||||
.filter_map(|e| {
|
||||
if let EntityType::RasterImage(img) = e {
|
||||
ImageModel::from_raster_image(img).map(|m| (img.common.handle, m))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// meshes (parallel tessellation)
|
||||
let mesh_entries: Vec<(Handle, EntityType)> = doc.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) =>
|
||||
Some((e.common().handle, e.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let meshes: HashMap<Handle, MeshModel> = mesh_entries
|
||||
.into_par_iter()
|
||||
.filter_map(|(handle, entity)| {
|
||||
let color = tessellate::aci_to_rgba(&entity.common().color);
|
||||
let model = match &entity {
|
||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
||||
_ => None,
|
||||
};
|
||||
model.map(|m| (handle, m))
|
||||
})
|
||||
.collect();
|
||||
|
||||
DerivedCaches { world_offset, local_extent_max, hatches, images, meshes }
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
pub camera: Rc<RefCell<Camera>>,
|
||||
pub selection: Rc<RefCell<SelectionState>>,
|
||||
|
|
@ -2043,6 +2146,7 @@ impl Scene {
|
|||
self.hatches.clear();
|
||||
|
||||
let model_block = self.model_space_block_handle();
|
||||
let world_offset = self.world_offset;
|
||||
|
||||
let entries: Vec<(Handle, EntityType)> = self
|
||||
.document
|
||||
|
|
@ -2054,26 +2158,29 @@ impl Scene {
|
|||
})
|
||||
.collect();
|
||||
|
||||
for (handle, kind) in entries {
|
||||
// Paper-space entities live in sheet coordinates — world_offset must not
|
||||
// be applied to them. Only model-space entities need the shift.
|
||||
let owner = kind.common().owner_handle;
|
||||
let offset = if owner == model_block { self.world_offset } else { [0.0; 3] };
|
||||
let model = match &kind {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
Self::hatch_model_from_dxf(dxf, color, offset)
|
||||
}
|
||||
EntityType::Solid(solid) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
Some(Self::solid_hatch_model(solid, color, offset))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(m) = model {
|
||||
self.hatches.insert(handle, m);
|
||||
}
|
||||
}
|
||||
use rayon::prelude::*;
|
||||
self.hatches = entries
|
||||
.into_par_iter()
|
||||
.filter_map(|(handle, kind)| {
|
||||
// Paper-space entities live in sheet coordinates — world_offset must not
|
||||
// be applied to them. Only model-space entities need the shift.
|
||||
let owner = kind.common().owner_handle;
|
||||
let offset = if owner == model_block { world_offset } else { [0.0; 3] };
|
||||
let model = match &kind {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
Self::hatch_model_from_dxf(dxf, color, offset)
|
||||
}
|
||||
EntityType::Solid(solid) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
Some(Self::solid_hatch_model(solid, color, offset))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
model.map(|m| (handle, m))
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.bump_geometry();
|
||||
}
|
||||
|
||||
|
|
@ -2084,32 +2191,34 @@ impl Scene {
|
|||
/// `Solid3D` entity is represented in the mesh cache.
|
||||
pub fn populate_meshes_from_document(&mut self) {
|
||||
self.meshes.clear();
|
||||
// Collect all ACIS-bearing entities: Solid3D, Region, Body.
|
||||
let entries: Vec<(Handle, EntityType)> = self
|
||||
// Collect all ACIS-bearing entities with their color resolved now,
|
||||
// so the parallel phase only sees owned data.
|
||||
let entries: Vec<(Handle, EntityType, [f32; 4])> = self
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) =>
|
||||
Some((e.common().handle, e.clone())),
|
||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
||||
let color = tessellate::aci_to_rgba(&e.common().color);
|
||||
Some((e.common().handle, e.clone(), color))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (handle, entity) in entries {
|
||||
let color = if let Some(e) = self.document.get_entity(handle) {
|
||||
tessellate::aci_to_rgba(&e.common().color)
|
||||
} else {
|
||||
[0.7, 0.7, 0.7, 1.0]
|
||||
};
|
||||
let model = match &entity {
|
||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(m) = model {
|
||||
self.meshes.insert(handle, m);
|
||||
}
|
||||
}
|
||||
|
||||
use rayon::prelude::*;
|
||||
self.meshes = entries
|
||||
.into_par_iter()
|
||||
.filter_map(|(handle, entity, color)| {
|
||||
let model = match &entity {
|
||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
||||
_ => None,
|
||||
};
|
||||
model.map(|m| (handle, m))
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.bump_geometry();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue