mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 777f960999 | |||
| d4f923a3e6 | |||
| bbb98928fb | |||
| 8e2b0cd60d | |||
| afd81380ab | |||
| 3a5195b0a0 | |||
| ee6b85605a | |||
| aa1fe28ddf | |||
| 5b0eb08473 | |||
| 9df8837d43 | |||
| 409ab1ed45 | |||
| 5e03fe2f4b | |||
| 37a586dd61 | |||
| a79f40a711 | |||
| b5728f284b | |||
| 5965439a15 | |||
| 586a358ed4 | |||
| 7770f8982d | |||
| 72ad6383ef | |||
| 0537623a75 | |||
| 65d288227e | |||
| fedb3bcdb1 | |||
| 097b8b7550 | |||
| af7ca3a8a2 | |||
| 27ddb725c7 | |||
| a06bdf5cfc | |||
| 33fdcdb3a0 | |||
| c07ab5f8f9 | |||
| fea32f8aac | |||
| 4927214976 | |||
| b621819c0c | |||
| ec552a5300 | |||
| e0cc740cc3 | |||
| 5de48e916d | |||
| 80af7bbe3e | |||
| 191e983423 | |||
| 2b9445b049 | |||
| 7ddf48ca16 | |||
| d2f6848377 | |||
| 0b6d6fecd8 | |||
| fa691707c2 | |||
| b9a55c74e2 | |||
| 2bb9c81069 | |||
| cedb51b8ea | |||
| 66d13059dd | |||
| 800ea40c0d | |||
| 20442eecd3 | |||
| 22d1cd7032 | |||
| 6a0dc18655 | |||
| e006dbf1af | |||
| 8241a750fa | |||
| 41779cf7bb | |||
| b01b9689c8 | |||
| 4426da6f5d | |||
| 5ead82e627 | |||
| c29b0d2f13 | |||
| 480fd8d517 | |||
| 4c09fa5355 |
@@ -349,20 +349,16 @@ impl Ahead {
|
||||
/// [`Ahead::evaluate`], setting `step` to the number of the command running (from 1), so a
|
||||
/// front end can say how far a batch has got.
|
||||
pub fn evaluate_counting(self, commands: &[(String, Value)], step: &std::sync::atomic::AtomicUsize) -> Result<Evaluated> {
|
||||
let stop = self.0.stop.clone();
|
||||
stop.within(|| {
|
||||
let (after, _) = self.0.preview_in_place(commands, Some(step))?;
|
||||
after.world_state()?.warm_display();
|
||||
Ok(Evaluated(after.model))
|
||||
})
|
||||
let (after, _) = self.0.preview_in_place(commands, Some(step))?;
|
||||
after.world_state()?.warm_display();
|
||||
Ok(Evaluated(after.model))
|
||||
}
|
||||
|
||||
/// Run `commands` one after another, each reply or error in turn (what they evaluate stays
|
||||
/// in the shared evaluation cache): the checks a front end makes before a command, such as
|
||||
/// what a delete takes with it (#2678), computed off its window thread.
|
||||
pub fn replies(mut self, commands: &[(String, Value)]) -> Vec<Result<Value>> {
|
||||
let stop = self.0.stop.clone();
|
||||
stop.within(|| commands.iter().map(|(id, params)| self.0.execute(id, params)).collect())
|
||||
commands.iter().map(|(id, params)| self.0.execute(id, params)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,10 +534,6 @@ pub struct Session {
|
||||
/// not a move of the design's, so the placements the design reports and Capture Position
|
||||
/// keeps leave it out ([`Session::uncaptured_moves`]).
|
||||
pub exploded: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
|
||||
/// Stops the work computing off the window thread for this design, shared with its copies.
|
||||
stop: solidcraft_kernel::Stop,
|
||||
/// A copy of a design ([`Session::scratch`]), sharing its token.
|
||||
is_copy: bool,
|
||||
world_cache: std::sync::Mutex<Option<WorldCache>>,
|
||||
/// The design the joints were last solved for, as the solve left it, and the model it read
|
||||
/// (see [`Session::solve_joints`]).
|
||||
@@ -802,8 +794,6 @@ impl Session {
|
||||
/// A session on `doc` evaluated as `model`, with what it hides.
|
||||
pub(crate) fn with_model(doc: Arc<Document>, visibility: Visibility, model: Model) -> Self {
|
||||
Session {
|
||||
stop: Default::default(),
|
||||
is_copy: false,
|
||||
saved: doc.clone(),
|
||||
doc,
|
||||
model,
|
||||
@@ -884,11 +874,6 @@ impl Session {
|
||||
/// design before the batch, which is what the batch promised (#3242). The design is what a
|
||||
/// checkpoint keeps, not the replaced session's file path (docs/mcp.md).
|
||||
pub fn replace_design(&mut self, fresh: Session) {
|
||||
// Nothing still computing for the design that leaves has anywhere to go. A copy leaves
|
||||
// the design it came from going, and its token with it.
|
||||
if !self.is_copy {
|
||||
self.stop_computing();
|
||||
}
|
||||
let auto_project = self.auto_project;
|
||||
let validation = self.validation();
|
||||
let single_surface_analysis = self.single_surface_analysis;
|
||||
@@ -920,19 +905,6 @@ impl Session {
|
||||
self.replacements = replacements;
|
||||
}
|
||||
|
||||
/// The token over the work running off the window thread for this design, on any of its
|
||||
/// copies: a job started from a copy runs [`solidcraft_kernel::Stop::within`] it.
|
||||
pub fn stop_token(&self) -> solidcraft_kernel::Stop {
|
||||
self.stop.clone()
|
||||
}
|
||||
|
||||
/// This design is going away: the work still computing for it stops at the kernel's next
|
||||
/// check, and its results go nowhere (they are only ever for this design's evaluation cache).
|
||||
/// A kernel call that doesn't check runs to its end, on a kernel of its own, holding up nothing.
|
||||
pub fn stop_computing(&self) {
|
||||
self.stop.stop();
|
||||
}
|
||||
|
||||
/// Install (or with `None` remove) the check [`Session::execute`] runs before every command,
|
||||
/// for an app that embeds the engine and decides which commands may run. A refusal comes back
|
||||
/// as [`EngineError::Refused`] and leaves the session as it was.
|
||||
@@ -1899,8 +1871,6 @@ impl Session {
|
||||
/// commands on without touching this one.
|
||||
pub fn scratch(&self) -> Session {
|
||||
Session {
|
||||
stop: self.stop.clone(),
|
||||
is_copy: true,
|
||||
doc: self.doc.clone(),
|
||||
model: self.model.clone(),
|
||||
undo: Vec::new(),
|
||||
@@ -2002,16 +1972,6 @@ impl Session {
|
||||
commands: &[(String, Value)],
|
||||
marker: Option<Option<usize>>,
|
||||
original: Option<Session>,
|
||||
) -> Result<ComputedCommands> {
|
||||
let stop = self.stop.clone();
|
||||
stop.within(|| self.compute_commands_under(commands, marker, original))
|
||||
}
|
||||
|
||||
fn compute_commands_under(
|
||||
self,
|
||||
commands: &[(String, Value)],
|
||||
marker: Option<Option<usize>>,
|
||||
original: Option<Session>,
|
||||
) -> Result<ComputedCommands> {
|
||||
let editor_original = original.as_ref().map(|session| session.doc.clone());
|
||||
let mut computed = self.scratch();
|
||||
|
||||
@@ -410,7 +410,6 @@ mod curved_seam_round;
|
||||
mod flush_join_loft;
|
||||
mod gltf_import;
|
||||
mod import_component;
|
||||
mod kernel_pool;
|
||||
mod large_step_import;
|
||||
mod loft_ring;
|
||||
mod loft_spacing;
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
//! A design's bodies are made and used on whichever kernel of the pool is free
|
||||
//! (`solidcraft_smlib`'s `core::Pool`). What it builds must not depend on which: to the last
|
||||
//! number, on a new kernel or one that has done much, while another is busy, with bodies made on
|
||||
//! one kernel going into operations on another, through the evaluation cache, undo and redo.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A hole, a round and a chamfer, each building on the last.
|
||||
fn features() -> Vec<(&'static str, Value)> {
|
||||
vec![
|
||||
("solid.box", json!({"length": 40, "width": 30, "height": 20})),
|
||||
("solid.cylinder", json!({"base": [12, 15, -1], "axis": [0, 0, 1], "radius": 3.4, "height": 30, "operation": "cut"})),
|
||||
("solid.fillet", json!({"edges": [[0, 0, 10]], "radius": 2})),
|
||||
("solid.chamfer", json!({"edges": [[20, 0, 20]], "distance": 1})),
|
||||
]
|
||||
}
|
||||
|
||||
fn join() -> (&'static str, Value) {
|
||||
("solid.box", json!({"corner": [25, 10, 20], "length": 10, "width": 10, "height": 5, "operation": "join"}))
|
||||
}
|
||||
|
||||
/// Everything a design built, exactly: each body's name, whole B-rep record and triangle count.
|
||||
fn built(s: &Session) -> Vec<String> {
|
||||
let st = s.model.state();
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let record = serde_json::to_string(b.body.record()).unwrap();
|
||||
format!("{} {} {record}", b.name, b.mesh().triangles.len())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn design(features: &[(&'static str, Value)]) -> Session {
|
||||
let mut s = Session::default();
|
||||
for (id, p) in features {
|
||||
run(&mut s, id, p.clone());
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn assert_same(what: &str, a: &[String], b: &[String]) {
|
||||
assert_eq!(a.len(), b.len(), "{what}: how many bodies");
|
||||
assert!(a == b, "{what}: the design built differently");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_design_rebuilds_the_same_on_any_kernel() {
|
||||
let f = features();
|
||||
let first = built(&design(&f));
|
||||
assert!(!first.is_empty());
|
||||
// On a new kernel, the one that built it above busy elsewhere.
|
||||
let held = solidcraft_kernel::hold_a_kernel();
|
||||
assert_same("a new kernel", &first, &built(&design(&f)));
|
||||
// And another new one, with both of those out of use.
|
||||
let held_too = solidcraft_kernel::hold_a_kernel();
|
||||
assert_same("another new kernel", &first, &built(&design(&f)));
|
||||
drop((held, held_too));
|
||||
// On one that has built plenty since.
|
||||
for _ in 0..3 {
|
||||
design(&f);
|
||||
}
|
||||
assert_same("a kernel that did much before", &first, &built(&design(&f)));
|
||||
}
|
||||
|
||||
/// A curvature round, whose procedural blends take their serials from the kernel: one body made
|
||||
/// on each of two kernels, the serials of neither mistaken for the other's in the kernel that
|
||||
/// solves stations for both.
|
||||
#[test]
|
||||
fn a_procedural_blend_builds_the_same_on_any_kernel() {
|
||||
let f = vec![
|
||||
("solid.box", json!({"length": 40, "width": 30, "height": 20})),
|
||||
("solid.fillet", json!({"edges": [[20, 0, 20]], "radius": 3, "type": "curvature"})),
|
||||
];
|
||||
let first = design(&f);
|
||||
let held = solidcraft_kernel::hold_a_kernel();
|
||||
let second = design(&f);
|
||||
let b = built(&second);
|
||||
assert_same("a new kernel", &built(&first), &b);
|
||||
let v = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
|
||||
let (mut first, mut second) = (first, second);
|
||||
let beside = v(&mut second);
|
||||
drop(held);
|
||||
assert_eq!(v(&mut first).to_bits(), beside.to_bits(), "one measured right after the other on one kernel");
|
||||
}
|
||||
|
||||
/// A modeled thread, a screw sweep and its boolean, on a short hole.
|
||||
#[test]
|
||||
fn a_thread_builds_the_same_on_any_kernel() {
|
||||
let f = vec![
|
||||
("solid.box", json!({"length": 20, "width": 20, "height": 4})),
|
||||
("solid.cylinder", json!({"base": [10, 10, -1], "axis": [0, 0, 1], "radius": 3.4, "height": 10, "operation": "cut"})),
|
||||
("solid.thread", json!({"face": [13.4, 10, 2], "modeled": true})),
|
||||
];
|
||||
let first = built(&design(&f));
|
||||
let held = solidcraft_kernel::hold_a_kernel();
|
||||
assert_same("a new kernel", &first, &built(&design(&f)));
|
||||
drop(held);
|
||||
}
|
||||
|
||||
/// Bodies from the evaluation cache and the undo stack go on into operations on another kernel:
|
||||
/// the join's boolean takes a body the held kernel made, undo restores it, redo brings back the
|
||||
/// join's, and each is what the whole design built in one go on a single kernel gives.
|
||||
#[test]
|
||||
fn bodies_from_another_kernel_go_on_through_the_cache_undo_and_redo() {
|
||||
let f = features();
|
||||
let mut all = f.clone();
|
||||
all.push(join());
|
||||
let (before, after) = (built(&design(&f)), built(&design(&all)));
|
||||
|
||||
let mut s = design(&f);
|
||||
let held = solidcraft_kernel::hold_a_kernel();
|
||||
let (id, p) = join();
|
||||
run(&mut s, id, p);
|
||||
assert_same("joined on another kernel", &after, &built(&s));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_same("undone", &before, &built(&s));
|
||||
drop(held);
|
||||
let held = solidcraft_kernel::hold_a_kernel();
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_same("redone", &after, &built(&s));
|
||||
// Rebuilt without the cache, every feature again, while that kernel stays out of use.
|
||||
s.model.no_cache = true;
|
||||
*s.model.cache.lock().unwrap() = Default::default();
|
||||
run(&mut s, "timeline.roll_to", json!({"position": 0}));
|
||||
run(&mut s, "timeline.roll_to", json!({}));
|
||||
assert_same("rebuilt from the start", &after, &built(&s));
|
||||
drop(held);
|
||||
// And the body goes on from there, back on the first kernel.
|
||||
run(&mut s, "solid.box", json!({"corner": [-5, 0, 0], "length": 5, "width": 5, "height": 5, "operation": "join"}));
|
||||
let mut more = all.clone();
|
||||
more.push(("solid.box", json!({"corner": [-5, 0, 0], "length": 5, "width": 5, "height": 5, "operation": "join"})));
|
||||
assert_same("a further join", &built(&design(&more)), &built(&s));
|
||||
}
|
||||
|
||||
/// A design rebuilt while other work keeps kernels of the pool busy on other threads builds what
|
||||
/// it builds alone, whichever kernels its calls happen to get.
|
||||
#[test]
|
||||
fn a_design_rebuilt_beside_other_work_builds_the_same() {
|
||||
let f = features();
|
||||
let alone = built(&design(&f));
|
||||
let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let busy: Vec<_> = (0..3)
|
||||
.map(|_| {
|
||||
let (done, f) = (done.clone(), f.clone());
|
||||
std::thread::spawn(move || {
|
||||
while !done.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
design(&f);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for _ in 0..3 {
|
||||
assert_same("beside other work", &alone, &built(&design(&f)));
|
||||
}
|
||||
done.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
for b in busy {
|
||||
b.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy that replaces its own design leaves the design it was copied from going: the token over
|
||||
/// that design's work is stopped only when that design itself closes or is replaced.
|
||||
#[test]
|
||||
fn a_copy_replacing_its_design_does_not_stop_the_design_it_came_from() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
|
||||
let token = s.stop_token();
|
||||
let new = [("file.new".to_string(), json!({}))];
|
||||
assert!(s.ahead().replies(&new).iter().all(Result::is_ok));
|
||||
assert!(!token.is_stopped(), "a copy evaluating ahead");
|
||||
let _ = s.scratch().compute_commands(&new, None);
|
||||
assert!(!token.is_stopped(), "a copy computing a dialog's commands");
|
||||
assert!(s.preview(&new).is_ok());
|
||||
assert!(!token.is_stopped(), "a preview");
|
||||
// It still works, and the design's own replacement does stop it.
|
||||
run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10}));
|
||||
run(&mut s, "file.new", json!({}));
|
||||
assert!(token.is_stopped(), "the design itself replaced");
|
||||
assert!(!s.stop_token().is_stopped());
|
||||
}
|
||||
@@ -454,18 +454,15 @@ const MAX_REFUSAL_PROBES: usize = 20_000;
|
||||
|
||||
impl Refusal {
|
||||
/// The refusal of `record` for `error`, with the face SMLib refuses on its own.
|
||||
pub(crate) fn of(record: &Brep, error: &solidcraft_smlib::Error) -> Refusal {
|
||||
fn of(record: &Brep, error: &solidcraft_smlib::Error) -> Refusal {
|
||||
if let solidcraft_smlib::Error::Internal(why) = error {
|
||||
return Refusal { face: None, surface: None, why: why.clone(), internal: true };
|
||||
}
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_refusal_face");
|
||||
// It is kept with the body for every later reader: a stopped job's probes must not decide it.
|
||||
let face = solidcraft_smlib::stop::unstoppable(|| {
|
||||
let alone = |f: usize| crate::surfaces::Parts::of(record, &[(f, false)]).and_then(|p| p.canonical()).map(|c| c.brep);
|
||||
(record.faces.len() <= MAX_REFUSAL_PROBES)
|
||||
.then(|| (0..record.faces.len()).find(|&f| alone(f).is_ok_and(|r| solidcraft_smlib::Brep::from_record(&r, true).is_err())))
|
||||
.flatten()
|
||||
});
|
||||
let alone = |f: usize| crate::surfaces::Parts::of(record, &[(f, false)]).and_then(|p| p.canonical()).map(|c| c.brep);
|
||||
let face = (record.faces.len() <= MAX_REFUSAL_PROBES)
|
||||
.then(|| (0..record.faces.len()).find(|&f| alone(f).is_ok_and(|r| solidcraft_smlib::Brep::from_record(&r, true).is_err())))
|
||||
.flatten();
|
||||
let surface = face.and_then(|f| record.faces.get(f)).map(|f| SurfaceKind::of(&f.surface));
|
||||
Refusal { face, surface, why: error.to_string(), internal: false }
|
||||
}
|
||||
@@ -548,10 +545,7 @@ impl Body {
|
||||
pub fn smlib(&self) -> Result<&solidcraft_smlib::Brep> {
|
||||
self.require_brep("an SMLib operation")?;
|
||||
self.smlib
|
||||
.get_or_init(|| {
|
||||
solidcraft_smlib::stop::unstoppable(|| solidcraft_smlib::Brep::from_record(&self.record, true))
|
||||
.map_err(|e| Refusal::of(&self.record, &e))
|
||||
})
|
||||
.get_or_init(|| solidcraft_smlib::Brep::from_record(&self.record, true).map_err(|e| Refusal::of(&self.record, &e)))
|
||||
.as_ref()
|
||||
.map_err(
|
||||
|r| if r.internal { KernelError::Internal(format!("SMLib: {}", r.why)) } else { KernelError::Failed(format!("SMLib: {}", r.why)) },
|
||||
|
||||
@@ -215,8 +215,6 @@ pub use prism_curve::{PlanarPiece, PlanarSpline};
|
||||
pub use prism_measure::{PrismSection, certified_prism_equivalent, certified_prism_section};
|
||||
pub use repair::{Hole, RebuiltFace, RefusedFace, RepairOptions, RepairReport, SewnGap, repair};
|
||||
pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew, sew_touching};
|
||||
pub use solidcraft_smlib::hold_a_kernel;
|
||||
pub use solidcraft_smlib::stop::Stop;
|
||||
pub use solidcraft_smlib::trace as smlib_trace;
|
||||
pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map};
|
||||
pub use step::step_export;
|
||||
|
||||
@@ -28,10 +28,9 @@ fn pool() -> Option<&'static rayon::ThreadPool> {
|
||||
/// thread. Waking the pool's workers costs tens of microseconds, so callers ask for it only
|
||||
/// when the items carry more work than that (decided from the work, not the item count).
|
||||
pub(crate) fn map<T: Sync, R: Send>(what: &'static str, items: &[T], parallel: bool, f: impl Fn(&T) -> Result<R> + Sync) -> Vec<Result<R>> {
|
||||
// The workers run under the caller's validation mode (`crate::validation`) and stop token.
|
||||
// The workers run under the caller's validation mode (`crate::validation`).
|
||||
let scope = crate::validation::current();
|
||||
let stop = solidcraft_smlib::stop::current();
|
||||
let one = |item: &T| solidcraft_smlib::stop::scoped(stop.clone(), || crate::validation::within(scope.clone(), || guard(what, || f(item))));
|
||||
let one = |item: &T| crate::validation::within(scope.clone(), || guard(what, || f(item)));
|
||||
// The browser build has no worker pool: everything runs on the calling thread.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = parallel;
|
||||
@@ -88,8 +87,7 @@ pub fn map_with_local_pools<T: Sync, R: Send>(items: &[T], max_threads: usize, f
|
||||
}
|
||||
let pool = cell.get().ok_or_else(|| crate::KernelError::Failed("missing mesh worker".into()))?;
|
||||
let scope = crate::validation::current();
|
||||
let stop = solidcraft_smlib::stop::current();
|
||||
pool.install(|| solidcraft_smlib::stop::scoped(stop, || crate::validation::within(scope, || guard("mesh worker", || f(item)))))
|
||||
pool.install(|| crate::validation::within(scope, || guard("mesh worker", || f(item))))
|
||||
})
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -103,12 +101,9 @@ pub fn map_with_local_pools<T: Sync, R: Send>(items: &[T], max_threads: usize, f
|
||||
/// `f` over `items` as [`for_each_on_threads`] runs it, results in item order (`None` where an
|
||||
/// item panicked).
|
||||
pub fn map_on_threads<T: Sync, R: Send>(items: &[T], max_threads: usize, f: impl Fn(&T) -> R + Sync) -> Vec<Option<R>> {
|
||||
// The threads run under the caller's validation mode (`crate::validation`) and stop token.
|
||||
// The threads run under the caller's validation mode (`crate::validation`).
|
||||
let scope = crate::validation::current();
|
||||
let stop = solidcraft_smlib::stop::current();
|
||||
let one = |item: &T| {
|
||||
solidcraft_smlib::stop::scoped(stop.clone(), || crate::validation::within(scope.clone(), || guard("parallel item", || Ok(f(item))).ok()))
|
||||
};
|
||||
let one = |item: &T| crate::validation::within(scope.clone(), || guard("parallel item", || Ok(f(item))).ok());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -2439,17 +2439,3 @@ fn a_box_cut_round_a_small_hole_of_a_long_plate_keeps_the_hole() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A refusal is kept with its body for every later reader: which face it names doesn't depend on
|
||||
/// whether the job that found it had been stopped, whose probes would all be refused.
|
||||
#[test]
|
||||
fn a_stopped_job_does_not_decide_which_face_a_refusal_names() {
|
||||
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();
|
||||
let why = solidcraft_smlib::Error::Argument;
|
||||
let plain = crate::body::Refusal::of(body.record(), &why);
|
||||
assert_eq!(plain.face, None, "every face of a box is taken on its own");
|
||||
let stop = crate::Stop::new();
|
||||
stop.stop();
|
||||
let stopped = stop.within(|| crate::body::Refusal::of(body.record(), &why));
|
||||
assert_eq!(stopped.face, plain.face);
|
||||
}
|
||||
|
||||
+23
-105
@@ -11,7 +11,7 @@ use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::{CancelToken, Kernel, KernelOptions, Rng, UpstreamFixes};
|
||||
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
|
||||
use solidcore::mass::integrator::properties::{PropertyAttributes, brep_volume};
|
||||
use solidcore::primitive::api_primitives as primitives;
|
||||
|
||||
@@ -412,8 +412,7 @@ const HELD_BACK: &[&str] = &["fillet_refuse_rounds_breaking_through"];
|
||||
|
||||
/// How far apart two kernels' procedural blend serials start. A body keeps the serials of the
|
||||
/// kernel that made it and may be used on any other, whose memo of solved stations is keyed by
|
||||
/// serial alone: the ranges keep one definition's stations from answering for another's. A serial
|
||||
/// only names a definition, so which range one comes from changes no answer.
|
||||
/// serial alone: the ranges keep one definition's stations from answering for another's.
|
||||
pub(crate) const BLEND_SERIALS_PER_KERNEL: u64 = 1 << 40;
|
||||
|
||||
/// A new kernel. Each takes the next range of blend serials, the first starting at 0.
|
||||
@@ -425,85 +424,30 @@ fn fresh() -> Kernel {
|
||||
k
|
||||
}
|
||||
|
||||
/// The kernels calls run on. The lock is only held to take or put back a kernel, never during a
|
||||
/// call: a call that runs long (or never returns) holds up nothing but its own body, so one
|
||||
/// design's long operation never delays another's. Calls one after another all get the same
|
||||
/// kernel; one that starts while another runs gets one of its own (solidcore keeps no global
|
||||
/// state, and a body's identities are valid only in the kernel that issued them, so any kernel
|
||||
/// takes any body).
|
||||
///
|
||||
/// A call stuck for good keeps its kernel and the thread it runs on, and costs nothing more: at
|
||||
/// most `most_idle` kernels wait between calls, however many calls once ran side by side. A job
|
||||
/// that starts again and sticks again holds one more each time, so at `most_running` calls a new
|
||||
/// call fails, saying so, rather than grow without end or wait for calls that may never return.
|
||||
pub(crate) struct Pool<K> {
|
||||
pub(crate) idle: Vec<K>,
|
||||
pub(crate) running: usize,
|
||||
pub(crate) most_idle: usize,
|
||||
pub(crate) most_running: usize,
|
||||
/// Most kernels kept for later calls; those over it are dropped when their call ends.
|
||||
const MOST_IDLE: usize = 8;
|
||||
|
||||
/// The kernels no call is using, the last used on top. The lock is only held to take or put back
|
||||
/// a kernel, never during a call: a call that runs long (or never returns) holds up nothing but
|
||||
/// its own body, so one design's long operation never delays another's. Calls one after another
|
||||
/// all get the same kernel, as a single kernel would serve them; one that starts while another
|
||||
/// runs gets one of its own (solidcore keeps no global state, and a body's identities are valid
|
||||
/// only in the kernel that issued them, so any kernel takes any body).
|
||||
static IDLE: Mutex<Vec<Kernel>> = Mutex::new(Vec::new());
|
||||
|
||||
fn take_kernel() -> Kernel {
|
||||
IDLE.lock().unwrap_or_else(PoisonError::into_inner).pop().unwrap_or_else(fresh)
|
||||
}
|
||||
|
||||
impl<K> Pool<K> {
|
||||
/// A kernel for a call, the last used first; `None` when too many are running.
|
||||
pub(crate) fn take(&mut self, make: impl FnOnce() -> K) -> Option<K> {
|
||||
if self.running >= self.most_running {
|
||||
return None;
|
||||
}
|
||||
self.running += 1;
|
||||
Some(self.idle.pop().unwrap_or_else(make))
|
||||
fn put_kernel(k: Kernel) {
|
||||
let mut idle = IDLE.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if idle.len() < MOST_IDLE {
|
||||
idle.push(k);
|
||||
}
|
||||
|
||||
/// A call ended, handing back its kernel if it is fit to use again.
|
||||
pub(crate) fn put(&mut self, kernel: Option<K>) {
|
||||
self.running = self.running.saturating_sub(1);
|
||||
if let Some(kernel) = kernel
|
||||
&& self.idle.len() < self.most_idle
|
||||
{
|
||||
self.idle.push(kernel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Most calls at once. Far above what any work needs: an operation runs on at most 16 threads.
|
||||
const MOST_RUNNING: usize = 256;
|
||||
|
||||
static POOL: Mutex<Pool<Kernel>> = Mutex::new(Pool { idle: Vec::new(), running: 0, most_idle: 8, most_running: MOST_RUNNING });
|
||||
|
||||
fn take_kernel() -> Option<Kernel> {
|
||||
POOL.lock().unwrap_or_else(PoisonError::into_inner).take(fresh)
|
||||
}
|
||||
|
||||
fn put_kernel(k: Option<Kernel>) {
|
||||
POOL.lock().unwrap_or_else(PoisonError::into_inner).put(k);
|
||||
}
|
||||
|
||||
/// What a call must not find from the calls before it: the same operation on the same bodies
|
||||
/// answers the same on any kernel, however much that kernel did first. (Its draws of random rays,
|
||||
/// the stations it remembers; marks, identities and what is only kept to reuse memory change no
|
||||
/// answer. Tolerances and fixes are the same on every kernel.)
|
||||
fn start_clean(k: &mut Kernel) {
|
||||
k.rng = Rng::new(KernelOptions::default().seed);
|
||||
*k.blend_station_memo.borrow_mut() = Default::default();
|
||||
k.fillet_point_step.set(None);
|
||||
k.upstream_ub_passed.clear();
|
||||
}
|
||||
|
||||
/// A kernel kept out of use until dropped ([`crate::hold_a_kernel`]).
|
||||
pub struct HeldKernel(Option<Kernel>);
|
||||
|
||||
impl Drop for HeldKernel {
|
||||
fn drop(&mut self) {
|
||||
put_kernel(self.0.take());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hold_a_kernel() -> HeldKernel {
|
||||
HeldKernel(take_kernel())
|
||||
}
|
||||
|
||||
/// Runs `f` on a kernel of its own. A panic in solidcore is caught: the kernel, which it may have
|
||||
/// left half-updated, is dropped and the call fails with [`Error::Internal`]. A token over this
|
||||
/// thread's work ([`crate::stop`]) stops it.
|
||||
/// left half-updated, is replaced by a fresh one and the call fails with [`Error::Internal`].
|
||||
pub(crate) fn run<T: dump::Answer>(call: &'static str, f: impl FnOnce(&mut Kernel) -> std::result::Result<T, solidcore::SolidError>) -> Result<T> {
|
||||
run_on(call, &[], f)
|
||||
}
|
||||
@@ -526,26 +470,7 @@ pub(crate) fn run_on<T: dump::Answer>(
|
||||
inputs: dump::Inputs<'_>,
|
||||
f: impl FnOnce(&mut Kernel) -> std::result::Result<T, solidcore::SolidError>,
|
||||
) -> Result<T> {
|
||||
let stop = crate::stop::token();
|
||||
if stop.as_ref().is_some_and(CancelToken::is_cancelled) {
|
||||
return Err(Error::Solidcore(format!("{call}: cancelled")));
|
||||
}
|
||||
call_on(call, inputs, stop, f)
|
||||
}
|
||||
|
||||
/// [`run_on`], stopped by `stop`.
|
||||
fn call_on<T: dump::Answer>(
|
||||
call: &'static str,
|
||||
inputs: dump::Inputs<'_>,
|
||||
stop: Option<CancelToken>,
|
||||
f: impl FnOnce(&mut Kernel) -> std::result::Result<T, solidcore::SolidError>,
|
||||
) -> Result<T> {
|
||||
let Some(mut k) = take_kernel() else {
|
||||
log::warn!(target: "smlib", "{call}: refused, {MOST_RUNNING} kernel calls are still running");
|
||||
return Err(Error::Solidcore(format!("{call}: {MOST_RUNNING} kernel calls are still running")));
|
||||
};
|
||||
start_clean(&mut k);
|
||||
k.cancel = stop.clone();
|
||||
let mut k = take_kernel();
|
||||
// A profiler span per call: where the kernel's time goes.
|
||||
let _span = solidcraft_profile::span("solidcore", call);
|
||||
let in_flight = dump::in_flight(call);
|
||||
@@ -570,11 +495,6 @@ fn call_on<T: dump::Answer>(
|
||||
}
|
||||
};
|
||||
drop(in_flight);
|
||||
// Stopped, it is no failure to look into, and solidcore may have left the kernel part way.
|
||||
if stop.is_some_and(|s| s.is_cancelled()) {
|
||||
put_kernel(None);
|
||||
return r;
|
||||
}
|
||||
let why = match &r {
|
||||
Ok(v) => v.refusal(),
|
||||
Err(e) => Some(e.to_string()),
|
||||
@@ -583,8 +503,7 @@ fn call_on<T: dump::Answer>(
|
||||
// The kernel the call ran on, or the fresh one that replaced it after a panic.
|
||||
dump::failed(&k, call, &why, inputs);
|
||||
}
|
||||
k.cancel = None;
|
||||
put_kernel(Some(k));
|
||||
put_kernel(k);
|
||||
r
|
||||
}
|
||||
|
||||
@@ -611,8 +530,7 @@ pub(crate) fn with_body<T: dump::Answer>(
|
||||
if body.0.is_poisoned() {
|
||||
return Err(Error::Internal(format!("{call}: the body was left unfinished by an earlier panic")));
|
||||
}
|
||||
// No token stops it: stopped part way, solidcore may leave the body it borrows half-changed.
|
||||
call_on(call, &[("brep", body)], None, |k| f(k, &mut body.0.lock().unwrap_or_else(PoisonError::into_inner)))
|
||||
run_on(call, &[("brep", body)], |k| f(k, &mut body.0.lock().unwrap_or_else(PoisonError::into_inner)))
|
||||
}
|
||||
|
||||
/// An axis-aligned box from its lower corner (`SmApiCreateBox`, as `smlib_make_box` checks it:
|
||||
|
||||
@@ -19,7 +19,6 @@ pub mod pcurves;
|
||||
mod sheets;
|
||||
mod split;
|
||||
pub mod ssi;
|
||||
pub mod stop;
|
||||
pub mod sweeps;
|
||||
pub mod trace;
|
||||
pub mod trim;
|
||||
@@ -233,13 +232,6 @@ pub fn current_call() -> Option<String> {
|
||||
core::dump::current()
|
||||
}
|
||||
|
||||
/// Keeps one of the kernels calls run on out of use until dropped: the next calls run on another.
|
||||
/// For tests of what happens when a design's bodies come from a kernel that is busy elsewhere.
|
||||
#[doc(hidden)]
|
||||
pub fn hold_a_kernel() -> core::HeldKernel {
|
||||
core::hold_a_kernel()
|
||||
}
|
||||
|
||||
/// Is this thread inside a solidcore call? A panic there is caught and becomes
|
||||
/// [`Error::Internal`]; crash reports mark it recovered.
|
||||
pub fn in_call() -> bool {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl Spec {
|
||||
impl Surface {
|
||||
/// solidcore's surface, made the first time it is needed.
|
||||
fn core(&self) -> Result<&crate::core::split::Tool> {
|
||||
self.core.get_or_init(|| crate::stop::unstoppable(|| crate::core::ssi::surface(&self.spec))).as_ref().map_err(Clone::clone)
|
||||
self.core.get_or_init(|| crate::core::ssi::surface(&self.spec)).as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
pub(crate) fn spec(&self) -> &Spec {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
//! Stopping the kernel calls of work nobody waits for any more: the jobs of a design that closed.
|
||||
//! Work runs [`Stop::within`] its design's token; once the token is stopped, each operation under
|
||||
//! it refuses to start, and one running stops when solidcore next checks ([`solidcore::kernel::Kernel::cancel`]).
|
||||
//!
|
||||
//! A stopped operation is an error like any other, and the caller's bodies are as they were: only
|
||||
//! operations that build on copies stop. Calls that answer from a body itself ([`crate::core::with_body`],
|
||||
//! measuring, say) always run to their end, since solidcore may leave what a stopped call borrows
|
||||
//! half-changed.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use solidcore::kernel::CancelToken;
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: RefCell<Option<Stop>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Stops the kernel work running under it, on every thread it reached.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Stop(CancelToken);
|
||||
|
||||
impl Stop {
|
||||
pub fn new() -> Stop {
|
||||
Stop::default()
|
||||
}
|
||||
|
||||
/// Stop the work under this token, now and from now on.
|
||||
pub fn stop(&self) {
|
||||
self.0.cancel();
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
self.0.is_cancelled()
|
||||
}
|
||||
|
||||
/// Run `f` with this token over the kernel work it does on this thread.
|
||||
pub fn within<R>(&self, f: impl FnOnce() -> R) -> R {
|
||||
scoped(Some(self.clone()), f)
|
||||
}
|
||||
}
|
||||
|
||||
/// The token over this thread's work, to hand to the worker threads it starts.
|
||||
pub fn current() -> Option<Stop> {
|
||||
CURRENT.with(|c| c.borrow().clone())
|
||||
}
|
||||
|
||||
/// Run `f` under `stop` (`None`: under none), as [`current`] was on the thread that started it.
|
||||
pub fn scoped<R>(stop: Option<Stop>, f: impl FnOnce() -> R) -> R {
|
||||
let previous = CURRENT.with(|c| c.replace(stop));
|
||||
struct Restore(Option<Option<Stop>>);
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
if let Some(previous) = self.0.take() {
|
||||
CURRENT.with(|c| *c.borrow_mut() = previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _restore = Restore(Some(previous));
|
||||
f()
|
||||
}
|
||||
|
||||
/// Run `f` so that no token stops it: for what fills a cell every later user of a body reads,
|
||||
/// which must not keep a stopped job's error.
|
||||
pub fn unstoppable<R>(f: impl FnOnce() -> R) -> R {
|
||||
scoped(None, f)
|
||||
}
|
||||
|
||||
/// solidcore's token of the work on this thread.
|
||||
pub(crate) fn token() -> Option<CancelToken> {
|
||||
CURRENT.with(|c| c.borrow().as_ref().map(|s| s.0.clone()))
|
||||
}
|
||||
@@ -78,7 +78,7 @@ fn calls_from_many_threads_are_serialised() {
|
||||
static DUMP_DIR_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// Entry points that ask no kernel, so have nothing to say here.
|
||||
const NO_KERNEL: [&str; 14] = [
|
||||
const NO_KERNEL: [&str; 13] = [
|
||||
"inertia_tensor",
|
||||
"is_valid",
|
||||
"failed_tests",
|
||||
@@ -92,7 +92,6 @@ const NO_KERNEL: [&str; 14] = [
|
||||
"word",
|
||||
"words",
|
||||
"in_call",
|
||||
"hold_a_kernel",
|
||||
];
|
||||
|
||||
const ANSWERING: [&str; 63] = [
|
||||
@@ -511,82 +510,3 @@ fn a_panic_beside_a_running_call_costs_only_its_own_kernel() {
|
||||
release.send(()).unwrap();
|
||||
held.join().unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// A stuck call keeps its kernel and nothing more: the pool never holds more than its limit of
|
||||
/// idle kernels however many calls ran side by side, and once as many calls as it allows are
|
||||
/// stuck, a new call is refused rather than growing it or waiting.
|
||||
#[test]
|
||||
fn the_kernel_pool_stays_bounded_when_calls_stick() {
|
||||
let mut pool = core::Pool { idle: Vec::new(), running: 0, most_idle: 1, most_running: 3 };
|
||||
let mut made = 0;
|
||||
let mut make = || {
|
||||
made += 1;
|
||||
made
|
||||
};
|
||||
let (a, b, c) = (pool.take(&mut make), pool.take(&mut make), pool.take(&mut make));
|
||||
assert_eq!((a, b, c), (Some(1), Some(2), Some(3)), "side by side, each on its own");
|
||||
assert_eq!(pool.take(&mut make), None, "refused, not waiting, not one more");
|
||||
pool.put(a);
|
||||
assert_eq!(pool.take(&mut make), Some(1), "room again, its kernel used again");
|
||||
// The other two never come back here: they still count, and hold no idle slot.
|
||||
pool.put(Some(1));
|
||||
pool.put(b);
|
||||
assert_eq!(pool.idle, [1], "no more idle than allowed");
|
||||
assert_eq!(pool.running, 1, "one still stuck");
|
||||
// A stopped call's kernel is not used again.
|
||||
pool.put(None);
|
||||
assert_eq!(pool.running, 0);
|
||||
assert_eq!(pool.idle, [1]);
|
||||
}
|
||||
|
||||
/// A stop reaches a running call at solidcore's next check and later calls before they start;
|
||||
/// the kernel it ran on is not used again; calls of other work and answers from a body go on.
|
||||
#[test]
|
||||
fn a_stopped_call_ends_at_its_next_check_and_holds_up_nothing() {
|
||||
let stop = stop::Stop::new();
|
||||
let (started, is_started) = std::sync::mpsc::channel();
|
||||
let running = {
|
||||
let stop = stop.clone();
|
||||
std::thread::spawn(move || {
|
||||
stop.within(|| {
|
||||
let first: Result<()> = core::run("stopped_call", |k| {
|
||||
let _ = started.send(());
|
||||
// A long solidcore loop, until it is stopped.
|
||||
loop {
|
||||
k.check_cancelled("stopped_call")?;
|
||||
std::thread::yield_now();
|
||||
}
|
||||
});
|
||||
let next = Brep::make_box([0.0; 3], [1.0; 3]);
|
||||
(first, next.map(|_| ()))
|
||||
})
|
||||
})
|
||||
};
|
||||
is_started.recv().unwrap();
|
||||
let body = answered(|| Brep::make_box([0.0; 3], [10.0; 3]).unwrap());
|
||||
stop.stop();
|
||||
let (first, next) = answered(move || running.join().unwrap());
|
||||
assert!(first.is_err_and(|e| e.to_string().contains("cancel")));
|
||||
assert!(next.is_err_and(|e| e.to_string().contains("cancelled")), "refused before it starts");
|
||||
// Under a stopped token, a body still answers, and is as it was.
|
||||
let v = stop.within(|| body.volume(1e-7)).unwrap();
|
||||
assert!((v - 1000.0).abs() < 1e-6, "{v}");
|
||||
assert!(stop.within(|| body.boolean(&body, BooleanOp::Union)).is_err(), "an operation is refused");
|
||||
// What fills a shared cell is not stopped.
|
||||
assert!(stop.within(|| stop::unstoppable(|| Brep::make_box([0.0; 3], [1.0; 3]))).is_ok());
|
||||
// And nothing else is affected.
|
||||
let v = body.boolean(&body, BooleanOp::Union).unwrap().unwrap().volume(1e-7).unwrap();
|
||||
assert!((v - 1000.0).abs() < 1e-6, "{v}");
|
||||
}
|
||||
|
||||
/// Every call starts with the same draws of random rays, whatever its kernel did before: what
|
||||
/// depends on them answers the same on any kernel of the pool.
|
||||
#[test]
|
||||
fn every_call_starts_clean_on_any_kernel() {
|
||||
let draw = |k: &mut solidcore::kernel::Kernel| Ok(k.rng.next_f64());
|
||||
let first = core::run("draw", draw).unwrap();
|
||||
assert_eq!(core::run("draw", draw).unwrap(), first, "the same kernel, later");
|
||||
let held = hold_a_kernel();
|
||||
assert_eq!(core::run("draw", draw).unwrap(), first, "another kernel");
|
||||
drop(held);
|
||||
}
|
||||
|
||||
@@ -572,11 +572,10 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
let next = if i + 1 < n { i + 1 } else { i - 1 };
|
||||
switch(app, next);
|
||||
}
|
||||
if let Some(slot) = app.docs.slots.remove(i) {
|
||||
slot.session.stop_computing();
|
||||
if let Some(a) = slot.autosave {
|
||||
a.discard();
|
||||
}
|
||||
if let Some(slot) = app.docs.slots.remove(i)
|
||||
&& let Some(a) = slot.autosave
|
||||
{
|
||||
a.discard();
|
||||
}
|
||||
if app.docs.active > i {
|
||||
app.docs.active -= 1;
|
||||
|
||||
@@ -725,33 +725,6 @@ fn a_design_closed_while_its_history_computes_holds_up_nothing() {
|
||||
assert_eq!(h.app.dialog.as_ref().and_then(|d| d.command()), Some("solid.extrude"));
|
||||
}
|
||||
|
||||
/// Closing a design or replacing it stops the kernel work still computing for it; showing another
|
||||
/// tab doesn't, since that design is still there to come back to.
|
||||
#[test]
|
||||
fn closing_a_design_stops_what_it_has_computing_and_a_tab_switch_does_not() {
|
||||
let mut h = block_boss_hole();
|
||||
let first = h.app.session.stop_token();
|
||||
let copy = h.app.session.ahead();
|
||||
crate::documents::new_tab(&mut h.app);
|
||||
assert!(!first.is_stopped(), "only another tab shown");
|
||||
let second = h.app.session.stop_token();
|
||||
assert!(!second.is_stopped());
|
||||
crate::documents::close(&mut h.app, 0, true);
|
||||
assert!(first.is_stopped(), "the first design closed");
|
||||
assert!(!second.is_stopped(), "this design goes on");
|
||||
assert!(
|
||||
copy.replies(&[("solid.box".to_string(), json!({"length": 1, "width": 1, "height": 1}))]).iter().all(Result::is_err),
|
||||
"its job gets nowhere"
|
||||
);
|
||||
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
|
||||
h.app.run("file.new", json!({})).unwrap();
|
||||
assert!(second.is_stopped(), "replaced in its tab");
|
||||
assert!(!h.app.session.stop_token().is_stopped());
|
||||
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
|
||||
crate::documents::close(&mut h.app, 0, true);
|
||||
assert!(!h.app.session.stop_token().is_stopped(), "the blank design that took the last tab's place");
|
||||
}
|
||||
|
||||
/// A design's history command stays with its tab: another tab doesn't wait for it and doesn't get
|
||||
/// its result, even once it is in. Shown again, the design picks it up.
|
||||
#[test]
|
||||
|
||||
@@ -438,10 +438,8 @@ fn spawn(pv: &mut PreviewState, key: u64, scratch: Session, cmds: Vec<(String, V
|
||||
// Watched by `progress`, so a job left behind for other inputs skips its remaining features.
|
||||
let watched = progress.clone();
|
||||
let spawned = std::thread::Builder::new().name("preview".into()).spawn(move || {
|
||||
let job = scratch.stop_token().within(|| {
|
||||
solidcraft_engine::doc::progress::watch(watched, || {
|
||||
if tool { compute_tool(&scratch, &cmds, colors) } else { compute(&scratch, &cmds, colors) }
|
||||
})
|
||||
let job = solidcraft_engine::doc::progress::watch(watched, || {
|
||||
if tool { compute_tool(&scratch, &cmds, colors) } else { compute(&scratch, &cmds, colors) }
|
||||
});
|
||||
let _ = tx.send(job);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user