indicatrix-cut 0.7.2

Desktop faceting-design editor: library browsing, spectral 3D rendering, material retargeting, and a solid inspection view.
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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
import { AngleItem, ConcaveFormData, DeepSolveTierRow, EditorTierItem, GearRemapRow, IndexChipItem, OptimizeChangeRow, OptimizeResultRow } from "../types.slint";

export global EditorModel {
    in property <bool> enabled: false;
    // Whether a REAL design is installed -- `false` only for the startup
    // placeholder (`EditorState::fresh`) before anything is created, loaded or
    // restored. Pushed from Rust (`gui::editor::view::push_has_design`, mirroring
    // `EditorState::has_design`) on every install path: New Design/templates,
    // Load Selected, Open/Open Recent/the startup restore. Gates the
    // empty-state card grid (`components/app/viewport_overlay.slint`): a design
    // with zero tiers is still a design and must stay visible.
    in property <bool> has_design: false;

    in-out property <int> selected_tier_index: -1;
    callback selected_tier_changed(int);
    changed selected_tier_index => { selected_tier_changed(selected_tier_index); }

    // Bumped whenever the tier-edit form must go blank. A monotonically increasing
    // counter (not a flag) so going from -1 to -1 again still fires `changed`.
    // EditorView watches this to re-seed/clear EditorInspector's form.
    in-out property <int> form_reset_pulse: 0;

    // Bumped by the Rust no-solve panel refresh (`gui::editor::view::panel_stale::
    // sync_tier_form_with_design`) whenever the design changed since the previous
    // refresh and the selected tier is still in range -- an edit that left the
    // selected tier selected (a handle drag, nudge, pin, adopt, facet edit, gear
    // remap, retarget/optimize apply, symmetry change, in-range Undo/Redo), unlike
    // `form_reset_pulse` above, which is for a selection that New/Load/Remove or an
    // out-of-range Undo/Redo cleared outright. `EditorView` watches this to re-seed
    // (not blank) `EditorInspector`'s form from whatever the edit left the selected
    // tier holding, so a form left open across such an edit does not keep showing
    // pre-edit values for Save Tier to write back.
    // `form_reseed_tier_count_changed` is set alongside it, just before, every
    // time -- read by the same watcher (a pulse carries no payload of its own).
    in-out property <int> form_reseed_pulse: 0;
    // Whether the edit that is about to bump `form_reseed_pulse` changed the tier
    // COUNT (a structural `AddTier`/`RemoveTier`, or its Undo/Redo) rather than just
    // some tiers' data in place -- see `EditorInspector.reseed_after_external_
    // change`'s own doc comment for what this changes about a dirty form.
    in-out property <bool> form_reseed_tier_count_changed: false;

    // Bumped by app.slint's Ctrl+F arm to focus the tier filter box. A pulse
    // (monotonically increasing counter) rather than a direct call because both
    // `EditorView` instantiations sit inside `if` branches, and an element id
    // inside a conditional is unreachable from the root `global_shortcuts`
    // FocusScope. Writing the same value fires no `changed`, so pressing Ctrl+F
    // twice must increment to fire focus twice.
    in-out property <int> filter_focus_pulse: 0;

    // Path the startup prompt offers to open, or "" for no offer. The Edit tab
    // still starts blank either way; this only offers to avoid surprising silent
    // reopening of yesterday's work, which would hide a crash-recovery file.
    in-out property <string> startup_restore_path: "";
    // Whether that path is a leftover AUTOSAVE (a previous run that did not shut
    // down cleanly) rather than an ordinary recent file. Changes the wording, and
    // the stakes: an autosave holds work that was never saved at all.
    in property <bool> startup_restore_is_autosave: false;
    callback startup_restore_accept();
    callback startup_restore_dismiss();
    // Removes the offered autosave file from disk and dismisses the prompt --
    // never offered for an ordinary recent file (only a leftover autosave is
    // ever safe to delete outright; see `startup_restore_is_autosave`).
    callback startup_restore_delete();

    in property <[EditorTierItem]> tiers: [];

    // The tier table's search/filter box. Slint owns the typed text; Rust
    // performs the substring test via the pure callback below. This keeps
    // typing responsive (no round trip) while letting each row's visibility
    // binding react to changes without explicit Rust pushes.
    in-out property <string> tier_filter_text: "";
    pure callback tier_matches_filter(string, string) -> bool;
    in property <bool> can_undo: false;
    in property <bool> can_redo: false;
    in property <string> status_text: "";
    in property <bool> status_is_problem: false;

    // A one-shot override for the tier-edit form's "Meets" combo, consumed by
    // `EditorInspector.load_tier_into_form` the next time `selected_tier_index`
    // actually changes (then reset back to `-1` there). This is how the tier
    // table's "Add Anchor" action (`editor_tier_table.slint`, shown on a row
    // `EditorTierItem.needs_anchor` flags) opens the Tier tab already set to
    // "Exact scale value" (`constraint_kind == 2`) instead of leaving the cutter to
    // notice and pick that combo entry themselves. `-1` (the default, and what
    // `load_tier_into_form` restores immediately after reading it) means "no
    // suggestion -- load the tier's own current kind," the path every ordinary
    // selection change takes.
    in-out property <int> suggested_constraint_kind: -1;

    // One-time explainer card shown when a design's solve first fails with
    // `MissingAnchor`. The dismiss callback argument is the "Don't show again"
    // checkbox state, which persists the suppression via `AppSettings`.
    in-out property <bool> anchor_explainer_open: false;
    callback anchor_explainer_dismiss(bool);

    in-out property <int> preform_shape_index: 1;
    in-out property <string> preform_half_width: "1.20";
    in-out property <string> preform_length_over_width: "1.00";
    in-out property <string> preform_depth: "1.20";
    // Millimeter equivalent of Half-Width/Depth above, shown read-only alongside
    // the editable model-unit fields; empty until girdle diameter anchors scale.
    in property <string> preform_half_width_mm_text: "";
    in property <string> preform_depth_mm_text: "";

    // Y-offset in millimeters (cutter's own measurement, not model units).
    // Unlike Half-Width/Depth, this is typed directly in mm; Rust converts
    // through the design's mm-per-unit factor when applying. Error toast if
    // girdle diameter has not anchored that scale yet.
    in-out property <string> preform_y_offset_mm: "0.00";
    callback apply_preform_y_offset(string);

    in-out property <string> girdle_diameter_mm: "";
    in-out property <string> specific_gravity_override: "";
    // `apply_yield_inputs`'s middle `int` argument used to be this
    // model's own (now-removed) `material_index` property, bound to a Yield-
    // tab combo whose selection `state::parse_yield_form` never actually read
    // -- `preform_tab.slint`'s Apply button now passes a literal `0` instead,
    // so this callback's signature stays unchanged for its one Rust handler
    // (`callbacks::tier_actions::design_forms::setup_apply_yield_inputs_callback`).
    callback apply_yield_inputs(string, int, string);

    in property <string> volumetric_yield_text: "";
    in property <string> carat_weight_text: "";
    in property <string> specific_gravity_used_text: "";
    in property <string> preform_fit_warning: "";

    in property <[string]> material_combo_options: ["(none)"];
    in-out property <int> material_combo_index: 0;
    in-out property <string> ri_override_text: "";
    in property <string> effective_ri_text: "";
    // Where `effective_ri_text` came from: typed override, catalogue material,
    // built-in preset, or legacy design value. Shown in the inspector alongside
    // the "Eff. RI" chip.
    in property <string> ri_source_text: "";
    in property <string> critical_angle_text: "";
    // Body-color variant of the design's material: "Material default" (index 0,
    // no override), then the fixed presets, then the last entry "Custom..." (the
    // picker), pushed from Rust (`indicatrix_editor::material::body_color_options`);
    // index `i >= 1` is preset `i - 1`.
    in property <[string]> body_color_options: ["Material default"];
    in-out property <int> body_color_index: 0;
    // The Design settings colour editor (the combo's last entry, "Custom...") keeps its
    // state in `BodyColorEditorModel` (`models/body_color_editor.slint`).
    // The linked viewport's material name with its color (`Sapphire (Yellow)`),
    // empty while the design has no color override or the viewport is unlinked.
    // Shown beside the Live Render tab's Render Material dropdown.
    in property <string> body_color_readout: "";
    // The design's material is a custom material whose color is a physics recipe. The per-design
    // body-color override replaces the whole absorption tensor, i.e. the physical color, so the
    // color combo is greyed out for it ("replaces the physical color") until a physics
    // override exists (follow-up). `physics_override_conflict` is true when an override is already
    // set on such a design -- shown as a warning (the override still wins until cleared).
    in property <bool> design_material_is_physics: false;
    in property <bool> physics_override_conflict: false;
    // (material combo index, RI override text, body color combo index).
    callback apply_design_material(int, string, int);

    // Inferred material shown as a guess, never a fact. Imported `.asc` files
    // carry only refractive index, so unnamed designs show a nearest-n_D guess
    // (tolerance 0.02). `material_guess_text` is the full label with "?";
    // empty when design names a material or nothing built-in is close enough.
    // `material_guess_name` is the bare species to write back on acceptance.
    // `material_guess_other_candidates_text` lists other built-ins in tooltip.
    in property <string> material_guess_text: "";
    in property <string> material_guess_name: "";
    in property <string> material_guess_other_candidates_text: "";
    // Explicit one-click confirmation of the guess (not a silent auto-fill).
    // Writes as an ordinary, undoable `Edit::SetMaterial`.
    callback set_material_from_guess(string);

    // Live critical-angle guidance in the Tier form's Angle field, updated on
    // every keystroke (not just on acceptance). Shows the same margin/estimate
    // the MARGIN column displays for saved rows. `level`: 0 safe / 1 marginal /
    // 2 windows / -1 nothing to show.
    in property <string> angle_live_margin_text: "";
    in property <int> angle_live_margin_level: -1;
    in property <bool> angle_live_margin_is_estimate: false;
    callback angle_live_preview(string);

    in-out property <int> gear_index: 1;
    in-out property <string> gear_custom_text: "96";
    callback gear_apply(int, string);
    in property <bool> gear_remap_open: false;
    in property <[GearRemapRow]> gear_remap_rows: [];
    callback gear_remap_confirm();
    callback gear_remap_cancel();

    in-out property <string> symmetry_order_text: "8";
    in-out property <bool> mirror: true;
    callback apply_symmetry(string, bool);
    // A live "N tiers would become incomplete orbits" dry-run preview of the
    // proposed Symmetry Order/Mirror change, shown before Apply commits anything.
    // Empty until the first request or when typed order doesn't parse.
    in property <string> symmetry_preview_text: "";
    callback request_symmetry_preview(string, bool);

    in property <string> material_suggestion_text: "";
    in property <string> material_suggestion_name: "";
    callback material_suggestion_accept(string);
    callback material_suggestion_dismiss();

    in-out property <bool> new_dialog_open: false;
    in-out property <int> new_preform_shape_index: 1;
    in-out property <string> new_preform_half_width: "1.50";
    in-out property <string> new_preform_length_over_width: "1.00";
    in-out property <string> new_preform_depth: "1.50";
    in-out property <int> new_gear_index: 0;
    in-out property <string> new_gear_custom_text: "96";
    in-out property <string> new_symmetry_order_text: "8";
    in-out property <bool> new_mirror: true;
    in-out property <int> new_material_index: 0;
    // Shared source of truth for material options (pushed from Rust every
    // refresh, rather than hand-maintained in the dialog).
    in property <[string]> new_material_options: [];
    // "Start from" template choice (0 empty, 1 standard round brilliant). On the
    // model rather than local to the dialog because both the Create button and
    // the Return-key handler need to read it; a dialog-local value would be
    // invisible to the second call site. Starts at 1: the dialog opens on the
    // round brilliant; the guides set it to 0 so their first step sees Empty.
    in-out property <int> new_template_index: 1;

    callback new_design_create(int, string, string, string, int, string, string, bool, int, int);

    callback load_selected();
    callback solve();
    in property <bool> solve_running: false;

    in-out property <int> auto_solve_budget_ms: 300;
    callback auto_solve_budget_ms_changed(int);
    changed auto_solve_budget_ms => { auto_solve_budget_ms_changed(auto_solve_budget_ms); }

    callback undo();
    callback redo();
    callback apply_preform(int, string, string, string);
    callback save_tier(int, string, int, string, string, string);
    // The concave tier form's Save/Add: `index` is the concave tier's position in
    // `design.concave_tiers`, or -1 to add one. A separate callback rather than a
    // wider `save_tier`, whose six arguments stay as they are.
    callback save_concave_tier(int, ConcaveFormData);
    // "+ Add Concave Tier" (tier table toolbar, command bar): Rust opens the inspector's
    // Tier tab on a blank concave form (`inspector_concave_mode`).
    callback add_concave_tier();
    // A concave tier read back as form fields at full precision (`concave_tier_form_fields`),
    // for the inspector to seed its draft from; `index` is the table position of a
    // concave row. Pure: it reads the design and changes nothing.
    pure callback concave_form_data(int) -> ConcaveFormData;
    callback remove_tier(int);
    callback toggle_detach(int);
    callback inline_set_angle(int, string);
    callback nudge_angle(int, float);
    callback duplicate_tier(int);
    callback toggle_multi_select(int);
    // Shift+click range select: replaces `multi_selected` set with all tiers
    // between the current selection anchor and this index, inclusive.
    callback select_tier_range(int);
    callback export_asc();
    // The same edited schedule as an experimental Gem Cut Studio `.gcs` file
    // (`gui::editor::native_io::export::setup_export_asc_callback`).
    callback export_gcs();
    // Printable cutting sequence as self-contained HTML with embedded diagram.
    callback export_cutting_sheet();
    // 2D diagram alone as PNG, sharing the cut sheet's rendering path.
    callback export_diagram();
    // Explicit "Save As" action; distinct from `save_native` (the File > Save
    // action, writing the `.indicatrix` design file) which writes directly to the
    // design's known file when one exists. `open_native` is File > Open.
    callback save_native_as();
    callback save_native();
    callback open_native();
    callback adopt_meet(int);

    // Multi-select batch actions and the tier list's own "N selected" indicator
    // (`editor_tier_table.slint`'s header) -- see `gui::editor::callbacks::
    // tier_actions::setup_clear_multi_select_callback`/`setup_remove_multi_selected_
    // callback`. Pushed alongside every `tiers` refresh from the same
    // `apply_multi_selection` call site that already patches each row's own
    // `multi_selected` flag, so the two never disagree.
    in property <int> multi_selected_count: 0;
    callback clear_multi_select();
    callback remove_multi_selected();

    // Row reorder via Alt+Up/Alt+Down or move buttons. Direction: -1 (earlier)
    // or +1 (later) in cut order.
    callback move_tier(int, int);

    // "Complete orbit" -- expands the tier's incomplete orbit unit(s) to their full
    // symmetric membership via `Design::add_orbit_member`, shown on the tier
    // table's ORBIT cell whenever `EditorTierItem::orbit_incomplete` is set.
    callback complete_orbit(int);

    // Per-facet editing callbacks (declared here only because a global's
    // callback must be declared in the file that owns the global -- see this
    // file's own module-level convention). The first
    // argument is always the tier index; the second is the index-wheel
    // position for the three facet callbacks, or the tooth count to rotate by
    // for `tier_rotate_indices`. No handler body belongs here -- see
    // `gui::editor::callbacks` for where these are actually wired.
    callback facet_remove(int, float);
    callback facet_toggle_detach(int, float);
    callback facet_add(int, float);
    callback tier_rotate_indices(int, float);
    callback tier_mirror_indices(int);

    // Tier-table actions, declared here for the same reason as the facet
    // callbacks above (the table lives in `editor_tier_table.slint`, which
    // cannot declare a callback on a global it does not own). Wired in
    // `gui::editor::callbacks::tier_actions`.
    //
    // `adopt_all` takes every tier whose solved angle the solver has already
    // proposed and applies the whole set as one undoable batch.
    // `pin_to_mast` takes the tier index to convert to a `ScaleReference`
    // constraint against the current solve's mast.
    // `highlight_tooth` takes an index-wheel position; -1 clears the tint.
    callback adopt_all();
    // Like `adopt_all` but restricted to the multi-selection, as one undo step.
    callback adopt_selected();
    // Rounding for pending gear remap: 0 Nearest, 1 Floor, 2 Ceil.
    callback gear_remap_set_rounding(int);
    callback pin_to_mast(int);
    callback highlight_tooth(int);

    // Chip row for the inspector's currently loaded tier: one entry per
    // index-wheel position, each flagging whether that occurrence is detached.
    // Top-level property (not a field on `EditorTierItem`) because the tier list
    // is built on a background thread and shipped inside a `Send` struct, which
    // cannot contain `ModelRc`. Pushed on every tier edit or selection change.
    in property <[IndexChipItem]> selected_tier_chips: [];

    // Cheater offset for the inspector's currently loaded tier. Top-level
    // property (like `selected_tier_chips` above) describing only the loaded
    // tier, not every row. Empty string means no offset set.
    in-out property <string> selected_tier_cheater_offset_text: "";
    callback apply_cheater_offset(int, string);

    // Per-tier cutter-authored free-text note. Top-level property describing
    // only the inspector's currently loaded tier. Empty string means no note set.
    in-out property <string> selected_tier_note_text: "";
    callback apply_tier_note(int, string);

    // Unsaved-changes tracking for Save/Discard/Cancel guards and UI badges.
    // Driven by Rust-side dirty tracking, but also recomputed reactively here:
    // watching `tiers` reassignment catches edits from all paths.
    in-out property <bool> is_dirty: false;
    callback recompute_dirty();
    changed tiers => { recompute_dirty(); }

    // The Save/Discard/Cancel dialog New/Load Selected/Open show in front of
    // their own destructive replace-the-design step whenever `is_dirty` is true --
    // see `gui::editor::callbacks::tier_actions::setup_unsaved_guard_dispatch` for
    // the Rust side, and `gui::editor::state::PendingUnsavedAction` for what "Save"/
    // "Discard" actually resume.
    in-out property <bool> unsaved_dialog_open: false;
    in property <string> unsaved_dialog_message: "";
    callback unsaved_dialog_save();
    callback unsaved_dialog_discard();
    callback unsaved_dialog_cancel();

    // Opening an older `.indicatrix.toml` sidecar found a paired `.asc` whose
    // fingerprint no longer matches it (the `.asc` was touched outside the editor
    // since the last save) -- rather than silently keeping or silently dropping the
    // sidecar's authored meet constraints, this blocks on an explicit choice.
    // `mismatch_dialog_can_apply` is `false` when the sidecar's own tier count no longer even agrees with the
    // freshly imported `.asc` (see `indicatrix_cut_core::native::TierOverlay::
    // SkippedTierCountMismatch`'s own doc comment) -- re-applying a per-index overlay
    // in that case can't mean anything, so "Apply Anyway" is not offered at all.
    in-out property <bool> mismatch_dialog_open: false;
    in property <bool> mismatch_dialog_can_apply: false;
    callback mismatch_dialog_apply_anyway();
    callback mismatch_dialog_asc_only();
    callback mismatch_dialog_cancel();

    // A single in-window Yes/No confirmation shared by every write-path
    // decision that needs one -- "this design is not a closed solid, write
    // anyway?" and "overwrite an existing file that belongs to a different
    // design?". Generic on purpose (heading/message/primary label are all
    // set by whichever call site opens it, mirroring
    // `unsaved_dialog_*`/`mismatch_dialog_*` above), since the two
    // situations never overlap and sharing one dialog is simpler than two
    // near-identical ones. `write_confirm_primary_label` names the
    // affirmative action ("Save Anyway", "Overwrite"); Cancel (always shown
    // by `ConfirmActionDialog`) is the decline, matching the native
    // dialog's own Yes/No shape -- declining produces no toast.
    in-out property <bool> write_confirm_open: false;
    in property <string> write_confirm_heading: "";
    in property <string> write_confirm_message: "";
    in property <string> write_confirm_primary_label: "";
    // Whether THIS particular write-confirm prompt offers "Don't ask again"
    // at all (`gui::editor::
    // native_io::ask_write_confirm`'s `suppress_key` -- `None` for a call site
    // that never offers it, e.g. the unsaved-changes/fingerprint-mismatch guards,
    // which are separate dialogs anyway). `write_confirm_dont_ask` is the
    // checkbox's own live state, bound two-way to `ConfirmActionDialog.dont_ask`
    // at the mount site (`app.slint`) -- `native_io`'s accept handler reads it
    // once, at the moment of acceptance, to decide whether to persist the
    // suppression.
    in property <bool> write_confirm_show_dont_ask: false;
    in-out property <bool> write_confirm_dont_ask: false;
    callback write_confirm_accept();
    callback write_confirm_cancel();

    in property <bool> deep_solve_available: false;
    in property <string> deep_solve_hint: "";
    in property <bool> deep_solve_running: false;
    in property <string> deep_solve_status: "";
    in property <bool> deep_solve_status_is_problem: false;
    // True whenever the design's
    // live generation has moved past the generation `deep_solve_status` was
    // computed for -- pushed on EVERY edit (`gui::editor::view::push_stale_content`),
    // not only at the moment the run completed, so this flips the instant a
    // further edit lands rather than only describing staleness that happened
    // DURING the run itself. Drives `ui/components/stale_badge.slint` next to the
    // Deep Solve line in `EditorStatusStrip`'s Details popup -- the result text
    // itself is never cleared, only badged (see that component's own doc
    // comment).
    in property <bool> deep_solve_stale: false;
    // [`deep_solve_stale`]'s Optimize counterpart -- true once the design has
    // moved on from the generation `EditorState::pending_optimize`'s currently
    // held (appliable) result was computed against.
    in property <bool> optimize_stale: false;
    callback deep_solve();
    // Abandons a running Solve/auto-solve. Unlike Deep Solve's
    // cancel this cannot stop the worker mid-solve -- `Design::solve` has no
    // checkpoint -- but it invalidates the in-flight result and gives the cutter
    // their editor back immediately.
    callback solve_cancel();
    callback deep_solve_cancel();
    // Per-tier table behind `deep_solve_status`'s aggregate verdict: one row per
    // tier whose mast the verified repair disagreed with. Empty on cancellation,
    // full agreement, or before a run completes.
    in property <[DeepSolveTierRow]> deep_solve_tier_rows: [];
    // Opt-in "Pin to verified mast" action: applies the verified repair as a
    // scale constraint for that tier (no-op with toast if the run is stale).
    callback pin_verified_mast(string);

    // Cutter's typed printed proportions (Vol/W^3, L/W, C/W, P/W, H/W) for
    // Deep Solve's external verification. Empty field means "not typed"; blank
    // text matches how catalogue figures tolerate missing columns.
    in-out property <string> printed_vol_w3_text: "";
    in-out property <string> printed_lw_text: "";
    in-out property <string> printed_cw_text: "";
    in-out property <string> printed_pw_text: "";
    in-out property <string> printed_hw_text: "";
    // Parses the five fields above and commits those that parse as positive
    // finite numbers into the design's printed proportions.
    callback apply_printed_proportions(string, string, string, string, string);

    in property <bool> optimize_available: false;
    in property <string> optimize_hint: "";
    in property <bool> optimize_running: false;
    in property <string> optimize_status: "";
    in property <bool> optimize_status_is_problem: false;
    in property <[OptimizeResultRow]> optimize_result_rows: [];
    // Per-tier table of changes: one row per tier a pending result would move,
    // with magnitude. Empty while there is no current un-applied result.
    in property <[OptimizeChangeRow]> optimize_change_rows: [];
    in property <bool> optimize_can_apply: false;
    in-out property <string> optimize_weight_windowing: "1.0";
    in-out property <string> optimize_weight_extinction: "1.0";
    in-out property <string> optimize_weight_tilt_brilliance: "1.0";
    // Optical weights above are free-form text (any positive number); yield is a
    // bounded 0..1 slider where 0.0 is the default (reproduces previous runs).
    in-out property <float> optimize_weight_yield: 0.0;
    // Face-up colour weight, signed, -3..3: negative favours a lighter stone, positive a
    // deeper and stronger colour, 0 leaves the colour out of the score.
    in-out property <float> optimize_weight_tone: 0.0;
    // Evaluation budget, seed, and polish settings. `optimize_only_selected`
    // restricts the run to the tier table's multi-selection.
    in-out property <string> optimize_budget_text: "800";
    in-out property <string> optimize_seed_text: "0";
    in-out property <bool> optimize_polish_enabled: true;
    in-out property <bool> optimize_only_selected: false;
    callback optimize(string, string, string, float, float);
    callback optimize_cancel();
    callback optimize_apply();
    // "Preview" toggle showing the pending result's candidate geometry in the
    // shared viewport before apply.
    callback optimize_preview_toggled(bool);

    // Derived notion of "what is running right now" computed from the three
    // `*_running` flags. Prevents overlapping operations. "" means nothing is
    // running; shown in the status strip so a cutter always sees activity.
    out property <string> busy_action: solve_running
        ? "Solve"
        : (deep_solve_running ? "Deep Solve" : (optimize_running ? "Optimize" : ""));

    in property <[string]> manufacturability_warnings: [];
    // Per-warning tier index: -1 for lines with no specific tier. Parallel to
    // `manufacturability_warnings` (same length, same order) to link warnings to
    // tiers in the table.
    in property <[int]> manufacturability_warning_tiers: [];

    // Solver's state as a real tri-state: one of "solving" | "stale" | "solved"
    // | "failed" (plain string, not enum). Lets the UI show persistent status and
    // the button respond correctly to edits without flipping color on every
    // keystroke.
    in property <string> solve_state: "solved";
    in property <int> last_solve_duration_ms: 0;
    // Facet count from the current solve (not tier count; facets are what a
    // cutter checks against published designs). 0 until a solve completes.
    in property <int> facet_count: 0;

    // `SolidPreviewModel.tier_cutoff`'s `changed` handler resubmits a replan on
    // every cutoff change, so dragging the "Cut: N/M" slider redraws the viewport
    // immediately. The slider uses an interaction callback (fires only on user
    // drag, keyboard, or click, not on tier-list updates) to avoid duplicate
    // replan calls and `borrow_mut` hazards from overlapping Slint handlers.

    // "Snapshot Design" captures the current design + its solved masts.
    // "Compare to Snapshot" diffs that snapshot against current state.
    callback snapshot_design();
    callback compare_to_snapshot();
    // Whether a snapshot has been captured this session. Lets the command bar
    // explain and gate "Compare to Snapshot" instead of leaving it always
    // enabled with no way to distinguish "nothing yet" from "available".
    in property <bool> has_snapshot: false;

    // Computes the full 4-axis tilt-performance sweep for the design currently
    // open (not a catalogue row) off the UI thread. Saves the result when the
    // design is in the catalogue; otherwise shows it but explains it wasn't
    // persisted.
    callback compute_tilt_curves_for_open_design();

    // Short label naming the design currently under edit: the paired `.asc`'s
    // bare file name or "Untitled design". Shown in the status strip.
    in property <string> design_label: "Untitled design";

    // Design metadata: `design_title` is `meta.headers[0]`; `design_extra_headers`
    // is every further header; `design_footnotes` every footnote line; each
    // joined/split on `"; "`. `design_gear_reference_angle` is plain text.
    in-out property <string> design_title: "";
    in-out property <string> design_extra_headers: "";
    in-out property <string> design_footnotes: "";
    in-out property <string> design_gear_reference_angle: "0.0";
    callback apply_design_meta(string, string, string, string);

    // Where the last Save / Export .asc wrote. `last_saved_path` is the
    // full path (for the reveal below); `last_saved_label` is the bare file name
    // (for the status strip). Both empty until the first write of the session.
    in property <string> last_saved_path: "";
    in property <string> last_saved_label: "";
    // Opens the folder containing `last_saved_path` in the OS file manager,
    // through the same per-platform dispatch the Help menu's user manual uses.
    // Wired in `gui::mod`, not `gui::editor`, because that is where the
    // dispatch helper lives.
    callback reveal_last_saved_path();

    // Proportion readouts a cutter quotes: table %, crown height, pavilion
    // depth, total depth, length-to-width. Read "-" when the design doesn't
    // solve/close rather than a misleading zero.
    in property <string> proportions_table_pct: "-";
    in property <string> proportions_crown_height: "-";
    in property <string> proportions_pavilion_depth: "-";
    in property <string> proportions_total_depth: "-";
    in property <string> proportions_length_to_width: "-";

    // Additional proportion readouts: girdle thickness and printed C/W%, P/W%,
    // G/W% ratios. Same "-" fallback and solve-dependence as above.
    in property <string> girdle_thickness_text: "-";
    in property <string> crown_to_width_text: "-";
    in property <string> pavilion_to_width_text: "-";
    in property <string> girdle_to_width_text: "-";

    // Verdict chips next to the proportion readouts, judged against reference
    // windows. `level`: 0 within / 1 near / 2 outside / -1 nothing to judge yet.
    // `reason` is the matched window's one-line explanation, "" at level -1.
    in property <int> proportion_verdict_table_level: -1;
    in property <string> proportion_verdict_table_reason: "";
    in property <int> proportion_verdict_crown_angle_level: -1;
    in property <string> proportion_verdict_crown_angle_reason: "";
    in property <int> proportion_verdict_pavilion_angle_level: -1;
    in property <string> proportion_verdict_pavilion_angle_reason: "";
    in property <int> proportion_verdict_total_depth_level: -1;
    in property <string> proportion_verdict_total_depth_reason: "";
    in property <int> proportion_verdict_girdle_level: -1;
    in property <string> proportion_verdict_girdle_reason: "";

    // The design's own cut order (angle/facet/index/notes), as the same rows
    // "Export Edited .asc" would write -- fed to a `CuttingTableView`-style
    // readout in a "Schedule" inspector tab, so the cutter can see the design
    // actually being edited rather than only ever the catalogue's original
    // schedule. Empty while the design is stale/unsolved --
    // `EditorInspector` shows an explanatory placeholder in that case rather
    // than a stale schedule that no longer matches the tier table.
    in property <[AngleItem]> cutting_rows: [];

    // A validation message for the tier-edit form, set from the same parse
    // errors "Save Tier"/"Add Tier" would otherwise only show as a corner
    // toast -- rendered under the form in
    // `EditorInspector` instead of/alongside the toast. Wired: `setup_save_
    // tier_callback`'s `report_tier_form_error` helper (`gui::editor::
    // callbacks::tier_actions`) sets this from every parse/apply error branch
    // and clears it back to "" on a successful save.
    in-out property <string> tier_form_error: "";

    // Which field `tier_form_error` above concerns --
    // "angle" | "name" | "indices" | "constraint" | "" (general/unclassified,
    // shown only under the form as today). Lets `editor_inspector.slint` put a red
    // border on the SPECIFIC offending control instead of only the shared message
    // under the whole form. Empty until `setup_save_tier_callback`'s
    // `report_tier_form_error` call sites (`gui::editor::callbacks::tier_actions`)
    // pass a second argument classifying the error text
    // they already build -- e.g. the "Angle '...' is not a number."/"exceeds 90"
    // messages classify as "angle", "Another tier is already named '...'" as
    // "name", "Index '...' ..." as "indices", and the meet-constraint messages
    // ("\"Meet named\" needs...", "Scale reference '...'", "No facet named
    // '...'") as "constraint". Cleared back to "" alongside `tier_form_error`
    // itself on a successful save.
    in-out property <string> tier_form_error_field: "";

    // The Edit-mode layout split (`editor_view.slint`/`editor_command_bar.slint`/
    // `editor_inspector.slint`): the command bar's current hover hint (one line,
    // shown under the button row -- see `EditorCommandBar`'s own doc comment),
    // and which of the inspector's Tier/Preform/Optimize tabs is showing (see
    // `EditorInspector`'s own doc comment). Purely Slint-local UI state -- Rust
    // never reads either.
    in-out property <string> hover_hint: "";
    in-out property <int> inspector_tab: 0;
    // Whether the inspector's Tier tab shows the concave form for an "Add" draft
    // (selection -1). A selected concave row shows it regardless. Set by Rust's
    // `add_concave_tier` and cleared in Slint when a flat row is selected or "New Tier"
    // is pressed, hence in-out. Purely UI state, like `inspector_tab`.
    in-out property <bool> inspector_concave_mode: false;

    // The resizable/collapsible dock layout: `dock_width` is the
    // viewport|dock split (`app.slint`'s edit-mode row, dragged via
    // `SplitHandle`), `inspector_height` the table|inspector split
    // (`editor_view.slint`, same handle). Of the three `*_collapsed` flags,
    // `settings_collapsed` (`editor_design_settings.slint`) and
    // `inspector_collapsed` (`editor_inspector.slint`) each gate a real
    // `SectionHeader`-wrapped section. `remap_collapsed` does NOT -- nothing
    // in `ui/` binds a `SectionHeader.collapsed`/`toggled` to it. It is
    // persisted and round-trips through `layout_changed` like the other four,
    // but nothing currently reads it back to collapse anything, so setting it
    // has no visible effect until a `SectionHeader` binds to it. Every one of
    // the five is persisted -- see `gui::editor_layout` (Rust) -- via
    // `layout_changed`, fired after each one's own value has already been
    // updated, so a listener always reads the new value, never the old one.
    in-out property <length> dock_width: 640px;
    in-out property <length> inspector_height: 260px;
    in-out property <bool> settings_collapsed: false;
    in-out property <bool> inspector_collapsed: false;
    // The tier table section (`editor_tier_table.slint`): collapsed, only its
    // "TIERS" header row shows and the inspector stretches to fill the dock (the
    // stored `inspector_height` is kept for when the table is expanded again).
    in-out property <bool> tier_table_collapsed: false;
    in-out property <bool> remap_collapsed: false;
    callback layout_changed();
    changed dock_width => { layout_changed(); }
    changed inspector_height => { layout_changed(); }
    changed settings_collapsed => { layout_changed(); }
    changed inspector_collapsed => { layout_changed(); }
    changed tier_table_collapsed => { layout_changed(); }
    changed remap_collapsed => { layout_changed(); }

    // Whether SOME text input in the Edit tab currently has
    // keyboard focus -- set from each `LineEdit`'s own `changed has-focus` (see
    // `editor_tier_table.slint`'s `TierAngleCell`/filter box/batch-offset field
    // and `new_design_dialog.slint`'s numeric fields for the pattern), read by
    // `app.slint`'s `global_shortcuts` to stop Ctrl+Z/Ctrl+Y from undoing a
    // design edit while a cutter is mid-correction on a facet name or a typed
    // number -- see that file's own Ctrl+Z/Ctrl+Y arms.
    //
    // A plain last-write-wins flag, the same convention `editor_tier_table.
    // slint`'s own `table_focused` already uses for one `FocusScope` -- here
    // several independent text fields across several files all write the same
    // property, so a focus move directly from one text field to another (Tab)
    // can in principle see the outgoing field's `false` land after the
    // incoming field's `true` and leave this reading `false` for one frame
    // while a field still has focus; low-severity (it only mis-times a Ctrl+Z
    // pressed in that exact instant) and accepted rather than a monotonic
    // per-field counter, to match the codebase's existing single-flag idiom.
    //
    // Every `LineEdit` in `editor_design_settings.slint` and every remaining
    // `LineEdit` in `editor_inspector.slint` (new facet position, rotate-by,
    // cheater offset, tier note, preform half-width/length-over-width/depth/
    // Y-offset, girdle diameter, specific gravity override, and the three
    // Optimize weights plus budget/seed) carries the same
    // `changed has-focus => { EditorModel.text_focus = self.has-focus; }`
    // handler as the Angle/Meets/Name/Indices fields.
    in-out property <bool> text_focus: false;

    // Builds `count` tiers via step series (e.g. emerald-cut ladder) as one
    // batch, applied as a single undo step. Takes name prefix, start angle,
    // angle step, tier count, comma-separated index list, and optional anchor.
    callback generate_step_series(string, string, string, int, string, string);

    // "Mirror to other block": duplicates the tier at `index` with angle negated
    // and name suffixed (e.g. `"'"`). Takes the name suffix as argument.
    callback mirror_tier_to_other_block(int, string);
}