Show a bounded native-cache preview before authoritative reconstruction (#463)
Refs #303. file.save / file.save_as can store a warm native geometry cache (cache: true, off by default) keyed to the document hash, app version and adapter revision. Opening shows a bounded preview from it until the rebuilt session replaces it. Any cache failure counts as a miss.
This commit is contained in:
1 parent
1abd989a67
commit
913583bfee
20 files changed
+943
-84
No files matched your search
@@ -40,7 +40,7 @@ pub struct Body {
|
||||
pub(crate) history: Option<std::sync::Arc<crate::history::History>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FaceSource {
|
||||
ExtrudeSide {
|
||||
segment: solidcraft_geom::Seg2,
|
||||
@@ -110,7 +110,7 @@ impl FaceSource {
|
||||
}
|
||||
|
||||
/// How a body looks beyond its colour, for export: its opacity and its faces' own colours.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Paint {
|
||||
/// 1 = opaque.
|
||||
pub opacity: f32,
|
||||
@@ -118,7 +118,7 @@ pub struct Paint {
|
||||
}
|
||||
|
||||
/// A face's own colour: the B-rep face index (`face_iter` order), sRGB as 0..1, opacity.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FacePaint {
|
||||
pub face: usize,
|
||||
pub color: [f32; 3],
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::body::{Body, FaceSource, Solid};
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// Where a result face came from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FaceOrigin {
|
||||
/// A face of input `operand` (its index in the operand's face order).
|
||||
Face { operand: usize, face: usize },
|
||||
@@ -28,7 +28,7 @@ pub enum FaceOrigin {
|
||||
}
|
||||
|
||||
/// How a result face relates to its origins.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Change {
|
||||
/// The same face as its origin (same region).
|
||||
Kept,
|
||||
@@ -42,7 +42,7 @@ pub enum Change {
|
||||
Generated,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FaceProv {
|
||||
pub origins: Vec<FaceOrigin>,
|
||||
pub change: Change,
|
||||
@@ -51,7 +51,7 @@ pub struct FaceProv {
|
||||
/// One side of a meeting: the origins of the face on that side of the edge. One origin, or
|
||||
/// several where that face was made from several (a merged face) and the edge lies along it
|
||||
/// as a whole. Never empty; sorted, without repeats, so equal sides compare equal.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Side(Vec<FaceOrigin>);
|
||||
|
||||
impl Side {
|
||||
@@ -99,7 +99,7 @@ impl From<FaceOrigin> for Side {
|
||||
}
|
||||
|
||||
/// Where a result edge came from.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum EdgeOrigin {
|
||||
/// (Part of) edge `edge` of input `operand`.
|
||||
Edge { operand: usize, edge: usize },
|
||||
@@ -110,14 +110,14 @@ pub enum EdgeOrigin {
|
||||
|
||||
/// A result edge: the result faces it separates and what it came from (empty when it is new
|
||||
/// and lies between faces new to the operation).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EdgeProv {
|
||||
pub faces: Vec<usize>,
|
||||
pub origins: Vec<EdgeOrigin>,
|
||||
}
|
||||
|
||||
/// History of an operation's result: one entry per face and per edge, in the result's orders.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct History {
|
||||
/// How many operands the operation had.
|
||||
pub operands: usize,
|
||||
|
||||
@@ -85,7 +85,7 @@ pub use lip::raise_band;
|
||||
pub use measure::{BodyMeasure, measure};
|
||||
pub use mesh_convert::convert_prismatic_mesh;
|
||||
pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
||||
pub use native::{decode_native_body, encode_native_body};
|
||||
pub use native::{decode_native_body, encode_native_body, native_preview_mesh};
|
||||
pub use offset::with_same_surface;
|
||||
pub use ops::{BoolOp, boolean, can_join_touching_faces, split_by_plane, transform, transform_matrix, union_apart};
|
||||
pub use polybool::{faceted_boolean, planar_boolean};
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::sync::Arc;
|
||||
mod adapter;
|
||||
mod binary;
|
||||
mod metadata;
|
||||
mod preview;
|
||||
pub use preview::native_preview_mesh;
|
||||
|
||||
// 01/02 were development-only prototypes; independent metadata records start at 03.
|
||||
const MAGIC: &[u8; 8] = b"SCBREP03";
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Bounded provisional tessellation. Fixed sampling avoids untrusted adaptive
|
||||
//! subdivision before output budgets can be checked; editing uses the normal mesh.
|
||||
use super::{Body, Result, invalid};
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
|
||||
use truck_modeling as mt;
|
||||
|
||||
fn curve_safe(c: &mt::Curve, depth: usize) -> bool {
|
||||
if depth > 4 {
|
||||
return false;
|
||||
}
|
||||
match c {
|
||||
mt::Curve::Line(_) => true,
|
||||
mt::Curve::BSplineCurve(c) => c.control_points().len() <= 256,
|
||||
mt::Curve::NurbsCurve(c) => c.control_points().len() <= 256,
|
||||
mt::Curve::IntersectionCurve(c) => {
|
||||
let leader = match c.leader() {
|
||||
mt::Leader::Polyline(p) => p.len() <= 256,
|
||||
mt::Leader::BSpline(p) => p.control_points().len() <= 256,
|
||||
};
|
||||
leader && surface_safe(c.surface0(), depth + 1) && surface_safe(c.surface1(), depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn surface_safe(s: &mt::Surface, depth: usize) -> bool {
|
||||
if depth > 4 {
|
||||
return false;
|
||||
}
|
||||
match s {
|
||||
mt::Surface::Plane(_) => true,
|
||||
mt::Surface::BSplineSurface(s) => s.control_points().iter().map(Vec::len).sum::<usize>() <= 256,
|
||||
mt::Surface::NurbsSurface(s) => s.control_points().iter().map(Vec::len).sum::<usize>() <= 256,
|
||||
mt::Surface::RevolutedCurve(s) => {
|
||||
!matches!(s.entity().entity_curve(), mt::Curve::IntersectionCurve(_)) && curve_safe(s.entity().entity_curve(), depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate read-only display. Boundary and grid sampling are fixed before
|
||||
/// triangulation, and the complete body's upper bound is checked before projection.
|
||||
pub fn native_preview_mesh(body: &Body, detail: usize, remaining: &mut usize) -> Result<Mesh> {
|
||||
crate::guard("bounded native preview", || {
|
||||
if body.mesh.is_some() {
|
||||
return Err(invalid("native preview requires B-rep"));
|
||||
}
|
||||
let detail = detail.clamp(1, 8);
|
||||
|
||||
let mut prepared = Vec::new();
|
||||
let mut estimate = 0usize;
|
||||
for face in body.solid.face_iter() {
|
||||
let surface = face.surface();
|
||||
let grid = if matches!(surface, mt::Surface::Plane(_)) { 1 } else { detail * 2 };
|
||||
if !surface_safe(&surface, 0) {
|
||||
return Err(invalid("preview surface complexity"));
|
||||
}
|
||||
let mut loops = Vec::new();
|
||||
let mut count = 0usize;
|
||||
for wire in face.absolute_boundaries() {
|
||||
let mut points = Vec::new();
|
||||
for edge in wire {
|
||||
let curve = edge.oriented_curve();
|
||||
if !curve_safe(&curve, 0) {
|
||||
return Err(invalid("preview curve complexity"));
|
||||
}
|
||||
let n = if matches!(curve, mt::Curve::Line(_)) { 1 } else { detail * 4 };
|
||||
count = count.saturating_add(n);
|
||||
if count > 4096 {
|
||||
return Err(invalid("preview boundary budget"));
|
||||
}
|
||||
let (a, b) = curve.range_tuple();
|
||||
for i in 0..n {
|
||||
let t = a * (1. - i as f64 / n as f64) + b * (i as f64 / n as f64);
|
||||
let point = match &curve {
|
||||
mt::Curve::IntersectionCurve(c) => c.leader().subs(t),
|
||||
_ => curve.subs(t),
|
||||
};
|
||||
if !point.x.is_finite() || !point.y.is_finite() || !point.z.is_finite() {
|
||||
return Err(invalid("preview sample"));
|
||||
}
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
let first = points.first().copied().ok_or_else(|| invalid("empty preview wire"))?;
|
||||
points.push(first);
|
||||
count = count.saturating_add(1);
|
||||
if count > 4096 {
|
||||
return Err(invalid("preview boundary budget"));
|
||||
}
|
||||
loops.push(points);
|
||||
}
|
||||
// Projection duplicates at most each input point, seams add eight
|
||||
// fixed polylines, and a planar triangulation has fewer than 2V faces.
|
||||
estimate = estimate.saturating_add(2 * (2 * count + 8 * (grid + 1) + (grid + 1).pow(2)) + 16);
|
||||
if estimate > *remaining {
|
||||
return Err(invalid("preview allocation budget"));
|
||||
}
|
||||
prepared.push((surface, face.orientation(), loops, grid));
|
||||
}
|
||||
*remaining -= estimate;
|
||||
let mut out = Mesh::default();
|
||||
for (surface, forward, loops, grid) in prepared {
|
||||
let mesh = truck_meshalgo::tessellation::preview_face(&surface, &loops, grid).ok_or_else(|| invalid("preview projection"))?;
|
||||
let mut count = 0;
|
||||
for triangle in mesh.faces().triangle_iter() {
|
||||
if out.triangles.len() >= estimate {
|
||||
return Err(invalid("preview output budget"));
|
||||
}
|
||||
let mut indices = [0u32; 3];
|
||||
for (slot, index) in indices.iter_mut().zip(triangle) {
|
||||
let p = mesh.positions().get(index.pos).ok_or_else(|| invalid("preview vertex"))?;
|
||||
if !p.x.is_finite() || !p.y.is_finite() || !p.z.is_finite() {
|
||||
return Err(invalid("preview vertex"));
|
||||
}
|
||||
*slot = u32::try_from(out.positions.len()).map_err(invalid)?;
|
||||
out.positions.push(Vec3::new(p.x, p.y, p.z));
|
||||
}
|
||||
if !forward {
|
||||
indices.swap(1, 2);
|
||||
}
|
||||
out.triangles.push(indices);
|
||||
count += 1;
|
||||
}
|
||||
if count == 0 {
|
||||
return Err(invalid("missing preview face"));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn native_preview_is_finite_and_budgeted_before_meshing() {
|
||||
for body in [
|
||||
crate::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap(),
|
||||
crate::sphere(Vec3::ZERO, 10.).unwrap(),
|
||||
crate::torus(Vec3::ZERO, 20., 4.).unwrap(),
|
||||
] {
|
||||
let body = crate::decode_native_body(&crate::encode_native_body(&body).unwrap()).unwrap();
|
||||
assert!(native_preview_mesh(&body, 8, &mut 0).is_err());
|
||||
let mut budget = 100_000;
|
||||
let mesh = native_preview_mesh(&body, 8, &mut budget).unwrap();
|
||||
assert!(!mesh.triangles.is_empty());
|
||||
assert!(mesh.triangles.len() <= 100_000 - budget);
|
||||
assert!(mesh.positions.iter().all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user