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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! The editor-side controller struct itself ([`SolidPreviewState`]) and its
//! UI-thread-facing entry points: construction, the cheap camera-follow
//! reproject/facet-overlay submissions, the replan submission, and the tier-
//! cutoff cache. See the parent module's doc comment for the two-worker split
//! these methods feed into.
use super::{
RedrawGate,
request::{PlanJob, RedrawRequest, ReplanRequest},
sink::PreviewSink,
types::{CameraPose, FacetOverlay},
};
use glam::Vec3;
use indicatrix_cut_core::{Design, ManufacturabilityWarning};
use indicatrix_solid::preview::{Outlines, SharedOutlines, StoneGeometryBuf};
use std::sync::{
Arc, Mutex, PoisonError, Weak,
atomic::{AtomicU64, Ordering},
mpsc::Sender,
};
/// The editor-side controller described in the parent module's doc comment.
pub struct SolidPreviewState {
pub(super) sink: Arc<dyn PreviewSink>,
/// Consumed only by the RENDER worker (`super::render`'s `spawn_worker`) --
/// the two-worker split, see the module doc comment.
pub(super) gate: Arc<RedrawGate<RedrawRequest>>,
/// Wake channel for the RENDER worker, created lazily on the first
/// `request_*`/finished-plan call. `None` until then, so a `SolidPreviewState`
/// never asked to redraw never spawns a thread.
pub(super) wake: Mutex<Option<Sender<()>>>,
/// Consumed only by the PLAN worker (`super::plan_worker`'s `spawn_plan_worker`)
/// -- a Separate queue from `gate` so a slow solve never blocks `Reproject` requests.
pub(super) plan_gate: Arc<RedrawGate<PlanJob>>,
/// [`Self::wake`]'s counterpart for the PLAN worker, created lazily the same
/// way on the first [`Self::request_replan`] call.
pub(super) plan_wake: Mutex<Option<Sender<()>>>,
/// A weak handle to this same value, filled in immediately after
/// construction -- lets the PLAN worker (which holds no other reference back
/// to `Self`) hand a finished `super::request::PlannedFrame` to the RENDER
/// worker through `super::render`'s `submit`, the exact same lazy-spawn-and-wake
/// path a `Reproject` call already uses, rather than duplicating that contract
/// a second time. `Weak`, not `Arc`: the PLAN worker thread must never be the
/// reason a `SolidPreviewState` outlives every caller's own handle to it.
pub(super) self_weak: Mutex<Weak<Self>>,
/// "Show through tier N" slider: `Some(n)` truncates the plane arrangement
/// to `design.tiers[..=n]`. Cached here for UI access via [`Self::set_tier_cutoff`].
pub(super) tier_cutoff: Mutex<Option<usize>>,
/// The Cut slider: `Some(k)` draws the stone after the first `k` cutting steps
/// (`Design::preview_steps`), `Some(0)` being the rough, `None` the finished stone.
/// Wins over [`Self::tier_cutoff`] in a replan; set by [`Self::set_cut_steps`].
pub(super) cut_steps: Mutex<Option<usize>>,
/// bumped by `gui::editor::auto_solve::scheduling::
/// reset_for_new_design` on every wholesale design replacement (New/Load
/// Selected/Open) via [`Self::bump_generation_floor`] -- lets the
/// PLAN worker (`super::plan_worker::spawn_plan_worker`) tell a `PlanJob`
/// queued or in flight for the design being REPLACED apart from one for
/// the design that replaced it, even though `self.plan_gate`
/// (`super::RedrawGate`) itself only ever coalesces to "the latest queued
/// job," never "the latest queued job for the CURRENT design." A `PlanJob`
/// whose own `generation` is below this floor is dropped rather than
/// solved and handed back as a frame.
pub(super) generation_floor: Arc<AtomicU64>,
/// The Slice tool's provisional-tier outline and the drag-follower outline, shared
/// with the RENDER worker's `WorkerMemory::outlines`. Written synchronously by
/// [`Self::set_outlines`] (from the overlay's `provisional` / `moved` fields) and
/// read by the worker when it DRAWS, so a `Planned` request that
/// rebuilds the style, or a later request that supersedes an overlay update in
/// the "latest wins" gate, can neither drop nor resurrect them.
pub(super) outlines: SharedOutlines,
/// When `Some`, replaces the `planes` of every [`Self::request_redraw_with_gear`]
/// call: the Slice tool sets it to the provisional design's planes so a camera
/// orbit, a view-mode switch or a background-solve redraw (all of which reproject
/// the COMMITTED `RenderContext::active_planes`) keeps showing the provisional
/// facet instead of snapping back to the committed stone.
pub(super) planes_override: Mutex<Option<Vec<(Vec3, f32)>>>,
/// The manufacturability findings the PLAN worker computed for its latest planned
/// designs. The worker writes an entry right AFTER it hands the frame on
/// ([`super::plan_worker`]: a frame never waits for the pass) and also delivers the
/// findings to the sink as their own update ([`super::PreviewSink::apply_findings`]).
/// The render worker looks the entry up when a planned frame reaches it -- it is there
/// only when the pass beat the render -- and copies it onto every frame it draws from
/// then on (the replan itself, but also the camera-follow reproject and overlay frames
/// that carry the last plan's masts forward), so the UI thread only DISPLAYS the
/// findings. Check 6 of the pass builds the stone's solid and the tools' polytopes,
/// which is far too slow for a UI thread -- see
/// `indicatrix_cut_core::manufacturability::concave_tool_warnings`.
pub(super) warnings: SharedWarnings,
}
/// One planned design's manufacturability findings, found again by the very `Arc` the plan
/// job carried (the planned frame hands the same allocation on), never by generation.
///
/// The editor's generation moves only when the design does, so a tier selection, a Cut slider
/// move or an idle replan plans the same design again under the same generation, with a fresh
/// `Arc`. Such a plan finds no entry of an earlier plan here, so its frame goes out without
/// findings unless its own pass beats the render. `gui::solid_sink`'s `LateRows` is what
/// brings the generation's findings back to its rows, from the copy it keeps.
pub(super) struct PlanFindings {
/// The design the findings were computed for. Held so the allocation cannot be freed and
/// reused for another design while this entry can still be looked up.
pub(super) design: Arc<Design>,
pub(super) warnings: Arc<Vec<ManufacturabilityWarning>>,
}
/// The few most recent [`PlanFindings`], newest last -- see [`SolidPreviewState::warnings`].
pub(super) type SharedWarnings = Arc<Mutex<Vec<PlanFindings>>>;
impl SolidPreviewState {
/// Builds a controller that hands every finished frame to `sink`. No thread is
/// spawned yet -- see [`Self::request_redraw`].
#[must_use]
pub fn new(sink: Arc<dyn PreviewSink>) -> Arc<Self> {
let state = Arc::new(Self {
sink,
gate: Arc::new(RedrawGate::new()),
wake: Mutex::new(None),
plan_gate: Arc::new(RedrawGate::new()),
plan_wake: Mutex::new(None),
self_weak: Mutex::new(Weak::new()),
tier_cutoff: Mutex::new(None),
cut_steps: Mutex::new(None),
generation_floor: Arc::new(AtomicU64::new(0)),
outlines: Arc::new(Mutex::new(Outlines::default())),
planes_override: Mutex::new(None),
warnings: Arc::new(Mutex::new(Vec::new())),
});
*state
.self_weak
.lock()
.unwrap_or_else(PoisonError::into_inner) = Arc::downgrade(&state);
state
}
/// Submits one cheap camera-follow redraw (see `super::request::RedrawRequest::
/// Reproject`) and returns immediately; the finished frame reaches
/// `super::sink::PreviewSink::apply` asynchronously. `size` must be the
/// viewport's LOGICAL size (points, not render resolution). `view_mode` (0
/// Solid / 1 Path-traced / 2 Both / 3 Diagram) decides whether the worker also
/// renders the "Both" mode's edges layer, or the Diagram mode's 2D facet
/// diagram, this frame -- a Diagram-mode request rebuilds the diagram at the
/// worker's last-known gear info/labels (see `super::state::DiagramMemory`)
/// since a `Reproject` request carries no `Design`. A burst of calls coalesces
/// to exactly the LAST one submitted -- see `RedrawGate`'s doc comment.
///
/// A thin `gear: None` wrapper over [`Self::request_redraw_with_gear`], kept at
/// this exact signature so every pre-existing caller (`gui::editor::view::
/// refresh_viewport`, `gui::editor::auto_solve`'s background-solve resubmit)
/// keeps compiling unchanged. `None` means the worker's last-known Diagram-mode
/// gear info is left as-is, which is only correct when that caller's design is
/// the same one the worker last replanned -- prefer
/// [`Self::request_redraw_with_gear`] with the loaded design's actual gear info
/// (`bridge::render_thread::RenderContext::design_gear`) wherever that is
/// available, as `gui::render::camera_lighting::resubmit_at_current_pose`
/// already does.
pub fn request_redraw(
&self,
planes: &[(Vec3, f32)],
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
) {
self.request_redraw_with_gear(planes, camera, size, view_mode, None);
}
/// Same contract as [`Self::request_redraw`], plus `gear`: the currently loaded
/// design's gear tooth count/reference angle (`bridge::render_thread::
/// RenderContext::design_gear`), threaded through to a Diagram-mode (`view_mode`
/// 3) reproject so it shows THIS design's gear wheel rather than whatever a
/// previous editor replan happened to leave in the worker's own
/// `super::state::DiagramMemory` -- see `super::request::RedrawRequest::
/// Reproject`'s `gear` field doc comment. `None` leaves the worker's
/// last-known gear info untouched.
pub fn request_redraw_with_gear(
&self,
planes: &[(Vec3, f32)],
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
gear: Option<(u32, f32)>,
) {
self.request_redraw_geometry(
StoneGeometryBuf::from_halfspaces(planes),
camera,
size,
view_mode,
gear,
);
}
/// [`Self::request_redraw_with_gear`] for a stone that may carry concave tools: the
/// camera-follow redraw of the whole `geometry`, planes, tools and the
/// `(tier, placement)` of each tool. Every caller that knows the design's tools
/// (`refresh_viewport`, the background-solve apply, a camera drag from
/// `RenderContext`) goes through here so a concave stone never loses its notches on
/// the way to the worker; the planes-only entry point wraps it.
///
/// The Slice tool's [`Self::set_planes_override`] still replaces the PLANES only: a
/// provisional flat facet is drawn over the committed stone, tools included.
pub fn request_redraw_geometry(
&self,
mut geometry: StoneGeometryBuf,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
gear: Option<(u32, f32)>,
) {
let override_planes = self
.planes_override
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
if let Some(planes) = override_planes {
geometry.planes = StoneGeometryBuf::from_halfspaces(&planes).planes;
}
self.submit(RedrawRequest::Reproject {
geometry,
camera,
size,
view_mode,
gear,
});
}
/// [`Self::request_redraw_geometry`] for a viewport that shows the COMMITTED design
/// only: the Slice tool's [`Self::set_planes_override`] is not applied. The Live
/// Render tab's Solid raster calls this, so the provisional tier -- an Edit-tab
/// preview of a slice that has not been kept -- never shows there.
pub fn request_redraw_committed_geometry(
&self,
geometry: StoneGeometryBuf,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
gear: Option<(u32, f32)>,
) {
self.submit(RedrawRequest::Reproject {
geometry,
camera,
size,
view_mode,
gear,
});
}
/// Submits a facet-id-keyed highlight update ("which facet did I click", multi-
/// select, hover) -- see `super::types::FacetOverlay`'s doc comment. Re-renders
/// at whatever camera/planes/size/`view_mode` the worker last used for a
/// [`Self::request_redraw_with_gear`]/[`Self::request_replan`] call, exactly
/// like an ordinary camera-follow reproject, so a caller that already has a
/// facet id from a pick buffer never needs to also track/resend the camera
/// pose or the design's planes just to show it. A no-op before the worker has
/// rendered anything at all.
pub fn request_facet_overlay(&self, overlay: FacetOverlay) {
self.submit(RedrawRequest::UpdateFacetOverlay(overlay));
}
/// Stores the provisional-tier and drag-follower outlines where the RENDER worker
/// reads them when it draws -- see [`Self::outlines`] -- so they survive a
/// `Planned` request and an overlay update superseded in the "latest wins" gate
/// alike. Does NOT redraw: follow with [`Self::request_facet_overlay`]. The
/// editor's 3D overlay path calls this with the merged overlay's two fields on
/// every update; the Diagram view's own hover/click overlays never do, so they
/// cannot clear an outline they know nothing about.
pub fn set_outlines(&self, provisional: &[u32], moved: &[u32]) {
let mut outlines = self.outlines.lock().unwrap_or_else(PoisonError::into_inner);
outlines.provisional.clear();
outlines.provisional.extend_from_slice(provisional);
outlines.moved.clear();
outlines.moved.extend_from_slice(moved);
}
/// Makes every later [`Self::request_redraw_with_gear`] draw `planes` instead of
/// the planes its caller passes (`None` restores the caller's). Used by the Slice
/// tool while a provisional facet is on screen -- see [`Self::planes_override`].
pub fn set_planes_override(&self, planes: Option<Vec<(Vec3, f32)>>) {
*self
.planes_override
.lock()
.unwrap_or_else(PoisonError::into_inner) = planes;
}
/// Submits a replan-on-edit request to the PLAN worker.
/// `live_update::plan_preview` never runs on the UI thread.
pub fn request_replan(&self, request: ReplanRequest) {
let tier_cutoff = *self
.tier_cutoff
.lock()
.unwrap_or_else(PoisonError::into_inner);
let cut_steps = *self
.cut_steps
.lock()
.unwrap_or_else(PoisonError::into_inner);
self.submit_plan(PlanJob {
design: request.design,
dirty: request.dirty,
last_solved: request.last_solved,
camera: request.camera,
size: request.size,
selected_tier: request.selected_tier,
n_d: request.n_d,
view_mode: request.view_mode,
generation: request.generation,
show_preform: request.show_preform,
enlarged_panel: request.enlarged_panel,
tier_cutoff,
cut_steps,
});
}
/// Sets the Cut slider for the NEXT [`Self::request_replan`]: `Some(k)` draws the
/// stone after the first `k` cutting steps (`Design::preview_steps`; `0` is the
/// rough, the preform alone), `None` the finished stone. Does NOT redraw; the
/// caller submits a replan afterwards, as `gui::editor::view::submit_preview_replan`
/// does after reading the slider (`gui::solid_preview::cut_slider`).
pub fn set_cut_steps(&self, steps: Option<usize>) {
*self
.cut_steps
.lock()
.unwrap_or_else(PoisonError::into_inner) = steps;
}
/// Sets the "show through tier N" cutoff: `Some(n)` truncates to
/// `design.tiers[..=n]`. Does NOT trigger a redraw; the caller must submit
/// [`Self::request_replan`] afterwards.
///
/// Called from `gui::editor::view::submit_preview_replan_for`, which reads
/// `SolidPreviewModel.tier_cutoff` (`-1` means "no cutoff", matching this
/// crate's existing `-1`-means-`None` convention for e.g.
/// `diagram_enlarged_panel`) and converts it with
/// `usize::try_from(cutoff).ok()` before calling this and then
/// `request_replan` -- the same three-file shape `show_preform_planes`/
/// `diagram_enlarged_panel` already went through, just with the read happening
/// here instead of as a `ReplanRequest` field (see this state's own
/// `tier_cutoff` field doc comment for why).
pub fn set_tier_cutoff(&self, cutoff: Option<usize>) {
*self
.tier_cutoff
.lock()
.unwrap_or_else(PoisonError::into_inner) = cutoff;
}
/// bumps [`Self::generation_floor`] to (at least) `floor` -- called
/// once per wholesale design replacement by `gui::editor::auto_solve::
/// scheduling::reset_for_new_design`. `fetch_max`, not a plain store: this
/// must never move BACKWARDS even if called out of order (defensive only
/// -- `EditorState::generation` is itself only ever bumped, never reset,
/// so a caller's own `floor` values are already non-decreasing in
/// practice).
pub fn bump_generation_floor(&self, floor: u64) {
self.generation_floor.fetch_max(floor, Ordering::Relaxed);
}
}