Accelerate current-view DXF export and keep desktop exports responsive (#1738)
Thanks @crono2250.
This commit is contained in:
1 parent
4bacd0cae0
commit
bba03803b7
20 files changed
+1319
-53
No files matched your search
@@ -1,6 +1,10 @@
|
||||
//! A drawing of the visible bodies from a supplied viewport camera. No screen pixels, grid,
|
||||
//! selection highlights or tessellation diagonals are exported.
|
||||
|
||||
#[path = "view_dxf_meshes.rs"]
|
||||
mod meshes;
|
||||
#[path = "view_dxf_progress.rs"]
|
||||
mod progress;
|
||||
#[path = "view_dxf_projection.rs"]
|
||||
mod projection;
|
||||
|
||||
@@ -46,28 +50,17 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(path) = path {
|
||||
solidcraft_io::vfs::check(path).map_err(|e| crate::EngineError::Other(e.to_string()))?;
|
||||
}
|
||||
let world = s.world_state();
|
||||
let mut meshes = Vec::new();
|
||||
let mut count = 0usize;
|
||||
for body in world.bodies.iter().filter(|b| s.body_visible(&b.name)) {
|
||||
let mesh = body.body.reference_display_mesh_angular(tolerance, solidcraft_kernel::DISPLAY_ANGLE)?;
|
||||
count = count.saturating_add(mesh.triangles.len()).saturating_add(mesh.edges.iter().map(Vec::len).sum::<usize>());
|
||||
if count > 1_000_000 {
|
||||
return Err(bad(CMD, "view too complex to export; hide bodies or increase tolerance"));
|
||||
}
|
||||
if mesh.triangles.is_empty() {
|
||||
return Err(bad(CMD, format!("{} has no exportable surface", body.name)));
|
||||
}
|
||||
meshes.push(mesh);
|
||||
}
|
||||
if meshes.is_empty() {
|
||||
return Err(bad(CMD, "no visible bodies to export"));
|
||||
}
|
||||
let lines = projection::drawing(&meshes, &cam).map_err(|e| bad(CMD, e))?;
|
||||
let started = std::time::Instant::now();
|
||||
progress::Stage::new("dxf_meshes", 1).check().map_err(|e| bad(CMD, e))?;
|
||||
let meshes = meshes::prepare(s, tolerance)?;
|
||||
let prepare_ms = started.elapsed().as_secs_f64() * 1000.;
|
||||
let drawing = projection::drawing(&meshes, &cam).map_err(|e| bad(CMD, e))?;
|
||||
let writing = progress::Stage::new("dxf_write", 1);
|
||||
writing.check().map_err(|e| bad(CMD, e))?;
|
||||
let mut visible = 0;
|
||||
let mut hidden = 0;
|
||||
let mut entities = Vec::new();
|
||||
for line in lines {
|
||||
for line in drawing.lines {
|
||||
if line.hidden && !dashed {
|
||||
continue;
|
||||
}
|
||||
@@ -86,9 +79,14 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
layers.push(DxfLayer { linetype: DxfLinetype::Hidden, ..DxfLayer::new("HIDDEN", 8) });
|
||||
}
|
||||
let text = solidcraft_io::write_dxf_drawing(&layers, &entities);
|
||||
writing.check().map_err(|e| bad(CMD, e))?;
|
||||
let mut result = json!({"visible":visible,"hidden":hidden,"units":"mm","tolerance":tolerance,"projection":if cam.fov > 0.0 {"perspective"} else {"orthographic"}});
|
||||
result["stats"] = json!({"bodies":meshes.placements.len(),"shared_meshes":meshes.meshes.len(),"triangles":meshes.triangles,
|
||||
"mesh_bytes":meshes.bytes,"worker_limit":meshes::WORKERS,"segments":drawing.segments,"occlusion_work":drawing.work,
|
||||
"prepare_ms":prepare_ms,"projection_ms":drawing.projection_ms,"occlusion_ms":drawing.occlusion_ms});
|
||||
if let Some(path) = path {
|
||||
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| crate::EngineError::Other(e.to_string()))?;
|
||||
solidcraft_io::vfs::check(path).map_err(|e| crate::EngineError::Other(e.to_string()))?;
|
||||
solidcraft_io::write_atomic(std::path::Path::new(path), text.as_bytes(), false).map_err(|e| crate::EngineError::Other(e.to_string()))?;
|
||||
result["path"] = json!(path);
|
||||
} else {
|
||||
result["dxf"] = json!(text);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Export-only mesh instances. Keep one geometry buffer per unambiguous definition, while
|
||||
//! retaining the current placed body's edge choices (tangent classification can vary after
|
||||
//! placement). Never modify the document, fuse parts, or share across separate exports.
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem::size_of;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use solidcraft_doc::{IDENTITY, Mat, ModelBody, mat_inverse, mat_mul};
|
||||
use solidcraft_geom::{Aabb3, Mesh, Vec3};
|
||||
use solidcraft_kernel::{KernelError, map_with_local_pools};
|
||||
|
||||
use super::{CMD, progress::Stage};
|
||||
use crate::params::bad;
|
||||
use crate::{Result, Session};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) const WORKERS: usize = 8;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) const WORKERS: usize = 1;
|
||||
const MESH_BYTES: usize = 512 * 1024 * 1024;
|
||||
const MAX_BODIES: usize = 100_000;
|
||||
|
||||
pub(super) struct Placement {
|
||||
pub mesh: usize,
|
||||
pub transform: Mat,
|
||||
pub visibility: Vec<bool>,
|
||||
}
|
||||
pub(super) struct Scene {
|
||||
pub meshes: Vec<Arc<Mesh>>,
|
||||
pub placements: Vec<Placement>,
|
||||
pub triangles: usize,
|
||||
pub bytes: usize,
|
||||
}
|
||||
|
||||
struct Group {
|
||||
first: usize,
|
||||
inverse: Mat,
|
||||
}
|
||||
struct Item<'a> {
|
||||
body: &'a ModelBody,
|
||||
group: usize,
|
||||
keep: bool,
|
||||
transform: Mat,
|
||||
}
|
||||
struct Prepared {
|
||||
mesh: Option<Arc<Mesh>>,
|
||||
visibility: Vec<bool>,
|
||||
triangles: usize,
|
||||
}
|
||||
|
||||
pub(super) fn claim(counter: &AtomicUsize, amount: usize, limit: usize) -> bool {
|
||||
let mut old = counter.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let Some(next) = old.checked_add(amount).filter(|n| *n <= limit) else { return false };
|
||||
match counter.compare_exchange_weak(old, next, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => return true,
|
||||
Err(current) => old = current,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn buffer_bytes(m: &Mesh) -> usize {
|
||||
m.positions.capacity() * size_of::<Vec3>()
|
||||
+ m.normals.capacity() * size_of::<Vec3>()
|
||||
+ m.triangles.capacity() * size_of::<[u32; 3]>()
|
||||
+ m.tri_face.capacity() * size_of::<u32>()
|
||||
+ m.edges.capacity() * size_of::<Vec<Vec3>>()
|
||||
+ m.edges.iter().map(|e| e.capacity() * size_of::<Vec3>()).sum::<usize>()
|
||||
+ m.seams.capacity()
|
||||
+ m.smooth_edges.capacity()
|
||||
+ m.sharp_edges.capacity()
|
||||
+ m.edge_faces.capacity() * size_of::<Vec<u32>>()
|
||||
+ m.edge_faces.iter().map(|e| e.capacity() * size_of::<u32>()).sum::<usize>()
|
||||
}
|
||||
|
||||
fn rigid(m: &Mat) -> bool {
|
||||
if m.iter().flatten().any(|v| !v.is_finite()) || m.iter().zip([0., 0., 0., 1.]).any(|(c, w)| (c[3] - w).abs() > 1e-10) {
|
||||
return false;
|
||||
}
|
||||
let axes = [Vec3::new(m[0][0], m[0][1], m[0][2]), Vec3::new(m[1][0], m[1][1], m[1][2]), Vec3::new(m[2][0], m[2][1], m[2][2])];
|
||||
let [x, y, z] = axes;
|
||||
(x.len2() - 1.).abs() < 1e-9
|
||||
&& (y.len2() - 1.).abs() < 1e-9
|
||||
&& (z.len2() - 1.).abs() < 1e-9
|
||||
&& x.dot(y).abs() < 1e-9
|
||||
&& x.dot(z).abs() < 1e-9
|
||||
&& y.dot(z).abs() < 1e-9
|
||||
&& (x.cross(y).dot(z) - 1.).abs() < 1e-9
|
||||
}
|
||||
|
||||
pub(super) fn prepare(s: &Session, tolerance: f64) -> Result<Scene> {
|
||||
let world = s.world_state();
|
||||
let state = s.model.state();
|
||||
let mut definitions = BTreeMap::new();
|
||||
for body in &state.bodies {
|
||||
*definitions.entry((body.component, body.definition_name(), body.feature)).or_insert(0usize) += 1;
|
||||
}
|
||||
let mut by_definition = BTreeMap::new();
|
||||
let mut groups: Vec<Group> = Vec::new();
|
||||
let mut items = Vec::new();
|
||||
for (index, body) in world.bodies.iter().enumerate().filter(|(_, b)| s.body_visible(&b.name)) {
|
||||
if items.len() >= MAX_BODIES {
|
||||
return Err(bad(CMD, "too many placed bodies to export"));
|
||||
}
|
||||
let key = (body.component, body.definition_name(), body.feature);
|
||||
let unambiguous = definitions.get(&key) == Some(&1);
|
||||
let reused = unambiguous
|
||||
.then(|| by_definition.get(&key).copied())
|
||||
.flatten()
|
||||
.and_then(|g: usize| groups.get(g).map(|group| (g, mat_mul(&body.source_transform, &group.inverse))))
|
||||
.filter(|(_, m)| rigid(m));
|
||||
let (group, transform) = if let Some(reused) = reused {
|
||||
reused
|
||||
} else {
|
||||
let g = groups.len();
|
||||
groups.push(Group { first: index, inverse: mat_inverse(&body.source_transform).unwrap_or(IDENTITY) });
|
||||
if unambiguous && mat_inverse(&body.source_transform).is_some() {
|
||||
by_definition.entry(key).or_insert(g);
|
||||
}
|
||||
(g, IDENTITY)
|
||||
};
|
||||
let keep = groups.get(group).is_some_and(|g| g.first == index);
|
||||
items.push(Item { body, group, keep, transform });
|
||||
}
|
||||
if items.is_empty() {
|
||||
return Err(bad(CMD, "no visible bodies to export"));
|
||||
}
|
||||
let progress = Stage::new("dxf_meshes", items.len());
|
||||
let bytes = AtomicUsize::new(0);
|
||||
let results = map_with_local_pools(&items, WORKERS, |item| {
|
||||
progress.check().map_err(|e| KernelError::Failed(e.into()))?;
|
||||
let mut mesh = item.body.body.reference_display_mesh_angular(tolerance, solidcraft_kernel::DISPLAY_ANGLE)?;
|
||||
if mesh.triangles.is_empty() {
|
||||
return Err(KernelError::Invalid(format!("{} has no exportable surface", item.body.name)));
|
||||
}
|
||||
let visibility = (0..mesh.edges.len()).map(|i| mesh.shaded_edge_visible(i)).collect();
|
||||
let triangles = mesh.triangles.len();
|
||||
let mesh = if item.keep {
|
||||
// These are owned export buffers, not the document's cached mesh. Picking and
|
||||
// shading data are unused by vector occlusion; edge choices were captured above.
|
||||
mesh.normals = Vec::new();
|
||||
mesh.tri_face = Vec::new();
|
||||
mesh.seams = Vec::new();
|
||||
mesh.smooth_edges = Vec::new();
|
||||
mesh.sharp_edges = Vec::new();
|
||||
mesh.edge_faces = Vec::new();
|
||||
if !claim(&bytes, buffer_bytes(&mesh), MESH_BYTES) {
|
||||
return Err(KernelError::Invalid("view exceeds export memory budget; hide bodies or increase tolerance".into()));
|
||||
}
|
||||
Some(Arc::new(mesh))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
progress.advance(1).map_err(|e| KernelError::Failed(e.into()))?;
|
||||
Ok(Prepared { mesh, visibility, triangles })
|
||||
});
|
||||
let mut prepared = results.into_iter().collect::<solidcraft_kernel::Result<Vec<_>>>()?;
|
||||
let mut meshes: Vec<Option<Arc<Mesh>>> = vec![None; groups.len()];
|
||||
for (item, p) in items.iter().zip(&mut prepared) {
|
||||
if let Some(mesh) = p.mesh.take()
|
||||
&& let Some(slot) = meshes.get_mut(item.group)
|
||||
{
|
||||
*slot = Some(mesh);
|
||||
}
|
||||
}
|
||||
let meshes = meshes.into_iter().map(|m| m.ok_or_else(|| bad(CMD, "missing export geometry"))).collect::<Result<Vec<_>>>()?;
|
||||
let mut placements = Vec::with_capacity(items.len());
|
||||
let mut triangles = 0usize;
|
||||
for (item, p) in items.iter().zip(prepared) {
|
||||
if meshes.get(item.group).is_none_or(|m| m.edges.len() != p.visibility.len()) {
|
||||
return Err(bad(CMD, "placed edge identity changed"));
|
||||
}
|
||||
triangles = triangles.saturating_add(p.triangles);
|
||||
placements.push(Placement { mesh: item.group, transform: item.transform, visibility: p.visibility });
|
||||
}
|
||||
Ok(Scene { meshes, placements, triangles, bytes: bytes.load(Ordering::Relaxed) })
|
||||
}
|
||||
|
||||
pub(super) fn transformed_bounds(mesh: &Mesh, m: &Mat) -> Aabb3 {
|
||||
let b = mesh.bounds();
|
||||
let mut out = Aabb3::EMPTY;
|
||||
for x in [b.min.x, b.max.x] {
|
||||
for y in [b.min.y, b.max.y] {
|
||||
for z in [b.min.z, b.max.z] {
|
||||
out.add(solidcraft_doc::apply_point(m, Vec3::new(x, y, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn only_proper_rigid_placements_share_geometry() {
|
||||
assert!(rigid(&solidcraft_doc::rigid(Vec3::new(1., 2., 3.), Vec3::ZERO, Vec3::Z, 0.7)));
|
||||
let mut m = IDENTITY;
|
||||
m[0][0] = -1.;
|
||||
assert!(!rigid(&m));
|
||||
m[0][0] = 2.;
|
||||
assert!(!rigid(&m));
|
||||
m[0][0] = f64::NAN;
|
||||
assert!(!rigid(&m));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Stage counters shared by the export worker and its bounded computation workers.
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub(super) const CANCELLED: &str = "DXF export cancelled";
|
||||
|
||||
pub(super) struct Stage {
|
||||
watch: Option<Arc<dyn solidcraft_kernel::watch::Watch>>,
|
||||
name: &'static str,
|
||||
total: usize,
|
||||
done: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl Stage {
|
||||
pub fn new(name: &'static str, total: usize) -> Self {
|
||||
let stage = Self { watch: solidcraft_kernel::watch::current(), name, total: total.max(1), done: Mutex::new(0) };
|
||||
if let Some(watch) = &stage.watch {
|
||||
watch.progress(name, 0, stage.total);
|
||||
}
|
||||
stage
|
||||
}
|
||||
pub fn check(&self) -> Result<(), &'static str> {
|
||||
if self.watch.as_ref().is_some_and(|w| w.cancelled()) { Err(CANCELLED) } else { Ok(()) }
|
||||
}
|
||||
pub fn advance(&self, count: usize) -> Result<(), &'static str> {
|
||||
self.check()?;
|
||||
if let Some(watch) = &self.watch {
|
||||
// Serialize reports as well as increments: eight workers must not report a
|
||||
// smaller completed count after a larger one.
|
||||
let mut done = self.done.lock().map_err(|_| "export progress unavailable")?;
|
||||
*done = done.saturating_add(count).min(self.total);
|
||||
watch.progress(self.name, *done, self.total);
|
||||
}
|
||||
self.check()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,27 @@
|
||||
//! Vector hidden-line removal: intersect each projected edge with triangle half planes and
|
||||
//! its depth plane, then union occluded parameter intervals. A BVH bounds the candidate work.
|
||||
use solidcraft_geom::{Aabb3, Mesh, Vec2, Vec3};
|
||||
#[cfg(test)]
|
||||
use solidcraft_geom::Mesh;
|
||||
use solidcraft_geom::{Aabb3, Vec2, Vec3};
|
||||
use solidcraft_io::{DxfEntity, DxfShape};
|
||||
use solidcraft_render::Camera;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[path = "view_dxf_scene.rs"]
|
||||
mod scene;
|
||||
|
||||
pub(super) struct Drawing {
|
||||
pub lines: Vec<Line>,
|
||||
pub work: usize,
|
||||
pub segments: usize,
|
||||
pub projection_ms: f64,
|
||||
pub occlusion_ms: f64,
|
||||
}
|
||||
|
||||
pub(super) fn drawing(meshes: &super::meshes::Scene, cam: &Camera) -> Result<Drawing, &'static str> {
|
||||
scene::drawing(meshes, cam)
|
||||
}
|
||||
|
||||
/// Keep the hidden curves connected: restarting a dash on every short chord would make a
|
||||
/// small circular hole look solid. Degree-two joins continue; junctions end the polyline.
|
||||
pub(super) fn connect(entities: Vec<DxfEntity>) -> Vec<DxfEntity> {
|
||||
@@ -50,6 +67,7 @@ pub(super) fn connect(entities: Vec<DxfEntity>) -> Vec<DxfEntity> {
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Line {
|
||||
pub a: Vec2,
|
||||
pub b: Vec2,
|
||||
@@ -73,15 +91,18 @@ fn overlap(a: &Aabb3, b: &Aabb3) -> bool {
|
||||
a.min.x <= b.max.x && b.min.x <= a.max.x && a.min.y <= b.max.y && b.min.y <= a.max.y
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
struct Triangle {
|
||||
p: [Vec3; 3],
|
||||
bounds: Aabb3,
|
||||
}
|
||||
#[cfg(test)]
|
||||
struct Tree {
|
||||
bounds: Aabb3,
|
||||
children: Option<(Box<Tree>, Box<Tree>)>,
|
||||
triangles: Vec<Triangle>,
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl Tree {
|
||||
fn new(mut triangles: Vec<Triangle>) -> Self {
|
||||
let b = triangles.iter().fold(Aabb3::EMPTY, |b, t| b.union(&t.bounds));
|
||||
@@ -213,6 +234,7 @@ fn merge_lines(lines: Vec<Line>, eps: f64) -> Vec<Line> {
|
||||
|
||||
/// B-rep edges plus curved silhouettes. Triangle adjacency is welded by position because
|
||||
/// different faces can have different vertex indices along their shared boundary.
|
||||
#[cfg(test)]
|
||||
fn edges(mesh: &Mesh, cam: &Camera, eps: f64) -> Vec<(Vec3, Vec3)> {
|
||||
let mut result = Vec::new();
|
||||
for (_, e) in mesh.edges.iter().enumerate().filter(|(i, _)| mesh.shaded_edge_visible(*i)) {
|
||||
@@ -241,7 +263,8 @@ fn edges(mesh: &Mesh, cam: &Camera, eps: f64) -> Vec<(Vec3, Vec3)> {
|
||||
result
|
||||
}
|
||||
|
||||
pub(super) fn drawing(meshes: &[Mesh], cam: &Camera) -> Result<Vec<Line>, &'static str> {
|
||||
#[cfg(test)]
|
||||
fn reference_drawing(meshes: &[Mesh], cam: &Camera) -> Result<Vec<Line>, &'static str> {
|
||||
let (right, up, back) = cam.basis();
|
||||
let scale = meshes.iter().fold(Aabb3::EMPTY, |b, m| b.union(&m.bounds())).diagonal().max(1.0);
|
||||
let eps = scale * 1e-8;
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
//! Two levels of occlusion acceleration: an instance tree over projected bounds, and one
|
||||
//! triangle hierarchy per shared mesh. Per-placement node bounds are small; projected
|
||||
//! triangle coordinates are evaluated only in visited leaves. Covered line intervals prune
|
||||
//! subsequent nodes and instances, including the entire query once coverage is complete.
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use solidcraft_doc::apply_point;
|
||||
use solidcraft_geom::{Aabb3, Mesh, Vec3};
|
||||
use solidcraft_render::Camera;
|
||||
|
||||
use super::super::meshes::{Placement, Scene, WORKERS, claim, transformed_bounds};
|
||||
use super::super::progress::Stage;
|
||||
use super::{Drawing, Line, bounds, clip, key, merge_lines, occluded, overlap, xy};
|
||||
|
||||
const TOO_COMPLEX: &str = "view exceeds export work budget; hide bodies or increase tolerance";
|
||||
const MAX_LINES: usize = 1_000_000;
|
||||
const MAX_SEGMENTS: usize = 4_000_000;
|
||||
const MAX_SHARED_TRIANGLES: usize = 2_000_000;
|
||||
const MAX_VIEW_NODES: usize = 256 * 1024 * 1024 / std::mem::size_of::<Aabb3>();
|
||||
const WORK: usize = 200_000_000;
|
||||
|
||||
fn parallel<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> Result<R, &'static str> + Sync) -> Result<Vec<R>, &'static str> {
|
||||
solidcraft_kernel::map_on_threads(items, WORKERS, f).into_iter().map(|r| r.ok_or("export worker failed")?).collect()
|
||||
}
|
||||
|
||||
struct Node {
|
||||
bounds: Aabb3,
|
||||
start: usize,
|
||||
end: usize,
|
||||
children: Option<(usize, usize)>,
|
||||
}
|
||||
struct Bvh {
|
||||
nodes: Vec<Node>,
|
||||
ids: Vec<usize>,
|
||||
}
|
||||
impl Bvh {
|
||||
fn new(boxes: &[Aabb3], flat: bool) -> Self {
|
||||
let mut tree = Self { nodes: Vec::new(), ids: (0..boxes.len()).collect() };
|
||||
if !boxes.is_empty() {
|
||||
tree.build(boxes, 0, boxes.len(), flat);
|
||||
}
|
||||
tree
|
||||
}
|
||||
fn build(&mut self, boxes: &[Aabb3], start: usize, end: usize, flat: bool) -> usize {
|
||||
let b = self.ids.get(start..end).into_iter().flatten().filter_map(|i| boxes.get(*i)).fold(Aabb3::EMPTY, |b, t| b.union(t));
|
||||
let index = self.nodes.len();
|
||||
self.nodes.push(Node { bounds: b, start, end, children: None });
|
||||
if end - start > 8 {
|
||||
let size = b.size();
|
||||
let axis = if !flat && size.z > size.x.max(size.y) {
|
||||
2
|
||||
} else if size.x > size.y {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let value = |i: &usize| {
|
||||
boxes.get(*i).map(|b| b.center()).map_or(0., |p| match axis {
|
||||
0 => p.x,
|
||||
1 => p.y,
|
||||
_ => p.z,
|
||||
})
|
||||
};
|
||||
let mid = start + (end - start) / 2;
|
||||
if let Some(ids) = self.ids.get_mut(start..end) {
|
||||
ids.select_nth_unstable_by(mid - start, |a, b| value(a).total_cmp(&value(b)).then_with(|| a.cmp(b)));
|
||||
}
|
||||
let left = self.build(boxes, start, mid, flat);
|
||||
let right = self.build(boxes, mid, end, flat);
|
||||
if let Some(node) = self.nodes.get_mut(index) {
|
||||
node.children = Some((left, right));
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
vertices: [u32; 2],
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
struct Side {
|
||||
key: [[i64; 3]; 2],
|
||||
vertices: [u32; 2],
|
||||
triangle: usize,
|
||||
}
|
||||
struct Shared {
|
||||
mesh: Arc<Mesh>,
|
||||
tree: Bvh,
|
||||
edges: Vec<Edge>,
|
||||
incident: Vec<usize>,
|
||||
}
|
||||
impl Shared {
|
||||
fn new(mesh: &Arc<Mesh>, eps: f64, progress: &Stage) -> Result<Self, &'static str> {
|
||||
let mut boxes = Vec::with_capacity(mesh.triangles.len());
|
||||
let mut sides = Vec::new();
|
||||
for (i, indices) in mesh.triangles.iter().enumerate() {
|
||||
if i.is_multiple_of(1024) {
|
||||
progress.check()?;
|
||||
}
|
||||
let points = mesh.tri(indices).ok_or("invalid export triangle")?;
|
||||
if points.iter().any(|p| !p.is_finite()) {
|
||||
return Err("model contains invalid coordinates");
|
||||
}
|
||||
boxes.push(bounds(points));
|
||||
for (a, b) in [(0, 1), (1, 2), (2, 0)] {
|
||||
let (Some(pa), Some(pb), Some(ia), Some(ib)) = (points.get(a), points.get(b), indices.get(a), indices.get(b)) else {
|
||||
return Err("invalid export edge");
|
||||
};
|
||||
let (ka, kb) = (key(*pa, eps), key(*pb, eps));
|
||||
if ka == kb {
|
||||
continue;
|
||||
}
|
||||
let (key, vertices) = if ka < kb { ([ka, kb], [*ia, *ib]) } else { ([kb, ka], [*ib, *ia]) };
|
||||
sides.push(Side { key, vertices, triangle: i });
|
||||
}
|
||||
}
|
||||
let tree = Bvh::new(&boxes, false);
|
||||
sides.sort_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.triangle.cmp(&b.triangle)));
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
let mut incident = Vec::with_capacity(sides.len());
|
||||
let mut previous = None;
|
||||
for side in sides {
|
||||
if previous != Some(side.key) {
|
||||
edges.push(Edge { vertices: side.vertices, start: incident.len(), end: incident.len() });
|
||||
previous = Some(side.key);
|
||||
}
|
||||
incident.push(side.triangle);
|
||||
if let Some(e) = edges.last_mut() {
|
||||
e.end = incident.len();
|
||||
}
|
||||
}
|
||||
Ok(Self { mesh: mesh.clone(), tree, edges, incident })
|
||||
}
|
||||
}
|
||||
|
||||
struct View<'a> {
|
||||
cam: &'a Camera,
|
||||
right: Vec3,
|
||||
up: Vec3,
|
||||
back: Vec3,
|
||||
}
|
||||
impl<'a> View<'a> {
|
||||
fn new(cam: &'a Camera) -> Self {
|
||||
let (right, up, back) = cam.basis();
|
||||
Self { cam, right, up, back }
|
||||
}
|
||||
fn project(&self, p: Vec3) -> Result<Vec3, &'static str> {
|
||||
if !p.is_finite() {
|
||||
return Err("model contains invalid coordinates");
|
||||
}
|
||||
let d = p - self.cam.target;
|
||||
let z = d.dot(self.back);
|
||||
let projected = if self.cam.fov > 0. {
|
||||
let w = self.cam.distance - z;
|
||||
if w <= self.cam.distance * 1e-6 {
|
||||
return Err("model crosses the camera eye plane; zoom out before exporting");
|
||||
}
|
||||
Vec3::new(d.dot(self.right) * self.cam.distance / w, d.dot(self.up) * self.cam.distance / w, 1. / w)
|
||||
} else {
|
||||
Vec3::new(d.dot(self.right), d.dot(self.up), z)
|
||||
};
|
||||
if !projected.is_finite() || projected.len() > 1e12 {
|
||||
return Err("projected coordinates exceed the export range");
|
||||
}
|
||||
Ok(projected)
|
||||
}
|
||||
fn triangle(&self, shared: &Shared, placed: &Placement, id: usize) -> Result<[Vec3; 3], &'static str> {
|
||||
let indices = shared.mesh.triangles.get(id).ok_or("invalid export triangle")?;
|
||||
let [a, b, c] = shared.mesh.tri(indices).ok_or("invalid export triangle")?;
|
||||
Ok([
|
||||
self.project(apply_point(&placed.transform, a))?,
|
||||
self.project(apply_point(&placed.transform, b))?,
|
||||
self.project(apply_point(&placed.transform, c))?,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
struct Projected {
|
||||
nodes: Vec<Aabb3>,
|
||||
segments: Vec<(Vec3, Vec3)>,
|
||||
}
|
||||
impl Projected {
|
||||
fn new(shared: &Shared, placed: &Placement, view: &View<'_>, eps: f64, segments: &AtomicUsize, progress: &Stage) -> Result<Self, &'static str> {
|
||||
let mut nodes = vec![Aabb3::EMPTY; shared.tree.nodes.len()];
|
||||
// Exact projected leaf bounds avoid false near-eye errors from empty corners of a
|
||||
// rotated model-space box. Interior bounds are just unions of the child bounds.
|
||||
let mut front = vec![false; shared.mesh.triangles.len()];
|
||||
for (i, node) in shared.tree.nodes.iter().enumerate().rev() {
|
||||
if i.is_multiple_of(1024) {
|
||||
progress.check()?;
|
||||
}
|
||||
let mut b = Aabb3::EMPTY;
|
||||
if let Some((left, right)) = node.children {
|
||||
b = nodes.get(left).ok_or("invalid export tree")?.union(nodes.get(right).ok_or("invalid export tree")?);
|
||||
} else {
|
||||
for id in shared.tree.ids.get(node.start..node.end).ok_or("invalid export tree")? {
|
||||
let p = view.triangle(shared, placed, *id)?;
|
||||
b = b.union(&bounds(p));
|
||||
if let Some(f) = front.get_mut(*id) {
|
||||
// A positive projective scale preserves the projected winding.
|
||||
*f = super::cross(xy(p[1] - p[0]), xy(p[2] - p[0])) > 0.;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(slot) = nodes.get_mut(i) {
|
||||
*slot = b;
|
||||
}
|
||||
}
|
||||
let mut result = Vec::new();
|
||||
let mut add = |a: Vec3, b: Vec3| -> Result<(), &'static str> {
|
||||
let (a, b) = (view.project(apply_point(&placed.transform, a))?, view.project(apply_point(&placed.transform, b))?);
|
||||
if xy(a).dist(xy(b)) > eps {
|
||||
if !claim(segments, 1, MAX_SEGMENTS) {
|
||||
return Err(TOO_COMPLEX);
|
||||
}
|
||||
result.push((a, b));
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for (i, edge) in shared.mesh.edges.iter().enumerate() {
|
||||
if !placed.visibility.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
for w in edge.windows(2) {
|
||||
if let (Some(a), Some(b)) = (w.first(), w.get(1)) {
|
||||
add(*a, *b)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in &shared.edges {
|
||||
let mut positive = false;
|
||||
let mut negative = false;
|
||||
for id in shared.incident.get(edge.start..edge.end).ok_or("invalid export adjacency")? {
|
||||
if front.get(*id).copied().ok_or("invalid export adjacency")? {
|
||||
positive = true;
|
||||
} else {
|
||||
negative = true;
|
||||
}
|
||||
}
|
||||
if (positive && negative) || edge.end - edge.start == 1 {
|
||||
let a = shared.mesh.positions.get(edge.vertices[0] as usize).ok_or("invalid silhouette")?;
|
||||
let b = shared.mesh.positions.get(edge.vertices[1] as usize).ok_or("invalid silhouette")?;
|
||||
add(*a, *b)?;
|
||||
}
|
||||
}
|
||||
Ok(Self { nodes, segments: result })
|
||||
}
|
||||
}
|
||||
|
||||
/// A sorted union, updated as occluders are found. A node's line interval already covered by
|
||||
/// nearer triangles cannot change the result and is skipped before visiting its children.
|
||||
#[derive(Default)]
|
||||
struct Coverage {
|
||||
spans: Vec<(f64, f64)>,
|
||||
}
|
||||
impl Coverage {
|
||||
fn contains(&self, lo: f64, hi: f64) -> bool {
|
||||
self.spans.iter().any(|(a, b)| *a <= lo && *b >= hi)
|
||||
}
|
||||
fn add(&mut self, mut lo: f64, mut hi: f64) -> Result<(), &'static str> {
|
||||
let first = self.spans.partition_point(|(_, b)| *b + 1e-9 < lo);
|
||||
let mut end = first;
|
||||
while let Some((a, b)) = self.spans.get(end) {
|
||||
if *a > hi + 1e-9 {
|
||||
break;
|
||||
}
|
||||
lo = lo.min(*a);
|
||||
hi = hi.max(*b);
|
||||
end += 1;
|
||||
}
|
||||
if first == end && self.spans.len() >= MAX_LINES / 2 {
|
||||
return Err(TOO_COMPLEX);
|
||||
}
|
||||
self.spans.splice(first..end, [(lo, hi)]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn rectangle(a: Vec3, b: Vec3, box_: &Aabb3) -> Option<(f64, f64)> {
|
||||
let (mut lo, mut hi) = (0., 1.);
|
||||
for (v0, v1) in [
|
||||
(a.x - box_.min.x, b.x - box_.min.x),
|
||||
(box_.max.x - a.x, box_.max.x - b.x),
|
||||
(a.y - box_.min.y, b.y - box_.min.y),
|
||||
(box_.max.y - a.y, box_.max.y - b.y),
|
||||
] {
|
||||
if !clip(&mut lo, &mut hi, v0, v1) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((lo, hi))
|
||||
}
|
||||
|
||||
struct Budget<'a> {
|
||||
global: &'a AtomicUsize,
|
||||
progress: &'a Stage,
|
||||
local: usize,
|
||||
spent: usize,
|
||||
}
|
||||
impl<'a> Budget<'a> {
|
||||
fn new(global: &'a AtomicUsize, progress: &'a Stage) -> Self {
|
||||
Self { global, progress, local: 0, spent: 0 }
|
||||
}
|
||||
fn spend(&mut self) -> Result<(), &'static str> {
|
||||
if self.local == 0 {
|
||||
self.progress.check()?;
|
||||
let mut old = self.global.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let granted = old.min(4096);
|
||||
if granted == 0 {
|
||||
return Err(TOO_COMPLEX);
|
||||
}
|
||||
match self.global.compare_exchange_weak(old, old - granted, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => {
|
||||
self.local = granted;
|
||||
break;
|
||||
}
|
||||
Err(current) => old = current,
|
||||
}
|
||||
}
|
||||
}
|
||||
self.local -= 1;
|
||||
self.spent += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Query<'a, 'b> {
|
||||
shared: &'a [Shared],
|
||||
scene: &'a Scene,
|
||||
projected: &'a [Projected],
|
||||
instances: &'a Bvh,
|
||||
view: &'a View<'a>,
|
||||
depth_eps: f64,
|
||||
a: Vec3,
|
||||
b: Vec3,
|
||||
bounds: Aabb3,
|
||||
covered: Coverage,
|
||||
budget: &'b mut Budget<'a>,
|
||||
}
|
||||
impl Query<'_, '_> {
|
||||
fn relevant(&self, b: &Aabb3) -> bool {
|
||||
if b.max.z <= self.a.z.min(self.b.z) + self.depth_eps || !overlap(b, &self.bounds) {
|
||||
return false;
|
||||
}
|
||||
rectangle(self.a, self.b, b).is_some_and(|(lo, hi)| !self.covered.contains(lo, hi))
|
||||
}
|
||||
fn instance_node(&mut self, index: usize) -> Result<(), &'static str> {
|
||||
self.budget.spend()?;
|
||||
let node = self.instances.nodes.get(index).ok_or("invalid instance tree")?;
|
||||
if !self.relevant(&node.bounds) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some((left, right)) = node.children {
|
||||
let depth = |i: usize| self.instances.nodes.get(i).map_or(f64::NEG_INFINITY, |n| n.bounds.max.z);
|
||||
let (near, far) = if depth(left) >= depth(right) { (left, right) } else { (right, left) };
|
||||
self.instance_node(near)?;
|
||||
if !self.covered.contains(0., 1.) {
|
||||
self.instance_node(far)?;
|
||||
}
|
||||
} else {
|
||||
for id in self.instances.ids.get(node.start..node.end).ok_or("invalid instance tree")? {
|
||||
self.mesh_node(*id, 0)?;
|
||||
if self.covered.contains(0., 1.) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn mesh_node(&mut self, instance: usize, index: usize) -> Result<(), &'static str> {
|
||||
self.budget.spend()?;
|
||||
let placed = self.scene.placements.get(instance).ok_or("invalid instance")?;
|
||||
let shared = self.shared.get(placed.mesh).ok_or("invalid shared geometry")?;
|
||||
let projected = self.projected.get(instance).ok_or("invalid instance bounds")?;
|
||||
let b = projected.nodes.get(index).ok_or("invalid mesh tree")?;
|
||||
if !self.relevant(b) {
|
||||
return Ok(());
|
||||
}
|
||||
let node = shared.tree.nodes.get(index).ok_or("invalid mesh tree")?;
|
||||
if let Some((left, right)) = node.children {
|
||||
let depth = |i: usize| projected.nodes.get(i).map_or(f64::NEG_INFINITY, |b| b.max.z);
|
||||
let (near, far) = if depth(left) >= depth(right) { (left, right) } else { (right, left) };
|
||||
self.mesh_node(instance, near)?;
|
||||
if !self.covered.contains(0., 1.) {
|
||||
self.mesh_node(instance, far)?;
|
||||
}
|
||||
} else {
|
||||
for id in shared.tree.ids.get(node.start..node.end).ok_or("invalid mesh tree")? {
|
||||
self.budget.spend()?;
|
||||
let points = self.view.triangle(shared, placed, *id)?;
|
||||
if let Some((lo, hi)) = occluded(self.a, self.b, points, self.depth_eps) {
|
||||
self.covered.add(lo, hi)?;
|
||||
if self.covered.contains(0., 1.) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn drawing(scene: &Scene, cam: &Camera) -> Result<Drawing, &'static str> {
|
||||
let start = Instant::now();
|
||||
let scale = scene
|
||||
.placements
|
||||
.iter()
|
||||
.filter_map(|p| scene.meshes.get(p.mesh).map(|m| transformed_bounds(m, &p.transform)))
|
||||
.fold(Aabb3::EMPTY, |b, p| b.union(&p))
|
||||
.diagonal()
|
||||
.max(1.);
|
||||
if !scale.is_finite() {
|
||||
return Err("model contains invalid coordinates");
|
||||
}
|
||||
let eps = scale * 1e-8;
|
||||
// Indexed input can have many triangles but very few positions. Bound adjacency/sort
|
||||
// scratch space as well as the retained mesh bytes before starting parallel builders.
|
||||
let unique_triangles = scene.meshes.iter().try_fold(0usize, |n, m| n.checked_add(m.triangles.len()));
|
||||
if unique_triangles.is_none_or(|n| n > MAX_SHARED_TRIANGLES) {
|
||||
return Err("view exceeds export memory budget; hide bodies or increase tolerance");
|
||||
}
|
||||
let progress = Stage::new("dxf_index", scene.meshes.len());
|
||||
let shared = parallel(&scene.meshes, |m| {
|
||||
progress.check()?;
|
||||
let shared = Shared::new(m, eps, &progress)?;
|
||||
progress.advance(1)?;
|
||||
Ok(shared)
|
||||
})?;
|
||||
let count = scene.placements.iter().try_fold(0usize, |n, p| n.checked_add(shared.get(p.mesh)?.tree.nodes.len()));
|
||||
if count.is_none_or(|n| n > MAX_VIEW_NODES) {
|
||||
return Err("view exceeds export memory budget; hide bodies or increase tolerance");
|
||||
}
|
||||
let view = View::new(cam);
|
||||
let count = AtomicUsize::new(0);
|
||||
let progress = Stage::new("dxf_projection", scene.placements.len());
|
||||
let projected = parallel(&scene.placements, |p| {
|
||||
progress.check()?;
|
||||
let projected = Projected::new(shared.get(p.mesh).ok_or("missing shared geometry")?, p, &view, eps, &count, &progress)?;
|
||||
progress.advance(1)?;
|
||||
Ok(projected)
|
||||
})?;
|
||||
let boxes = projected.iter().map(|p| p.nodes.first().copied().unwrap_or(Aabb3::EMPTY)).collect::<Vec<_>>();
|
||||
let mut instances = Bvh::new(&boxes, true);
|
||||
// The view is fixed for the entire drawing. Sort the at-most-eight members of each
|
||||
// instance leaf once, rather than allocating/sorting on every line query.
|
||||
for node in instances.nodes.iter().filter(|n| n.children.is_none()) {
|
||||
if let Some(ids) = instances.ids.get_mut(node.start..node.end) {
|
||||
ids.sort_unstable_by(|a, b| {
|
||||
boxes
|
||||
.get(*b)
|
||||
.map_or(f64::NEG_INFINITY, |b| b.max.z)
|
||||
.total_cmp(&boxes.get(*a).map_or(f64::NEG_INFINITY, |b| b.max.z))
|
||||
.then_with(|| a.cmp(b))
|
||||
});
|
||||
}
|
||||
}
|
||||
let segment_count = count.load(Ordering::Relaxed);
|
||||
let projection_ms = start.elapsed().as_secs_f64() * 1000.;
|
||||
let start = Instant::now();
|
||||
let depth_eps = if cam.fov > 0. { 1e-8 / cam.distance } else { eps };
|
||||
let global = AtomicUsize::new(WORK);
|
||||
let output = AtomicUsize::new(0);
|
||||
let chunks = projected.iter().flat_map(|p| p.segments.chunks(128)).collect::<Vec<_>>();
|
||||
let progress = Stage::new("dxf_occlusion", segment_count);
|
||||
let results = parallel(&chunks, |chunk| {
|
||||
progress.check()?;
|
||||
let mut budget = Budget::new(&global, &progress);
|
||||
let mut lines = Vec::new();
|
||||
for &(a, b) in *chunk {
|
||||
let mut query = Query {
|
||||
shared: &shared,
|
||||
scene,
|
||||
projected: &projected,
|
||||
instances: &instances,
|
||||
view: &view,
|
||||
depth_eps,
|
||||
a,
|
||||
b,
|
||||
bounds: bounds([a, b]),
|
||||
covered: Coverage::default(),
|
||||
budget: &mut budget,
|
||||
};
|
||||
query.instance_node(0)?;
|
||||
let mut cursor = 0.;
|
||||
let mut emit = |lo: f64, hi: f64, hidden: bool| -> Result<(), &'static str> {
|
||||
let (p, q) = (xy(a + (b - a) * lo), xy(a + (b - a) * hi));
|
||||
if p.dist(q) > eps {
|
||||
if !claim(&output, 1, MAX_LINES) {
|
||||
return Err(TOO_COMPLEX);
|
||||
}
|
||||
lines.push(Line { a: p, b: q, hidden });
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for (lo, hi) in query.covered.spans {
|
||||
emit(cursor, lo, false)?;
|
||||
emit(lo, hi, true)?;
|
||||
cursor = hi;
|
||||
}
|
||||
emit(cursor, 1., false)?;
|
||||
}
|
||||
progress.advance(chunk.len())?;
|
||||
Ok((lines, budget.spent))
|
||||
})?;
|
||||
let work = results.iter().map(|(_, n)| *n).sum();
|
||||
let lines = merge_lines(results.into_iter().flat_map(|(lines, _)| lines).collect(), eps);
|
||||
progress.check()?;
|
||||
Ok(Drawing { lines, work, segments: segment_count, projection_ms, occlusion_ms: start.elapsed().as_secs_f64() * 1000. })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn coverage_handles_disjoint_spans_and_early_complete_coverage() {
|
||||
let mut c = Coverage::default();
|
||||
c.add(0.4, 0.6).unwrap();
|
||||
c.add(0., 0.2).unwrap();
|
||||
assert!(c.contains(0.05, 0.15));
|
||||
assert!(!c.contains(0., 1.));
|
||||
c.add(0.2, 0.9).unwrap();
|
||||
c.add(0.8, 1.).unwrap();
|
||||
assert_eq!(c.spans, vec![(0., 1.)]);
|
||||
}
|
||||
|
||||
fn box_mesh() -> Mesh {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.positions = vec![
|
||||
Vec3::new(0., 0., 0.),
|
||||
Vec3::new(8., 0., 0.),
|
||||
Vec3::new(8., 6., 0.),
|
||||
Vec3::new(0., 6., 0.),
|
||||
Vec3::new(0., 0., 3.),
|
||||
Vec3::new(8., 0., 3.),
|
||||
Vec3::new(8., 6., 3.),
|
||||
Vec3::new(0., 6., 3.),
|
||||
];
|
||||
mesh.triangles =
|
||||
vec![[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]];
|
||||
mesh.edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
|
||||
.into_iter()
|
||||
.map(|(a, b)| vec![mesh.positions[a], mesh.positions[b]])
|
||||
.collect();
|
||||
mesh
|
||||
}
|
||||
|
||||
fn canonical(lines: Vec<Line>) -> Vec<([i64; 2], [i64; 2], bool)> {
|
||||
let k = |p: solidcraft_geom::Vec2| [(p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64];
|
||||
let mut result = lines
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
let (a, b) = (k(l.a), k(l.b));
|
||||
if a < b { (a, b, l.hidden) } else { (b, a, l.hidden) }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_unstable();
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_rotated_instances_match_flat_occlusion_in_orthographic_and_perspective_views() {
|
||||
use std::sync::Arc;
|
||||
let mesh = box_mesh();
|
||||
let mut placements = Vec::new();
|
||||
let mut flat = Vec::new();
|
||||
for (offset, angle) in [(Vec3::ZERO, 0.), (Vec3::new(3., 2., 4.), 0.3), (Vec3::new(2., 1., -4.), -0.6)] {
|
||||
let transform = solidcraft_doc::rigid(offset, Vec3::ZERO, Vec3::Z, angle);
|
||||
placements.push(Placement { mesh: 0, transform, visibility: vec![true; mesh.edges.len()] });
|
||||
let mut placed = mesh.clone();
|
||||
placed.positions = placed.positions.iter().map(|p| apply_point(&transform, *p)).collect();
|
||||
placed.edges = placed.edges.iter().map(|e| e.iter().map(|p| apply_point(&transform, *p)).collect()).collect();
|
||||
flat.push(placed);
|
||||
}
|
||||
let scene = Scene { meshes: vec![Arc::new(mesh)], placements, triangles: 36, bytes: 0 };
|
||||
for (yaw, pitch, roll) in [(0., std::f64::consts::FRAC_PI_2, 0.), (0.7, 0.4, 0.2), (0., 0., 0.)] {
|
||||
for fov in [0., 0.6] {
|
||||
let cam = Camera { target: Vec3::new(4., 3., 1.), yaw, pitch, roll, fov, distance: 100., ..Default::default() };
|
||||
let expected = super::super::reference_drawing(&flat, &cam).unwrap();
|
||||
let actual = drawing(&scene, &cam).unwrap();
|
||||
assert_eq!(canonical(actual.lines), canonical(expected), "yaw={yaw}, pitch={pitch}, fov={fov}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_surface_over_one_million_triangles_keeps_one_mesh_and_exports_all_placements() {
|
||||
use solidcraft_render::StandardView;
|
||||
use std::sync::Arc;
|
||||
let mut mesh = Mesh::default();
|
||||
for y in 0..=100 {
|
||||
for x in 0..=100 {
|
||||
mesh.positions.push(Vec3::new(x as f64 / 10., y as f64 / 10., 0.));
|
||||
}
|
||||
}
|
||||
for y in 0..100u32 {
|
||||
for x in 0..100u32 {
|
||||
let a = y * 101 + x;
|
||||
mesh.triangles.extend([[a, a + 1, a + 102], [a, a + 102, a + 101]]);
|
||||
}
|
||||
}
|
||||
mesh.edges = vec![
|
||||
vec![Vec3::ZERO, Vec3::new(10., 0., 0.)],
|
||||
vec![Vec3::new(10., 0., 0.), Vec3::new(10., 10., 0.)],
|
||||
vec![Vec3::new(10., 10., 0.), Vec3::new(0., 10., 0.)],
|
||||
vec![Vec3::new(0., 10., 0.), Vec3::ZERO],
|
||||
];
|
||||
let placements = (0..60)
|
||||
.map(|i| Placement {
|
||||
mesh: 0,
|
||||
transform: solidcraft_doc::rigid(Vec3::new((i % 10) as f64 * 20., (i / 10) as f64 * 20., 0.), Vec3::ZERO, Vec3::Z, 0.),
|
||||
visibility: vec![true; 4],
|
||||
})
|
||||
.collect();
|
||||
let scene = Scene { meshes: vec![Arc::new(mesh)], placements, triangles: 1_200_000, bytes: 0 };
|
||||
let mut cam = Camera { target: Vec3::new(95., 55., 0.), fov: 0., distance: 300., ..Default::default() };
|
||||
cam.set_view(StandardView::Top);
|
||||
let result = drawing(&scene, &cam).unwrap();
|
||||
assert_eq!(scene.meshes.len(), 1);
|
||||
assert_eq!(result.lines.len(), 240);
|
||||
assert!(result.lines.iter().all(|l| !l.hidden));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,49 @@ fn export(s: &mut Session, c: Camera, hidden: &str) -> Value {
|
||||
run(s, CMD, json!({"camera":c,"hidden":hidden}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_cancellation_at_each_stage_preserves_the_destination_and_document() {
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
struct CancelAt {
|
||||
stage: &'static str,
|
||||
cancelled: AtomicBool,
|
||||
reached: AtomicBool,
|
||||
}
|
||||
impl solidcraft_kernel::watch::Watch for CancelAt {
|
||||
fn cancelled(&self) -> bool {
|
||||
self.cancelled.load(Ordering::Relaxed)
|
||||
}
|
||||
fn progress(&self, stage: &'static str, done: usize, _total: usize) {
|
||||
if stage == self.stage && (done > 0 || stage == "dxf_write") {
|
||||
self.reached.store(true, Ordering::Relaxed);
|
||||
self.cancelled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-dxf-cancel-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("drawing.dxf");
|
||||
let mut s = sample();
|
||||
let doc = s.doc.clone();
|
||||
let undo = s.undo.len();
|
||||
for stage in ["dxf_meshes", "dxf_index", "dxf_projection", "dxf_occlusion", "dxf_write"] {
|
||||
std::fs::write(&path, b"previous drawing").unwrap();
|
||||
let watch = Arc::new(CancelAt { stage, cancelled: AtomicBool::new(false), reached: AtomicBool::new(false) });
|
||||
let result = solidcraft_kernel::watch::watching(watch.clone(), || {
|
||||
s.execute(CMD, &json!({"camera":camera(StandardView::Top),"path":path.to_string_lossy()}))
|
||||
});
|
||||
assert!(result.unwrap_err().to_string().contains("cancelled"), "{stage}");
|
||||
assert!(watch.reached.load(Ordering::Relaxed), "{stage}");
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"previous drawing", "{stage}");
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_blind_back_hole_is_omitted_or_dashed_and_front_hole_is_visible() {
|
||||
let mut s = sample();
|
||||
@@ -100,3 +143,32 @@ fn view_dxf_rejects_invalid_options_and_near_eye_geometry_without_panicking() {
|
||||
c.fov = 4.;
|
||||
assert!(s.execute(CMD, &json!({"camera":c})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_keeps_every_occurrence_while_sharing_geometry() {
|
||||
let mut s = Session::default();
|
||||
let component = run(&mut s, "component.create", json!({"name":"Repeated part"}))["component"].as_u64().unwrap();
|
||||
run(&mut s, "solid.box", json!({"length":8,"width":6,"height":3}));
|
||||
for x in [15., 30.] {
|
||||
run(&mut s, "occurrence.copy", json!({"component":component,"translate":[x,0,0]}));
|
||||
}
|
||||
run(&mut s, "component.activate", json!({"component":0}));
|
||||
let doc = s.doc.clone();
|
||||
let result = export(&mut s, camera(StandardView::Top), "dashed");
|
||||
assert_eq!(result["stats"]["bodies"], 3);
|
||||
assert_eq!(result["stats"]["shared_meshes"], 1);
|
||||
assert_eq!(result["stats"]["worker_limit"], meshes::WORKERS);
|
||||
assert!(result["hidden"].as_u64().unwrap() > 0);
|
||||
let lines = solidcraft_io::read_dxf(result["dxf"].as_str().unwrap()).unwrap();
|
||||
let mut centers = std::collections::BTreeSet::new();
|
||||
for line in lines {
|
||||
if let solidcraft_io::Geom2::Line(a, b) = line {
|
||||
// Each translated copy has a visible top perimeter, including the offscreen one.
|
||||
if (a.y - b.y).abs() > 5.9 {
|
||||
centers.insert(((a.x + 20.) / 15.).floor() as i64);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(centers.len() >= 3, "{centers:?}");
|
||||
assert!(std::sync::Arc::ptr_eq(&doc, &s.doc));
|
||||
}
|
||||
Reference in new issue
Block a user