brep_render/runner.rs
1//! The history-run seam — the async RUN machine behind a trait, mirroring the
2//! platform-seam shape of `brep-app/src/store.rs`'s `ModelStore` (a trait with
3//! impls behind it, async surfaced via a poll idiom).
4//!
5//! A history run is split SUBMIT → POLL/APPLY: [`HistoryRunner::submit_run`]
6//! kicks off a run tagged with a monotonic generation, and the completed
7//! [`RunReply`] is drained later via [`HistoryRunner::poll_run`]. The runner OWNS
8//! the [`SceneRunner`](crate::pipeline::SceneRunner) — so a future thread/worker
9//! impl owns the resident registry that `execute_history` populates — and holds
10//! the delta baseline across reruns.
11//!
12//! This slice ships the DEFAULT [`InlineRunner`]: it runs on `submit_run` and
13//! stashes the reply for an immediate `poll_run`, so the run stays synchronous
14//! and byte-identical to the pre-seam in-process run. A native-thread impl (M2b)
15//! and a wasm-worker impl (M3) slot in behind the SAME trait — `submit_run`
16//! defers the work and `poll_run` surfaces it a frame (or many) later, so the
17//! `EngineState::pump` caller never changes.
18
19/// A completed history run, tagged with the [`generation`](Self::generation) it
20/// was submitted under so the applier can drop stale replies (a newer run that
21/// finished first). The [`output`](Self::output) is the [`SceneRunner`] delta to
22/// apply to the display scene.
23///
24/// [`SceneRunner`]: crate::pipeline::SceneRunner
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct RunReply {
27 /// The monotonic generation this run was submitted under.
28 pub generation: u64,
29 /// The delta snapshot + report the run produced.
30 pub output: crate::pipeline::RunOutput,
31}
32
33/// A feature about to EXECUTE inside an in-flight run (cached replays are
34/// instant and are not reported): which one, of how many, under which
35/// generation. Posted by the runner before the kernel starts the feature, so
36/// the UI can name what it is waiting on — and, after a cancel, what it was
37/// waiting on.
38#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub struct RunProgress {
40 /// The generation of the run this belongs to (see [`RunReply::generation`]).
41 pub generation: u64,
42 /// Zero-based position of the feature in the request.
43 pub index: usize,
44 /// The request's feature count.
45 pub total: usize,
46 pub feature_id: String,
47 pub feature_type: String,
48}
49
50/// A STEP text to PROBE for product structure on the runner — the parse that
51/// `brep_kernel::read_step_assembly` performs, which builds every product's
52/// bodies and takes seconds on a real assembly (8.3 s natively for a 5 MB,
53/// 140-product file), so it must not run on the browser's main thread.
54#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
55pub struct StepProbeRequest {
56 pub id: u64,
57 pub text: String,
58}
59
60/// The probe's answer: the parsed assembly (`Some`), no structure (`None`),
61/// or a parse failure. The assembly crosses back to the main side whole — its
62/// JSON is large (74 MB for the file above) but the trip costs a third of a
63/// second against the seconds the parse took.
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65pub struct StepProbeReply {
66 pub id: u64,
67 pub result: Result<Option<brep_kernel::StepAssembly>, String>,
68}
69
70/// Which exact measurement a [`MeasureQuery`] wants, mirroring the object-info
71/// kinds ([`crate::metadata`]): a whole solid, one named face, or one named edge.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
73pub enum MeasureKind {
74 Solid,
75 Face,
76 Edge,
77}
78
79/// A per-object MEASUREMENT request routed to the runner (which owns the warm
80/// registry). The runner resolves `owner` to its resident handle and measures by
81/// [`kind`](Self::kind); the reply is the object-info JSON fragment MINUS the
82/// main-injected `name`/`creatingFeature` fields. Tagged with a monotonic
83/// [`id`](Self::id) so the main side can pair the reply with its pending request.
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
85pub struct MeasureQuery {
86 /// Monotonic request id (main pairs the reply back by it).
87 pub id: u64,
88 /// The measurement to run.
89 pub kind: MeasureKind,
90 /// The OWNING solid name (a solid is its own owner; a face/edge names its solid).
91 pub owner: String,
92 /// The face/edge NAME to measure (ignored for a whole-solid query).
93 pub entity: String,
94 /// The density (mass per mm³) to scale a solid's weight (ignored for face/edge).
95 pub density: f64,
96}
97
98/// A completed [`MeasureQuery`]: the object-info measurement fields as a JSON
99/// fragment (WITHOUT `name`/`creatingFeature`, which the main thread injects from
100/// its eager provenance), tagged with the request [`id`](Self::id).
101#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
102pub struct MeasureReply {
103 /// The request id this answers.
104 pub id: u64,
105 /// The measurement JSON fragment (see [`measure_json`]).
106 pub result: String,
107}
108
109/// A request for the B-rep TOPOLOGY of resident solids, by the handle each
110/// display was tessellated from. The drawing sheet's exact hidden-line pass
111/// reads faces and edges, and the resident registry that holds them is
112/// thread-local to the runner, so the main side asks for exactly the solids a
113/// placement draws and does not already hold. Nothing else ever asks, so a
114/// document with no sheet never pays for the topology crossing.
115#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
116pub struct TopologyRequest {
117 /// Monotonic request id (main pairs the reply back by it).
118 pub id: u64,
119 /// `(solid name, resident handle)` per solid wanted.
120 pub solids: Vec<(String, u32)>,
121}
122
123/// A completed [`TopologyRequest`]: a clone of every requested solid the
124/// registry still holds, `(name, handle, solid)`. A handle the registry no
125/// longer holds (a later run freed it) is left out.
126#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
127pub struct TopologyReply {
128 /// The request id this answers.
129 pub id: u64,
130 pub solids: Vec<(String, u32, brep_kernel::BrepSolid)>,
131}
132
133/// Answer a topology request against the resident registry of the thread this
134/// runs on. Shared by every runner.
135fn topology_reply(request: TopologyRequest) -> TopologyReply {
136 let solids = request
137 .solids
138 .into_iter()
139 .filter_map(|(name, handle)| {
140 brep_kernel::registered_solid_clone(handle)
141 .ok()
142 .map(|solid| (name, handle, solid))
143 })
144 .collect();
145 TopologyReply { id: request.id, solids }
146}
147
148/// A drawing sheet's exact hidden-line pass for ONE placement, handed to the
149/// runner so the UI thread never runs it (see
150/// [`crate::sheets::project::LinesJob`]). The job names the runner's own
151/// resident solids by handle and content fingerprint, so nothing crosses the
152/// seam but the question and the lines.
153#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
154pub struct SheetLinesRequest {
155 /// Monotonic request id (main pairs the reply back by it).
156 pub id: u64,
157 pub job: crate::sheets::project::LinesJob,
158}
159
160/// A completed [`SheetLinesRequest`]: the placement's lines relative to its
161/// position, or why the runner could not draw them (a handle its registry no
162/// longer holds, or holds different geometry under).
163#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
164pub struct SheetLinesReply {
165 /// The request id this answers.
166 pub id: u64,
167 pub key: String,
168 pub lines: Result<crate::sheets::project::ExactLines, String>,
169}
170
171/// Answer a sheet-lines request against the resident registry of the thread
172/// this runs on. Shared by every runner.
173fn sheet_lines_reply(request: SheetLinesRequest) -> SheetLinesReply {
174 SheetLinesReply {
175 id: request.id,
176 lines: crate::sheets::project::run_lines_job(&request.job),
177 key: request.job.key,
178 }
179}
180
181pub use brep_reconstruction::stl_conversion::{
182 ConversionPolicy, StlConversionOptions, StlConversionOutput,
183};
184
185/// Mesh encoding accepted by the off-thread reconstruction channel.
186#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
187pub enum MeshImportFormat {
188 Stl,
189 Obj,
190 /// 3MF (core specification): a package whose build instances are flattened
191 /// into one millimetre triangle soup before this common chain runs.
192 ThreeMf,
193}
194
195/// A mesh reconstruction request. The byte payload moves to the native thread
196/// or is serialized to the browser worker; no parsing or fitting happens on UI.
197#[derive(Debug, serde::Serialize, serde::Deserialize)]
198pub struct MeshImportRequest {
199 pub id: u64,
200 pub format: MeshImportFormat,
201 pub bytes: Vec<u8>,
202 pub options: StlConversionOptions,
203}
204
205/// Completed off-thread reconstruction. Success carries validated STEP and
206/// diagnostics for a preview or an ordinary IMPORT3D history insertion.
207#[derive(Debug, serde::Serialize, serde::Deserialize)]
208pub struct MeshImportReply {
209 pub id: u64,
210 pub result: Result<StlConversionOutput, String>,
211}
212
213/// Validate explicitly installed packages away from the UI thread.
214#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
215pub struct PluginInstallRequest {
216 pub id: u64,
217 pub packages: serde_json::Value,
218}
219#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
220pub struct PluginInstallReply {
221 pub id: u64,
222 pub result: Result<brep_plugins::Registry, String>,
223}
224fn validate_plugins(request: PluginInstallRequest) -> PluginInstallReply {
225 let result = serde_json::from_value(request.packages)
226 .map_err(|e| format!("plugin packages: {e}"))
227 .and_then(brep_plugins::Runtime::new)
228 .map(|runtime| runtime.registry().clone());
229 PluginInstallReply { id: request.id, result }
230}
231
232/// A callback request against an immutable document/selection snapshot.
233#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
234pub struct PluginActionRequest {
235 pub id: u64,
236 pub pins: Vec<brep_plugins::PluginPin>,
237 pub action: String,
238 pub input: serde_json::Value,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub script: Option<String>,
241}
242
243#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
244pub struct PluginActionReply {
245 pub id: u64,
246 pub result: Result<brep_plugins::ActionPlan, String>,
247}
248
249/// The history-run seam: SUBMIT a run, POLL for its completed reply, RESET the
250/// delta baseline, plus a QUERY channel for per-object measurements (routed to the
251/// runner so the warm registry answers them, never the cold main-side one). The
252/// Inline impl runs everything synchronously; a later thread/worker impl defers
253/// the work and surfaces the replies through the same poll idiom.
254pub trait HistoryRunner {
255 fn submit_plugin_install(&mut self, _request: PluginInstallRequest) -> bool { false }
256 fn poll_plugin_install(&mut self) -> Option<PluginInstallReply> { None }
257 /// Trusted installed content, separate from saved document dependency pins.
258 fn sync_plugins(&mut self, _revision: u64, _fetch: &mut dyn FnMut() -> serde_json::Value) {}
259 /// False explicitly reports a runner without action support.
260 fn submit_plugin_action(&mut self, _request: PluginActionRequest) -> bool { false }
261 fn poll_plugin_action(&mut self) -> Option<PluginActionReply> { None }
262
263 /// Submit a history run tagged with a monotonic generation. The runner executes
264 /// it (immediately for Inline; on a background thread later) and makes the reply
265 /// available via [`poll_run`](Self::poll_run).
266 fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64);
267 /// Non-blocking: the next completed run reply, if any (drained each frame).
268 fn poll_run(&mut self) -> Option<RunReply>;
269 /// Submit a per-object measurement query (answered against the runner's warm
270 /// registry). Inline computes it immediately; a thread impl computes it on the
271 /// runner thread and surfaces it via [`poll_query`](Self::poll_query).
272 fn submit_query(&mut self, query: MeasureQuery);
273 /// Non-blocking: the next completed measurement reply, if any.
274 fn poll_query(&mut self) -> Option<MeasureReply>;
275 /// Submit RANSAC mesh reconstruction to the runner's thread/worker.
276 fn submit_mesh_import(&mut self, request: MeshImportRequest);
277 /// Non-blocking: the next completed mesh reconstruction, if any.
278 fn poll_mesh_import(&mut self) -> Option<MeshImportReply>;
279 /// Submit a STEP product-structure probe (see [`StepProbeRequest`]).
280 fn submit_step_probe(&mut self, request: StepProbeRequest);
281 /// Non-blocking: the next completed STEP probe, if any.
282 fn poll_step_probe(&mut self) -> Option<StepProbeReply>;
283 /// Submit a topology request (see [`TopologyRequest`]). The default drops
284 /// it: a runner that never answers leaves a sheet on its mesh
285 /// approximation, which the drawing reports.
286 fn submit_topology(&mut self, _request: TopologyRequest) {}
287 /// Non-blocking: the next completed topology reply, if any.
288 fn poll_topology(&mut self) -> Option<TopologyReply> {
289 None
290 }
291 /// Submit a sheet placement's exact pass (see [`SheetLinesRequest`]).
292 /// `false` when this runner cannot run one — the engine then runs it on
293 /// its own thread, as every projection did before the pass moved off it.
294 fn submit_sheet_lines(&mut self, _request: SheetLinesRequest) -> bool {
295 false
296 }
297 /// Non-blocking: the next completed sheet-lines reply, if any.
298 fn poll_sheet_lines(&mut self) -> Option<SheetLinesReply> {
299 None
300 }
301 /// Drop the delta baseline (document switch → full rebuild).
302 fn reset(&mut self);
303
304 /// Bring the runner's resident PARTS LIBRARY up to `revision`, calling
305 /// `fetch` ONLY when it is not already there. Called immediately before
306 /// every [`submit_run`](Self::submit_run).
307 ///
308 /// This is the whole point of the library channel: the library is sent
309 /// when it CHANGES (insert, document load, refresh), not on every run.
310 /// Cheap to call — the revision comparison is an integer, and `fetch`
311 /// (which clones the store) never runs in the steady state. The default is
312 /// a no-op for [`InlineRunner`], which shares the caller's kernel store.
313 fn sync_parts_library(
314 &mut self,
315 _revision: u64,
316 _fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
317 ) {
318 }
319
320 /// True when the runner REFUSED a run because its resident parts library
321 /// could not serve it (see [`Reply::NeedPartsLibrary`]). It has already
322 /// forgotten its copy, so the next `sync_parts_library` reinstalls; the
323 /// caller must re-submit the run. Never true for a runner that shares the
324 /// caller's store.
325 fn poll_library_request(&mut self) -> bool {
326 false
327 }
328
329 /// Non-blocking: the MOST RECENT progress report of the in-flight run, with
330 /// any older ones discarded (only the latest feature matters). `None` for
331 /// a synchronous runner — Inline has finished before anyone could ask.
332 fn poll_progress(&mut self) -> Option<RunProgress> {
333 None
334 }
335
336 /// ABANDON the in-flight work and start over with an EMPTY resident
337 /// registry. Returns `false` when there is nothing this runner can abandon
338 /// (Inline: the run already completed on the caller's thread). A `true`
339 /// means every handle the caller holds is now invalid, no reply is coming
340 /// for anything submitted so far, and the parts library must be re-sent —
341 /// the caller reconciles its generations and pending sets accordingly
342 /// (`EngineState::cancel_run`). Nothing inside a feature is interruptible:
343 /// the thread runner lets its old thread finish the feature it is on and
344 /// stop at the next boundary; the browser worker is terminated outright.
345 fn cancel(&mut self) -> bool {
346 false
347 }
348}
349
350/// Measure `query` against `runner`'s resident geometry and emit the object-info
351/// JSON FRAGMENT — EXACTLY the fields [`crate::metadata::EngineState::object_info_json`]
352/// emits for that kind EXCEPT `name` and `creatingFeature` (the main thread injects
353/// those from its eager provenance, so the merged output is byte-identical to the
354/// pre-seam in-process result). Shared verbatim by the Inline and thread runners so
355/// both produce the identical fragment. A missing handle / kernel error yields
356/// `{ "ok": false, "message": .. }` (main injects `name`).
357fn measure_json(runner: &crate::pipeline::SceneRunner, query: &MeasureQuery) -> String {
358 let Some(handle) = runner.handle_of(&query.owner) else {
359 return serde_json::json!({
360 "ok": false,
361 "message": format!("solid '{}' has no resident geometry", query.owner),
362 })
363 .to_string();
364 };
365 match query.kind {
366 MeasureKind::Solid => {
367 match brep_kernel::mass_properties_handle_native(handle, query.density) {
368 Ok(properties) => {
369 let edge_total =
370 brep_kernel::solid_edge_length_total_native(handle).unwrap_or(0.0);
371 serde_json::json!({
372 "ok": true,
373 "kind": "solid",
374 "volume": properties.volume,
375 "surfaceArea": properties.surface_area,
376 "edgeLengthTotal": edge_total,
377 "density": properties.density,
378 "weight": properties.mass,
379 })
380 .to_string()
381 }
382 Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
383 }
384 }
385 MeasureKind::Face => match brep_kernel::face_measurements_native(handle, &query.entity) {
386 Ok((area, edge_total, surface_type)) => serde_json::json!({
387 "ok": true,
388 "kind": "face",
389 "solid": query.owner,
390 "surfaceType": surface_type,
391 "area": area,
392 "edgeLengthTotal": edge_total,
393 })
394 .to_string(),
395 Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
396 },
397 MeasureKind::Edge => match brep_kernel::edge_length_native(handle, &query.entity) {
398 Ok(length) => serde_json::json!({
399 "ok": true,
400 "kind": "edge",
401 "solid": query.owner,
402 "length": length,
403 })
404 .to_string(),
405 Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
406 },
407 }
408}
409
410/// The default, SYNCHRONOUS runner: runs on submit, stashes the reply for an
411/// immediate poll. Behavior-identical to the pre-seam in-process run.
412pub struct InlineRunner {
413 /// The scene-free history runner it owns (delta baseline lives here).
414 runner: crate::pipeline::SceneRunner,
415 /// Completed replies awaiting a poll. For Inline this holds exactly one entry
416 /// between a `submit_run` and the immediately-following `poll_run`.
417 pending: std::collections::VecDeque<RunReply>,
418 /// Completed measurement replies awaiting a poll (computed synchronously on
419 /// `submit_query`, popped on `poll_query`) — the synchronous mirror of the
420 /// thread runner's query buffer, so the object-info path resolves same-call.
421 query_pending: std::collections::VecDeque<MeasureReply>,
422 mesh_import_pending: std::collections::VecDeque<MeshImportReply>,
423 step_probe_pending: std::collections::VecDeque<StepProbeReply>,
424 topology_pending: std::collections::VecDeque<TopologyReply>,
425 sheet_lines_pending: std::collections::VecDeque<SheetLinesReply>,
426 plugin_action_pending: std::collections::VecDeque<PluginActionReply>,
427 plugin_install_pending: std::collections::VecDeque<PluginInstallReply>,
428}
429
430impl InlineRunner {
431 pub fn new() -> Self {
432 Self {
433 runner: crate::pipeline::SceneRunner::new(),
434 pending: std::collections::VecDeque::new(),
435 query_pending: std::collections::VecDeque::new(),
436 mesh_import_pending: std::collections::VecDeque::new(),
437 step_probe_pending: std::collections::VecDeque::new(),
438 topology_pending: std::collections::VecDeque::new(),
439 sheet_lines_pending: std::collections::VecDeque::new(),
440 plugin_action_pending: std::collections::VecDeque::new(),
441 plugin_install_pending: std::collections::VecDeque::new(),
442 }
443 }
444}
445
446impl HistoryRunner for InlineRunner {
447 fn submit_plugin_install(&mut self, request: PluginInstallRequest) -> bool {
448 self.plugin_install_pending.push_back(validate_plugins(request));
449 true
450 }
451 fn poll_plugin_install(&mut self) -> Option<PluginInstallReply> {
452 self.plugin_install_pending.pop_front()
453 }
454 fn sync_plugins(&mut self, revision: u64, fetch: &mut dyn FnMut() -> serde_json::Value) {
455 if self.runner.plugin_revision != Some(revision) {
456 self.runner.set_plugins(revision, fetch());
457 }
458 }
459 fn submit_plugin_action(&mut self, request: PluginActionRequest) -> bool {
460 self.plugin_action_pending.push_back(self.runner.plugin_action(request));
461 true
462 }
463 fn poll_plugin_action(&mut self) -> Option<PluginActionReply> {
464 self.plugin_action_pending.pop_front()
465 }
466
467 fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64) {
468 let output = self.runner.run(&request);
469 self.pending.push_back(RunReply { generation, output });
470 }
471
472 /// Nothing to send: Inline runs on the CALLER's thread against the CALLER's
473 /// kernel store, so the library the caller would hand over is already the
474 /// one this run resolves against. (Which is also why Inline never needs the
475 /// preflight — there is only one store, and the caller seeds it directly.)
476 fn sync_parts_library(
477 &mut self,
478 _revision: u64,
479 _fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
480 ) {
481 }
482
483 fn poll_run(&mut self) -> Option<RunReply> {
484 self.pending.pop_front()
485 }
486
487 fn submit_query(&mut self, query: MeasureQuery) {
488 let result = measure_json(&self.runner, &query);
489 self.query_pending.push_back(MeasureReply { id: query.id, result });
490 }
491
492 fn poll_query(&mut self) -> Option<MeasureReply> {
493 self.query_pending.pop_front()
494 }
495
496 fn submit_mesh_import(&mut self, request: MeshImportRequest) {
497 self.mesh_import_pending
498 .push_back(reconstruct_mesh(request));
499 }
500
501 fn poll_mesh_import(&mut self) -> Option<MeshImportReply> {
502 self.mesh_import_pending.pop_front()
503 }
504
505 fn submit_step_probe(&mut self, request: StepProbeRequest) {
506 self.step_probe_pending.push_back(probe_step(request));
507 }
508
509 fn poll_step_probe(&mut self) -> Option<StepProbeReply> {
510 self.step_probe_pending.pop_front()
511 }
512
513 fn submit_topology(&mut self, request: TopologyRequest) {
514 self.topology_pending.push_back(topology_reply(request));
515 }
516
517 fn poll_topology(&mut self) -> Option<TopologyReply> {
518 self.topology_pending.pop_front()
519 }
520
521 /// Runs the pass at once, on the caller's thread: an inline runner's
522 /// registry IS the caller's, so the answer is ready for the next poll.
523 fn submit_sheet_lines(&mut self, request: SheetLinesRequest) -> bool {
524 self.sheet_lines_pending.push_back(sheet_lines_reply(request));
525 true
526 }
527
528 fn poll_sheet_lines(&mut self) -> Option<SheetLinesReply> {
529 self.sheet_lines_pending.pop_front()
530 }
531
532 fn reset(&mut self) {
533 self.runner.reset();
534 }
535}
536
537impl Default for InlineRunner {
538 fn default() -> Self {
539 Self::new()
540 }
541}
542
543// ===========================================================================
544// Shared run/query PROTOCOL (M3b): the `Command`/`Reply` message pair and the
545// `process_command` step. Both async runners speak it — the native `ThreadRunner`
546// ships the enums over `mpsc` (no serialization), and the wasm `WorkerRunner`
547// serializes them to JSON for `postMessage`. Hence the serde derives (the enums
548// carry serde types: `HistoryRequest`/`MeasureQuery`/`RunReply`/`MeasureReply`)
549// and `process_command` being un-gated + shared so both drivers do the SAME work.
550// ===========================================================================
551
552/// A command sent main → runner (thread channel OR worker `postMessage`) over one
553/// ordered stream (so a `Reset` before a `Run` stays before it). `Run` carries the
554/// whole request; the driver coalesces consecutive `Run`s to shed a slider drag's
555/// backlog (the thread in `thread_main`, the worker's main side in `WorkerRunner`).
556#[derive(serde::Serialize, serde::Deserialize)]
557pub enum Command {
558 ValidatePlugins(PluginInstallRequest),
559 SetPlugins { revision: u64, packages: serde_json::Value },
560 PluginAction(PluginActionRequest),
561 Run {
562 request: brep_kernel::HistoryRequest,
563 generation: u64,
564 /// The [`brep_kernel::parts_library_revision`] this run was built
565 /// against. The runner refuses to execute a run stamped for a library
566 /// it does not hold — see [`process_command`].
567 #[serde(default)]
568 parts_library_revision: u64,
569 },
570 /// Install the parts library on the runner's OWN kernel store. Sent only
571 /// when the library actually changes (insert, document load, refresh),
572 /// never per run: it carries the whole embedded-part payload, which for an
573 /// imported STEP assembly is megabytes, and stringifying that on the
574 /// browser's main thread once per edit is what froze the UI.
575 ///
576 /// Deliberately its OWN command rather than an optional field on `Run`:
577 /// both drivers COALESCE consecutive runs (see [`thread_main`] and
578 /// `WorkerRunner::submit_run`), so a library riding on a run could be
579 /// dropped with it. A `SetPartsLibrary` is never coalesced away, and the
580 /// stream is ordered, so it always lands before the run that needs it.
581 SetPartsLibrary {
582 library: brep_kernel::PartsLibraryMap,
583 revision: u64,
584 },
585 Query(MeasureQuery),
586 MeshImport(MeshImportRequest),
587 StepProbe(StepProbeRequest),
588 Topology(TopologyRequest),
589 SheetLines(SheetLinesRequest),
590 Reset,
591}
592
593/// A reply sent runner → main; the main side demuxes it into per-kind buffers.
594#[derive(serde::Serialize, serde::Deserialize)]
595pub enum Reply {
596 ValidatedPlugins(PluginInstallReply),
597 PluginAction(PluginActionReply),
598 Run(RunReply),
599 Query(MeasureReply),
600 MeshImport(MeshImportReply),
601 StepProbe(StepProbeReply),
602 Topology(TopologyReply),
603 SheetLines(SheetLinesReply),
604 /// The runner REFUSED a run because its resident parts library could not
605 /// serve it (a part the request references is missing, or the run was
606 /// stamped for a different library revision). No geometry was touched; the
607 /// main side re-sends the library and re-submits. Refusing is the whole
608 /// point — running with the wrong library would be silently wrong
609 /// geometry, which is far worse than one extra round trip.
610 NeedPartsLibrary,
611 /// A feature of the in-flight run is about to execute (see
612 /// [`RunProgress`]). Posted from INSIDE `process_command`, before its
613 /// `Reply::Run`; the main side keeps only the latest.
614 Progress(RunProgress),
615}
616
617/// The probe itself — the one parse of a structured STEP import (see
618/// `EngineState::submit_step_probe`), run wherever the runner runs.
619fn probe_step(request: StepProbeRequest) -> StepProbeReply {
620 StepProbeReply {
621 id: request.id,
622 result: brep_kernel::read_step_assembly(&request.text),
623 }
624}
625
626fn reconstruct_mesh(request: MeshImportRequest) -> MeshImportReply {
627 let result = (|| {
628 use brep_reconstruction::stl_conversion::{
629 binary_stl_coordinate_precision_tolerance, convert_stl_mesh_to_step,
630 };
631 use brep_reconstruction::{Mesh, Vec3};
632
633 // A 3MF build's placements, kept only to name each body afterwards.
634 let mut instances = Vec::new();
635 let mut reader_messages = Vec::new();
636 let (mesh, positions, indices, coordinate_precision_tolerance) = match request.format {
637 MeshImportFormat::Stl => {
638 use brep_reconstruction::stl::{parse_stl_bytes, StlFormat, StlReadOptions};
639 let read_options = StlReadOptions {
640 weld_tolerance: (request.options.weld_tolerance >= 0.0)
641 .then_some(request.options.weld_tolerance),
642 };
643 let imported = parse_stl_bytes(&request.bytes, &read_options)
644 .map_err(|error| format!("STL import failed: {error}"))?;
645 let positions = imported
646 .mesh
647 .vertices
648 .iter()
649 .flat_map(|point| [point.x, point.y, point.z])
650 .collect::<Vec<_>>();
651 let indices = imported
652 .mesh
653 .triangles
654 .iter()
655 .flatten()
656 .copied()
657 .collect::<Vec<_>>();
658 let precision = if imported.format == StlFormat::Binary {
659 binary_stl_coordinate_precision_tolerance(&imported.mesh)
660 } else {
661 0.0
662 };
663 if !imported.dropped_facets.is_empty() {
664 reader_messages.push(format!(
665 "the STL reader dropped {} of {} facet(s) with no area: {}",
666 imported.dropped_facets.len(),
667 imported.source_triangle_count,
668 imported.dropped_facets.iter().take(8).map(ToString::to_string).collect::<Vec<_>>().join("; ")
669 ));
670 }
671 (imported.mesh, positions, indices, precision)
672 }
673 MeshImportFormat::Obj => {
674 let text = std::str::from_utf8(&request.bytes)
675 .map_err(|_| "OBJ import failed: file is not UTF-8 text".to_string())?;
676 let obj = brep_kernel::read_obj(text)
677 .map_err(|error| format!("OBJ import failed: {error}"))?;
678 let vertices = obj
679 .positions
680 .chunks_exact(3)
681 .map(|point| Vec3::new(point[0], point[1], point[2]))
682 .collect::<Vec<_>>();
683 let triangles = obj
684 .indices
685 .chunks_exact(3)
686 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
687 .collect::<Vec<_>>();
688 (
689 Mesh::new(vertices, triangles),
690 obj.positions,
691 obj.indices,
692 0.0,
693 )
694 }
695 // The 3MF reader has already applied the file's unit and every
696 // build/component transform, so what arrives here is the same kind
697 // of millimetre triangle soup an STL carries — including a build
698 // that places SEVERAL instances, which is several bodies in that
699 // one buffer. The chain splits a soup into its vertex-connected
700 // components and reconstructs each as its own body, so the whole
701 // build goes through in ONE call and the poses are already in the
702 // coordinates.
703 MeshImportFormat::ThreeMf => {
704 let model = brep_kernel::read_3mf(&request.bytes)
705 .map_err(|error| format!("3MF import failed: {error}"))?;
706 instances = model.instances;
707 let vertices = model
708 .positions
709 .chunks_exact(3)
710 .map(|point| Vec3::new(point[0], point[1], point[2]))
711 .collect::<Vec<_>>();
712 let triangles = model
713 .indices
714 .chunks_exact(3)
715 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
716 .collect::<Vec<_>>();
717 (
718 Mesh::new(vertices, triangles),
719 model.positions,
720 model.indices,
721 0.0,
722 )
723 }
724 };
725 let mut options = request.options;
726 options.coordinate_precision_tolerance = options.coordinate_precision_tolerance
727 .max(coordinate_precision_tolerance);
728 convert_stl_mesh_to_step(
729 &mesh,
730 &positions,
731 Some(&indices),
732 &options,
733 "Imported mesh",
734 "MM",
735 "",
736 )
737 .map(|mut output| {
738 name_bodies(&mut output.report, &instances);
739 output.report.messages.splice(0..0, reader_messages);
740 output
741 })
742 .map_err(|error| format!("RANSAC reconstruction failed: {error}"))
743 })();
744 MeshImportReply {
745 id: request.id,
746 result,
747 }
748}
749
750/// Give each body the name of the 3MF object its first source triangle came
751/// from, so a refusal can say WHICH object is missing. Bodies are found in the
752/// flattened soup the reader laid out instance by instance.
753fn name_bodies(
754 report: &mut brep_reconstruction::stl_conversion::StlConversionReport,
755 instances: &[brep_kernel::ThreeMfInstance],
756) {
757 for component in &mut report.components {
758 component.source_name = instances
759 .iter()
760 .find(|instance| {
761 (instance.first_triangle..instance.first_triangle + instance.triangle_count)
762 .contains(&component.first_triangle)
763 })
764 .and_then(|instance| instance.name.clone());
765 }
766}
767
768/// The notice for bodies a multi-body import left out, one clause per body,
769/// named by the source's own name when it has one; `None` when every body was
770/// built.
771pub(crate) fn refused_bodies_notice(
772 report: &brep_reconstruction::stl_conversion::StlConversionReport,
773) -> Option<String> {
774 let refused = report
775 .components
776 .iter()
777 .filter_map(|component| {
778 component.refusal.as_ref().map(|reason| {
779 let body = match &component.source_name {
780 Some(name) => format!("body {} \"{name}\"", component.index + 1),
781 None => format!(
782 "body {} (from source triangle {})",
783 component.index + 1,
784 component.first_triangle
785 ),
786 };
787 format!("{body}: {reason}")
788 })
789 })
790 .collect::<Vec<_>>();
791 (!refused.is_empty()).then(|| {
792 format!(
793 "mesh import: {} of {} bodies could not be reconstructed and are NOT in the model — {}",
794 refused.len(),
795 report.components.len(),
796 refused.join("; ")
797 )
798 })
799}
800
801/// Execute ONE [`Command`] against `runner` and return the [`Reply`] it produces,
802/// if any. The shared step both async drivers run: the native [`thread_main`]
803/// calls it for each (post-coalescing) command on the runner thread; the wasm
804/// `WorkerRunner`'s `worker_entry` calls it per `postMessage` on the worker.
805/// `Run` → a [`Reply::Run`]; `Query` → a [`Reply::Query`]; `Reset` drops the delta
806/// baseline AND the runner's OWN kernel history cache (the resident registry lives
807/// with the runner — thread or worker — not on main) and yields no reply.
808///
809/// `progress` is called before every feature a `Run` actually executes, with
810/// the report the driver should ship as [`Reply::Progress`]; returning `false`
811/// stops the run at that boundary (the native thread's cooperative cancel —
812/// the worker is terminated instead and always returns `true`).
813pub fn process_command(
814 runner: &mut crate::pipeline::SceneRunner,
815 command: Command,
816 progress: &mut dyn FnMut(RunProgress) -> bool,
817) -> Option<Reply> {
818 match command {
819 Command::ValidatePlugins(request) => Some(Reply::ValidatedPlugins(validate_plugins(request))),
820 Command::SetPlugins { revision, packages } => {
821 runner.set_plugins(revision, packages);
822 None
823 }
824 Command::PluginAction(request) => Some(Reply::PluginAction(runner.plugin_action(request))),
825 Command::Run {
826 request,
827 generation,
828 parts_library_revision,
829 } => {
830 // PREFLIGHT (see `Reply::NeedPartsLibrary`). Two independent
831 // checks, because neither alone is enough:
832 //
833 // * the revision stamp catches CHANGED content the sender knows
834 // about but this store has not received;
835 // * `missing_library_parts` catches content that is simply GONE
836 // here — the orphan GC at the end of every run drops entries no
837 // ACOMP in THAT run referenced, so an undo to zero components
838 // empties this store while the sender's (which never ran) keeps
839 // everything and its revision never moves. Only looking at the
840 // actual content sees that.
841 let stale = match runner.parts_library_revision {
842 Some(installed) => installed != parts_library_revision,
843 // Nothing installed yet. Accept a run stamped 0 — that is
844 // either an empty library or a caller driving `submit_run`
845 // directly (the runner tests); the content preflight below is
846 // what actually protects the run. A non-zero stamp with nothing
847 // installed IS a gap: refuse it.
848 None => parts_library_revision != 0,
849 };
850 // Only a part the sender DID send and this store has since lost is
851 // worth asking for again. One it never sent is a dangling reference
852 // in the document itself: run it, so the ACOMP feature reports it
853 // the way it always has. (An install inserts every incoming name,
854 // so right after one this set can only be empty — which is what
855 // makes the ask-and-retry terminate.)
856 let recoverable = brep_kernel::missing_library_parts(&request)
857 .iter()
858 .any(|name| runner.parts_library_names.contains(name));
859 if stale || recoverable {
860 runner.parts_library_revision = None;
861 return Some(Reply::NeedPartsLibrary);
862 }
863 let output = runner.run_observed(&request, &mut |event| {
864 progress(RunProgress {
865 generation,
866 index: event.index,
867 total: event.total,
868 feature_id: event.id.to_string(),
869 feature_type: event.feature_type.to_string(),
870 })
871 });
872 Some(Reply::Run(RunReply { generation, output }))
873 }
874 Command::SetPartsLibrary { library, revision } => {
875 runner.parts_library_names = library.keys().cloned().collect();
876 brep_kernel::install_parts_library(&library);
877 runner.parts_library_revision = Some(revision);
878 None
879 }
880 Command::Query(query) => {
881 let result = measure_json(runner, &query);
882 Some(Reply::Query(MeasureReply { id: query.id, result }))
883 }
884 Command::MeshImport(request) => Some(Reply::MeshImport(reconstruct_mesh(request))),
885 Command::StepProbe(request) => Some(Reply::StepProbe(probe_step(request))),
886 Command::Topology(request) => Some(Reply::Topology(topology_reply(request))),
887 Command::SheetLines(request) => Some(Reply::SheetLines(sheet_lines_reply(request))),
888 Command::Reset => {
889 // A document switch: drop the delta baseline AND this runner's OWN kernel
890 // history cache (the resident registry lives here, not on main), mirroring
891 // `set_history_json`'s main-thread clear so a new model rebuilds fully and
892 // the old model's handles are freed.
893 runner.reset();
894 brep_kernel::clear_history_cache();
895 None
896 }
897 }
898}
899
900// ===========================================================================
901// ThreadRunner (M2b): a persistent std::thread that OWNS the SceneRunner, so a
902// history run — and per-object measurement queries — execute OFF the main thread
903// and the native UI stays responsive during a run AND during selection. Native
904// only: `std::thread` + `std::sync::mpsc` do not exist on wasm32 (M3b lands a
905// worker impl behind this same trait), so the whole thing is cfg-gated out there.
906// ===========================================================================
907
908/// The persistent-thread runner. `submit_*`/`reset` push [`Command`]s down the
909/// channel; `poll_*` first DRAIN every ready [`Reply`] into the two demux buffers,
910/// then pop the matching one. The `SceneRunner` (and thus the kernel's resident
911/// registry it warms) lives ENTIRELY on the thread — it is never shared — so the
912/// only cross-thread traffic is the `Send` command/reply payloads.
913#[cfg(not(target_arch = "wasm32"))]
914pub struct ThreadRunner {
915 /// Main → thread. `Option` so [`Drop`] can take + drop it, ending the thread's
916 /// blocking `recv` after already-submitted work finishes.
917 tx: Option<std::sync::mpsc::Sender<Command>>,
918 /// Thread → main.
919 rx: std::sync::mpsc::Receiver<Reply>,
920 /// Dropped without joining so closing a busy preview never stalls the UI.
921 handle: Option<std::thread::JoinHandle<()>>,
922 /// Demuxed completed run replies awaiting `poll_run`.
923 run_buf: std::collections::VecDeque<RunReply>,
924 /// Demuxed completed measurement replies awaiting `poll_query`.
925 query_buf: std::collections::VecDeque<MeasureReply>,
926 mesh_import_buf: std::collections::VecDeque<MeshImportReply>,
927 step_probe_buf: std::collections::VecDeque<StepProbeReply>,
928 topology_buf: std::collections::VecDeque<TopologyReply>,
929 sheet_lines_buf: std::collections::VecDeque<SheetLinesReply>,
930 plugin_action_buf: std::collections::VecDeque<PluginActionReply>,
931 plugin_install_buf: std::collections::VecDeque<PluginInstallReply>,
932 sent_plugin_revision: Option<u64>,
933 /// Progress reports of the in-flight run, oldest first (`poll_progress`
934 /// keeps the newest).
935 progress_buf: std::collections::VecDeque<RunProgress>,
936 /// The parts-library revision last SENT down the channel (`None` = never,
937 /// or the thread dropped it). Lives here rather than on the caller so
938 /// "reset forgets the library" is a local property of this object.
939 sent_library_revision: Option<u64>,
940 /// The thread refused a run for want of its library (drained by
941 /// [`HistoryRunner::poll_library_request`]).
942 library_requested: bool,
943 /// The cooperative stop flag THIS thread checks between features. Each
944 /// spawn gets its own, so a cancelled thread keeps its raised flag while
945 /// the replacement starts clean.
946 stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
947}
948
949#[cfg(not(target_arch = "wasm32"))]
950impl ThreadRunner {
951 pub fn new() -> Self {
952 let (tx, cmd_rx) = std::sync::mpsc::channel::<Command>();
953 let (reply_tx, rx) = std::sync::mpsc::channel::<Reply>();
954 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
955 let thread_stop = stop.clone();
956 let handle = std::thread::Builder::new()
957 .name("brep-history-runner".to_string())
958 .spawn(move || thread_main(cmd_rx, reply_tx, thread_stop))
959 .expect("spawn brep-history-runner thread");
960 Self {
961 tx: Some(tx),
962 rx,
963 handle: Some(handle),
964 run_buf: std::collections::VecDeque::new(),
965 query_buf: std::collections::VecDeque::new(),
966 mesh_import_buf: std::collections::VecDeque::new(),
967 step_probe_buf: std::collections::VecDeque::new(),
968 topology_buf: std::collections::VecDeque::new(),
969 sheet_lines_buf: std::collections::VecDeque::new(),
970 plugin_action_buf: std::collections::VecDeque::new(),
971 plugin_install_buf: std::collections::VecDeque::new(),
972 sent_plugin_revision: None,
973 progress_buf: std::collections::VecDeque::new(),
974 sent_library_revision: None,
975 library_requested: false,
976 stop,
977 }
978 }
979
980 /// Pull every ready reply off the channel and demux it into the run/query
981 /// buffers (so a `poll_run` never swallows a query reply and vice versa).
982 fn drain(&mut self) {
983 while let Ok(reply) = self.rx.try_recv() {
984 match reply {
985 Reply::ValidatedPlugins(reply) => self.plugin_install_buf.push_back(reply),
986 Reply::PluginAction(reply) => self.plugin_action_buf.push_back(reply),
987 Reply::Run(run) => self.run_buf.push_back(run),
988 Reply::Query(query) => self.query_buf.push_back(query),
989 Reply::MeshImport(reply) => self.mesh_import_buf.push_back(reply),
990 Reply::StepProbe(reply) => self.step_probe_buf.push_back(reply),
991 Reply::Topology(reply) => self.topology_buf.push_back(reply),
992 Reply::SheetLines(reply) => self.sheet_lines_buf.push_back(reply),
993 Reply::Progress(progress) => self.progress_buf.push_back(progress),
994 Reply::NeedPartsLibrary => {
995 // The thread dropped (or never had) the library this run
996 // needs. Forget what we believe it holds so the next
997 // `sync_parts_library` re-installs, and flag the refused
998 // run for the caller to re-submit.
999 self.sent_library_revision = None;
1000 self.library_requested = true;
1001 }
1002 }
1003 }
1004 }
1005}
1006
1007#[cfg(not(target_arch = "wasm32"))]
1008impl Default for ThreadRunner {
1009 fn default() -> Self {
1010 Self::new()
1011 }
1012}
1013
1014/// The runner thread's whole life: block on the next command, batch it with every
1015/// other command already queued, COALESCE consecutive `Run`s (a `Run` immediately
1016/// followed by another `Run` — with no `Query`/`Reset` between — is dropped; only
1017/// the last of each consecutive group runs), then process the batch IN ORDER so a
1018/// `Reset` or a `Query` interleaved between two runs keeps its place. Exits when
1019/// the command sender is dropped (`recv` errors) or the reply receiver is gone (a
1020/// `send` errors — the main side went away).
1021///
1022/// `stop` is the cooperative cancel: raised by [`ThreadRunner::cancel`] on a
1023/// thread that has already been abandoned, it is checked before every feature
1024/// a run executes, so the abandoned thread finishes the feature it is on and
1025/// exits at the next boundary instead of running the rest of the history for
1026/// a receiver that is gone.
1027#[cfg(not(target_arch = "wasm32"))]
1028fn thread_main(
1029 cmd_rx: std::sync::mpsc::Receiver<Command>,
1030 reply_tx: std::sync::mpsc::Sender<Reply>,
1031 stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
1032) {
1033 let mut runner = crate::pipeline::SceneRunner::new();
1034 while let Ok(first) = cmd_rx.recv() {
1035 if stop.load(std::sync::atomic::Ordering::Relaxed) {
1036 return;
1037 }
1038 // Gather this command plus everything else already waiting.
1039 let mut batch = vec![first];
1040 loop {
1041 match cmd_rx.try_recv() {
1042 Ok(command) => batch.push(command),
1043 Err(_) => break, // Empty or Disconnected — process what we have.
1044 }
1045 }
1046 // A Run immediately followed by another Run is coalesced away (its result
1047 // would be overwritten before anything observed it); a Run followed by a
1048 // Query/Reset/end still runs, so interleaved work sees the right geometry.
1049 let mut run_here: Vec<bool> = vec![true; batch.len()];
1050 for i in 0..batch.len() {
1051 if matches!(batch[i], Command::Run { .. })
1052 && matches!(batch.get(i + 1), Some(Command::Run { .. }))
1053 {
1054 run_here[i] = false;
1055 }
1056 }
1057 // Process the KEPT commands in order through the SHARED `process_command`
1058 // (byte-identical work to the wasm worker's per-message step), sending each
1059 // reply it produces; a coalesced-away Run is skipped without touching the
1060 // runner, so interleaved Query/Reset still see the right geometry.
1061 for (i, command) in batch.into_iter().enumerate() {
1062 if !run_here[i] {
1063 continue;
1064 }
1065 let mut progress = |report: RunProgress| {
1066 // A lost receiver means the main side abandoned this thread:
1067 // stop at this boundary rather than finishing for nobody.
1068 reply_tx.send(Reply::Progress(report)).is_ok()
1069 && !stop.load(std::sync::atomic::Ordering::Relaxed)
1070 };
1071 if let Some(reply) = process_command(&mut runner, command, &mut progress) {
1072 if reply_tx.send(reply).is_err() {
1073 return; // The reply receiver is gone — the main side went away.
1074 }
1075 }
1076 }
1077 }
1078}
1079
1080#[cfg(not(target_arch = "wasm32"))]
1081impl HistoryRunner for ThreadRunner {
1082 fn submit_plugin_install(&mut self, request: PluginInstallRequest) -> bool {
1083 self.tx.as_ref().is_some_and(|tx| tx.send(Command::ValidatePlugins(request)).is_ok())
1084 }
1085 fn poll_plugin_install(&mut self) -> Option<PluginInstallReply> {
1086 self.drain();
1087 self.plugin_install_buf.pop_front()
1088 }
1089 fn sync_plugins(&mut self, revision: u64, fetch: &mut dyn FnMut() -> serde_json::Value) {
1090 if self.sent_plugin_revision != Some(revision) {
1091 if let Some(tx) = &self.tx {
1092 if tx.send(Command::SetPlugins { revision, packages: fetch() }).is_ok() {
1093 self.sent_plugin_revision = Some(revision);
1094 }
1095 }
1096 }
1097 }
1098 fn submit_plugin_action(&mut self, request: PluginActionRequest) -> bool {
1099 self.tx.as_ref().is_some_and(|tx| tx.send(Command::PluginAction(request)).is_ok())
1100 }
1101 fn poll_plugin_action(&mut self) -> Option<PluginActionReply> {
1102 self.drain();
1103 self.plugin_action_buf.pop_front()
1104 }
1105
1106 fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64) {
1107 if let Some(tx) = &self.tx {
1108 let _ = tx.send(Command::Run {
1109 request,
1110 generation,
1111 parts_library_revision: self.sent_library_revision.unwrap_or(0),
1112 });
1113 }
1114 }
1115
1116 fn sync_parts_library(
1117 &mut self,
1118 revision: u64,
1119 fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
1120 ) {
1121 if self.sent_library_revision == Some(revision) {
1122 return;
1123 }
1124 if let Some(tx) = &self.tx {
1125 let _ = tx.send(Command::SetPartsLibrary {
1126 library: fetch(),
1127 revision,
1128 });
1129 self.sent_library_revision = Some(revision);
1130 }
1131 }
1132
1133 fn poll_library_request(&mut self) -> bool {
1134 self.drain();
1135 std::mem::take(&mut self.library_requested)
1136 }
1137
1138 fn poll_run(&mut self) -> Option<RunReply> {
1139 self.drain();
1140 self.run_buf.pop_front()
1141 }
1142
1143 fn submit_query(&mut self, query: MeasureQuery) {
1144 if let Some(tx) = &self.tx {
1145 let _ = tx.send(Command::Query(query));
1146 }
1147 }
1148
1149 fn poll_query(&mut self) -> Option<MeasureReply> {
1150 self.drain();
1151 self.query_buf.pop_front()
1152 }
1153
1154 fn submit_mesh_import(&mut self, request: MeshImportRequest) {
1155 if let Some(tx) = &self.tx {
1156 let _ = tx.send(Command::MeshImport(request));
1157 }
1158 }
1159
1160 fn poll_mesh_import(&mut self) -> Option<MeshImportReply> {
1161 self.drain();
1162 self.mesh_import_buf.pop_front()
1163 }
1164
1165 fn submit_step_probe(&mut self, request: StepProbeRequest) {
1166 if let Some(tx) = &self.tx {
1167 let _ = tx.send(Command::StepProbe(request));
1168 }
1169 }
1170
1171 fn poll_step_probe(&mut self) -> Option<StepProbeReply> {
1172 self.drain();
1173 self.step_probe_buf.pop_front()
1174 }
1175
1176 fn submit_topology(&mut self, request: TopologyRequest) {
1177 if let Some(tx) = &self.tx {
1178 let _ = tx.send(Command::Topology(request));
1179 }
1180 }
1181
1182 fn poll_topology(&mut self) -> Option<TopologyReply> {
1183 self.drain();
1184 self.topology_buf.pop_front()
1185 }
1186
1187 fn submit_sheet_lines(&mut self, request: SheetLinesRequest) -> bool {
1188 match &self.tx {
1189 Some(tx) => tx.send(Command::SheetLines(request)).is_ok(),
1190 None => false,
1191 }
1192 }
1193
1194 fn poll_sheet_lines(&mut self) -> Option<SheetLinesReply> {
1195 self.drain();
1196 self.sheet_lines_buf.pop_front()
1197 }
1198
1199 fn poll_progress(&mut self) -> Option<RunProgress> {
1200 self.drain();
1201 let latest = self.progress_buf.pop_back();
1202 self.progress_buf.clear();
1203 latest
1204 }
1205
1206 /// Abandon the thread: raise its stop flag, close its channels, and spawn a
1207 /// fresh thread with a fresh registry. The old thread cannot be interrupted
1208 /// inside a feature — it finishes the one it is on (burning CPU until
1209 /// then), sees the flag, and exits. Always `true`: there is always a
1210 /// thread to replace.
1211 fn cancel(&mut self) -> bool {
1212 self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
1213 self.tx.take();
1214 self.handle.take();
1215 *self = Self::new();
1216 true
1217 }
1218
1219 fn reset(&mut self) {
1220 self.sent_plugin_revision = None;
1221 self.plugin_action_buf.clear();
1222 if let Some(tx) = &self.tx {
1223 let _ = tx.send(Command::Reset);
1224 }
1225 // A reset is a wholesale document switch: any replies still buffered from
1226 // the old model are stale — drop them (a fresh run/query supersedes).
1227 self.run_buf.clear();
1228 self.query_buf.clear();
1229 self.mesh_import_buf.clear();
1230 self.step_probe_buf.clear();
1231 self.topology_buf.clear();
1232 self.sheet_lines_buf.clear();
1233 self.progress_buf.clear();
1234 // `Command::Reset` clears the thread's kernel store, library included,
1235 // so what we believe it holds is void.
1236 self.sent_library_revision = None;
1237 self.library_requested = false;
1238 }
1239}
1240
1241#[cfg(not(target_arch = "wasm32"))]
1242impl Drop for ThreadRunner {
1243 fn drop(&mut self) {
1244 // Closing a document or cancelling an import preview must not block
1245 // the UI on reconstruction. Closing the channel lets the worker exit
1246 // and release its registry once already-submitted work finishes.
1247 self.tx.take();
1248 self.handle.take();
1249 }
1250}
1251