Skip to main content

proof_engine/editor/
kit_panel.rs

1//! Kit Parameter Panel — real-time sliders for every rendering kit.
2//!
3//! # Overview
4//!
5//! The kit panel exposes every tunable parameter from all eight kits:
6//! LightingKit, MaterialKit, ClothingKit, HairKit, PhysicsKit, RenderKit,
7//! BoneKit (envelope radii), and ModelKit (cross-section knots), plus the full
8//! post-processing pipeline (bloom, AO, SSS, Fresnel, etc.).
9//!
10//! All changes are immediately reflected in the viewport.  Every slider
11//! mutation records a `KitEdit` into the undo stack so changes are reversible.
12//!
13//! # Slider types
14//!
15//! - `FloatSlider` — clamped f32 with optional logarithmic scale.
16//! - `ColorPicker` — RGBA colour with hex input and HSV wheel.
17//! - `Vec3Field`   — three linked floats (drag-any-axis).
18//! - `EnumPicker`  — dropdown for tagged enum fields.
19//! - `BoolToggle`  — checkbox for on/off flags.
20//! - `IntSlider`   — clamped i32.
21
22use glam::{Vec2, Vec3, Vec4};
23use std::collections::HashMap;
24
25// ─────────────────────────────────────────────────────────────────────────────
26// Slider value types
27// ─────────────────────────────────────────────────────────────────────────────
28
29/// A clamped f32 with optional logarithmic mapping.
30#[derive(Debug, Clone)]
31pub struct FloatSlider {
32    pub label:     String,
33    pub value:     f32,
34    pub min:       f32,
35    pub max:       f32,
36    pub step:      f32,
37    pub logarithmic: bool,
38    pub tooltip:   Option<String>,
39    pub unit:      Option<&'static str>,
40    pub precision: usize,
41}
42
43impl FloatSlider {
44    pub fn new(label: impl Into<String>, value: f32, min: f32, max: f32) -> Self {
45        Self {
46            label: label.into(), value, min, max,
47            step: (max - min) / 1000.0,
48            logarithmic: false,
49            tooltip: None,
50            unit: None,
51            precision: 3,
52        }
53    }
54
55    pub fn log(mut self) -> Self { self.logarithmic = true; self }
56    pub fn unit(mut self, u: &'static str) -> Self { self.unit = Some(u); self }
57    pub fn tip(mut self, t: impl Into<String>) -> Self { self.tooltip = Some(t.into()); self }
58    pub fn prec(mut self, p: usize) -> Self { self.precision = p; self }
59
60    pub fn set(&mut self, v: f32) {
61        self.value = if self.logarithmic {
62            v.clamp(self.min.max(1e-9), self.max)
63        } else {
64            v.clamp(self.min, self.max)
65        };
66    }
67
68    pub fn normalized(&self) -> f32 {
69        if self.logarithmic {
70            let a = self.min.max(1e-9).ln();
71            let b = self.max.ln();
72            (self.value.max(1e-9).ln() - a) / (b - a)
73        } else {
74            (self.value - self.min) / (self.max - self.min)
75        }
76    }
77
78    pub fn set_normalized(&mut self, t: f32) {
79        let v = if self.logarithmic {
80            let a = self.min.max(1e-9).ln();
81            let b = self.max.ln();
82            (a + t * (b - a)).exp()
83        } else {
84            self.min + t * (self.max - self.min)
85        };
86        self.set(v);
87    }
88
89    pub fn display(&self) -> String {
90        if let Some(u) = self.unit {
91            format!("{:.prec$} {u}", self.value, prec = self.precision)
92        } else {
93            format!("{:.prec$}", self.value, prec = self.precision)
94        }
95    }
96}
97
98/// A clamped i32 slider.
99#[derive(Debug, Clone)]
100pub struct IntSlider {
101    pub label: String,
102    pub value: i32,
103    pub min:   i32,
104    pub max:   i32,
105    pub step:  i32,
106    pub tooltip: Option<String>,
107}
108
109impl IntSlider {
110    pub fn new(label: impl Into<String>, value: i32, min: i32, max: i32) -> Self {
111        Self { label: label.into(), value, min, max, step: 1, tooltip: None }
112    }
113    pub fn set(&mut self, v: i32) { self.value = v.clamp(self.min, self.max); }
114    pub fn increment(&mut self) { self.set(self.value + self.step); }
115    pub fn decrement(&mut self) { self.set(self.value - self.step); }
116}
117
118/// An RGBA colour picker.
119#[derive(Debug, Clone)]
120pub struct ColorPicker {
121    pub label: String,
122    pub value: Vec4,
123    pub hdr:   bool,
124    pub tooltip: Option<String>,
125}
126
127impl ColorPicker {
128    pub fn new(label: impl Into<String>, value: Vec4) -> Self {
129        Self { label: label.into(), value, hdr: false, tooltip: None }
130    }
131    pub fn hdr(mut self) -> Self { self.hdr = true; self }
132
133    pub fn to_hex(&self) -> String {
134        let r = (self.value.x.clamp(0.0, 1.0) * 255.0) as u8;
135        let g = (self.value.y.clamp(0.0, 1.0) * 255.0) as u8;
136        let b = (self.value.z.clamp(0.0, 1.0) * 255.0) as u8;
137        let a = (self.value.w.clamp(0.0, 1.0) * 255.0) as u8;
138        format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
139    }
140
141    pub fn from_hex(s: &str) -> Option<Vec4> {
142        let s = s.trim_start_matches('#');
143        if s.len() < 6 { return None; }
144        let r = u8::from_str_radix(&s[0..2], 16).ok()? as f32 / 255.0;
145        let g = u8::from_str_radix(&s[2..4], 16).ok()? as f32 / 255.0;
146        let b = u8::from_str_radix(&s[4..6], 16).ok()? as f32 / 255.0;
147        let a = if s.len() >= 8 {
148            u8::from_str_radix(&s[6..8], 16).ok()? as f32 / 255.0
149        } else { 1.0 };
150        Some(Vec4::new(r, g, b, a))
151    }
152
153    pub fn to_hsv(&self) -> (f32, f32, f32) {
154        let r = self.value.x; let g = self.value.y; let b = self.value.z;
155        let max = r.max(g).max(b);
156        let min = r.min(g).min(b);
157        let delta = max - min;
158        let v = max;
159        let s = if max > 0.0 { delta / max } else { 0.0 };
160        let h = if delta < 1e-6 { 0.0 }
161        else if max == r { 60.0 * (((g - b) / delta).rem_euclid(6.0)) }
162        else if max == g { 60.0 * ((b - r) / delta + 2.0) }
163        else             { 60.0 * ((r - g) / delta + 4.0) };
164        (h, s, v)
165    }
166}
167
168/// A boolean toggle.
169#[derive(Debug, Clone)]
170pub struct BoolToggle {
171    pub label: String,
172    pub value: bool,
173    pub tooltip: Option<String>,
174}
175
176impl BoolToggle {
177    pub fn new(label: impl Into<String>, value: bool) -> Self {
178        Self { label: label.into(), value, tooltip: None }
179    }
180    pub fn toggle(&mut self) { self.value = !self.value; }
181}
182
183/// A 3-component Vec3 field with linked/unlinked dragging.
184#[derive(Debug, Clone)]
185pub struct Vec3Field {
186    pub label:  String,
187    pub value:  Vec3,
188    pub min:    f32,
189    pub max:    f32,
190    pub step:   f32,
191    pub linked: bool,
192    pub tooltip: Option<String>,
193}
194
195impl Vec3Field {
196    pub fn new(label: impl Into<String>, value: Vec3, min: f32, max: f32) -> Self {
197        Self { label: label.into(), value, min, max, step: 0.01, linked: false, tooltip: None }
198    }
199
200    pub fn set_x(&mut self, v: f32) {
201        let v = v.clamp(self.min, self.max);
202        if self.linked {
203            let scale = if self.value.x.abs() > 1e-6 { v / self.value.x } else { 1.0 };
204            self.value *= scale;
205        } else {
206            self.value.x = v;
207        }
208    }
209    pub fn set_y(&mut self, v: f32) {
210        let v = v.clamp(self.min, self.max);
211        if self.linked {
212            let scale = if self.value.y.abs() > 1e-6 { v / self.value.y } else { 1.0 };
213            self.value *= scale;
214        } else {
215            self.value.y = v;
216        }
217    }
218    pub fn set_z(&mut self, v: f32) {
219        let v = v.clamp(self.min, self.max);
220        if self.linked {
221            let scale = if self.value.z.abs() > 1e-6 { v / self.value.z } else { 1.0 };
222            self.value *= scale;
223        } else {
224            self.value.z = v;
225        }
226    }
227}
228
229/// An enum dropdown field.
230#[derive(Debug, Clone)]
231pub struct EnumPicker {
232    pub label:   String,
233    pub options: Vec<String>,
234    pub index:   usize,
235    pub tooltip: Option<String>,
236}
237
238impl EnumPicker {
239    pub fn new(label: impl Into<String>, options: Vec<String>, index: usize) -> Self {
240        Self { label: label.into(), options, index, tooltip: None }
241    }
242    pub fn selected(&self) -> &str {
243        self.options.get(self.index).map(|s| s.as_str()).unwrap_or("")
244    }
245    pub fn select(&mut self, idx: usize) { self.index = idx.min(self.options.len().saturating_sub(1)); }
246    pub fn next(&mut self) { self.select(self.index + 1); }
247    pub fn prev(&mut self) { self.select(self.index.saturating_sub(1)); }
248}
249
250// ─────────────────────────────────────────────────────────────────────────────
251// KitParam — a single parameter in the panel
252// ─────────────────────────────────────────────────────────────────────────────
253
254/// The discriminant for a kit parameter widget.
255#[derive(Debug, Clone)]
256pub enum KitParam {
257    Float(FloatSlider),
258    Int(IntSlider),
259    Color(ColorPicker),
260    Vec3(Vec3Field),
261    Bool(BoolToggle),
262    Enum(EnumPicker),
263    Separator(String),
264    Header(String),
265}
266
267impl KitParam {
268    pub fn label(&self) -> &str {
269        match self {
270            KitParam::Float(s) => &s.label,
271            KitParam::Int(s)   => &s.label,
272            KitParam::Color(s) => &s.label,
273            KitParam::Vec3(s)  => &s.label,
274            KitParam::Bool(s)  => &s.label,
275            KitParam::Enum(s)  => &s.label,
276            KitParam::Separator(l) => l,
277            KitParam::Header(l)    => l,
278        }
279    }
280
281    pub fn is_editable(&self) -> bool {
282        !matches!(self, KitParam::Separator(_) | KitParam::Header(_))
283    }
284}
285
286// ─────────────────────────────────────────────────────────────────────────────
287// KitGroup
288// ─────────────────────────────────────────────────────────────────────────────
289
290/// A collapsible group of parameters (one per kit).
291#[derive(Debug, Clone)]
292pub struct KitGroup {
293    pub name:      String,
294    pub collapsed: bool,
295    pub params:    Vec<(String, KitParam)>,
296}
297
298impl KitGroup {
299    pub fn new(name: impl Into<String>) -> Self {
300        Self { name: name.into(), collapsed: false, params: Vec::new() }
301    }
302
303    pub fn push(&mut self, key: impl Into<String>, param: KitParam) {
304        self.params.push((key.into(), param));
305    }
306
307    pub fn get(&self, key: &str) -> Option<&KitParam> {
308        self.params.iter().find(|(k, _)| k == key).map(|(_, p)| p)
309    }
310
311    pub fn get_mut(&mut self, key: &str) -> Option<&mut KitParam> {
312        self.params.iter_mut().find(|(k, _)| k == key).map(|(_, p)| p)
313    }
314
315    pub fn get_float(&self, key: &str) -> Option<f32> {
316        match self.get(key)? {
317            KitParam::Float(s) => Some(s.value),
318            _ => None,
319        }
320    }
321
322    pub fn set_float(&mut self, key: &str, v: f32) {
323        if let Some(KitParam::Float(s)) = self.get_mut(key) { s.set(v); }
324    }
325
326    pub fn get_bool(&self, key: &str) -> Option<bool> {
327        match self.get(key)? {
328            KitParam::Bool(b) => Some(b.value),
329            _ => None,
330        }
331    }
332
333    pub fn get_color(&self, key: &str) -> Option<Vec4> {
334        match self.get(key)? {
335            KitParam::Color(c) => Some(c.value),
336            _ => None,
337        }
338    }
339
340    pub fn get_vec3(&self, key: &str) -> Option<Vec3> {
341        match self.get(key)? {
342            KitParam::Vec3(v) => Some(v.value),
343            _ => None,
344        }
345    }
346}
347
348// ─────────────────────────────────────────────────────────────────────────────
349// KitPanelState — the full state of all kit sliders
350// ─────────────────────────────────────────────────────────────────────────────
351
352/// A single mutation event for undo purposes.
353#[derive(Debug, Clone)]
354pub struct KitEdit {
355    pub group:    String,
356    pub key:      String,
357    pub old:      KitEditValue,
358    pub new:      KitEditValue,
359}
360
361#[derive(Debug, Clone)]
362pub enum KitEditValue {
363    Float(f32),
364    Int(i32),
365    Color(Vec4),
366    Vec3(Vec3),
367    Bool(bool),
368    Enum(usize),
369}
370
371/// The top-level kit parameter panel.
372#[derive(Debug)]
373pub struct KitPanel {
374    pub groups:    Vec<KitGroup>,
375    undo_stack:    Vec<KitEdit>,
376    redo_stack:    Vec<KitEdit>,
377    pub search:    String,
378    pub dirty:     bool,
379}
380
381impl KitPanel {
382    pub fn new() -> Self {
383        let mut panel = Self {
384            groups: Vec::new(),
385            undo_stack: Vec::new(),
386            redo_stack: Vec::new(),
387            search: String::new(),
388            dirty: false,
389        };
390        panel.build_all_groups();
391        panel
392    }
393
394    fn build_all_groups(&mut self) {
395        self.groups.push(Self::build_lighting_kit());
396        self.groups.push(Self::build_bloom_group());
397        self.groups.push(Self::build_ao_group());
398        self.groups.push(Self::build_sss_group());
399        self.groups.push(Self::build_hair_kit());
400        self.groups.push(Self::build_cloth_kit());
401        self.groups.push(Self::build_physics_kit());
402        self.groups.push(Self::build_render_kit());
403        self.groups.push(Self::build_postfx_group());
404        self.groups.push(Self::build_camera_group());
405        self.groups.push(Self::build_env_group());
406    }
407
408    // ── Kit builders ──────────────────────────────────────────────────────
409
410    fn build_lighting_kit() -> KitGroup {
411        let mut g = KitGroup::new("LightingKit");
412        g.push("header_key",      KitParam::Header("Key Light".into()));
413        g.push("key_dir",         KitParam::Vec3(Vec3Field::new("Key Direction", Vec3::new(-0.6, -0.8, 0.4), -1.0, 1.0)));
414        g.push("key_color",       KitParam::Color(ColorPicker::new("Key Color", Vec4::new(1.0, 0.97, 0.90, 1.0))));
415        g.push("key_intensity",   KitParam::Float(FloatSlider::new("Key Intensity", 2.2, 0.0, 10.0).unit("lx")));
416        g.push("sep_fills",       KitParam::Separator("Fill Lights".into()));
417        g.push("fill1_dir",       KitParam::Vec3(Vec3Field::new("Fill1 Dir", Vec3::new(0.8, -0.3, 0.5), -1.0, 1.0)));
418        g.push("fill1_color",     KitParam::Color(ColorPicker::new("Fill1 Color", Vec4::new(0.5, 0.6, 0.9, 1.0))));
419        g.push("fill1_intensity", KitParam::Float(FloatSlider::new("Fill1 Intensity", 0.6, 0.0, 5.0)));
420        g.push("fill2_dir",       KitParam::Vec3(Vec3Field::new("Fill2 Dir", Vec3::new(-0.2, 0.5, -0.8), -1.0, 1.0)));
421        g.push("fill2_color",     KitParam::Color(ColorPicker::new("Fill2 Color", Vec4::new(0.2, 0.3, 0.5, 1.0))));
422        g.push("fill2_intensity", KitParam::Float(FloatSlider::new("Fill2 Intensity", 0.3, 0.0, 5.0)));
423        g.push("fill3_dir",       KitParam::Vec3(Vec3Field::new("Fill3 Dir", Vec3::new(0.0, 0.8, 0.0), -1.0, 1.0)));
424        g.push("fill3_color",     KitParam::Color(ColorPicker::new("Fill3 Color", Vec4::new(0.3, 0.4, 0.6, 1.0))));
425        g.push("fill3_intensity", KitParam::Float(FloatSlider::new("Fill3 Intensity", 0.2, 0.0, 5.0)));
426        g.push("sep_rim",         KitParam::Separator("Rim Light".into()));
427        g.push("rim_dir",         KitParam::Vec3(Vec3Field::new("Rim Dir", Vec3::new(0.0, 0.2, -1.0), -1.0, 1.0)));
428        g.push("rim_color",       KitParam::Color(ColorPicker::new("Rim Color", Vec4::new(0.9, 0.95, 1.0, 1.0))));
429        g.push("rim_intensity",   KitParam::Float(FloatSlider::new("Rim Intensity", 1.8, 0.0, 8.0)));
430        g.push("sep_ambient",     KitParam::Separator("Hemisphere Ambient".into()));
431        g.push("ambient_sky",     KitParam::Color(ColorPicker::new("Sky Color", Vec4::new(0.3, 0.45, 0.7, 1.0))));
432        g.push("ambient_ground",  KitParam::Color(ColorPicker::new("Ground Color", Vec4::new(0.2, 0.18, 0.15, 1.0))));
433        g.push("ambient_strength",KitParam::Float(FloatSlider::new("Ambient Strength", 0.4, 0.0, 2.0)));
434        g.push("tonemap",         KitParam::Enum(EnumPicker::new("Tonemap",
435            vec!["ACES".into(),"Reinhard".into(),"Filmic".into(),"Uncharted2".into(),"Linear".into()], 0)));
436        g.push("exposure",        KitParam::Float(FloatSlider::new("Exposure", 1.0, 0.1, 8.0).log()));
437        g
438    }
439
440    fn build_bloom_group() -> KitGroup {
441        let mut g = KitGroup::new("Bloom");
442        g.push("enabled",     KitParam::Bool(BoolToggle::new("Bloom Enabled", true)));
443        g.push("intensity",   KitParam::Float(FloatSlider::new("Intensity", 2.8, 0.0, 20.0)));
444        g.push("radius",      KitParam::Float(FloatSlider::new("Radius", 12.0, 0.5, 128.0)));
445        g.push("threshold",   KitParam::Float(FloatSlider::new("Threshold", 0.8, 0.0, 4.0)));
446        g.push("scatter",     KitParam::Float(FloatSlider::new("Scatter", 0.7, 0.0, 1.0)));
447        g.push("tint",        KitParam::Color(ColorPicker::new("Tint", Vec4::new(1.0, 0.95, 0.85, 1.0))));
448        g.push("spectral",    KitParam::Bool(BoolToggle::new("Spectral Dispersion", true)));
449        g.push("lut_enabled", KitParam::Bool(BoolToggle::new("LUT Grade", false)));
450        g.push("chromatic",   KitParam::Float(FloatSlider::new("Chromatic Aberration", 0.0028, 0.0, 0.2).prec(4)));
451        g.push("grain",       KitParam::Float(FloatSlider::new("Film Grain", 0.025, 0.0, 0.5)));
452        g.push("vignette",    KitParam::Float(FloatSlider::new("Vignette", 0.3, 0.0, 1.0)));
453        g
454    }
455
456    fn build_ao_group() -> KitGroup {
457        let mut g = KitGroup::new("AmbientOcclusion");
458        g.push("enabled",    KitParam::Bool(BoolToggle::new("SDF AO Enabled", true)));
459        g.push("strength",   KitParam::Float(FloatSlider::new("AO Strength", 1.0, 0.0, 3.0)));
460        g.push("steps",      KitParam::Int(IntSlider::new("AO Steps", 5, 2, 12)));
461        g.push("step_size",  KitParam::Float(FloatSlider::new("Step Size", 0.012, 0.001, 0.1)));
462        g.push("decay",      KitParam::Float(FloatSlider::new("Geometric Decay", 0.82, 0.1, 1.0)));
463        g.push("falloff",    KitParam::Float(FloatSlider::new("Falloff Power", 1.2, 0.1, 4.0)));
464        g.push("tint",       KitParam::Color(ColorPicker::new("AO Tint", Vec4::new(0.05, 0.05, 0.08, 1.0))));
465        g
466    }
467
468    fn build_sss_group() -> KitGroup {
469        let mut g = KitGroup::new("SubsurfaceScattering");
470        g.push("enabled",    KitParam::Bool(BoolToggle::new("SSS Enabled", true)));
471        g.push("strength",   KitParam::Float(FloatSlider::new("SSS Strength", 0.6, 0.0, 2.0)));
472        g.push("thickness",  KitParam::Float(FloatSlider::new("Probe Depth", 0.04, 0.001, 0.2)));
473        g.push("scatter_r",  KitParam::Float(FloatSlider::new("Scatter R", 0.8, 0.0, 4.0)));
474        g.push("scatter_g",  KitParam::Float(FloatSlider::new("Scatter G", 0.4, 0.0, 4.0)));
475        g.push("scatter_b",  KitParam::Float(FloatSlider::new("Scatter B", 0.3, 0.0, 4.0)));
476        g.push("tint",       KitParam::Color(ColorPicker::new("SSS Tint", Vec4::new(0.9, 0.4, 0.3, 1.0))));
477        g.push("distortion", KitParam::Float(FloatSlider::new("Normal Distortion", 0.1, 0.0, 1.0)));
478        g.push("power",      KitParam::Float(FloatSlider::new("Scatter Power", 1.4, 0.1, 8.0)));
479        g
480    }
481
482    fn build_hair_kit() -> KitGroup {
483        let mut g = KitGroup::new("HairKit");
484        g.push("strands",      KitParam::Int(IntSlider::new("Strand Count", 500, 10, 2000)));
485        g.push("length",       KitParam::Float(FloatSlider::new("Length", 0.18, 0.01, 1.0)));
486        g.push("width",        KitParam::Float(FloatSlider::new("Width", 0.004, 0.001, 0.04)));
487        g.push("wave_freq",    KitParam::Float(FloatSlider::new("Wave Frequency", 2.5, 0.0, 20.0)));
488        g.push("wave_amp",     KitParam::Float(FloatSlider::new("Wave Amplitude", 0.015, 0.0, 0.1)));
489        g.push("root_color",   KitParam::Color(ColorPicker::new("Root Color", Vec4::new(0.12, 0.08, 0.05, 1.0))));
490        g.push("tip_color",    KitParam::Color(ColorPicker::new("Tip Color", Vec4::new(0.20, 0.15, 0.09, 1.0))));
491        g.push("kk_primary",   KitParam::Float(FloatSlider::new("KK Primary Spec", 0.4, 0.0, 2.0)));
492        g.push("kk_secondary", KitParam::Float(FloatSlider::new("KK Secondary Spec", 0.2, 0.0, 1.0)));
493        g.push("kk_shift",     KitParam::Float(FloatSlider::new("KK Spec Shift", 0.03, -0.1, 0.1)));
494        g.push("anisotropy",   KitParam::Float(FloatSlider::new("Anisotropy", 0.85, 0.0, 1.0)));
495        g.push("clump",        KitParam::Float(FloatSlider::new("Clump Factor", 0.3, 0.0, 1.0)));
496        g.push("scatter",      KitParam::Float(FloatSlider::new("Scatter Density", 12.0, 1.0, 30.0)));
497        g
498    }
499
500    fn build_cloth_kit() -> KitGroup {
501        let mut g = KitGroup::new("ClothingKit");
502        g.push("push_radius",  KitParam::Float(FloatSlider::new("Garment Push Radius", 0.015, 0.0, 0.1)));
503        g.push("seam_dark",    KitParam::Float(FloatSlider::new("Seam Darkening", 0.3, 0.0, 1.0)));
504        g.push("contact_shad", KitParam::Float(FloatSlider::new("Contact Shadow", 0.4, 0.0, 1.0)));
505        g.push("wrinkle_amp",  KitParam::Float(FloatSlider::new("Wrinkle Amplitude", 0.008, 0.0, 0.05)));
506        g.push("wrinkle_freq", KitParam::Float(FloatSlider::new("Wrinkle Frequency", 18.0, 1.0, 60.0)));
507        g.push("fabric_roughness", KitParam::Float(FloatSlider::new("Fabric Roughness", 0.85, 0.0, 1.0)));
508        g.push("iridescence",  KitParam::Float(FloatSlider::new("Leather Iridescence", 0.1, 0.0, 1.0)));
509        g.push("thin_film_ior",KitParam::Float(FloatSlider::new("Thin-Film IOR", 1.45, 1.0, 3.0)));
510        g
511    }
512
513    fn build_physics_kit() -> KitGroup {
514        let mut g = KitGroup::new("PhysicsKit");
515        g.push("inertia_hair",   KitParam::Float(FloatSlider::new("Hair Inertia", 0.92, 0.0, 1.0)));
516        g.push("inertia_cloth",  KitParam::Float(FloatSlider::new("Cloth Inertia", 0.85, 0.0, 1.0)));
517        g.push("inertia_skin",   KitParam::Float(FloatSlider::new("Skin Inertia", 0.98, 0.0, 1.0)));
518        g.push("gravity",        KitParam::Vec3(Vec3Field::new("Gravity", Vec3::new(0.0, -9.81, 0.0), -20.0, 20.0)));
519        g.push("wind_dir",       KitParam::Vec3(Vec3Field::new("Wind Direction", Vec3::new(1.0, 0.0, 0.0), -1.0, 1.0)));
520        g.push("wind_strength",  KitParam::Float(FloatSlider::new("Wind Strength", 0.0, 0.0, 5.0)));
521        g.push("drag",           KitParam::Float(FloatSlider::new("Air Drag", 0.02, 0.0, 1.0)));
522        g.push("collision",      KitParam::Bool(BoolToggle::new("SDF Collision", true)));
523        g.push("substeps",       KitParam::Int(IntSlider::new("Substeps", 4, 1, 16)));
524        g
525    }
526
527    fn build_render_kit() -> KitGroup {
528        let mut g = KitGroup::new("RenderKit");
529        g.push("render_scale",   KitParam::Float(FloatSlider::new("Render Scale", 1.0, 0.25, 8.0)));
530        g.push("n_copies",       KitParam::Int(IntSlider::new("Oversampling (n_copies)", 1, 1, 32)));
531        g.push("dof_enabled",    KitParam::Bool(BoolToggle::new("Depth of Field", false)));
532        g.push("dof_focus",      KitParam::Float(FloatSlider::new("DoF Focus Dist", 2.0, 0.1, 50.0)));
533        g.push("dof_aperture",   KitParam::Float(FloatSlider::new("DoF Aperture", 0.05, 0.0, 1.0)));
534        g.push("dof_jitter",     KitParam::Float(FloatSlider::new("DoF Bokeh Jitter", 0.012, 0.0, 0.1)));
535        g.push("alpha_scale",    KitParam::Float(FloatSlider::new("Alpha Scale", 1.0, 0.01, 4.0)));
536        g.push("emission_scale", KitParam::Float(FloatSlider::new("Emission Scale", 1.0, 0.0, 8.0)));
537        g.push("taa",            KitParam::Bool(BoolToggle::new("TAA Jitter", true)));
538        g.push("fxaa",           KitParam::Bool(BoolToggle::new("FXAA", true)));
539        g.push("motion_blur",    KitParam::Bool(BoolToggle::new("Motion Blur", true)));
540        g.push("motion_samples", KitParam::Int(IntSlider::new("Motion Blur Samples", 8, 2, 16)));
541        g.push("scanlines",      KitParam::Bool(BoolToggle::new("Scanlines", false)));
542        g.push("scanline_int",   KitParam::Float(FloatSlider::new("Scanline Intensity", 0.06, 0.0, 1.0)));
543        g
544    }
545
546    fn build_postfx_group() -> KitGroup {
547        let mut g = KitGroup::new("PostFX");
548        g.push("ssr",             KitParam::Bool(BoolToggle::new("SSR (SDF)", true)));
549        g.push("god_rays",        KitParam::Bool(BoolToggle::new("God Rays", true)));
550        g.push("god_ray_density", KitParam::Float(FloatSlider::new("God Ray Density", 0.4, 0.0, 1.0)));
551        g.push("heat_haze",       KitParam::Bool(BoolToggle::new("Heat Haze", false)));
552        g.push("heat_strength",   KitParam::Float(FloatSlider::new("Heat Strength", 0.005, 0.0, 0.05)));
553        g.push("auto_expose",     KitParam::Bool(BoolToggle::new("Auto Exposure", true)));
554        g.push("ae_speed",        KitParam::Float(FloatSlider::new("Auto Exposure Speed", 2.0, 0.1, 10.0)));
555        g.push("ae_min",          KitParam::Float(FloatSlider::new("AE Min EV", -3.0, -8.0, 0.0)));
556        g.push("ae_max",          KitParam::Float(FloatSlider::new("AE Max EV",  3.0,  0.0, 8.0)));
557        g.push("edge_sharpen",    KitParam::Float(FloatSlider::new("Edge Sharpen", 0.3, 0.0, 2.0)));
558        g.push("atm_scatter",     KitParam::Bool(BoolToggle::new("Atmospheric Scatter", true)));
559        g.push("atm_density",     KitParam::Float(FloatSlider::new("Atmosphere Density", 0.05, 0.0, 0.5)));
560        g.push("gi_bleed",        KitParam::Bool(BoolToggle::new("Cross-Material GI Bleed", true)));
561        g.push("gi_strength",     KitParam::Float(FloatSlider::new("GI Bleed Strength", 0.2, 0.0, 1.0)));
562        g
563    }
564
565    fn build_camera_group() -> KitGroup {
566        let mut g = KitGroup::new("Camera");
567        g.push("fov",        KitParam::Float(FloatSlider::new("Field of View", 65.0, 10.0, 120.0).unit("°")));
568        g.push("near",       KitParam::Float(FloatSlider::new("Near Plane", 0.01, 0.001, 1.0).log()));
569        g.push("far",        KitParam::Float(FloatSlider::new("Far Plane", 1000.0, 10.0, 50000.0).log()));
570        g.push("orbit_dist", KitParam::Float(FloatSlider::new("Orbit Distance", 3.0, 0.1, 100.0)));
571        g.push("orbit_speed",KitParam::Float(FloatSlider::new("Orbit Speed", 1.5, 0.01, 10.0)));
572        g.push("pan_speed",  KitParam::Float(FloatSlider::new("Pan Speed", 2.0, 0.01, 20.0)));
573        g.push("fly_speed",  KitParam::Float(FloatSlider::new("Fly Speed", 5.0, 0.1, 50.0)));
574        g.push("spring_k",   KitParam::Float(FloatSlider::new("Spring K (trauma)", 8.0, 0.0, 40.0)));
575        g.push("trauma_dec", KitParam::Float(FloatSlider::new("Trauma Decay", 3.0, 0.1, 20.0)));
576        g
577    }
578
579    fn build_env_group() -> KitGroup {
580        let mut g = KitGroup::new("Environment");
581        g.push("sky_enabled",  KitParam::Bool(BoolToggle::new("Sky Enabled", true)));
582        g.push("sky_type",     KitParam::Enum(EnumPicker::new("Sky Type",
583            vec!["Nishita".into(),"Solid".into(),"HDRI".into(),"Gradient".into()], 0)));
584        g.push("sky_color",    KitParam::Color(ColorPicker::new("Sky Color", Vec4::new(0.1, 0.2, 0.5, 1.0))));
585        g.push("sun_dir",      KitParam::Vec3(Vec3Field::new("Sun Direction", Vec3::new(0.5, 0.8, 0.3), -1.0, 1.0)));
586        g.push("sun_color",    KitParam::Color(ColorPicker::new("Sun Color", Vec4::new(1.0, 0.97, 0.85, 1.0))));
587        g.push("sun_strength", KitParam::Float(FloatSlider::new("Sun Strength", 5.0, 0.0, 20.0)));
588        g.push("fog_enabled",  KitParam::Bool(BoolToggle::new("Volumetric Fog", false)));
589        g.push("fog_density",  KitParam::Float(FloatSlider::new("Fog Density", 0.01, 0.0, 0.5)));
590        g.push("fog_color",    KitParam::Color(ColorPicker::new("Fog Color", Vec4::new(0.7, 0.75, 0.85, 1.0))));
591        g.push("fog_height",   KitParam::Float(FloatSlider::new("Fog Height Falloff", 1.0, 0.01, 10.0)));
592        g.push("grid",         KitParam::Bool(BoolToggle::new("Ground Grid", true)));
593        g.push("grid_color",   KitParam::Color(ColorPicker::new("Grid Color", Vec4::new(0.3, 0.3, 0.3, 0.5))));
594        g
595    }
596
597    // ── Lookup helpers ────────────────────────────────────────────────────
598
599    pub fn group(&self, name: &str) -> Option<&KitGroup> {
600        self.groups.iter().find(|g| g.name == name)
601    }
602
603    pub fn group_mut(&mut self, name: &str) -> Option<&mut KitGroup> {
604        self.groups.iter_mut().find(|g| g.name == name)
605    }
606
607    pub fn get_float(&self, group: &str, key: &str) -> Option<f32> {
608        self.group(group)?.get_float(key)
609    }
610
611    pub fn set_float(&mut self, group: &str, key: &str, v: f32) {
612        let old = self.get_float(group, key);
613        if let Some(g) = self.group_mut(group) {
614            g.set_float(key, v);
615            self.dirty = true;
616            if let Some(old_v) = old {
617                self.undo_stack.push(KitEdit {
618                    group: group.into(), key: key.into(),
619                    old: KitEditValue::Float(old_v),
620                    new: KitEditValue::Float(v),
621                });
622                self.redo_stack.clear();
623            }
624        }
625    }
626
627    pub fn get_bool(&self, group: &str, key: &str) -> Option<bool> {
628        self.group(group)?.get_bool(key)
629    }
630
631    pub fn toggle_bool(&mut self, group: &str, key: &str) {
632        let old = self.get_bool(group, key);
633        if let Some(g) = self.group_mut(group) {
634            if let Some(KitParam::Bool(b)) = g.get_mut(key) {
635                b.toggle();
636                self.dirty = true;
637                if let Some(ov) = old {
638                    self.undo_stack.push(KitEdit {
639                        group: group.into(), key: key.into(),
640                        old: KitEditValue::Bool(ov),
641                        new: KitEditValue::Bool(!ov),
642                    });
643                    self.redo_stack.clear();
644                }
645            }
646        }
647    }
648
649    pub fn get_color(&self, group: &str, key: &str) -> Option<Vec4> {
650        self.group(group)?.get_color(key)
651    }
652
653    pub fn set_color(&mut self, group: &str, key: &str, v: Vec4) {
654        let old = self.get_color(group, key);
655        if let Some(g) = self.group_mut(group) {
656            if let Some(KitParam::Color(c)) = g.get_mut(key) {
657                c.value = v;
658                self.dirty = true;
659                if let Some(ov) = old {
660                    self.undo_stack.push(KitEdit {
661                        group: group.into(), key: key.into(),
662                        old: KitEditValue::Color(ov),
663                        new: KitEditValue::Color(v),
664                    });
665                    self.redo_stack.clear();
666                }
667            }
668        }
669    }
670
671    // ── Undo / redo ───────────────────────────────────────────────────────
672
673    pub fn undo(&mut self) {
674        if let Some(edit) = self.undo_stack.pop() {
675            // Revert the change
676            let rev_val = match &edit.old {
677                KitEditValue::Float(v) => { self.group_mut(&edit.group).map(|g| g.set_float(&edit.key, *v)); }
678                KitEditValue::Bool(v)  => {
679                    if let Some(g) = self.group_mut(&edit.group) {
680                        if let Some(KitParam::Bool(b)) = g.get_mut(&edit.key) { b.value = *v; }
681                    }
682                }
683                KitEditValue::Color(v) => {
684                    if let Some(g) = self.group_mut(&edit.group) {
685                        if let Some(KitParam::Color(c)) = g.get_mut(&edit.key) { c.value = *v; }
686                    }
687                }
688                _ => {}
689            };
690            let _ = rev_val;
691            self.redo_stack.push(edit);
692            self.dirty = true;
693        }
694    }
695
696    pub fn redo(&mut self) {
697        if let Some(edit) = self.redo_stack.pop() {
698            match &edit.new {
699                KitEditValue::Float(v) => { self.group_mut(&edit.group).map(|g| g.set_float(&edit.key, *v)); }
700                KitEditValue::Bool(v) => {
701                    if let Some(g) = self.group_mut(&edit.group) {
702                        if let Some(KitParam::Bool(b)) = g.get_mut(&edit.key) { b.value = *v; }
703                    }
704                }
705                KitEditValue::Color(v) => {
706                    if let Some(g) = self.group_mut(&edit.group) {
707                        if let Some(KitParam::Color(c)) = g.get_mut(&edit.key) { c.value = *v; }
708                    }
709                }
710                _ => {}
711            };
712            self.undo_stack.push(edit);
713            self.dirty = true;
714        }
715    }
716
717    // ── Search ────────────────────────────────────────────────────────────
718
719    pub fn search_results(&self) -> Vec<(&str, &str, &KitParam)> {
720        if self.search.is_empty() { return Vec::new(); }
721        let query = self.search.to_lowercase();
722        let mut results = Vec::new();
723        for group in &self.groups {
724            for (key, param) in &group.params {
725                if param.label().to_lowercase().contains(&query) || key.to_lowercase().contains(&query) {
726                    results.push((group.name.as_str(), key.as_str(), param));
727                }
728            }
729        }
730        results
731    }
732
733    // ── Display ───────────────────────────────────────────────────────────
734
735    pub fn status_line(&self) -> String {
736        let total_params: usize = self.groups.iter().map(|g| g.params.len()).sum();
737        format!("Kit Panel — {} groups {} params | {} edits in history",
738            self.groups.len(), total_params, self.undo_stack.len())
739    }
740}
741
742impl Default for KitPanel { fn default() -> Self { Self::new() } }
743
744// ─────────────────────────────────────────────────────────────────────────────
745// Tests
746// ─────────────────────────────────────────────────────────────────────────────
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    fn all_groups_build() {
754        let panel = KitPanel::new();
755        assert!(panel.groups.len() >= 8);
756    }
757
758    #[test]
759    fn get_set_float() {
760        let mut panel = KitPanel::new();
761        panel.set_float("Bloom", "intensity", 5.0);
762        assert!((panel.get_float("Bloom", "intensity").unwrap() - 5.0).abs() < 1e-5);
763    }
764
765    #[test]
766    fn undo_float() {
767        let mut panel = KitPanel::new();
768        panel.set_float("Bloom", "intensity", 5.0);
769        panel.undo();
770        let v = panel.get_float("Bloom", "intensity").unwrap();
771        assert!((v - 2.8).abs() < 1e-5); // default value restored
772    }
773
774    #[test]
775    fn toggle_bool() {
776        let mut panel = KitPanel::new();
777        let before = panel.get_bool("Bloom", "enabled").unwrap();
778        panel.toggle_bool("Bloom", "enabled");
779        let after = panel.get_bool("Bloom", "enabled").unwrap();
780        assert_ne!(before, after);
781    }
782
783    #[test]
784    fn slider_normalized() {
785        let s = FloatSlider::new("x", 0.5, 0.0, 1.0);
786        assert!((s.normalized() - 0.5).abs() < 1e-5);
787    }
788
789    #[test]
790    fn color_hex_roundtrip() {
791        let c = ColorPicker::new("col", Vec4::new(1.0, 0.0, 0.5, 1.0));
792        let hex = c.to_hex();
793        let back = ColorPicker::from_hex(&hex).unwrap();
794        assert!((back.x - 1.0).abs() < 0.01);
795    }
796
797    #[test]
798    fn search_finds_result() {
799        let mut panel = KitPanel::new();
800        panel.search = "bloom".into();
801        let results = panel.search_results();
802        assert!(!results.is_empty());
803    }
804}