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
//! Wires `super::super::retarget` (the "Retarget for material" proposal
//! builder/applier) and `ui/components/retarget_dialog.slint` into the Edit tab --
//! `setup_*` functions, one per `RetargetDialog`/`EditorView` callback, mirroring
//! `solve_actions`'s Optimize callbacks in shape.
//!
//! # Where the target material comes from
//!
//! [`material::resolve_target_selection`] resolves the proposal's TARGET from
//! `RetargetModel`'s OWN `target_material_index`/`target_ri_override_text`
//! properties, NOT from `state.design.material` or the design settings panel's
//! combo: this dialog owns its target end to end, seeded from the design's current
//! material on `open()` (see [`proposal::setup_retarget_open_callback`]) and changed
//! only by this dialog's own picker. `retarget::apply` still only ever produces
//! `Edit::RetargetAngles` -- see [`apply::setup_retarget_apply_callback`] for how a
//! target material change actually reaches `design.material`.
//!
//! # Optimize is a search the cutter starts, and it runs off the UI thread
//!
//! Shift mode is pure angle arithmetic and stays synchronous. Optimize starts from the Shift
//! result and searches every crown and pavilion angle inside a range the cutter picks
//! (`indicatrix_editor::retarget::search`), which takes seconds to minutes, so it never runs
//! until the Search button is pressed and then runs on a worker thread of [`optimize_run`]'s
//! own. Switching to Optimize only lists the Shift angles the search would start from.
//! When the search ends, the report (up to three valid options, best first) waits in
//! [`optimize_run`]'s results slot; picking one copies it into [`RETARGET_ASYNC`]'s `pending`
//! proposal and into [`check_run`]'s slot (its verdict, masts and optical table), so Apply,
//! the embedded comparison and the compare window all read an Optimize option exactly like a
//! Shift result. [`RETARGET_ASYNC`] is this dialog's own module-local run tracker:
//! deliberately NOT a new `EditorState` field (that group is owned elsewhere) and NOT
//! threaded through any `setup_retarget_*` call site (`gui::editor::mod`'s wiring is likewise
//! owned elsewhere) -- a `thread_local!` is sound here because Slint's event loop is
//! single-threaded, exactly like `EditorState`'s own `RefCell`. The worker's completion
//! handler is `Send`, so it cannot capture `Rc<RefCell<EditorState>>`; it hands the report
//! back through `upgrade_in_event_loop`, and [`apply::setup_retarget_apply_callback`] reads
//! the pending proposal back out on the main thread.
//!
//! # Shift mode's validity check runs off the UI thread too
//!
//! Building the Shift plan is angle arithmetic and stays synchronous, but judging it (does
//! the retargeted stone still close, keep its girdle, keep its table on the crown side)
//! needs two solves, two meshes and three small optical traces. [`check_run`] runs that on
//! a worker thread behind a serial guard and keeps the result -- verdict, the re-anchored
//! masts and the optical table -- in its own `thread_local!`, because
//! `EditorState::pending_retarget` keeps its older proposal-only type for the web app.
//! Apply is disabled while the verdict is `Checking...` or `Not valid`; the Rust-side
//! [`apply::setup_retarget_apply_callback`] enforces the same gate for the Return key and
//! the compare window's "Keep after". Compare stays available for an invalid change.
//!
//! # Slint-free view-model split, for testability
//!
//! [`proposal_view::plan_view`]/[`proposal_view::candidate_row_views`]/
//! [`material::resolve_target_selection`]/[`apply::apply_pending_retarget`] take and
//! return only plain Rust types, so this group's `tests` module can exercise the
//! actual decision logic directly -- `proposal_view::push_retarget_view`/
//! `proposal_view::push_target_readout` are the only two functions that touch a
//! Slint type, thin enough not to need tests.
//!
//! # Module layout
//!
//! Split by concern: [`material`] (target material resolution/display),
//! [`proposal_view`] (the Slint-free proposal view model and its `push_*`
//! renderers), [`proposal`] (the open/proposal-changed callbacks and Shift mode's
//! own synchronous rebuild), [`check_run`] (Shift mode's off-thread validity check and
//! optical comparison), [`optimize_run`] (the Optimize search, its progress, its options
//! and the pick of one), [`apply`] (committing a pending proposal, and closing the dialog), and
//! [`snapshot`] (the separate "Snapshot Design"/"Compare to Snapshot" feature that
//! also lives in this dialog). [`RETARGET_ASYNC`] and [`apply_ghost_preview_or_revert`]
//! are shared by more than one of those siblings, so they stay here rather than in
//! any one of them. [`compare_hooks`] is the narrow read-only surface the visual
//! before/after compare window (`gui::editor::compare`) reaches this dialog's pending
//! proposal and snapshot through -- it never commits anything itself; its "Keep
//! after" invokes `RetargetModel.apply()`, i.e. [`apply::setup_retarget_apply_callback`].
pub use ;
pub use ;
pub use ;
pub use ;
use ;
use crate::;
use Design;
use ComponentHandle;
use ;
thread_local!
/// [`RETARGET_ASYNC`]'s payload.
/// Points the dialog's embedded comparison pane (`MainWindow`'s own `CompareModel`
/// instance, handled in `gui::editor::compare`) at the proposal just built:
/// `solved` re-opens the embedded session on it, otherwise the session is dropped so
/// the pane never keeps showing a proposal that no longer exists. The handlers read
/// the editor state, so callers must not hold a borrow of it across this call.
/// Shows `design` retargeted by `proposal` as a ghost overlay
/// in the shared solid viewport, iff `RetargetModel.preview_enabled` is on AND
/// `proposal` is `Some` (a `None` proposal -- an anchored-tier refusal, a solve
/// error -- has nothing to preview). Returns whether the candidate was queued on the
/// background solve worker: the viewport applies the ghost when that solve lands and
/// drops it if a newer request superseded it. The caller resubmits the real, live
/// design through the ordinary [`view::submit_preview_replan`] path when it wasn't
/// queued (see both call sites).
///
/// Builds the candidate via [`Design::apply_edit`] on a clone -- explicitly
/// documented as safe for exactly this ("usable standalone by a caller that wants
/// edit/undo without a `History` stack") -- rather than
/// [`super::super::state::EditorState::apply`], so this never touches
/// `History`/`is_dirty`/the design generation: a preview must never look like a
/// real edit to anything else in this crate.
/// `design` with `proposal`'s angle retarget and the re-anchored `anchors` applied, on a
/// clone -- the candidate geometry both the viewport ghost
/// ([`apply_ghost_preview_or_revert`]) and the visual compare window's "after" side
/// (`gui::editor::compare`, via [`compare_hooks::retarget_compare_inputs`]) show. `None`
/// when the retarget edit no longer applies to `design` (a stale tier index).
///
/// The exact edit Apply commits ([`retarget::apply_with_anchors`]), so what the cutter
/// compares is what they would keep. [`Design::apply_edit`] on a clone, never
/// [`super::super::state::EditorState::apply`]: see [`apply_ghost_preview_or_revert`]'s
/// own doc comment for why a preview must never touch `History`. The target MATERIAL is
/// the caller's to attach when it matters (the compare window does; the solid ghost has
/// no material to show).
///
/// Tiers that follow a relation are brought into line afterwards, as the editor session does
/// when Apply commits the edit; a relation that cannot be satisfied leaves them where the
/// edit put them (the verdict says so).
pub