Skip to main content

pfx_editor/
inspect.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use pfx_load::scene::{
5    BodyShape, EditError, PatchGroup, Projection, Scene, SceneEdit, Shadow, Target, Value,
6};
7use pfx_play::characters::{self, Setting};
8use pfx_play::{Edit, Kind, TunableValue, Tunables, body_volume};
9
10use crate::outline::Item;
11
12pub type Vec3 = [f32; 3];
13
14pub const SHADOWS: [&str; 3] = ["cast", "only", "none"];
15pub const SUN_MODELS: [&str; 2] = ["daylight", "authored"];
16pub const SKY_KINDS: [&str; 4] = ["analytic", "hdr", "room", "mix"];
17pub const PROJECTIONS: [&str; 2] = ["perspective", "orthographic"];
18pub const NONE: &str = "";
19pub const TUNABLES: &str = "tunables";
20pub const CHARACTER: &str = "character";
21pub const PLANES: [&str; 2] = ["xy", "yz"];
22pub const KINDS: [&str; 2] = ["3d", "2d"];
23
24#[derive(Clone, Debug, PartialEq)]
25pub enum Field {
26    Number(f32),
27    Pair([f32; 2]),
28    Vector(Vec3),
29    Color(Vec3),
30    Toggle(bool),
31    Choice { value: String, options: Vec<String> },
32    Text(String),
33    Planes(Vec<[f32; 4]>),
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Range {
38    pub min: f32,
39    pub max: f32,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Row {
44    pub label: String,
45    pub target: Target,
46    pub path: Vec<String>,
47    pub field: Field,
48    pub unit: &'static str,
49    pub speed: f32,
50    pub range: Option<Range>,
51    pub pending: bool,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct Change {
56    pub target: Target,
57    pub path: Vec<String>,
58    pub value: Option<Value>,
59}
60
61impl Change {
62    pub fn apply(&self, edit: &mut SceneEdit) -> Result<PatchGroup, EditError> {
63        let path: Vec<&str> = self.path.iter().map(String::as_str).collect();
64        match &self.value {
65            Some(value) => edit.set(&self.target, &path, value.clone()),
66            None => edit.unset(&self.target, &path),
67        }
68    }
69
70    pub fn describe(&self) -> String {
71        format!("{} {}", self.target, self.path.join("."))
72    }
73
74    pub fn tunable(&self) -> Option<&str> {
75        match (&self.target, self.path.as_slice()) {
76            (Target::Scene, [first, name]) if first == TUNABLES => Some(name),
77            _ => None,
78        }
79    }
80
81    pub fn filed(&self, scene: &Scene) -> Change {
82        let body = match (&self.target, self.path.as_slice()) {
83            (Target::Object(name), [first, key]) if first == "body" && key == "mass" => {
84                scene.bodies.get(name)
85            }
86            _ => None,
87        };
88        let mass = self.value.as_ref().and_then(number_of);
89        match (body, mass) {
90            (Some(body), Some(mass)) => Change {
91                target: self.target.clone(),
92                path: vec!["body".to_string(), "density".to_string()],
93                value: Some(Value::from(mass / body_volume(body.shape))),
94            },
95            _ => self.clone(),
96        }
97    }
98
99    pub fn live(&self, tunables: &Tunables) -> Result<Edit, String> {
100        let Some(value) = &self.value else {
101            return Err(format!("{}: play mode cannot unset a key", self.describe()));
102        };
103        let Some(name) = self.tunable() else {
104            let path: Vec<&str> = self.path.iter().map(String::as_str).collect();
105            return Ok(Edit::scene(self.target.clone(), &path, value.clone()));
106        };
107        let tunable = tunables
108            .get(name)
109            .ok_or_else(|| format!("no tunable {name}"))?;
110        let parsed = match (tunable.kind, value) {
111            (Kind::Bool, Value::Bool(on)) => Some(TunableValue::Bool(*on)),
112            (Kind::Int, value) => {
113                number_of(value).map(|amount| TunableValue::Int(amount.round() as i64))
114            }
115            (Kind::Float, value) => number_of(value).map(TunableValue::Float),
116            (Kind::Vector, value) => vector_of(value).map(TunableValue::Vector),
117            _ => None,
118        };
119        parsed
120            .map(|value| Edit::tunable(name, value))
121            .ok_or_else(|| format!("tunable {name}: {value:?} is not a {}", tunable.kind.word()))
122    }
123}
124
125fn number_of(value: &Value) -> Option<f32> {
126    match value {
127        Value::Float(value) => Some(*value),
128        Value::Int(value) => Some(*value as f32),
129        _ => None,
130    }
131}
132
133fn vector_of(value: &Value) -> Option<Vec3> {
134    let Value::Array(items) = value else {
135        return None;
136    };
137    if items.len() != 3 {
138        return None;
139    }
140    let mut out = [0.0; 3];
141    for (slot, item) in out.iter_mut().zip(items) {
142        *slot = number_of(item)?;
143    }
144    Some(out)
145}
146
147pub fn field_of(field: &Field, value: &Value) -> Option<Field> {
148    Some(match (field, value) {
149        (Field::Number(_), value) => Field::Number(number_of(value)?),
150        (Field::Vector(_), value) => Field::Vector(vector_of(value)?),
151        (Field::Color(_), value) => Field::Color(vector_of(value)?),
152        (Field::Pair(_), Value::Array(items)) if items.len() == 2 => {
153            Field::Pair([number_of(&items[0])?, number_of(&items[1])?])
154        }
155        (Field::Toggle(_), Value::Bool(on)) => Field::Toggle(*on),
156        (Field::Choice { options, .. }, Value::Text(text)) => Field::Choice {
157            value: text.clone(),
158            options: options.clone(),
159        },
160        (Field::Text(_), Value::Text(text)) => Field::Text(text.clone()),
161        _ => return None,
162    })
163}
164
165pub fn overlay(rows: &mut [Row], live: &BTreeMap<String, Value>) {
166    for row in rows {
167        let key = format!("{} {}", row.target, row.path.join("."));
168        row.pending = false;
169        if let Some(value) = live.get(&key)
170            && let Some(field) = field_of(&row.field, value)
171        {
172            row.field = field;
173            row.pending = true;
174        }
175    }
176}
177
178fn value_of(field: &Field) -> Option<Value> {
179    Some(match field {
180        Field::Number(value) => Value::from(*value),
181        Field::Pair(value) => Value::from(*value),
182        Field::Vector(value) | Field::Color(value) => Value::from(*value),
183        Field::Toggle(value) => Value::from(*value),
184        Field::Choice { value, .. } if value.is_empty() => return None,
185        Field::Choice { value, .. } | Field::Text(value) => Value::from(value.as_str()),
186        Field::Planes(planes) => {
187            Value::Array(planes.iter().map(|plane| Value::from(*plane)).collect())
188        }
189    })
190}
191
192impl Row {
193    pub fn change(&self, field: Field) -> Option<Change> {
194        if std::mem::discriminant(&field) != std::mem::discriminant(&self.field)
195            || field == self.field
196        {
197            return None;
198        }
199        let field = match (field, self.range) {
200            (Field::Number(value), Some(range)) => Field::Number(value.clamp(range.min, range.max)),
201            (field, _) => field,
202        };
203        if field == self.field {
204            return None;
205        }
206        Some(Change {
207            target: self.target.clone(),
208            path: self.path.clone(),
209            value: value_of(&field),
210        })
211    }
212}
213
214#[derive(Clone, Debug, Default, PartialEq)]
215pub struct Raw {
216    pub tables: BTreeMap<String, (PathBuf, toml::Table)>,
217}
218
219pub const SINGLETONS: [&str; 5] = ["sun", "sky", "haze", "camera", "finish"];
220
221impl Raw {
222    pub fn of(edit: &SceneEdit) -> Raw {
223        let mut tables = BTreeMap::new();
224        for file in edit.files() {
225            let Some(text) = edit.text(file) else {
226                continue;
227            };
228            let Ok(parsed) = toml::from_str::<toml::Table>(text) else {
229                continue;
230            };
231            for key in SINGLETONS {
232                if let Some(toml::Value::Table(table)) = parsed.get(key) {
233                    tables
234                        .entry(key.to_string())
235                        .or_insert_with(|| (file.to_path_buf(), table.clone()));
236                }
237            }
238        }
239        Raw { tables }
240    }
241
242    pub fn table(&self, key: &str) -> Option<&toml::Table> {
243        self.tables.get(key).map(|(_, table)| table)
244    }
245
246    pub fn file(&self, key: &str) -> Option<&PathBuf> {
247        self.tables.get(key).map(|(file, _)| file)
248    }
249}
250
251fn number(value: Option<&toml::Value>) -> Option<f32> {
252    match value? {
253        toml::Value::Float(value) => Some(*value as f32),
254        toml::Value::Integer(value) => Some(*value as f32),
255        _ => None,
256    }
257}
258
259fn vector(value: Option<&toml::Value>) -> Option<Vec3> {
260    let toml::Value::Array(items) = value? else {
261        return None;
262    };
263    if items.len() != 3 {
264        return None;
265    }
266    let mut out = [0.0; 3];
267    for (slot, item) in out.iter_mut().zip(items) {
268        *slot = number(Some(item))?;
269    }
270    Some(out)
271}
272
273fn text(value: Option<&toml::Value>) -> Option<String> {
274    match value? {
275        toml::Value::String(text) => Some(text.clone()),
276        _ => None,
277    }
278}
279
280struct Rows {
281    target: Target,
282    rows: Vec<Row>,
283}
284
285impl Rows {
286    fn new(target: Target) -> Rows {
287        Rows {
288            target,
289            rows: Vec::new(),
290        }
291    }
292
293    fn push(&mut self, path: &[&str], field: Field) -> &mut Row {
294        self.rows.push(Row {
295            label: path.join("."),
296            target: self.target.clone(),
297            path: path.iter().map(|key| key.to_string()).collect(),
298            field,
299            unit: "",
300            speed: 0.01,
301            range: None,
302            pending: false,
303        });
304        self.rows.last_mut().expect("a row was just pushed")
305    }
306
307    fn number(&mut self, path: &[&str], value: f32) -> &mut Row {
308        self.push(path, Field::Number(value))
309    }
310
311    fn unit(&mut self, path: &[&str], value: f32) {
312        self.number(path, value).range = Some(Range { min: 0.0, max: 1.0 });
313    }
314
315    fn positive(&mut self, path: &[&str], value: f32) {
316        self.number(path, value).range = Some(Range {
317            min: 0.0,
318            max: f32::MAX,
319        });
320    }
321
322    fn degrees(&mut self, path: &[&str], value: f32) {
323        let row = self.number(path, value);
324        row.unit = "°";
325        row.speed = 0.5;
326    }
327
328    fn vector(&mut self, path: &[&str], value: Vec3) {
329        self.push(path, Field::Vector(value));
330    }
331
332    fn color(&mut self, path: &[&str], value: Vec3) {
333        self.push(path, Field::Color(value));
334    }
335
336    fn toggle(&mut self, path: &[&str], value: bool) {
337        self.push(path, Field::Toggle(value));
338    }
339
340    fn choice<'a>(
341        &mut self,
342        path: &[&str],
343        value: impl Into<String>,
344        options: impl IntoIterator<Item = &'a str>,
345    ) {
346        self.push(
347            path,
348            Field::Choice {
349                value: value.into(),
350                options: options.into_iter().map(str::to_string).collect(),
351            },
352        );
353    }
354
355    fn text(&mut self, path: &[&str], value: impl Into<String>) {
356        self.push(path, Field::Text(value.into()));
357    }
358}
359
360fn shadow_name(shadow: Shadow) -> &'static str {
361    match shadow {
362        Shadow::Cast => "cast",
363        Shadow::Only => "only",
364        Shadow::None => "none",
365    }
366}
367
368fn materials(scene: &Scene) -> Vec<&str> {
369    std::iter::once(NONE).chain(scene.library.names()).collect()
370}
371
372fn object_rows(scene: &Scene, name: &str) -> Vec<Row> {
373    let Some(object) = scene.object(name) else {
374        return Vec::new();
375    };
376    let mut rows = Rows::new(Target::Object(name.to_string()));
377    rows.vector(&["at"], object.at);
378    rows.push(&["rotate"], Field::Vector(object.rotate)).unit = "°";
379    rows.vector(&["scale"], object.scale);
380    rows.choice(
381        &["material"],
382        object.material.clone().unwrap_or_default(),
383        materials(scene),
384    );
385    rows.choice(&["shadow"], shadow_name(object.shadow), SHADOWS);
386    rows.toggle(&["two_sided"], object.two_sided);
387    rows.toggle(&["hidden"], object.hidden);
388    rows.push(&["clip"], Field::Planes(object.clip.clone()));
389    let parents = std::iter::once(NONE).chain(
390        scene
391            .objects
392            .iter()
393            .map(|other| other.name.as_str())
394            .filter(|other| *other != name),
395    );
396    rows.choice(
397        &["parent"],
398        object.parent.clone().unwrap_or_default(),
399        parents,
400    );
401    if let Some(body) = scene.bodies.get(name) {
402        rows.positive(&["body", "mass"], body.density * body_volume(body.shape));
403        rows.positive(&["body", "friction"], body.friction);
404        rows.positive(&["body", "restitution"], body.restitution);
405        rows.push(&["body", "damping"], Field::Pair(body.damping));
406        rows.vector(&["body", "velocity"], body.velocity);
407        match body.shape {
408            BodyShape::Box { half } => rows.vector(&["body", "half"], half),
409            BodyShape::Sphere { radius } => rows.positive(&["body", "radius"], radius),
410            BodyShape::Capsule {
411                half_height,
412                radius,
413            }
414            | BodyShape::Cylinder {
415                half_height,
416                radius,
417            } => {
418                rows.positive(&["body", "half_height"], half_height);
419                rows.positive(&["body", "radius"], radius);
420            }
421        }
422    }
423    if let Some(character) = scene.characters.get(name) {
424        for (key, setting) in characters::settings(character) {
425            let path = [CHARACTER, key];
426            match setting {
427                Setting::Number(value) => match key {
428                    "max_climb" => rows.degrees(&path, value),
429                    "coyote" | "jump_buffer" => {
430                        let row = rows.number(&path, value);
431                        row.speed = 0.1;
432                        row.unit = "ticks";
433                        row.range = Some(Range {
434                            min: 0.0,
435                            max: f32::MAX,
436                        });
437                    }
438                    "variable_jump" => rows.unit(&path, value),
439                    _ => rows.positive(&path, value),
440                },
441                Setting::Toggle(on) => rows.toggle(&path, on),
442                Setting::Word(word) if key == "kind" => rows.choice(&path, word, KINDS),
443                Setting::Pair(pair) => {
444                    rows.push(&path, Field::Pair(pair));
445                }
446                Setting::Word(word) => rows.choice(&path, word, PLANES),
447            }
448        }
449    }
450    rows.rows
451}
452
453fn sound_rows(scene: &Scene, name: &str) -> Vec<Row> {
454    let Some(sound) = scene.sounds.get(name) else {
455        return Vec::new();
456    };
457    let mut rows = Rows::new(Target::Sound(name.to_string()));
458    rows.positive(&["volume"], sound.volume);
459    rows.number(&["pan"], sound.pan).range = Some(Range {
460        min: -1.0,
461        max: 1.0,
462    });
463    rows.toggle(&["loop"], sound.looping);
464    rows.rows
465}
466
467fn tunable_rows(tunables: &Tunables, group: &str) -> Vec<Row> {
468    let mut rows = Rows::new(Target::Scene);
469    for tunable in tunables.iter().filter(|tunable| tunable.group == group) {
470        let path = [TUNABLES, tunable.name.as_str()];
471        let bound = |value: Option<TunableValue>, at: f32| match value {
472            Some(TunableValue::Float(value)) => value,
473            Some(TunableValue::Int(value)) => value as f32,
474            _ => at,
475        };
476        let row = match tunable.default {
477            TunableValue::Float(value) => rows.number(&path, value),
478            TunableValue::Int(value) => {
479                let row = rows.number(&path, value as f32);
480                row.speed = 0.1;
481                row
482            }
483            TunableValue::Bool(value) => rows.push(&path, Field::Toggle(value)),
484            TunableValue::Vector(value) => rows.push(&path, Field::Vector(value)),
485        };
486        row.label = tunable.name.clone();
487        if matches!(tunable.kind, Kind::Float | Kind::Int)
488            && (tunable.min.is_some() || tunable.max.is_some())
489        {
490            row.range = Some(Range {
491                min: bound(tunable.min, f32::MIN),
492                max: bound(tunable.max, f32::MAX),
493            });
494        }
495    }
496    rows.rows
497}
498
499fn material_rows(scene: &Scene, name: &str) -> Vec<Row> {
500    let Some(material) = scene.library.get(name) else {
501        return Vec::new();
502    };
503    let family = serde_json::to_value(material.family)
504        .ok()
505        .and_then(|value| value.as_str().map(str::to_string))
506        .unwrap_or_default();
507    let mut rows = Rows::new(Target::Material(name.to_string()));
508    rows.text(&["family"], family);
509    rows.color(&["base"], material.base);
510    rows.unit(&["roughness"], material.roughness);
511    rows.unit(&["metalness"], material.metalness);
512    rows.unit(&["specular"], material.specular);
513    rows.unit(&["clearcoat"], material.clearcoat);
514    rows.unit(&["clearcoat_roughness"], material.clearcoat_roughness);
515    rows.unit(&["sheen"], material.sheen);
516    rows.unit(&["transmission"], material.transmission);
517    rows.positive(&["ior"], material.ior);
518    rows.positive(&["thickness"], material.thickness);
519    rows.unit(&["subsurface"], material.subsurface);
520    rows.positive(&["absorption"], material.absorption);
521    rows.vector(&["emission"], material.emission);
522    rows.rows
523}
524
525fn light_rows(scene: &Scene, name: &str) -> Vec<Row> {
526    let Some(light) = scene.lights.get(name) else {
527        return Vec::new();
528    };
529    let mut rows = Rows::new(Target::Light(name.to_string()));
530    rows.vector(&["position"], light.position);
531    rows.color(&["color"], light.color);
532    rows.positive(&["intensity"], light.intensity);
533    rows.positive(&["radius"], light.radius);
534    rows.positive(&["range"], light.range);
535    rows.toggle(&["shadow"], light.shadow);
536    rows.rows
537}
538
539fn emitter_rows(scene: &Scene, name: &str) -> Vec<Row> {
540    let Some(emitter) = scene.emitters.get(name) else {
541        return Vec::new();
542    };
543    let mut rows = Rows::new(Target::Emitter(name.to_string()));
544    rows.vector(&["position"], emitter.position);
545    rows.positive(&["radius"], emitter.radius);
546    rows.color(&["color"], emitter.color);
547    rows.positive(&["intensity"], emitter.intensity);
548    rows.rows
549}
550
551fn mesh_rows(scene: &Scene, name: &str) -> Vec<Row> {
552    let Some(mesh) = scene.meshes.get(name) else {
553        return Vec::new();
554    };
555    let mut rows = Rows::new(Target::Mesh(name.to_string()));
556    rows.text(&["file"], crate::outline::relative(&scene.path, &mesh.file));
557    rows.text(&["node"], mesh.node.clone().unwrap_or_default());
558    rows.rows
559}
560
561fn node_rows(scene: &Scene, mesh: &str, node: &str) -> Vec<Row> {
562    let Some(part) = scene
563        .meshes
564        .get(mesh)
565        .and_then(|found| found.parts.iter().find(|part| part.node == node))
566    else {
567        return Vec::new();
568    };
569    let overrides = &part.overrides;
570    let mut rows = Rows::new(Target::Node {
571        mesh: mesh.to_string(),
572        node: node.to_string(),
573    });
574    rows.choice(
575        &["material"],
576        overrides.material.clone().unwrap_or_default(),
577        materials(scene),
578    );
579    rows.toggle(&["hidden"], overrides.hidden);
580    rows.choice(
581        &["shadow"],
582        overrides.shadow.map(shadow_name).unwrap_or(NONE),
583        std::iter::once(NONE).chain(SHADOWS),
584    );
585    rows.toggle(&["two_sided"], overrides.two_sided.unwrap_or(false));
586    rows.rows
587}
588
589fn sun_rows(raw: &Raw) -> Vec<Row> {
590    let empty = toml::Table::new();
591    let table = raw.table("sun").unwrap_or(&empty);
592    let mut rows = Rows::new(Target::Sun);
593    let model = text(table.get("model")).unwrap_or_else(|| "daylight".to_string());
594    rows.choice(&["model"], model.clone(), SUN_MODELS);
595    rows.number(&["hour"], number(table.get("hour")).unwrap_or(12.0))
596        .range = Some(Range {
597        min: 0.0,
598        max: 24.0,
599    });
600    rows.number(&["day"], number(table.get("day")).unwrap_or(172.0));
601    rows.degrees(&["latitude"], number(table.get("latitude")).unwrap_or(45.0));
602    rows.degrees(&["heading"], number(table.get("heading")).unwrap_or(180.0));
603    if model == "authored" {
604        rows.number(
605            &["reference_hour"],
606            number(table.get("reference_hour")).unwrap_or(12.0),
607        );
608        rows.vector(
609            &["toward"],
610            vector(table.get("toward")).unwrap_or([0.0, 1.0, 0.0]),
611        );
612        rows.color(&["color"], vector(table.get("color")).unwrap_or([1.0; 3]));
613        rows.positive(
614            &["irradiance"],
615            number(table.get("irradiance")).unwrap_or(1.0),
616        );
617    }
618    rows.number(&["radius"], number(table.get("radius")).unwrap_or(0.0))
619        .range = Some(Range {
620        min: 0.0,
621        max: 10.0,
622    });
623    rows.rows
624}
625
626fn sky_rows(raw: &Raw) -> Vec<Row> {
627    let empty = toml::Table::new();
628    let table = raw.table("sky").unwrap_or(&empty);
629    let mut rows = Rows::new(Target::Sky);
630    let kind = text(table.get("kind")).unwrap_or_default();
631    rows.choice(&["kind"], kind.clone(), SKY_KINDS);
632    match kind.as_str() {
633        "analytic" => {
634            rows.number(
635                &["turbidity"],
636                number(table.get("turbidity")).unwrap_or(3.0),
637            )
638            .range = Some(Range {
639                min: 1.0,
640                max: 10.0,
641            });
642            rows.color(
643                &["ground_albedo"],
644                vector(table.get("ground_albedo")).unwrap_or([0.2; 3]),
645            );
646            rows.positive(&["ambient"], number(table.get("ambient")).unwrap_or(1.0));
647        }
648        "hdr" | "room" => {
649            if kind == "hdr" {
650                rows.text(&["path"], text(table.get("path")).unwrap_or_default());
651            }
652            rows.degrees(
653                &["rotation_deg"],
654                number(table.get("rotation_deg")).unwrap_or(0.0),
655            );
656            rows.positive(
657                &["intensity"],
658                number(table.get("intensity")).unwrap_or(1.0),
659            );
660            if kind == "room"
661                && let Some(toml::Value::Table(room)) = table.get("room")
662            {
663                for key in ["floor", "wall", "ceiling"] {
664                    if let Some(value) = vector(room.get(key)) {
665                        rows.color(&["room", key], value);
666                    }
667                }
668            }
669        }
670        _ => {}
671    }
672    rows.rows
673}
674
675const HAZE_VECTORS: [&str; 2] = ["lo", "hi"];
676const HAZE_NUMBERS: [&str; 8] = [
677    "amount", "fog", "smoke", "mist", "floor", "phase", "back", "reach",
678];
679const HAZE_COLORS: [&str; 2] = ["gold", "ambient"];
680
681fn haze_rows(scene: &Scene, raw: &Raw) -> Vec<Row> {
682    let mut rows = Rows::new(Target::Haze);
683    let Some(haze) = &scene.haze else {
684        return rows.rows;
685    };
686    let empty = toml::Table::new();
687    let table = raw.table("haze").unwrap_or(&empty);
688    rows.vector(&[HAZE_VECTORS[0]], haze.lo);
689    rows.vector(&[HAZE_VECTORS[1]], haze.hi);
690    for key in HAZE_NUMBERS {
691        let value = match key {
692            "amount" => number(table.get(key)).unwrap_or(0.0),
693            "fog" => haze.fog,
694            "smoke" => haze.smoke,
695            "mist" => haze.mist,
696            "floor" => haze.floor,
697            "phase" => haze.phase,
698            "back" => haze.back,
699            _ => haze.reach,
700        };
701        if key == "amount" || key == "phase" {
702            let (min, max) = if key == "amount" {
703                (0.0, 1.0)
704            } else {
705                (-0.9, 0.9)
706            };
707            rows.number(&[key], value).range = Some(Range { min, max });
708        } else {
709            rows.positive(&[key], value);
710        }
711    }
712    rows.color(&[HAZE_COLORS[0]], haze.gold);
713    rows.color(&[HAZE_COLORS[1]], haze.ambient);
714    rows.rows
715}
716
717fn camera_rows(scene: &Scene, raw: &Raw) -> Vec<Row> {
718    let camera = scene.camera_or_default();
719    let empty = toml::Table::new();
720    let table = raw.table("camera").unwrap_or(&empty);
721    let mut rows = Rows::new(Target::Camera);
722    rows.vector(&["at"], camera.at);
723    rows.vector(&["look_at"], camera.look_at);
724    rows.vector(&["up"], camera.up);
725    match camera.projection {
726        Projection::Perspective { fov } => {
727            rows.choice(&["projection"], PROJECTIONS[0], PROJECTIONS);
728            if table.contains_key("focal") {
729                rows.positive(&["focal"], number(table.get("focal")).unwrap_or(50.0));
730                rows.positive(&["sensor"], number(table.get("sensor")).unwrap_or(24.0));
731            } else {
732                rows.degrees(&["fov"], fov);
733            }
734            if let Some(depth) = camera.depth_of_field {
735                rows.positive(&["fstop"], depth.fstop);
736                rows.positive(&["focus"], depth.distance);
737            }
738        }
739        Projection::Orthographic { height } => {
740            rows.choice(&["projection"], PROJECTIONS[1], PROJECTIONS);
741            rows.positive(&["height"], height);
742        }
743    }
744    rows.positive(&["near"], camera.near);
745    rows.positive(&["far"], camera.far);
746    let exposure = raw
747        .table("finish")
748        .and_then(|finish| number(finish.get("exposure")))
749        .unwrap_or(1.0);
750    rows.target = Target::Finish;
751    rows.number(&["exposure"], exposure).range = Some(Range {
752        min: 0.001,
753        max: f32::MAX,
754    });
755    rows.rows
756}
757
758fn finish_rows(raw: &Raw) -> Vec<Row> {
759    let mut rows = Rows::new(Target::Finish);
760    let Some(table) = raw.table("finish") else {
761        return rows.rows;
762    };
763    for (key, value) in table {
764        match value {
765            toml::Value::Float(_) | toml::Value::Integer(_) => {
766                rows.number(&[key], number(Some(value)).unwrap_or(0.0));
767            }
768            toml::Value::Boolean(on) => rows.toggle(&[key], *on),
769            toml::Value::String(text) => rows.text(&[key], text.clone()),
770            toml::Value::Array(_) if vector(Some(value)).is_some() => {
771                rows.vector(&[key], vector(Some(value)).unwrap_or([0.0; 3]));
772            }
773            _ => {}
774        }
775    }
776    rows.rows
777}
778
779pub fn rows(scene: &Scene, raw: &Raw, tunables: &Tunables, item: &Item) -> Vec<Row> {
780    match item {
781        Item::Object(name) => object_rows(scene, name),
782        Item::Sound(name) => sound_rows(scene, name),
783        Item::Tunables(group) => tunable_rows(tunables, group),
784        Item::Material(name) => material_rows(scene, name),
785        Item::Light(name) => light_rows(scene, name),
786        Item::Emitter(name) => emitter_rows(scene, name),
787        Item::Mesh(name) => mesh_rows(scene, name),
788        Item::Node { mesh, node } => node_rows(scene, mesh, node),
789        Item::Sun => sun_rows(raw),
790        Item::Sky => sky_rows(raw),
791        Item::Haze => haze_rows(scene, raw),
792        Item::Camera => camera_rows(scene, raw),
793        Item::Finish => finish_rows(raw),
794        Item::File(_) | Item::Scene(_) | Item::Folder(_) | Item::Asset(_) | Item::Layer(_) => {
795            Vec::new()
796        }
797    }
798}