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
//! Material resolution and the render-time override stack.
//!
//! By-name lookup and every opt-in override layered on top of it
//! (inclusion/subsurface scattering, crystal-axis orientation, facet edge rounding,
//! physical stone size). Moved out of the desktop viewer unchanged so the browser app
//! resolves a design's rendered material exactly the same way for the same settings --
//! see `crate::render_setup`'s own doc comment.
use measure_model_width;
use crate::;
use Vec3;
/// Resolves the current gem material by name: custom materials take priority over the
/// built-in presets.
///
/// Returns `None` when `material_name` matches neither table -- deliberately no
/// `materials[0]` (Diamond) fallback, so a caller cannot silently
/// trace/export/tilt-sweep an unrecognized or unset name as a different stone.
/// Prefers `material_override` over the plain by-name lookup [`resolve_material`]
/// already does.
///
/// Like `resolve_material` -- returns `None` rather than substituting a different
/// stone when neither the override nor the name resolves.
/// Every opt-in render-time material override bundled into one struct, to keep call
/// sites' argument lists short.
/// Applies every [`MaterialOverrides`] field on top of a resolved base material.
///
/// Each one is opt-in and skips its underlying `GemMaterial::with_*` call entirely at
/// its off position, so a material with nothing dialled in renders bit-identical to
/// before these controls existed.
///
/// # Absorption units
///
/// The stone size sets the material's `absorption_path_scale` through
/// [`absorption_path_scale_for`], by the material's own [`AbsorptionUnit`]: a
/// `ModelUnit` material (the built-in table apart from the zoned Ametrine, legacy colour
/// triples, fantasy customs) scales
/// by `stone_width_mm / 7 / MODEL_UNIT_FACE_UP_PATH` (the width factor is 1 while unset, so
/// the scale is never left at 1.0: the calibration always applies); a `PerMm` one (band colours,
/// physics recipes, the zoned Ametrine) by
/// `stone_width_mm / model_width`, with 7 mm for an unset width. See [`AbsorptionUnit`].
///
/// `model_width` is the active design's own girdle width in model units, already
/// resolved by the caller (see [`crate::render_setup::measure_model_width`]) -- this
/// function does no plane-arrangement measurement of its own, so a caller with a
/// persistent cache keyed on the design's geometry (the desktop's `StoneWidthCache`)
/// only pays for a remeasure when the design actually changes, and a one-shot caller
/// can just measure fresh and pass the result straight through. It is only read for a
/// `PerMm` material: ask [`needs_model_width`] before measuring.
/// `material` sized for a stone of `stone_width_mm` on `planes` and nothing else dialled in.
///
/// The stone-size step of [`apply_material_overrides`] alone, measuring the plane arrangement
/// only when the material's unit needs it ([`needs_model_width`]).
///
/// For the callers that score or sweep a stone outside the render loop (tone/tilt metrics, the
/// optimizer, comparisons): they must see the absorption the render shows, so they take the
/// same size rule instead of tracing the bare resolved material. `stone_width_mm == 0.0` is the
/// render's "not set" (a `ModelUnit` material then gets the calibration scale
/// `1 / MODEL_UNIT_FACE_UP_PATH`; `planes` is only read for a `PerMm` material).
/// The stone width in millimetres the absorption was calibrated for, and the width assumed
/// when none is set: 7 mm.
///
/// A [`AbsorptionUnit::ModelUnit`] material renders exactly as it was tuned at this width; a
/// [`AbsorptionUnit::PerMm`] material uses it when no stone size is set.
pub const PHYSICS_DEFAULT_STONE_WIDTH_MM: f32 = 7.0;
/// The representative face-up light path, in model units, that a [`AbsorptionUnit::ModelUnit`]
/// absorption's "one unit" swatch stands for.
///
/// Built-in coloured materials, the toolbar colour presets, legacy `absorption_rgb` triples and
/// fantasy customs are tuned so their swatch looks right over ONE model unit of path
/// (`indicatrix_cut_core::material::color::FANTASY_PATH_UNITS`). Light that leaves a face-up stone
/// has travelled several chords (the tone metric's `mean_path_units`, about 2.5 for a round
/// brilliant), so rendering those numbers at scale 1.0 is several times too dark (the 2026-10-08
/// 10.87 mm green cubic zirconia came out black). The `ModelUnit` scale is therefore divided by
/// this constant: absorption is linear in the scale, so over `MODEL_UNIT_FACE_UP_PATH` units of
/// path the stone shows exactly the colour the swatch shows over one unit, at 7 mm or with no
/// size set, and a larger or smaller stone scales physically from there (`width / 7`).
///
/// Chosen from the tone metric (`FaceUpTone::mean_path_units`, light tent, colourless stone) for
/// the standard round brilliant and the emerald cut; the test
/// `model_unit_face_up_path_matches_the_measured_cuts` re-measures it and prints the values.
/// Measured 2026-10-08 by that test: round brilliant mean path 2.51 (mixture-equivalent 2.54),
/// emerald cut 2.52 (equivalent 2.49), median 2.52.
/// Stored numbers (vault `absorption_rgb`, `.toml` `body_color_override`, the built-in band peaks)
/// are NOT changed: the constant is applied once, here. The colour-to-triple solver
/// (`nearest_legacy_rgb`) keeps solving at one unit, so its outputs stay comparable with stored
/// triples and with the swatch.
pub const MODEL_UNIT_FACE_UP_PATH: f32 = 2.52;
/// Returns the effective stone width in mm: the requested one, or the 7 mm default
/// ([`PHYSICS_DEFAULT_STONE_WIDTH_MM`]) when `requested_mm` is 0.0 (unspecified).
/// Whether [`apply_material_overrides`] reads the design's model-unit girdle width for
/// `material` (only a [`AbsorptionUnit::PerMm`] material does), so a caller can skip the plane
/// measurement otherwise.
/// The `absorption_path_scale` a stone of `stone_width_mm` gets for absorption numbers of
/// `unit`, or `None` when the material keeps the scale it has.
///
/// * [`AbsorptionUnit::ModelUnit`]: `(effective width / 7 mm) / MODEL_UNIT_FACE_UP_PATH`,
/// independent of the design's model width, so the tuned look never depends on how a file
/// happens to be normalised. While no width is set (`stone_width_mm <= 0`) the width factor is
/// 1.0, so an unset size and 7 mm both give `1 / MODEL_UNIT_FACE_UP_PATH`: over the face-up path
/// the stone then shows the colour the swatch shows over one model unit.
/// * [`AbsorptionUnit::PerMm`]: `effective width / model_width` (millimetres per model unit),
/// the effective width being 7 mm while none is set. `None` when `model_width` is missing or
/// not positive.
///
/// `None` as well for a non-finite or non-positive result.