1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! The editor-side controller for the solid preview.
//!
//! Owns a [`mesh_cache::MeshCache`] and a [`raster::SolidRasterizer`] on one
//! dedicated worker thread, and coalesces a burst of redraw requests (an orbit
//! drag, a fast-typed edit) down to whichever was submitted LAST, via the same
//! [`RedrawGate`] contract `bridge::render_thread::frame_helpers::push_frame_to_ui`
//! already relies on for the path-traced view.
//!
//! # Threading
//!
//! [`SolidPreviewState::request_redraw`]/[`SolidPreviewState::request_replan`] must be
//! called from the UI thread and return immediately: they only submit into the
//! [`RedrawGate`] and, if this call won the race to be the one that must act, wake
//! the worker thread over a plain [`std::sync::mpsc`] channel. The worker thread
//! (spawned lazily on the first call) owns the [`mesh_cache::MeshCache`] and
//! [`raster::SolidRasterizer`] -- neither is `Sync`, and there is no reason for
//! either to be: exactly one thread ever touches them. Once a frame is ready, the
//! worker calls [`PreviewSink::apply`], whose only real (Slint-backed)
//! implementation hops back to the UI thread via `slint::Weak::upgrade_in_event_loop`
//! to do the actual image swap -- that hop lives on the trait implementation's
//! side, not in this module.
//!
//! # Planning happens on a WORKER thread, never the UI thread
//!
//! `resolve_dirty` was measured at 1.89s for a single-tier edit on the 103-tier
//! "CrackOtto-Step" design, and calling it on the UI thread would freeze the editor
//! that long on every keystroke. [`request::ReplanRequest`] carries everything
//! [`super::live_update::plan_preview`] needs (a `Design` clone, dirty tier set,
//! previous solve, camera/size/selection/RI/view-mode). The
//! UI thread never blocks on any of that; [`PreviewSink::apply`] hands the result
//! back asynchronously.
//!
//! # Two workers: planning vs. rendering
//!
//! Splitting `solve`/`resolve_dirty` and mesh-build/rasterize across separate
//! threads prevents `Reproject` requests from stalling behind slow solves.
//!
//! - The **PLAN worker** ([`plan_worker`]) is the ONLY
//! thread that ever calls [`plan_worker::build_planned_frame`]/`live_update::plan_preview` --
//! the expensive half. It has its own [`RedrawGate`] (`SolidPreviewState::
//! plan_gate`), which coalesces a burst of edits to "whichever solve is
//! currently running, then the latest one queued behind it" as it always did,
//! except now that queue is entirely separate from the render worker's own.
//! - The **RENDER worker** ([`render`]) still owns the
//! [`mesh_cache::MeshCache`]/[`raster::SolidRasterizer`] pair and is the only
//! thread that ever touches them (neither is `Sync`) -- it now handles THREE
//! request kinds instead of two: [`request::RedrawRequest::Reproject`],
//! [`request::RedrawRequest::UpdateFacetOverlay`], and the plan worker's own finished
//! [`request::RedrawRequest::Planned`] frame. None of the three ever calls
//! `plan_preview`/`Design::solve`, so this worker's own queue is never blocked
//! by a slow solve -- a camera drag submitted mid-solve renders against the
//! last-known planes immediately, exactly like an ordinary `Reproject` always
//! has.
//!
//! The mesh check for "Unbounded" status is split: [`plan_worker::build_planned_frame`]
//! (PLAN worker) does everything else; [`state::resolve_planned_state`] (RENDER worker)
//! finishes once it knows whether the arrangement closes, reusing the exact same
//! `mesh_cache.get_or_build` call immediately after -- guaranteed cache hit.
//!
//! The PLAN worker hands a finished [`request::PlannedFrame`] to the RENDER worker through
//! [`SolidPreviewState::submit`] -- the SAME lazy-spawn-and-wake path a
//! `Reproject`/`UpdateFacetOverlay` call already uses -- via a [`std::sync::Weak`] handle
//! back to this same `SolidPreviewState` ([`SolidPreviewState::self_weak`]),
//! rather than duplicating that spawn-on-first-use contract a second time.
//!
//! # Where `last_solved` lives
//!
//! `EditorState` is `Rc<RefCell<..>>`, which is why it cannot be reached from
//! [`PreviewSink::apply`]'s WORKER-thread call, and `PreviewSink` itself must stay
//! `Send + Sync` -- an `Rc`/`RefCell` cannot be a field of a type with that bound.
//! So the round trip goes through the wiring layer instead: [`PreviewSink::apply`]
//! is handed the freshly solved [`super::live_update::PreviewPlan::solved`] (`Send`
//! plain data), and `gui::mod`'s real [`PreviewSink`] stashes it into a plain
//! `Arc<Mutex<Option<Vec<SolvedTier>>>>` shared with `gui::editor`'s callbacks,
//! which read it back out building the next [`request::ReplanRequest`].
//!
//! # `PreviewSink`: kept generic over Slint on purpose
//!
//! [`SolidPreviewState`] is built against the [`PreviewSink`] trait rather than a
//! `slint::Weak<MainWindow>` directly, so a fake, synchronous sink can drive this
//! module's tests with no Slint window (see `tests::FakeSink`).
//!
//! # The pipeline itself lives in `indicatrix_solid::preview`
//!
//! The planner (`build_planned_frame`), the request types, the RENDER worker's
//! state machine (`WorkerMemory`/`resolve_request_state`) and its draw step
//! (`render_request`) moved to the shared crate so the web app runs the very same
//! functions on its main thread; this module keeps the threads, the gates and the
//! Slint pixel-buffer conversion, and re-exports the moved items at their old
//! paths. `tests::pins` holds the byte-identity pins recorded before the move.
use FacetMap;
use ;
use crateRedrawGate;
pub use SolidPreviewState;
pub use ReplanRequest;
pub use ;
pub use ;