Skip to main content

pfx_load/scene/
build.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use pfx_core::anim::normalize_weights;
7use pfx_core::camera::{DEG, Depth, Hush, Preset};
8use pfx_core::daylight::{Authored, Daylight, REFERENCE_HOUR, Reading};
9use pfx_core::sky::AnalyticSky;
10use pfx_materials::{Library, Material};
11use pfx_scene::types as file;
12use pfx_scene::{Diagnostic, Entry, Project};
13use sha2::{Digest, Sha256};
14
15use super::{
16    ALPHA_CUTOFF, Camera, Content, DepthOfField, Emitter, Environment, Finish, Geometry,
17    GeometrySkin, Haze, IDENTITY, Light, Location, Matrix, Motion, Mover, MoverKind, NodeOverride,
18    Object, Part, Projection, Scene, SceneError, SceneMesh, SceneSky, Shadow, Sun, Text,
19    TraceSettings, model, multiply,
20};
21use crate::{
22    ColorSpace, Font, Image, Mesh, Pixels, Primitive, RoomLamp, RoomSky, SkinRig, Sky, sha256,
23};
24
25pub(super) struct Reader<'a> {
26    pub(super) files: BTreeMap<PathBuf, [u8; 32]>,
27    root: &'a Path,
28    overlay: &'a BTreeMap<PathBuf, String>,
29}
30
31impl Reader<'_> {
32    pub(super) fn read(&mut self, path: &Path) -> Result<Vec<u8>, String> {
33        let held = path
34            .strip_prefix(self.root)
35            .ok()
36            .and_then(|relative| self.overlay.get(relative));
37        let bytes = match held {
38            Some(text) => text.clone().into_bytes(),
39            None => {
40                std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?
41            }
42        };
43        self.files.insert(path.to_path_buf(), sha256(&bytes));
44        Ok(bytes)
45    }
46}
47
48pub(super) fn line_of(text: &str, offset: usize) -> usize {
49    text.as_bytes()[..offset.min(text.len())]
50        .iter()
51        .filter(|&&b| b == b'\n')
52        .count()
53        + 1
54}
55
56fn column_of(text: &str, offset: usize) -> usize {
57    let offset = offset.min(text.len());
58    let start = text[..offset].rfind('\n').map_or(0, |at| at + 1);
59    text[start..offset].chars().count() + 1
60}
61
62#[derive(Clone, Copy)]
63pub(super) enum Step<'a> {
64    Key(&'a str),
65    Named(&'a str, &'a str),
66    Index(&'a str, usize),
67}
68
69pub(super) struct Places<'a> {
70    pub(super) root: PathBuf,
71    texts: RefCell<BTreeMap<PathBuf, Option<String>>>,
72    overlay: &'a BTreeMap<PathBuf, String>,
73}
74
75fn descend<'t, 'i>(
76    value: &'t toml::Spanned<toml::de::DeValue<'i>>,
77    step: Step<'_>,
78) -> Option<&'t toml::Spanned<toml::de::DeValue<'i>>> {
79    let table = match value.get_ref() {
80        toml::de::DeValue::Table(table) => table,
81        _ => return None,
82    };
83    match step {
84        Step::Key(key) => table.get(key),
85        Step::Named(list, name) => match table.get(list)?.get_ref() {
86            toml::de::DeValue::Array(items) => items.iter().find(|item| {
87                match item.get_ref() {
88                toml::de::DeValue::Table(entry) => entry.get("name").is_some_and(|value| {
89                    matches!(value.get_ref(), toml::de::DeValue::String(text) if text == name)
90                }),
91                _ => false,
92            }
93            }),
94            _ => None,
95        },
96        Step::Index(list, place) => match table.get(list)?.get_ref() {
97            toml::de::DeValue::Array(items) => items.get(place),
98            _ => None,
99        },
100    }
101}
102
103impl<'a> Places<'a> {
104    pub(super) fn new(root: &Path, overlay: &'a BTreeMap<PathBuf, String>) -> Self {
105        Self {
106            root: root.to_path_buf(),
107            texts: RefCell::new(BTreeMap::new()),
108            overlay,
109        }
110    }
111
112    fn text(&self, file: &Path) -> Option<String> {
113        if let Some(text) = self.overlay.get(file) {
114            return Some(text.clone());
115        }
116        self.texts
117            .borrow_mut()
118            .entry(file.to_path_buf())
119            .or_insert_with(|| std::fs::read_to_string(self.root.join(file)).ok())
120            .clone()
121    }
122
123    pub(super) fn locate(&self, file: &Path, steps: &[Step<'_>]) -> (Option<usize>, Option<usize>) {
124        let Some(text) = self.text(file) else {
125            return (None, None);
126        };
127        let Ok(top) = toml::de::DeTable::parse(&text) else {
128            return (None, None);
129        };
130        let top = toml::Spanned::new(top.span(), toml::de::DeValue::Table(top.into_inner()));
131        let mut at = &top;
132        let mut found = None;
133        for step in steps {
134            match descend(at, *step) {
135                Some(next) => {
136                    at = next;
137                    found = Some(next.span().start);
138                }
139                None => break,
140            }
141        }
142        match found {
143            Some(offset) => (Some(line_of(&text, offset)), Some(column_of(&text, offset))),
144            None => (None, None),
145        }
146    }
147
148    pub(super) fn error<T>(
149        &self,
150        file: &Path,
151        steps: &[Step<'_>],
152        message: impl Into<String>,
153    ) -> Result<T, SceneError> {
154        Err(self.refusal(file, steps, message))
155    }
156
157    pub(super) fn refusal(
158        &self,
159        file: &Path,
160        steps: &[Step<'_>],
161        message: impl Into<String>,
162    ) -> SceneError {
163        let (line, column) = self.locate(file, steps);
164        SceneError {
165            file: self.root.join(file),
166            line,
167            column,
168            message: message.into(),
169            related: Vec::new(),
170        }
171    }
172
173    pub(super) fn holder(&self, files: &[PathBuf], key: &str) -> PathBuf {
174        files
175            .iter()
176            .find(|file| {
177                self.text(file).is_some_and(|text| {
178                    toml::de::DeTable::parse(&text)
179                        .is_ok_and(|table| table.get_ref().get(key).is_some())
180                })
181            })
182            .cloned()
183            .unwrap_or_default()
184    }
185}
186
187pub(super) fn inner(key: &str) -> &str {
188    key.rsplit('/').next().unwrap_or(key)
189}
190
191pub(super) fn diagnostic(root: &Path, diagnostic: &Diagnostic) -> SceneError {
192    SceneError {
193        file: root.join(&diagnostic.file),
194        line: Some(diagnostic.line as usize),
195        column: Some(diagnostic.column as usize),
196        message: diagnostic.message.clone(),
197        related: diagnostic
198            .related
199            .iter()
200            .map(|place| Location {
201                file: root.join(&place.file),
202                line: place.line as usize,
203                column: place.column as usize,
204            })
205            .collect(),
206    }
207}
208
209pub(super) fn refused(root: &Path, diagnostics: &[Diagnostic]) -> SceneError {
210    diagnostics
211        .iter()
212        .find(|found| found.is_error())
213        .or_else(|| diagnostics.first())
214        .map(|found| diagnostic(root, found))
215        .unwrap_or_else(|| SceneError::new(root, None, "the scene does not load"))
216}
217
218pub(super) fn absolute(path: &Path) -> PathBuf {
219    std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
220}
221
222pub fn project_root(path: &Path) -> PathBuf {
223    Project::root_of(path)
224}
225
226pub(super) fn project(root: &Path) -> Result<Project, SceneError> {
227    let folder = if root.as_os_str().is_empty() {
228        Path::new(".")
229    } else {
230        root
231    };
232    Project::open(folder).map_err(|found| diagnostic(root, &found))
233}
234
235pub(super) fn open(path: &Path, hour: Option<f64>) -> Result<Scene, SceneError> {
236    let root = project_root(path);
237    let project = project(&root)?;
238    let absolute = absolute(path);
239    let resolved = project
240        .scene(&absolute)
241        .map_err(|found| refused(&root, &found))?;
242    build(path, &root, &resolved, hour, &BTreeMap::new())
243}
244
245pub(super) fn build(
246    path: &Path,
247    root: &Path,
248    resolved: &pfx_scene::Scene,
249    hour: Option<f64>,
250    overlay: &BTreeMap<PathBuf, String>,
251) -> Result<Scene, SceneError> {
252    let places = Places::new(root, overlay);
253    let mut reader = Reader {
254        files: BTreeMap::new(),
255        root,
256        overlay,
257    };
258    for file in &resolved.files {
259        reader
260            .read(&root.join(file))
261            .map_err(|message| SceneError::new(&root.join(file), None, message))?;
262    }
263    let library = build_library(&places, &resolved.materials)?;
264    let mut meshes = BTreeMap::new();
265    for entry in &resolved.meshes {
266        meshes.insert(entry.key.clone(), build_mesh(&places, entry, &mut reader)?);
267    }
268    let sun = match &resolved.sun {
269        Some(sun) => {
270            let holder = places.holder(&resolved.files, "sun");
271            Some(build_sun(&places, &holder, sun, hour)?)
272        }
273        None => None,
274    };
275    let sky = match &resolved.sky {
276        Some(sky) => {
277            let holder = places.holder(&resolved.files, "sky");
278            Some(build_sky(&places, &holder, sky, sun.as_ref(), &mut reader)?)
279        }
280        None => None,
281    };
282    let mut contents = BTreeMap::new();
283    for entry in &resolved.contents {
284        contents.insert(
285            entry.key.clone(),
286            build_content(&places, entry, &mut reader)?,
287        );
288    }
289    let mut texts = BTreeMap::new();
290    for entry in &resolved.texts {
291        texts.insert(entry.key.clone(), build_text(&places, entry, &mut reader)?);
292    }
293    let mut objects = build_objects(&places, &resolved.objects, &meshes)?;
294    let animations = super::anim::build_animations(
295        &places,
296        &resolved.objects,
297        &mut objects,
298        &meshes,
299        &mut contents,
300        &mut reader,
301    )?;
302    place_texts(&mut texts, &objects);
303    let movers = resolved
304        .movers
305        .iter()
306        .map(|entry| (entry.key.clone(), build_mover(&entry.value)))
307        .collect();
308    let lights = resolved
309        .lights
310        .iter()
311        .map(|entry| (entry.key.clone(), build_light(&entry.value)))
312        .collect();
313    let emitters = resolved
314        .emitters
315        .iter()
316        .map(|entry| (entry.key.clone(), build_emitter(&entry.value)))
317        .collect();
318    let camera = match &resolved.camera {
319        Some(camera) => {
320            let holder = places.holder(&resolved.files, "camera");
321            Some(build_camera(&places, &holder, camera)?)
322        }
323        None => None,
324    };
325    let finish = match &resolved.finish {
326        Some(finish) => {
327            let holder = places.holder(&resolved.files, "finish");
328            Some(build_finish(&places, &holder, finish, &mut reader)?)
329        }
330        None => None,
331    };
332    let plates = match &resolved.plates {
333        Some(plates) => {
334            let holder = places.holder(&resolved.files, "plates");
335            Some(super::plates::build(
336                &places,
337                &holder,
338                plates,
339                &resolved.proxies,
340                &objects,
341                &mut reader,
342            )?)
343        }
344        None => None,
345    };
346    let bodies = super::play::build_bodies(&places, &resolved.objects, &objects, &meshes)?;
347    let triggers = super::play::build_triggers(&places, &resolved.objects, &objects, &meshes)?;
348    let body_layers = super::play::body_layers(&resolved.objects);
349    let characters = super::play::build_characters(&places, &resolved.objects, &objects, &meshes)?;
350    let mut sounds = BTreeMap::new();
351    for entry in &resolved.sounds {
352        sounds.insert(
353            entry.key.clone(),
354            super::play::build_sound(&places, entry, &mut reader)?,
355        );
356    }
357    let mut scene = Scene {
358        path: path.to_path_buf(),
359        files: BTreeMap::new(),
360        library,
361        fallback: resolved.fallback.clone(),
362        meshes,
363        objects,
364        sun,
365        sky,
366        haze: resolved.haze.as_ref().map(build_haze),
367        movers,
368        lights,
369        emitters,
370        camera,
371        finish,
372        trace: resolved
373            .trace
374            .map(|trace| TraceSettings {
375                transmissive_shadows: trace.transmissive_shadows,
376                clamp_indirect: trace.clamp_indirect.unwrap_or(file::Trace::CLAMP_INDIRECT),
377                filter_glossy: trace.filter_glossy.unwrap_or(file::Trace::FILTER_GLOSSY),
378            })
379            .unwrap_or_default(),
380        contents,
381        texts,
382        plates,
383        physics: resolved.physics.as_ref().map(super::play::build_physics),
384        bodies,
385        body_layers,
386        characters,
387        animations,
388        triggers,
389        layers: resolved.layers.clone(),
390        sounds,
391        warnings: resolved
392            .warnings
393            .iter()
394            .map(|found| diagnostic(root, found))
395            .collect(),
396    };
397    super::tiles::expand(&mut scene, root, resolved, &places, overlay)?;
398    scene.files.extend(reader.files);
399    Ok(scene)
400}
401
402fn build_library(
403    places: &Places,
404    entries: &[Entry<file::Material>],
405) -> Result<Library, SceneError> {
406    let mut library = Library::new();
407    for entry in entries {
408        let steps = [Step::Key("materials"), Step::Key(inner(&entry.key))];
409        let mut value = toml::Value::try_from(&entry.value)
410            .map_err(|error| places.refusal(&entry.file, &steps, error.to_string()))?;
411        if let Some(table) = value.as_table_mut() {
412            table.remove("id");
413            table.remove("authoring");
414        }
415        let text = toml::to_string(&value)
416            .map_err(|error| places.refusal(&entry.file, &steps, error.to_string()))?;
417        let material = Material::from_toml(&text).map_err(|message| {
418            places.refusal(
419                &entry.file,
420                &steps,
421                format!("material {}: {message}", entry.key),
422            )
423        })?;
424        library
425            .insert(&entry.key, material)
426            .map_err(|message| places.refusal(&entry.file, &steps, message))?;
427    }
428    Ok(library)
429}
430
431fn build_haze(haze: &file::Haze) -> Haze {
432    let amount = haze.amount.unwrap_or(0.0);
433    Haze {
434        lo: haze.lo,
435        hi: haze.hi,
436        fog: haze.fog.unwrap_or(amount / 6.0),
437        smoke: haze.smoke.unwrap_or(amount * 20.0),
438        mist: haze.mist.unwrap_or(amount * 0.2),
439        floor: haze.floor.unwrap_or(amount * 2.0),
440        phase: haze.phase.unwrap_or(0.3),
441        back: haze.back.unwrap_or(0.5),
442        gold: haze.gold.unwrap_or([1.0; 3]),
443        ambient: haze.ambient.unwrap_or([0.03; 3]),
444        reach: haze.reach.unwrap_or(3.0),
445        unmapped: haze.unmapped.unwrap_or(0.0),
446        seed: haze.seed.unwrap_or(0),
447    }
448}
449
450fn build_mover(mover: &file::Mover) -> Mover {
451    Mover {
452        objects: mover.objects.clone(),
453        kind: match mover.kind {
454            file::MoverKind::Turn => MoverKind::Turn,
455            file::MoverKind::Slide => MoverKind::Slide,
456        },
457        pivot: mover.pivot.unwrap_or([0.0; 3]),
458        axis: super::unit(mover.axis),
459        travel: mover.travel,
460        period: mover.period.unwrap_or(file::Mover::PERIOD),
461        motion: match mover.motion.unwrap_or_default() {
462            file::Motion::Swing => Motion::Swing,
463            file::Motion::Loop => Motion::Loop,
464            file::Motion::Once => Motion::Once,
465        },
466        offset: mover.offset.unwrap_or(0.0),
467        clip: mover.clip.clone(),
468    }
469}
470
471pub(super) fn finite(values: &[f32]) -> bool {
472    values.iter().all(|value| value.is_finite())
473}
474
475fn shadow(shadow: file::Shadow) -> Shadow {
476    match shadow {
477        file::Shadow::Cast => Shadow::Cast,
478        file::Shadow::Only => Shadow::Only,
479        file::Shadow::None => Shadow::None,
480    }
481}
482
483fn hasher_add(hasher: &mut Sha256, values: impl IntoIterator<Item = f32>) {
484    for value in values {
485        hasher.update(value.to_le_bytes());
486    }
487}
488
489fn build_mesh(
490    places: &Places,
491    entry: &Entry<file::Mesh>,
492    reader: &mut Reader,
493) -> Result<SceneMesh, SceneError> {
494    let name = entry.key.as_str();
495    let steps = [Step::Key("mesh"), Step::Key(inner(name))];
496    let error = |message: String| places.refusal(&entry.file, &steps, message);
497    let mesh = &entry.value;
498    let file = places.root.join(&mesh.file);
499    let bytes = reader.read(&file).map_err(error)?;
500    let buffers: RefCell<Vec<(PathBuf, Vec<u8>)>> = RefCell::new(Vec::new());
501    let directory = file.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
502    let parsed = Mesh::parse_with(&bytes, |uri| {
503        let path = directory.join(uri);
504        let data =
505            std::fs::read(&path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
506        buffers.borrow_mut().push((path, data.clone()));
507        Ok(data)
508    })
509    .map_err(|message| SceneError::new(&file, None, message))?;
510    for (path, data) in buffers.into_inner() {
511        reader.files.insert(path, sha256(&data));
512    }
513    let roots: Vec<(u32, bool)> = match &mesh.node {
514        Some(node) => {
515            let found: Vec<u32> = parsed
516                .nodes
517                .iter()
518                .enumerate()
519                .filter(|(_, n)| &n.name == node)
520                .map(|(index, _)| index as u32)
521                .collect();
522            match found.as_slice() {
523                [one] => vec![(*one, false)],
524                [] => {
525                    return Err(error(format!(
526                        "mesh {name}: {} has no node {node}",
527                        mesh.file
528                    )));
529                }
530                _ => {
531                    return Err(error(format!(
532                        "mesh {name}: {} has {} nodes named {node}",
533                        mesh.file,
534                        found.len()
535                    )));
536                }
537            }
538        }
539        None => parsed.roots.iter().map(|&root| (root, true)).collect(),
540    };
541    let overrides: BTreeMap<String, NodeOverride> = mesh
542        .nodes
543        .iter()
544        .map(|(node, file)| {
545            (
546                node.clone(),
547                NodeOverride {
548                    material: file.material.clone(),
549                    hidden: file.hidden,
550                    shadow: file.shadow.map(shadow),
551                    two_sided: file.two_sided,
552                },
553            )
554        })
555        .collect();
556    let mut parts = Vec::new();
557    let mut visited = BTreeSet::new();
558    let mut rigs = Rigging::default();
559    for (root, own) in roots {
560        walk(
561            &parsed,
562            root,
563            IDENTITY,
564            own,
565            &overrides,
566            name,
567            &mut parts,
568            &mut visited,
569            &mut rigs,
570        )
571        .map_err(error)?;
572    }
573    if rigs.skins.len() > 1 {
574        return Err(error(format!(
575            "mesh {name}: {} holds {} skins; a scene mesh takes one",
576            mesh.file,
577            rigs.skins.len()
578        )));
579    }
580    let rig = match rigs.skins.into_values().next() {
581        Some(found) => {
582            for part in &mut parts {
583                part.joint = None;
584            }
585            Some(Arc::new(found.rig))
586        }
587        None => {
588            let found = parsed
589                .node_rig(&rigs.nodes)
590                .map_err(|message| error(format!("mesh {name}: {message}")))?;
591            if found.is_none() {
592                for part in &mut parts {
593                    part.joint = None;
594                }
595            }
596            found.map(Arc::new)
597        }
598    };
599    for node in overrides.keys() {
600        if !visited.contains(node) {
601            return Err(places.refusal(
602                &entry.file,
603                &[
604                    Step::Key("mesh"),
605                    Step::Key(inner(name)),
606                    Step::Key("nodes"),
607                    Step::Key(node),
608                ],
609                format!("mesh {name}: the override names node {node}, which is not in this mesh"),
610            ));
611        }
612    }
613    if parts.is_empty() {
614        return Err(error(format!(
615            "mesh {name}: {} holds no triangles",
616            mesh.file
617        )));
618    }
619    let mut hasher = Sha256::new();
620    for part in &parts {
621        hasher.update(part.node.as_bytes());
622        hasher.update([0]);
623        hasher.update(part.geometry.hash);
624        hasher_add(&mut hasher, part.transform.iter().flatten().copied());
625        hasher.update(format!("{:?}{:?}", part.material, part.overrides).as_bytes());
626    }
627    if let Some(rig) = &rig {
628        hasher.update(format!("{rig:?}").as_bytes());
629    }
630    Ok(SceneMesh {
631        file,
632        node: mesh.node.clone(),
633        parts,
634        rig,
635        hash: hasher.finalize().into(),
636    })
637}
638
639#[derive(Default)]
640struct Rigging {
641    skins: BTreeMap<u32, SkinRig>,
642    nodes: Vec<(u32, bool)>,
643}
644
645#[allow(clippy::too_many_arguments)]
646fn walk(
647    mesh: &Mesh,
648    index: u32,
649    parent: Matrix,
650    own: bool,
651    overrides: &BTreeMap<String, NodeOverride>,
652    name: &str,
653    parts: &mut Vec<Part>,
654    visited: &mut BTreeSet<String>,
655    rigs: &mut Rigging,
656) -> Result<(), String> {
657    let node = &mesh.nodes[index as usize];
658    visited.insert(node.name.clone());
659    rigs.nodes.push((index, own));
660    let joint = rigs.nodes.len() - 1;
661    let transform = if own {
662        multiply(parent, node.transform)
663    } else {
664        parent
665    };
666    let node_override = overrides.get(&node.name).cloned().unwrap_or_default();
667    if let Some(group) = node.group {
668        for primitive in &mesh.groups[group as usize].primitives {
669            let mut geometry = geometry(primitive)
670                .map_err(|message| format!("mesh {name}, node {}: {message}", node.name))?;
671            let mut placed = transform;
672            if let (Some(skin), Some(joints), Some(weights)) =
673                (node.skin, &primitive.joints, &primitive.weights)
674            {
675                if let std::collections::btree_map::Entry::Vacant(entry) = rigs.skins.entry(skin) {
676                    entry.insert(mesh.rig(skin).map_err(|message| {
677                        format!("mesh {name}, node {}: {message}", node.name)
678                    })?);
679                }
680                let mut weights = weights.clone();
681                normalize_weights(&mut weights);
682                let joints = rigs.skins[&skin]
683                    .joints(joints, &weights)
684                    .map_err(|message| format!("mesh {name}, node {}: {message}", node.name))?;
685                geometry = geometry.with_skin(GeometrySkin { joints, weights });
686                placed = IDENTITY;
687            }
688            parts.push(Part {
689                node: node.name.clone(),
690                joint: Some(joint),
691                transform: placed,
692                geometry: Arc::new(geometry),
693                material: primitive
694                    .material
695                    .map(|material| mesh.materials[material as usize].name.clone()),
696                overrides: node_override.clone(),
697            });
698        }
699    }
700    for &child in &node.children {
701        walk(
702            mesh, child, transform, true, overrides, name, parts, visited, rigs,
703        )?;
704    }
705    Ok(())
706}
707
708pub(super) fn geometry(primitive: &Primitive) -> Result<Geometry, String> {
709    let count = primitive.positions.len();
710    if !primitive.indices.len().is_multiple_of(3) {
711        return Err("the index count is not a multiple of three".into());
712    }
713    if primitive
714        .indices
715        .iter()
716        .any(|&index| index as usize >= count)
717    {
718        return Err("an index is past the last vertex".into());
719    }
720    if !finite(&primitive.positions.concat()) {
721        return Err("a position is not finite".into());
722    }
723    let normals = if primitive.normals.is_empty() {
724        smooth_normals(&primitive.positions, &primitive.indices)
725    } else {
726        primitive.normals.clone()
727    };
728    let tangents = if primitive.tangents.is_empty() {
729        vec![[1.0, 0.0, 0.0, 1.0]; count]
730    } else {
731        primitive.tangents.clone()
732    };
733    let uvs = if primitive.uvs.is_empty() {
734        vec![[0.0; 2]; count]
735    } else {
736        primitive.uvs.clone()
737    };
738    Ok(Geometry::new(
739        primitive.positions.clone(),
740        normals,
741        tangents,
742        uvs,
743        primitive.uvs1.clone(),
744        primitive.indices.clone(),
745    ))
746}
747
748impl Geometry {
749    pub fn new(
750        positions: Vec<[f32; 3]>,
751        normals: Vec<[f32; 3]>,
752        tangents: Vec<[f32; 4]>,
753        uvs: Vec<[f32; 2]>,
754        uvs1: Option<Vec<[f32; 2]>>,
755        indices: Vec<u32>,
756    ) -> Self {
757        let mut hasher = Sha256::new();
758        hasher.update((positions.len() as u64).to_le_bytes());
759        hasher_add(&mut hasher, positions.iter().flatten().copied());
760        hasher_add(&mut hasher, normals.iter().flatten().copied());
761        hasher_add(&mut hasher, tangents.iter().flatten().copied());
762        hasher_add(&mut hasher, uvs.iter().flatten().copied());
763        if let Some(uvs1) = &uvs1 {
764            hasher.update([1]);
765            hasher_add(&mut hasher, uvs1.iter().flatten().copied());
766        }
767        for index in &indices {
768            hasher.update(index.to_le_bytes());
769        }
770        Self {
771            positions,
772            normals,
773            tangents,
774            uvs,
775            uvs1,
776            indices,
777            skin: None,
778            hash: hasher.finalize().into(),
779        }
780    }
781
782    pub fn with_skin(mut self, skin: GeometrySkin) -> Self {
783        let mut hasher = Sha256::new();
784        hasher.update(self.hash);
785        hasher.update([2]);
786        for joints in &skin.joints {
787            for joint in joints {
788                hasher.update(joint.to_le_bytes());
789            }
790        }
791        hasher_add(&mut hasher, skin.weights.iter().flatten().copied());
792        self.hash = hasher.finalize().into();
793        self.skin = Some(Arc::new(skin));
794        self
795    }
796}
797
798fn smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
799    let mut normals = vec![[0.0_f32; 3]; positions.len()];
800    for triangle in indices.chunks_exact(3) {
801        let [a, b, c] = [0, 1, 2].map(|k| positions[triangle[k] as usize]);
802        let face = super::cross(super::sub(b, a), super::sub(c, a));
803        for &index in triangle {
804            for axis in 0..3 {
805                normals[index as usize][axis] += face[axis];
806            }
807        }
808    }
809    normals
810        .into_iter()
811        .map(|normal| {
812            let unit = super::unit(normal);
813            if super::dot(unit, unit) > 0.5 {
814                unit
815            } else {
816                [0.0, 1.0, 0.0]
817            }
818        })
819        .collect()
820}
821
822fn build_objects(
823    places: &Places,
824    entries: &[Entry<file::Object>],
825    meshes: &BTreeMap<String, SceneMesh>,
826) -> Result<Vec<Object>, SceneError> {
827    let mut objects = Vec::new();
828    let mut local = Vec::new();
829    for (place, entry) in entries.iter().enumerate() {
830        let value = &entry.value;
831        let name = entry.key.as_str();
832        let steps = [Step::Named("object", inner(name))];
833        let mesh = value.mesh.clone().unwrap_or_default();
834        if let Some(found) = meshes.get(&mesh) {
835            for node in value.materials.keys() {
836                if !found.parts.iter().any(|part| &part.node == node) {
837                    return places.error(
838                        &entry.file,
839                        &[
840                            Step::Named("object", inner(name)),
841                            Step::Key("materials"),
842                            Step::Key(node),
843                        ],
844                        format!(
845                            "object {name}: materials names node {node}, which mesh {mesh} has not"
846                        ),
847                    );
848                }
849            }
850        } else if value.prefab.is_none() {
851            return places.error(
852                &entry.file,
853                &steps,
854                format!("object {name} names mesh {mesh}, which the scene has not"),
855            );
856        }
857        let scale = value.scale_axes();
858        local.push(model(value.at, value.rotate, scale));
859        objects.push(Object {
860            name: entry.key.clone(),
861            mesh,
862            at: value.at,
863            rotate: value.rotate,
864            scale,
865            parent: value.parent.clone(),
866            model: IDENTITY,
867            material: value.material.clone(),
868            materials: value.materials.clone(),
869            shadow: shadow(value.shadow.unwrap_or_default()),
870            two_sided: value.two_sided,
871            hidden: value.hidden,
872            clip: value.clip.clone(),
873            content: value.content.clone(),
874            id: value.pick.unwrap_or(place as u32 + 1),
875            alpha_cutoff: value.alpha_cutoff.unwrap_or(ALPHA_CUTOFF),
876            face_camera: value.face_camera,
877            dynamic: value.dynamic,
878        });
879    }
880    let index: BTreeMap<String, usize> = objects
881        .iter()
882        .enumerate()
883        .map(|(place, object)| (object.name.clone(), place))
884        .collect();
885    for place in 0..objects.len() {
886        let mut matrix = local[place];
887        let mut at = place;
888        let mut steps = 0;
889        while let Some(parent) = &objects[at].parent {
890            steps += 1;
891            let Some(&above) = index.get(parent) else {
892                break;
893            };
894            if steps > objects.len() {
895                let entry = &entries[place];
896                return places.error(
897                    &entry.file,
898                    &[Step::Named("object", inner(&entry.key))],
899                    format!("object {}: its parents form a cycle", entry.key),
900                );
901            }
902            at = above;
903            matrix = multiply(local[at], matrix);
904        }
905        objects[place].model = matrix;
906    }
907    Ok(objects)
908}
909
910fn build_light(light: &file::Light) -> Light {
911    Light {
912        position: light.position,
913        color: light.color.unwrap_or(file::Light::COLOR),
914        intensity: light.intensity,
915        radius: light.radius.unwrap_or(file::Light::RADIUS),
916        range: light.range.unwrap_or(file::Light::RANGE),
917        shadow: light.shadow.unwrap_or(file::Light::SHADOW),
918    }
919}
920
921fn build_emitter(emitter: &file::Emitter) -> Emitter {
922    Emitter {
923        position: emitter.position,
924        radius: emitter.radius,
925        color: emitter.color.unwrap_or([1.0; 3]),
926        intensity: emitter.intensity,
927    }
928}
929
930fn build_sun(
931    places: &Places,
932    holder: &Path,
933    sun: &file::Sun,
934    hour: Option<f64>,
935) -> Result<Sun, SceneError> {
936    let steps = [Step::Key("sun")];
937    let hour = hour.or(sun.hour);
938    let radius = sun.radius.unwrap_or(0.0);
939    let reference_hour = sun.reference_hour.unwrap_or(REFERENCE_HOUR);
940    let daylight = |hour: Option<f64>| {
941        let fields = [
942            ("hour", hour),
943            ("day", sun.day),
944            ("latitude", sun.latitude),
945            ("heading", sun.heading),
946        ]
947        .into_iter()
948        .filter_map(|(key, value)| value.map(|value| (key, Reading::Number(value))));
949        Daylight::parse(fields).map_err(|message| places.refusal(holder, &steps, message))
950    };
951    match sun.model {
952        file::SunModel::Daylight => {
953            let daylight = daylight(hour)?;
954            let light = daylight.light(reference_hour);
955            Ok(Sun {
956                direction: daylight.sun().y_up.map(|value| value as f32),
957                color: light.colour.map(|value| value as f32),
958                intensity: light.intensity as f32,
959                hour: daylight.hour as f32,
960                daylight: Some(daylight),
961                reference_hour,
962                radius,
963            })
964        }
965        file::SunModel::Authored => {
966            let (Some(toward), Some(irradiance)) = (sun.toward, sun.irradiance) else {
967                return places.error(
968                    holder,
969                    &steps,
970                    "an authored sun needs toward and irradiance",
971                );
972            };
973            let color = sun.color.unwrap_or([1.0; 3]);
974            let daylight = daylight(Some(hour.unwrap_or(reference_hour)))?;
975            if daylight.hour == reference_hour {
976                return Ok(Sun {
977                    direction: super::unit(toward),
978                    color,
979                    intensity: irradiance,
980                    hour: daylight.hour as f32,
981                    daylight: None,
982                    reference_hour,
983                    radius,
984                });
985            }
986            let peak = color.into_iter().fold(0.0_f32, f32::max);
987            let followed = daylight.authored(
988                &Authored {
989                    toward: toward.map(f64::from),
990                    colour: color.map(f64::from),
991                    sky_fill: 0.0,
992                },
993                reference_hour,
994            );
995            Ok(Sun {
996                direction: super::unit(followed.toward.map(|value| value as f32)),
997                color: followed.colour.map(|value| value as f32 * peak),
998                intensity: irradiance * followed.intensity as f32,
999                hour: daylight.hour as f32,
1000                daylight: None,
1001                reference_hour,
1002                radius,
1003            })
1004        }
1005    }
1006}
1007
1008fn kind_name(kind: file::SkyKind) -> &'static str {
1009    match kind {
1010        file::SkyKind::Analytic => "analytic",
1011        file::SkyKind::Hdr => "hdr",
1012        file::SkyKind::Room => "room",
1013        file::SkyKind::Mix => "mix",
1014    }
1015}
1016
1017fn room_of(room: &file::Room) -> RoomSky {
1018    RoomSky {
1019        width: room.width,
1020        floor: room.floor,
1021        wall: room.wall,
1022        ceiling: room.ceiling,
1023        lights: room
1024            .lights
1025            .iter()
1026            .map(|lamp| RoomLamp {
1027                name: lamp.name.clone(),
1028                az: lamp.az,
1029                el: lamp.el,
1030                power: lamp.power,
1031                color: lamp.color,
1032                width: lamp.width,
1033                height: lamp.height,
1034                soft: lamp.soft,
1035                slats: lamp.slats,
1036                open: lamp.open,
1037            })
1038            .collect(),
1039    }
1040}
1041
1042fn build_sky(
1043    places: &Places,
1044    holder: &Path,
1045    sky: &file::Sky,
1046    sun: Option<&Sun>,
1047    reader: &mut Reader,
1048) -> Result<SceneSky, SceneError> {
1049    let steps = [Step::Key("sky")];
1050    let mut hasher = Sha256::new();
1051    hasher.update(kind_name(sky.kind).as_bytes());
1052    let rotation = sky.rotation_deg.unwrap_or(0.0);
1053    let intensity = sky.intensity.unwrap_or(file::Sky::INTENSITY);
1054    let environment = match sky.kind {
1055        file::SkyKind::Analytic => {
1056            let turbidity = sky.turbidity.unwrap_or(file::Sky::TURBIDITY);
1057            let albedo = sky.ground_albedo.unwrap_or(file::Sky::GROUND_ALBEDO);
1058            let mut analytic = match sun
1059                .and_then(|sun| sun.daylight.map(|daylight| (daylight, sun.reference_hour)))
1060            {
1061                Some((daylight, reference)) => {
1062                    AnalyticSky::new(daylight, reference, turbidity, albedo)
1063                }
1064                None => {
1065                    let sun = sun.copied().unwrap_or_else(Sun::dark);
1066                    AnalyticSky {
1067                        sun: sun.direction,
1068                        sun_colour: sun.color,
1069                        sun_intensity: sun.intensity,
1070                        ambient: 1.0,
1071                        turbidity,
1072                        ground_albedo: albedo.map(|v| v.clamp(0.0, 1.0)),
1073                    }
1074                }
1075            };
1076            if let Some(ambient) = sky.ambient {
1077                analytic.ambient = ambient;
1078            }
1079            hasher_add(
1080                &mut hasher,
1081                analytic
1082                    .sun
1083                    .into_iter()
1084                    .chain(analytic.sun_colour)
1085                    .chain([analytic.sun_intensity, analytic.ambient, analytic.turbidity])
1086                    .chain(analytic.ground_albedo),
1087            );
1088            Environment::Analytic(analytic)
1089        }
1090        file::SkyKind::Hdr => {
1091            let Some(relative) = &sky.path else {
1092                return places.error(holder, &steps, "an hdr sky needs a path");
1093            };
1094            let path = places.root.join(relative);
1095            let bytes = reader
1096                .read(&path)
1097                .map_err(|message| places.refusal(holder, &steps, message))?;
1098            hasher.update(sha256(&bytes));
1099            let mut parsed =
1100                Sky::parse(&bytes).map_err(|message| SceneError::new(&path, None, message))?;
1101            if let Some(prepare) = sky.prepare {
1102                hasher.update(
1103                    format!(
1104                        "PrepareFile {{ cap: {:?}, balance: {:?}, mean: {:?} }}",
1105                        prepare.cap, prepare.balance, prepare.mean
1106                    )
1107                    .as_bytes(),
1108                );
1109                if let Some(cap) = prepare.cap {
1110                    parsed = parsed.capped_luminance(cap);
1111                }
1112                if let Some(balance) = prepare.balance {
1113                    parsed = parsed.balanced_to(balance);
1114                }
1115                if let Some(mean) = prepare.mean {
1116                    parsed = parsed.scaled_to_mean_luminance(mean);
1117                }
1118            }
1119            hasher_add(&mut hasher, [rotation, intensity]);
1120            Environment::Hdr(Arc::new(
1121                parsed.rotated(-rotation.to_radians()).exposed(intensity),
1122            ))
1123        }
1124        file::SkyKind::Room => {
1125            let Some(room) = &sky.room else {
1126                return places.error(holder, &steps, "a room sky needs a [sky.room] table");
1127            };
1128            let room = room_of(room);
1129            room.check()
1130                .map_err(|message| places.refusal(holder, &steps, message))?;
1131            hasher.update(format!("{room:?}").as_bytes());
1132            hasher_add(&mut hasher, [rotation, intensity]);
1133            Environment::Hdr(Arc::new(
1134                Sky::room(&room)
1135                    .rotated(-rotation.to_radians())
1136                    .exposed(intensity),
1137            ))
1138        }
1139        file::SkyKind::Mix => return build_mix(places, holder, sky, sun, reader),
1140    };
1141    Ok(SceneSky {
1142        kind: kind_name(sky.kind).into(),
1143        environment,
1144        hash: hasher.finalize().into(),
1145    })
1146}
1147
1148fn build_mix(
1149    places: &Places,
1150    holder: &Path,
1151    sky: &file::Sky,
1152    sun: Option<&Sun>,
1153    reader: &mut Reader,
1154) -> Result<SceneSky, SceneError> {
1155    let mut hasher = Sha256::new();
1156    hasher.update(b"mix");
1157    let mut layers = Vec::new();
1158    for layer in &sky.layer {
1159        let weight = layer.weight.unwrap_or(file::Sky::WEIGHT);
1160        let built = build_sky(places, holder, layer, sun, reader)?;
1161        hasher.update(built.hash);
1162        hasher_add(&mut hasher, [weight]);
1163        layers.push((weight, built.environment));
1164    }
1165    Ok(SceneSky {
1166        kind: "mix".into(),
1167        environment: Environment::Hdr(Arc::new(super::mix::bake(&layers))),
1168        hash: hasher.finalize().into(),
1169    })
1170}
1171
1172fn build_camera(
1173    places: &Places,
1174    holder: &Path,
1175    camera: &file::Camera,
1176) -> Result<Camera, SceneError> {
1177    let steps = [Step::Key("camera")];
1178    let base = Camera::default();
1179    let projection = match camera.projection.unwrap_or_default() {
1180        file::Projection::Perspective => {
1181            let fov = match (camera.fov, camera.focal) {
1182                (Some(fov), _) => fov,
1183                (None, Some(focal)) => {
1184                    let sensor = camera.sensor.unwrap_or(file::Camera::SENSOR);
1185                    2.0 * (sensor / (2.0 * focal)).atan().to_degrees()
1186                }
1187                (None, None) => file::Camera::FOV,
1188            };
1189            Projection::Perspective { fov }
1190        }
1191        file::Projection::Orthographic => {
1192            let Some(height) = camera.height else {
1193                return places.error(
1194                    holder,
1195                    &steps,
1196                    "an orthographic camera needs a positive height",
1197                );
1198            };
1199            Projection::Orthographic { height }
1200        }
1201    };
1202    let mut built = Camera {
1203        projection,
1204        at: camera.at.unwrap_or(base.at),
1205        look_at: camera.look_at.unwrap_or(base.look_at),
1206        up: camera.up.unwrap_or(base.up),
1207        near: camera.near.unwrap_or(base.near),
1208        far: camera.far.unwrap_or(base.far),
1209        shift: camera.shift.unwrap_or([0.0; 2]),
1210        depth_of_field: None,
1211        preset: preset(camera.preset.as_ref()),
1212    };
1213    if let (Projection::Perspective { fov }, Some(fstop)) = (built.projection, camera.fstop) {
1214        let distance = camera.focus.unwrap_or_else(|| {
1215            super::dot(
1216                super::sub(built.look_at, built.at),
1217                super::sub(built.look_at, built.at),
1218            )
1219            .sqrt()
1220        });
1221        let sensor = camera.sensor.unwrap_or(file::Camera::SENSOR);
1222        let focal = camera
1223            .focal
1224            .unwrap_or_else(|| sensor / (2.0 * (fov.to_radians() * 0.5).tan()))
1225            / 1000.0;
1226        if !(distance.is_finite() && distance > focal) {
1227            return places.error(
1228                holder,
1229                &steps,
1230                "camera fstop must be positive and focus beyond the focal length",
1231            );
1232        }
1233        built.depth_of_field = Some(DepthOfField {
1234            fstop,
1235            distance,
1236            focal,
1237        });
1238    }
1239    Ok(built)
1240}
1241
1242fn preset(file: Option<&file::Preset>) -> Preset {
1243    let mut preset = Preset::default();
1244    let Some(file) = file else {
1245        return preset;
1246    };
1247    let set = |target: &mut f32, value: Option<f32>, scale: f32| {
1248        if let Some(value) = value {
1249            *target = value * scale;
1250        }
1251    };
1252    set(&mut preset.gain, file.gain, 1.0);
1253    if let Some(orbit) = file.orbit {
1254        set(&mut preset.orbit.stiffness, orbit.stiffness, 1.0);
1255        set(&mut preset.orbit.yaw, orbit.yaw, DEG);
1256        set(&mut preset.orbit.pitch, orbit.pitch, DEG);
1257        set(&mut preset.orbit.hold, orbit.hold, 1.0);
1258    }
1259    if let Some(look) = file.look {
1260        set(&mut preset.look.stiffness, look.stiffness, 1.0);
1261        set(&mut preset.look.lean, look.lean, 1.0);
1262        set(&mut preset.look.yaw, look.yaw, DEG);
1263        set(&mut preset.look.pitch, look.pitch, DEG);
1264        set(&mut preset.look.sign, look.sign, 1.0);
1265    }
1266    if let Some(zoom) = file.zoom {
1267        set(&mut preset.zoom.stiffness, zoom.stiffness, 1.0);
1268        set(&mut preset.zoom.hold, zoom.hold, 1.0);
1269        set(&mut preset.zoom.max, zoom.max, 1.0);
1270        set(&mut preset.zoom.rate, zoom.rate, 1.0);
1271    }
1272    if let Some(wander) = file.wander {
1273        set(&mut preset.wander.idle, wander.idle, 1.0);
1274        set(&mut preset.wander.every, wander.every, 1.0);
1275        set(&mut preset.wander.hold, wander.hold, 1.0);
1276    }
1277    if let Some(nudge) = file.nudge {
1278        set(&mut preset.nudge.stiffness, nudge.stiffness, 1.0);
1279        set(&mut preset.nudge.pitch, nudge.pitch, DEG);
1280        set(&mut preset.nudge.hold, nudge.hold, 1.0);
1281        set(&mut preset.nudge.floor, nudge.floor, 1.0);
1282    }
1283    if let Some(file::Hush {
1284        floor: Some(floor),
1285        stiffness: Some(stiffness),
1286        blocks_cursor: Some(blocks_cursor),
1287    }) = file.hush
1288    {
1289        preset.hush = Some(Hush {
1290            floor,
1291            stiffness,
1292            blocks_cursor,
1293        });
1294    }
1295    if let Some(file::Depth {
1296        focus: Some(focus),
1297        blur: Some(blur),
1298        floor: Some(floor),
1299    }) = file.depth
1300    {
1301        preset.depth = Some(Depth { focus, blur, floor });
1302    }
1303    preset
1304}
1305
1306fn pass_line(text: &str, place: usize) -> Option<usize> {
1307    text.lines()
1308        .enumerate()
1309        .filter(|(_, line)| {
1310            line.split('#')
1311                .next()
1312                .unwrap_or("")
1313                .replace([' ', '\t'], "")
1314                == "[[finish.pass]]"
1315        })
1316        .nth(place)
1317        .map(|(number, _)| number + 1)
1318}
1319
1320fn build_finish(
1321    places: &Places,
1322    holder: &Path,
1323    finish: &file::Finish,
1324    reader: &mut Reader,
1325) -> Result<Finish, SceneError> {
1326    let steps = [Step::Key("finish")];
1327    let text = places.text(holder).unwrap_or_default();
1328    let raw: toml::Table = toml::from_str::<toml::Table>(&text)
1329        .ok()
1330        .and_then(|mut top| top.remove("finish"))
1331        .and_then(|value| value.as_table().cloned())
1332        .unwrap_or_default();
1333    let mut hasher = Sha256::new();
1334    let mut base = raw.clone();
1335    let passes = match base.remove("pass") {
1336        Some(toml::Value::Array(items)) => items,
1337        _ => Vec::new(),
1338    };
1339    let mut chain = match &finish.file {
1340        Some(relative) => {
1341            let path = places.root.join(relative);
1342            let bytes = reader
1343                .read(&path)
1344                .map_err(|message| places.refusal(holder, &steps, message))?;
1345            hasher.update(sha256(&bytes));
1346            let text = String::from_utf8(bytes)
1347                .map_err(|_| SceneError::new(&path, None, "the finish is not UTF-8"))?;
1348            pfx_post::parse_str(&text)
1349                .map_err(|error| SceneError::new(&path, None, error.to_string()))?
1350        }
1351        None => {
1352            hasher.update(base.to_string().as_bytes());
1353            pfx_post::parse(&base)
1354                .map_err(|error| places.refusal(holder, &steps, format!("[finish]: {error}")))?
1355        }
1356    };
1357    for (place, item) in passes.iter().enumerate() {
1358        let refuse = |message: String| {
1359            let mut error = places.refusal(holder, &steps, message);
1360            if let Some(line) = pass_line(&text, place) {
1361                error.line = Some(line);
1362                error.column = Some(1);
1363            }
1364            error
1365        };
1366        let Some(pass) = item.as_table() else {
1367            return Err(refuse("a [[finish.pass]] is a table of finish keys".into()));
1368        };
1369        let built =
1370            pfx_post::parse(pass).map_err(|error| refuse(format!("[[finish.pass]]: {error}")))?;
1371        if built.passes.is_empty() {
1372            return Err(refuse("a [[finish.pass]] builds no pass".into()));
1373        }
1374        hasher.update(b"pass");
1375        hasher.update(pass.to_string().as_bytes());
1376        chain.passes.extend(built.passes);
1377    }
1378    Ok(Finish {
1379        chain,
1380        hash: hasher.finalize().into(),
1381    })
1382}
1383
1384fn build_content(
1385    places: &Places,
1386    entry: &Entry<file::Content>,
1387    reader: &mut Reader,
1388) -> Result<Content, SceneError> {
1389    let steps = [Step::Key("content"), Step::Key(inner(&entry.key))];
1390    let path = places.root.join(&entry.value.image);
1391    let bytes = reader
1392        .read(&path)
1393        .map_err(|message| places.refusal(&entry.file, &steps, message))?;
1394    content_from(path.clone(), &bytes).map_err(|message| SceneError::new(&path, None, message))
1395}
1396
1397pub(super) fn content_from(path: PathBuf, bytes: &[u8]) -> Result<Content, String> {
1398    let image = Image::png(bytes)?;
1399    let rgba = match image.pixels {
1400        Pixels::Eight(bytes) => bytes,
1401        Pixels::Sixteen(values) => values.into_iter().map(|value| (value >> 8) as u8).collect(),
1402    };
1403    Ok(Content {
1404        image: path,
1405        width: image.width,
1406        height: image.height,
1407        srgb: image.space == ColorSpace::Srgb,
1408        rgba: Arc::new(rgba),
1409        hash: sha256(bytes),
1410    })
1411}
1412
1413fn place_texts(texts: &mut BTreeMap<String, Text>, objects: &[Object]) {
1414    for (key, text) in texts.iter_mut() {
1415        let Some((placement, _)) = key.rsplit_once('/') else {
1416            continue;
1417        };
1418        if let Some(object) = objects.iter().find(|object| object.name == placement) {
1419            text.place = object.model;
1420        }
1421    }
1422}
1423
1424fn build_text(
1425    places: &Places,
1426    entry: &Entry<file::Text>,
1427    reader: &mut Reader,
1428) -> Result<Text, SceneError> {
1429    let steps = [Step::Key("text"), Step::Key(inner(&entry.key))];
1430    let text = &entry.value;
1431    let path = places.root.join(&text.font);
1432    let bytes = reader
1433        .read(&path)
1434        .map_err(|message| places.refusal(&entry.file, &steps, message))?;
1435    let font = Font::parse(&bytes).map_err(|message| SceneError::new(&path, None, message))?;
1436    let Some(family) = font.families.first().cloned() else {
1437        return Err(SceneError::new(&path, None, "the font names no family"));
1438    };
1439    Ok(Text {
1440        text: super::filled(&text.text),
1441        font: path,
1442        font_bytes: Arc::new(bytes),
1443        family,
1444        lit: text.lit,
1445        size: text.size,
1446        color: text.color.unwrap_or(file::Text::COLOR),
1447        at: text.at,
1448        rotate: text.rotate,
1449        place: IDENTITY,
1450        dynamic: text.dynamic,
1451    })
1452}