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
//! Deciding whether a solve should run synchronously, be auto-solved after a debounce,
//! or be skipped, plus recording/reading this design's last measured solve time.
use ;
use crate::;
use ComponentHandle;
use ;
/// Solve cost is driven by plane count, not tier count -- a
/// wide-orbit round-brilliant tier emits many planes at once (the corpus's largest
/// measured design is 103 tiers but 210 planes, `mod.rs`'s own baseline), so a small
/// tier count can still be an expensive, UI-thread-blocking solve.
///
/// A rough estimate, not a re-measurement of this crate's own corpus: roughly double
/// a 16-tier reference point at the corpus's own ~2 planes-per-tier ratio, kept
/// comfortably under the 210-plane/5.9s worst case.
const SYNC_SOLVE_PLANE_LIMIT: usize = 32;
/// Once a design has a real measured solve time, that measurement
/// is a far better signal than any plane-count estimate -- a design that solved in
/// under this long most recently is still fine to solve again synchronously,
/// regardless of how many planes it has.
const SYNC_SOLVE_TIME_LIMIT: Duration = from_millis;
/// Whether a solve for a design with `plane_count` planes should still run
/// synchronously on the UI thread (the "New"/Load/explicit-Solve fast path,
/// `super::super::view::refresh_all`'s sync branch) rather than through
/// `super::dispatch::dispatch_background_solve`, which handles a plane-count cutoff
/// instead of a tier-count one. Prefers this design's own last REAL measured solve
/// time (see [`last_solve`]) when one exists, since a real measurement beats
/// any estimate; falls back to `plane_count` only for a design that has never
/// solved yet (a fresh "New" design, most commonly).
///
/// Pure and unit tested directly, mirroring [`should_schedule_auto_solve`]'s own
/// "measurement first, otherwise estimate" shape -- `last_solve` is a parameter
/// here (rather than read from `Runtime` internally) for exactly the same
/// testability reason that function documents on itself.
pub
/// Debounce delay between an edit landing and an eligible auto-solve actually
/// dispatching -- short enough that a burst of keystrokes (typing an angle, say) only
/// ever triggers the LAST one, long enough that it reads as "just happened," not
/// "laggy."
const AUTO_SOLVE_DEBOUNCE: Duration = from_millis;
/// Whether an edit that just landed should schedule a debounced auto-solve --
/// `budget` `0` disables auto-solve outright (today's stale-marker-only behaviour);
/// otherwise, a design with no measurement YET (`last_solve: None` -- nothing has
/// solved since the last New/Load) is scheduled optimistically: a fresh design's
/// schedule starts empty and solves near-instantly, so refusing to try would only
/// delay that design's very first auto-solve for no reason. Once a real measurement
/// exists, it alone decides.
///
/// Pure and unit tested directly -- the one genuinely decision-shaped piece of logic
/// in this module.
pub const
/// The banner text shown while auto-solve is disabled FOR THIS DESIGN specifically
/// (as opposed to the user having set the budget to `0` outright) -- this design's own
/// last measured solve already exceeds the configured budget.
pub
/// Records a just-completed solve's wall time (background or synchronous) as this
/// design's new estimate for [`should_schedule_auto_solve`]'s next decision.
pub
/// The currently loaded design's last REAL measured solve time, if any -- the
/// production counterpart to the `#[cfg(test)]`-only
/// [`last_measured_solve_duration`] below, exposed so `super::super::view::refresh_all`
/// can pass a real measurement to [`should_solve_synchronously`] for every caller that
/// is NOT replacing `EditorState` wholesale (an explicit Solve/Adopt/Optimize Apply/etc.
/// on the design already loaded) instead of unconditionally wiping it via
/// [`reset_for_new_design`] first, which made that rule unreachable on exactly
/// the actions it exists for.
pub
/// This design's last measured solve time -- a test-only window onto `Runtime`'s
/// thread-local state, so `record_solve_duration`/`reset_for_new_design` can be
/// asserted on directly. Identical in body to the production [`last_solve`] getter
/// just above; kept as its own `#[cfg(test)]` function (rather than tests calling
/// [`last_solve`] directly) purely so a rename of either one does not silently
/// change what the other means to read.
///
/// `should_schedule_auto_solve` reads `Runtime::last_solve` itself (via
/// `super::dispatch`'s `on_edit` caller). `view::refresh_all`'s sync-vs-background
/// decision reads [`last_solve`] for every caller that is NOT replacing
/// `EditorState` wholesale, and passes `None` only for the `wholesale: true` case,
/// right after [`reset_for_new_design`] has cleared the measurement -- reaching back
/// for the value it just cleared would judge a freshly loaded design by the previous
/// one's solve time, precisely the case where the two have nothing to do with each
/// other.
pub
/// Clears the running estimate, drops any pending debounced auto-solve, and
/// invalidates every still-in-flight background solve -- called whenever
/// `EditorState` itself is replaced wholesale (New/Load Selected/Open Native, all via
/// `super::super::view::refresh_all`), since a different design's solve cost, debounce
/// timer, and any dispatch still running against the OLD design have nothing to do
/// with the one that just replaced it.
///
/// # Why bumping `current_seq` here still matters
///
/// A background solve captures its own `generation: Arc<AtomicU64>` snapshot at
/// dispatch time (see `super::dispatch::dispatch_background_solve`). New/Load
/// Selected/Open Native all go through `EditorState::replace_wholesale` (see e.g.
/// `callbacks::tier_actions::apply_loaded_design`) -- which, despite the name,
/// REUSES the very SAME `Arc<AtomicU64>` across the replacement and bumps it once
/// (see `EditorState::replace_wholesale`'s own doc comment). So
/// `super::dispatch::apply_background_solve_result`'s `generation` check alone WOULD
/// already catch a dispatch from the design being replaced, once that dispatch's
/// (potentially multi-second) completion finally arrives -- but bumping
/// `Runtime::current_seq` here retires it immediately instead: every one of this
/// function's callers reaches it (via `refresh_all`, called unconditionally at the
/// top of every New/Load Selected/Open Native path) BEFORE that stale completion's
/// `upgrade_in_event_loop` closure can run (both run on the UI/event-loop thread, so
/// ordering is never racy), so `super::dispatch::is_current` now correctly fails
/// right away -- both the "Solving..." banner/ticker
/// (`super::dispatch::spawn_solving_ticker`'s own `is_current(seq)` check) and the
/// eventual completion stop describing the design that was just replaced, rather
/// than the banner sitting on a stale "Solving... (103 tiers)" message until that
/// old dispatch happens to finish and the generation check silently drops it.
///
/// Dropping `debounce` cancels whatever single-shot auto-solve [`on_edit`] had
/// pending against the design being replaced -- Slint stops a `Timer`'s callback
/// once the `Timer` itself is dropped (see `Runtime::debounce`'s own doc comment).
/// Dropping `idle_replan` is the identical fix for
/// `super::replan::schedule_idle_replan_if_stale`'s own timer: without it, a
/// partial-frame idle replan armed for the design being replaced would otherwise fire
/// up to `super::replan::IDLE_REPLAN_DEBOUNCE` later and resubmit that OLD design's
/// masts on top of whatever this reset is about to load -- see that function's own
/// doc comment for the epoch check this pairs with.
///
/// Safe to call at any time: bumping `current_seq`/dropping `debounce`/`idle_replan`
/// with no solve in flight is a no-op beyond the wasted counter tick.
pub
/// Called at the end of `super::super::view::refresh_editor_panel_stale` -- every edit
/// callback's stale-refresh path calls that function with a fixed, already-established
/// signature, so this hook reads everything it needs (the budget property, `state`'s
/// design/generation) rather than asking for new parameters. See
/// [`should_schedule_auto_solve`] for the eligibility rule.
pub