use super::CancelToken;
use crate::csg_tree::CsgNode;
use crate::linalg::Vec3;
use crate::manifold::Manifold;
use crate::types::{Error, OpType};
use std::time::{Duration, Instant};
fn slow_pair() -> (Manifold, Manifold) {
let a = Manifold::sphere(1.0, 256);
let b = Manifold::sphere(1.0, 256).translate(Vec3::new(0.5, 0.0, 0.0));
(a, b)
}
#[test]
fn token_starts_uncancelled_and_cancel_is_observable_through_clones() {
let token = CancelToken::new();
assert!(!token.is_cancelled());
let clone = token.clone();
assert!(!clone.is_cancelled());
token.cancel();
assert!(token.is_cancelled());
assert!(clone.is_cancelled(), "clones must share the flag");
token.cancel();
assert!(token.is_cancelled());
}
#[test]
fn no_token_path_is_unchanged() {
let (a, b) = (
Manifold::cube(Vec3::splat(1.0), true),
Manifold::sphere(0.6, 32),
);
let plain = a.boolean(&b, OpType::Subtract);
let with_none = a.boolean_with_token(&b, OpType::Subtract, None);
assert_eq!(plain.status(), Error::NoError);
assert_eq!(with_none.status(), Error::NoError);
let (m0, m1) = (plain.get_mesh_gl(-1), with_none.get_mesh_gl(-1));
assert_eq!(m0.vert_properties, m1.vert_properties);
assert_eq!(m0.tri_verts, m1.tri_verts);
}
#[test]
fn pre_cancelled_token_returns_cancelled_promptly() {
let token = CancelToken::new();
token.cancel();
let (a, b) = slow_pair();
let uncancelled = {
let start = Instant::now();
let result = a.boolean_with_token(&b, OpType::Add, None);
assert_eq!(result.status(), Error::NoError);
start.elapsed()
};
let start = Instant::now();
let result = a.boolean_with_token(&b, OpType::Add, Some(&token));
let cancelled_elapsed = start.elapsed();
assert_eq!(result.status(), Error::Cancelled);
assert!(result.is_empty(), "a cancelled result must be empty");
assert!(
cancelled_elapsed * 4 < uncancelled,
"pre-cancelled boolean took {cancelled_elapsed:?}, uncancelled takes {uncancelled:?}"
);
}
#[test]
fn pre_cancelled_token_short_circuits_csg_tree_evaluation() {
let token = CancelToken::new();
token.cancel();
let leaves: Vec<CsgNode> = (0..8)
.map(|i| {
CsgNode::leaf(
Manifold::sphere(1.0, 64)
.translate(Vec3::new(i as f64 * 0.3, 0.0, 0.0))
.as_impl()
.clone(),
)
})
.collect();
let tree = CsgNode::op_n(OpType::Add, leaves);
let result = tree.evaluate_with_token(Some(&token));
assert_eq!(result.status, Error::Cancelled);
assert_eq!(result.num_tri(), 0);
}
#[test]
fn empty_input_fast_paths_still_honour_a_pre_cancelled_token() {
let token = CancelToken::new();
token.cancel();
let empty = Manifold::empty();
let cube = Manifold::cube(Vec3::splat(1.0), true);
assert_eq!(
empty.boolean_with_token(&cube, OpType::Add, Some(&token)).status(),
Error::Cancelled
);
assert_eq!(
cube.boolean_with_token(&empty, OpType::Add, Some(&token)).status(),
Error::Cancelled
);
}
#[test]
fn cancel_from_another_thread_interrupts_a_boolean_in_flight() {
let (a, b) = slow_pair();
let start = Instant::now();
let baseline = a.boolean_with_token(&b, OpType::Add, None);
let uncancelled = start.elapsed();
assert_eq!(baseline.status(), Error::NoError);
assert!(
uncancelled > Duration::from_millis(20),
"test input is too fast ({uncancelled:?}) to be a meaningful cancel target"
);
let token = CancelToken::new();
let worker_token = token.clone();
let (started_tx, started_rx) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
started_tx.send(()).expect("main thread went away");
let start = Instant::now();
let status = a
.boolean_with_token(&b, OpType::Add, Some(&worker_token))
.status();
(status, start.elapsed())
});
started_rx.recv().expect("worker never started");
std::thread::sleep((uncancelled / 16).max(Duration::from_millis(1)));
token.cancel();
let (status, cancelled_elapsed) = worker.join().expect("worker panicked");
assert_eq!(status, Error::Cancelled);
assert!(
cancelled_elapsed * 2 < uncancelled,
"cancelled boolean took {cancelled_elapsed:?}, which is not well under \
the uncancelled {uncancelled:?}"
);
}
#[test]
fn a_used_then_cancelled_token_does_not_affect_later_ops_with_a_fresh_token() {
let a = Manifold::cube(Vec3::splat(1.0), true);
let b = Manifold::sphere(0.6, 32);
let used = CancelToken::new();
let first = a.boolean_with_token(&b, OpType::Subtract, Some(&used));
assert_eq!(first.status(), Error::NoError);
used.cancel();
assert_eq!(first.status(), Error::NoError);
let fresh = CancelToken::new();
let second = a.boolean_with_token(&b, OpType::Subtract, Some(&fresh));
assert_eq!(second.status(), Error::NoError);
assert!(!fresh.is_cancelled());
let untokened = a.boolean_with_token(&b, OpType::Subtract, None);
let (m_first, m_second, m_none) = (
first.get_mesh_gl(-1),
second.get_mesh_gl(-1),
untokened.get_mesh_gl(-1),
);
assert_eq!(m_first.tri_verts, m_second.tri_verts);
assert_eq!(m_second.tri_verts, m_none.tri_verts);
assert_eq!(m_second.vert_properties, m_none.vert_properties);
assert_eq!(
a.boolean_with_token(&b, OpType::Subtract, Some(&used)).status(),
Error::Cancelled
);
}
#[test]
fn a_live_token_produces_identical_geometry_above_the_parallel_thresholds() {
let a = Manifold::sphere(1.0, 128);
let b = Manifold::sphere(1.0, 128).translate(Vec3::new(0.5, 0.0, 0.0));
assert!(
a.as_impl().halfedge.len() > 10_000,
"input must cross the intersect12 parallel threshold, got {} halfedges",
a.as_impl().halfedge.len()
);
let live = CancelToken::new();
let tokened = a.boolean_with_token(&b, OpType::Add, Some(&live));
let untokened = a.boolean_with_token(&b, OpType::Add, None);
assert_eq!(tokened.status(), Error::NoError);
assert_eq!(untokened.status(), Error::NoError);
assert!(!live.is_cancelled());
let (mt, mu) = (tokened.get_mesh_gl(-1), untokened.get_mesh_gl(-1));
assert!(mt.tri_verts.len() > 30_000, "result should be substantial");
assert_eq!(mt.num_prop, mu.num_prop);
assert_eq!(mt.vert_properties, mu.vert_properties);
assert_eq!(mt.tri_verts, mu.tri_verts);
assert_eq!(mt.run_index, mu.run_index);
assert_eq!(mt.face_id, mu.face_id);
for op in [OpType::Subtract, OpType::Intersect] {
let with = a.boolean_with_token(&b, op, Some(&live));
let without = a.boolean_with_token(&b, op, None);
assert_eq!(with.status(), Error::NoError, "{op:?}");
assert_eq!(
with.get_mesh_gl(-1).vert_properties,
without.get_mesh_gl(-1).vert_properties,
"{op:?} vertex positions diverged between the tokened and \
untokened parallel paths"
);
assert_eq!(
with.get_mesh_gl(-1).tri_verts,
without.get_mesh_gl(-1).tri_verts,
"{op:?} triangle order diverged between the tokened and \
untokened parallel paths"
);
}
}
#[test]
fn cancelled_status_survives_the_csg_tree_root() {
let token = CancelToken::new();
token.cancel();
let a = Manifold::cube(Vec3::splat(1.0), true);
let b = Manifold::cube(Vec3::splat(1.0), true).translate(Vec3::new(0.5, 0.0, 0.0));
let tree = CsgNode::op(
OpType::Subtract,
CsgNode::leaf(a.as_impl().clone()),
CsgNode::leaf(b.as_impl().clone()),
);
assert_eq!(tree.evaluate_with_token(Some(&token)).status, Error::Cancelled);
assert_eq!(tree.evaluate().status, Error::NoError);
}