1use glam::{Vec2, Vec3, Vec4};
23use std::collections::HashMap;
24
25#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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 pub fn undo(&mut self) {
674 if let Some(edit) = self.undo_stack.pop() {
675 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 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 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#[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); }
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}