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
//! Helpers shared by the local and remote detail-load paths: angle-side
//! classification, the filtered-row-count callback, display formatting, the
//! material guess, and clearing the detail pane.
use crate::;
use ;
/// The angle magnitude (degrees) at or above which a catalogue schedule row is
/// treated as describing the girdle itself rather than a crown or pavilion facet --
/// a fallback heuristic (see [`sides_from_angle_sequence`]'s own doc
/// comment for why a sign-based classifier cannot work on this data).
const GIRDLE_ANGLE_THRESHOLD_DEG: f64 = 89.5;
/// Parses a catalogue angle-settings row's `angle` text the same way
/// `indicatrix_vault::local::parse_angle_deg` does -- that function is private to the
/// vault crate, so this reimplements its two-line body: strip a trailing `\u{b0}`
/// degree sign, then a plain `f64` parse. `None` for text that still doesn't parse.
pub
/// Derives every row's [`AngleItem::side`] (`-1` pavilion, `1` crown, `0` neither) from
/// the ORDER a schedule lists `angles` in, not from the sign of the angle text.
///
/// Measured on the real catalogue: 50,809 of 50,817 stored `angle_settings.angle`
/// values end in `\u{b0}` (so a bare `str::parse` fails on virtually every row unless
/// that's stripped first, reporting `0`/neither side for everything), and every stored pavilion angle is
/// written UNSIGNED -- so even after stripping the degree sign, a sign check
/// (`deg < 0.0` / `deg > 0.0`) still reports every real row as `0`. The Pavilion/Crown
/// filter pills in `cutting_table.slint` were therefore dead for every stored design.
///
/// This crate's own schedule rows (`gui::editor::state::cutting_schedule_rows`) take
/// the side from the SOLVER's per-tier block classification instead, which needs a
/// solved [`indicatrix_cut_core::Design`] and is `pub(super)` to `gui::editor` -- not
/// reachable from this module (which deliberately never names that feature-gated
/// `Design` type in its own signatures, see `resolve_catalogue_planes_for_entry`'s own
/// doc comment) without pulling in the whole editor pipeline just to classify a
/// display-only column. This is the documented fallback in its place: a catalogue
/// schedule reliably lists crown facets first, then the girdle row(s) (~90 degrees),
/// then pavilion facets -- the same order `.asc`/`GemCad` schedules and this crate's own
/// tier table use. So the FIRST row whose angle is at least
/// [`GIRDLE_ANGLE_THRESHOLD_DEG`] marks the crown/pavilion boundary: every row at or
/// past that magnitude (there may be more than one girdle-adjacent facet) reports `0`
/// (the girdle itself, neither side); every row before the boundary is crown (`1`);
/// every row strictly after it is pavilion (`-1`). A row whose angle text doesn't
/// parse at all also reports `0`.
pub
/// Counts `angles` under `mode` (0 = All, 1 = Pavilion, 2 = Crown) by
/// [`AngleItem::side`] -- exactly `cutting_table.slint`'s own `row_shown` predicate
/// (side < 0 pavilion, side > 0 crown), pulled out so [`setup_filtered_row_count_callback`]
/// stays a thin Slint-callback wrapper.
pub
/// Registers `LibraryModel::filtered_row_count` -- the per-mode row count
/// `cutting_table.slint`'s "All Steps (N)"/Pavilion/Crown pills need, which cannot
/// be computed in Slint itself (see the comment above that file's row-count
/// pills for why: Slint has no general loop or array `.filter`/`.reduce`, and a
/// `pure function` there cannot even recurse to fake one). Reads
/// `LibraryModel.current_angles` fresh on every call rather than
/// taking it as a second argument -- see [`count_angles_for_mode`]'s doc comment,
/// and `library.slint`'s own doc comment on `filtered_row_count` for why this
/// still reacts correctly to a `current_angles` change.
/// Clears the detail pane's display fields and drops the current row selection.
///
/// Shared by [`crate::gui::library::local::organize`]'s delete handler (the original
/// "the selected design is gone" case) and
/// [`crate::gui::library::search::apply_diagram_list_to_ui`] (a search/
/// filter refresh that drops the previously selected row off the new result list must
/// not leave `LibraryModel.selected_entry_id` pointing at a design no longer even
/// visible in the list it came from). Deliberately leaves `RenderContext`/the 3D
/// viewport untouched: unlike an actual delete, the design itself hasn't changed here,
/// only whether the LIST currently shows it, so there is nothing wrong with the
/// viewport going on tracing it.
/// Formats a stored proportion string to 3 decimal places for the detail header's
/// metric chips -- DISPLAY ONLY. What actually lives in the database is never touched
/// by this (see `indicatrix_vault::db::sqlite::Database::update_diagram_metadata`,
/// which always round-trips a proportion's full, unrounded text -- the user's own
/// instruction is "store everything, discard nothing, don't recalculate").
///
/// Parses first: only a value that parses cleanly as a finite `f64` gets reformatted.
/// Anything that doesn't parse -- a scraped legacy string in some other format, or a
/// future value this app didn't write -- passes through completely untouched, so it
/// can never be coerced into a misleading "0.000". Checked against the real
/// ~3,187-design catalogue (`facet_diagrams.sqlite`): every non-null `lw_ratio`/
/// `hw_ratio`/`cw_ratio`/`pw_ratio`/`volume` value there is already a plain
/// REAL-affinity number with no exceptions, but the model type is `Option<String>` and
/// nothing guarantees that stays true forever, so this stays defensive rather than
/// assuming it.
/// [`format_proportion`] over an `Option<&str>`, collapsing `None` to `""` -- the same
/// "empty string hides the chip" convention every other optional field on
/// `DiagramDetailData` already uses (see `detail_header.slint`'s `if root.detail.xxx
/// != "":` chips).
pub
/// Trims `text` and turns a blank result into `None` -- the metadata editor's
/// convention for "the user cleared this field", matching
/// `Database::rename_diagram_entry`'s own trim. `None` is what
/// [`indicatrix_vault::model::metadata_update::MetadataUpdate`] stores as a field's new
/// value in that case, not an empty-string sentinel -- consistent with every other
/// optional column this app writes.
pub
/// [`DiagramDetailData::material_guess`]'s own computation -- a catalogue row
/// records only a refractive index string (`ri_text`, `None`/unparseable
/// reads as "nothing to guess"), so this is the only honest way to name the
/// stone for the card ("Inferred material shown as a guess, never as a fact").
/// Formatted
/// identically to the editor's own material-guess badge ("Sapphire? (from RI
/// 1.76)") so the two never disagree about wording; `""` when nothing built
/// in is close enough within [`MATERIAL_MATCH_TOLERANCE`] -- hides the chip
/// entirely rather than showing an empty/misleading one.
pub