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 = ⊤
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}