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}