1use std::collections::BTreeMap;
18use std::path::PathBuf;
19
20use serde::Serialize;
21use serde_json::{Value, json};
22
23use crate::json::JsonError;
24use crate::texformat::TexFormat;
25use crate::vector::{cross, dot};
26use crate::world::{Component, Entity};
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct Name(pub String);
31impl Component for Name {
32 const NAME: &'static str = "core.name";
33 const GEOMETRY: crate::Geometry = crate::Geometry::None;
34 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
36 crate::world::Linearity::Invariant
37 }
38 fn to_json(&self) -> Result<Option<Value>, JsonError> {
39 crate::json::to_value(self).map(Some)
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
46pub struct Source(pub String);
47impl Component for Source {
48 const NAME: &'static str = "core.source";
49 const GEOMETRY: crate::Geometry = crate::Geometry::None;
50 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
52 crate::world::Linearity::Invariant
53 }
54 fn to_json(&self) -> Result<Option<Value>, JsonError> {
55 crate::json::to_value(self).map(Some)
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
65pub struct Transform {
66 pub cols: [[f32; 3]; 4],
67}
68impl Component for Transform {
69 const NAME: &'static str = "core.transform";
70 const GEOMETRY: crate::Geometry = crate::Geometry::None;
71 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
73 crate::world::Linearity::Invariant
74 }
75 fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
76 finite(self.cols.iter().flatten().copied(), "transform")
77 }
78 fn to_json(&self) -> Result<Option<Value>, JsonError> {
79 crate::json::to_value(self).map(Some)
80 }
81}
82
83pub const DEGENERATE_BASIS: f64 = 1e-6;
87pub const SHEAR_TOLERANCE: f64 = 1e-4;
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum TransformKind {
94 Regular,
96 Degenerate,
98 Sheared,
100 Mirrored,
102}
103
104impl Transform {
105 pub const IDENTITY: Transform = Transform {
106 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., 0.]],
107 };
108
109 pub fn determinant(&self) -> f64 {
112 let c: [[f64; 3]; 3] = std::array::from_fn(|i| self.cols[i].map(f64::from));
113 dot(c[0], cross(c[1], c[2]))
114 }
115
116 pub fn facing(position: [f32; 3], direction: [f32; 3]) -> Option<Transform> {
120 let unit = |v: [f32; 3]| {
121 let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
122 (n > 0.0 && n.is_finite()).then(|| v.map(|x| x / n))
123 };
124 let cross = |a: [f32; 3], b: [f32; 3]| {
125 [
126 a[1] * b[2] - a[2] * b[1],
127 a[2] * b[0] - a[0] * b[2],
128 a[0] * b[1] - a[1] * b[0],
129 ]
130 };
131 let z = unit(direction.map(|x| -x))?;
132 let x = unit(cross([0.0, 1.0, 0.0], z)).or_else(|| unit(cross([0.0, 0.0, 1.0], z)))?;
133 let y = cross(z, x);
134 Some(Transform {
135 cols: [x, y, z, position],
136 })
137 }
138
139 pub fn apply(&self, p: [f32; 3]) -> [f32; 3] {
141 std::array::from_fn(|i| {
142 (f64::from(self.cols[3][i])
143 + (0..3)
144 .map(|j| f64::from(self.cols[j][i]) * f64::from(p[j]))
145 .sum::<f64>()) as f32
146 })
147 }
148
149 pub fn from_columns4(cols: [[f32; 4]; 4], tolerance: f32) -> Result<Transform, String> {
153 let last = cols.map(|c| c[3]);
154 if !last
156 .iter()
157 .zip([0.0, 0.0, 0.0, 1.0])
158 .all(|(&w, want)| (w - want).abs() <= tolerance)
159 {
160 return Err(format!(
161 "a matrix whose last row is {last:?}, not 0 0 0 1 (not affine)"
162 ));
163 }
164 Ok(Transform {
165 cols: cols.map(|c| [c[0], c[1], c[2]]),
166 })
167 }
168
169 pub fn compose(&self, inner: &Transform) -> Transform {
173 let o = self.cols.map(|c| c.map(f64::from));
174 let linear = |v: [f32; 3]| -> [f64; 3] {
175 let v = v.map(f64::from);
176 std::array::from_fn(|r| o[0][r] * v[0] + o[1][r] * v[1] + o[2][r] * v[2])
177 };
178 let [x, y, z, t] = inner.cols.map(linear);
179 Transform {
180 cols: [
181 x.map(|v| v as f32),
182 y.map(|v| v as f32),
183 z.map(|v| v as f32),
184 std::array::from_fn(|r| (t[r] + o[3][r]) as f32),
185 ],
186 }
187 }
188
189 pub fn classify(&self) -> TransformKind {
194 let c: [[f64; 3]; 3] = std::array::from_fn(|i| self.cols[i].map(f64::from));
195 let len: [f64; 3] = std::array::from_fn(|i| dot(c[i], c[i]).sqrt());
196 let det = dot(c[0], cross(c[1], c[2]));
197 if degenerate(&c, det) {
198 return TransformKind::Degenerate;
199 }
200 let u: [[f64; 3]; 3] = std::array::from_fn(|i| c[i].map(|x| x / len[i]));
201 if [(0, 1), (0, 2), (1, 2)]
202 .iter()
203 .any(|&(i, j)| dot(u[i], u[j]).abs() > SHEAR_TOLERANCE)
204 {
205 return TransformKind::Sheared;
206 }
207 if det < 0.0 {
208 TransformKind::Mirrored
209 } else {
210 TransformKind::Regular
211 }
212 }
213}
214
215fn degenerate(c: &[[f64; 3]; 3], det: f64) -> bool {
219 let len: [f64; 3] = std::array::from_fn(|i| dot(c[i], c[i]).sqrt());
220 det.is_nan()
221 || len.iter().any(|&l| l < DEGENERATE_BASIS)
222 || det.abs() <= DEGENERATE_BASIS * len[0] * len[1] * len[2]
223}
224
225fn finite(values: impl Iterator<Item = f32>, what: &str) -> Result<(), String> {
227 match values.enumerate().find(|(_, x)| !x.is_finite()) {
228 Some((i, x)) => Err(format!("{what}: value {i} is {x}, not finite")),
229 None => Ok(()),
230 }
231}
232
233#[derive(Clone, Copy, Debug, PartialEq)]
241pub struct Linear {
242 c: [[f64; 3]; 3],
243 cof: [[f64; 3]; 3],
245 det: f64,
246}
247
248impl Linear {
249 pub fn of(t: &Transform) -> Linear {
251 Linear::new(std::array::from_fn(|i| t.cols[i]))
252 }
253
254 pub fn new(cols: [[f32; 3]; 3]) -> Linear {
256 Linear::of_f64(cols.map(|col| col.map(f64::from)))
257 }
258
259 pub fn of_f64(c: [[f64; 3]; 3]) -> Linear {
262 let cof = [cross(c[1], c[2]), cross(c[2], c[0]), cross(c[0], c[1])];
263 let det = dot(c[0], cof[0]);
264 Linear { c, cof, det }
265 }
266
267 pub fn mirror_x() -> Linear {
269 Linear::new([[-1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
270 }
271
272 pub fn determinant(&self) -> f64 {
273 self.det
274 }
275
276 pub fn singular(&self) -> bool {
280 !self.det.is_finite() || degenerate(&self.c, self.det)
281 }
282
283 pub fn inverse_columns(&self) -> Option<[[f64; 3]; 3]> {
286 if self.singular() {
287 return None;
288 }
289 Some(std::array::from_fn(|j| {
292 std::array::from_fn(|r| self.cof[r][j] / self.det)
293 }))
294 }
295
296 pub fn mirrors(&self) -> bool {
298 self.det < 0.0
299 }
300
301 pub fn similarity(&self) -> Option<f64> {
304 let len: [f64; 3] = self.c.map(|v| dot(v, v).sqrt());
305 let s = len[0];
306 let tol = 1e-6 * s;
307 let orthogonal = [(0, 1), (0, 2), (1, 2)]
308 .iter()
309 .all(|&(i, j)| dot(self.c[i], self.c[j]).abs() <= tol * s);
310 (s > 0.0 && orthogonal && len.iter().all(|l| (l - s).abs() <= tol)).then_some(s)
311 }
312
313 pub fn axis_map(&self) -> Option<[usize; 3]> {
316 let mut out = [0; 3];
317 for (k, col) in self.c.iter().enumerate() {
318 let nonzero: Vec<usize> = (0..3).filter(|&r| col[r] != 0.0).collect();
319 match nonzero[..] {
320 [r] => out[k] = r,
321 _ => return None,
322 }
323 }
324 Some(out)
325 }
326
327 fn apply(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
328 std::array::from_fn(|r| m[0][r] * v[0] + m[1][r] * v[1] + m[2][r] * v[2])
329 }
330
331 pub fn point(&self, p: [f32; 3]) -> [f32; 3] {
333 Self::apply(&self.c, p.map(f64::from)).map(|x| x as f32)
334 }
335
336 pub fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
339 if self.singular() {
340 return None;
341 }
342 let v = Self::apply(&self.cof, n.map(f64::from)).map(|x| x * self.det.signum());
343 Some(unit(v))
344 }
345
346 pub fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
349 if self.singular() {
350 return None;
351 }
352 let v = unit(Self::apply(&self.c, [t[0], t[1], t[2]].map(f64::from)));
353 let w = if self.mirrors() { -t[3] } else { t[3] };
354 Some([v[0], v[1], v[2], w])
355 }
356}
357
358fn unit(v: [f64; 3]) -> [f32; 3] {
360 crate::vector::unit(v).map_or([0.0; 3], |u| u.map(|x| x as f32))
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
365pub struct Instance {
366 pub prototype: Entity,
367 pub variant: u32,
370}
371impl Component for Instance {
372 const NAME: &'static str = "core.instance";
373 const GEOMETRY: crate::Geometry = crate::Geometry::None;
374 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
376 crate::world::Linearity::Invariant
377 }
378 fn to_json(&self) -> Result<Option<Value>, JsonError> {
379 crate::json::to_value(self).map(Some)
380 }
381 fn references(&self, out: &mut Vec<Entity>) {
382 out.push(self.prototype);
383 }
384 fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
386 if w.has::<Transform>(e) {
387 Ok(())
388 } else {
389 Err("a placement without a core.transform".into())
390 }
391 }
392}
393
394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
396pub enum ShadowCast {
397 Off,
398 On,
399 Only,
401}
402
403#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize)]
409pub struct Shadows {
410 pub cast: Option<ShadowCast>,
411 pub receive: Option<bool>,
412}
413impl Component for Shadows {
414 const NAME: &'static str = "core.shadows";
415 const GEOMETRY: crate::Geometry = crate::Geometry::None;
416 const VERSION: u32 = 2;
418 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
420 crate::world::Linearity::Invariant
421 }
422 fn to_json(&self) -> Result<Option<Value>, JsonError> {
423 crate::json::to_value(self).map(Some)
424 }
425 fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
427 if w.has::<Instance>(e) || w.has::<Mesh>(e) {
428 Ok(())
429 } else {
430 Err("shadows on an entity that draws nothing (no core.instance or core.mesh)".into())
431 }
432 }
433}
434
435#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
437pub enum Semantic {
438 Position,
439 Normal,
440 Tangent(u8),
442 TexCoord(u8),
444 Color(u8),
445 Joints(u8),
446 Weights(u8),
447 ShaderInput(u8),
451 Point(u8),
455}
456
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
460pub enum Moves {
461 Point,
463 Normal,
465 Tangent,
467 Not,
469}
470
471impl Semantic {
472 pub const fn moves(self) -> Moves {
474 match self {
475 Semantic::Position | Semantic::Point(_) => Moves::Point,
476 Semantic::Normal => Moves::Normal,
477 Semantic::Tangent(_) => Moves::Tangent,
478 Semantic::TexCoord(_)
479 | Semantic::Color(_)
480 | Semantic::Joints(_)
481 | Semantic::Weights(_)
482 | Semantic::ShaderInput(_) => Moves::Not,
483 }
484 }
485}
486
487impl From<Semantic> for crate::shader::Attribute {
489 fn from(s: Semantic) -> Self {
490 use crate::shader::Attribute as A;
491 match s {
492 Semantic::Position => A::Position,
493 Semantic::Normal => A::Normal,
494 Semantic::Tangent(n) => A::Tangent(n),
495 Semantic::TexCoord(n) => A::TexCoord(n),
496 Semantic::Color(n) => A::Color(n),
497 Semantic::Joints(n) => A::Joints(n),
498 Semantic::Weights(n) => A::Weights(n),
499 Semantic::ShaderInput(n) => A::ShaderInput(n),
500 Semantic::Point(n) => A::Point(n),
501 }
502 }
503}
504
505impl From<crate::shader::Attribute> for Semantic {
506 fn from(a: crate::shader::Attribute) -> Self {
507 use crate::shader::Attribute as A;
508 match a {
509 A::Position => Semantic::Position,
510 A::Normal => Semantic::Normal,
511 A::Tangent(n) => Semantic::Tangent(n),
512 A::TexCoord(n) => Semantic::TexCoord(n),
513 A::Color(n) => Semantic::Color(n),
514 A::Joints(n) => Semantic::Joints(n),
515 A::Weights(n) => Semantic::Weights(n),
516 A::ShaderInput(n) => Semantic::ShaderInput(n),
517 A::Point(n) => Semantic::Point(n),
518 }
519 }
520}
521
522#[derive(Clone, Debug, PartialEq)]
525pub enum Stream {
526 F32x2(Vec<[f32; 2]>),
527 F32x3(Vec<[f32; 3]>),
528 F32x4(Vec<[f32; 4]>),
529 U8x4(Vec<[u8; 4]>),
530 U16x4(Vec<[u16; 4]>),
531}
532
533impl Stream {
534 pub fn len(&self) -> usize {
535 match self {
536 Stream::F32x2(v) => v.len(),
537 Stream::F32x3(v) => v.len(),
538 Stream::F32x4(v) => v.len(),
539 Stream::U8x4(v) => v.len(),
540 Stream::U16x4(v) => v.len(),
541 }
542 }
543
544 pub fn is_empty(&self) -> bool {
545 self.len() == 0
546 }
547
548 pub fn byte_len(&self) -> u64 {
550 let element = match self {
551 Stream::F32x2(_) => 8,
552 Stream::F32x3(_) => 12,
553 Stream::F32x4(_) => 16,
554 Stream::U8x4(_) => 4,
555 Stream::U16x4(_) => 8,
556 };
557 self.len() as u64 * element
558 }
559}
560
561#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
563pub enum Topology {
564 Triangles,
565 Lines,
566 Points,
567}
568
569#[derive(Clone, Debug, PartialEq)]
572pub struct Primitive {
573 pub topology: Topology,
574 pub streams: BTreeMap<Semantic, Stream>,
576 pub indices: Option<Vec<u32>>,
578 pub material: Option<Entity>,
580}
581
582impl Primitive {
583 pub fn positions(&self) -> Option<&[[f32; 3]]> {
584 match self.streams.get(&Semantic::Position)? {
585 Stream::F32x3(v) => Some(v),
586 _ => None,
587 }
588 }
589
590 pub fn vertex_count(&self) -> usize {
591 self.streams.get(&Semantic::Position).map_or(0, Stream::len)
592 }
593}
594
595pub trait VertexMap {
598 fn point(&self, p: [f32; 3]) -> [f32; 3];
600 fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]>;
602 fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]>;
604}
605
606impl VertexMap for crate::Linear {
607 fn point(&self, p: [f32; 3]) -> [f32; 3] {
608 crate::Linear::point(self, p)
609 }
610 fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
611 crate::Linear::normal(self, n)
612 }
613 fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
614 crate::Linear::tangent(self, t)
615 }
616}
617
618#[derive(Clone, Copy, Debug, PartialEq)]
620pub struct Offset(pub [f32; 3]);
621
622impl VertexMap for Offset {
623 fn point(&self, p: [f32; 3]) -> [f32; 3] {
624 std::array::from_fn(|k| p[k] + self.0[k])
625 }
626 fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
627 Some(n)
628 }
629 fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
630 Some(t)
631 }
632}
633
634pub fn move_streams(streams: &mut BTreeMap<Semantic, Stream>, map: &impl VertexMap) -> Result<(), String> {
638 let mut moved = Vec::new();
639 for (sem, stream) in streams.iter() {
640 let turned = match (sem.moves(), stream) {
641 (Moves::Not, _) => continue,
642 (Moves::Point, Stream::F32x3(v)) => Stream::F32x3(v.iter().map(|x| map.point(*x)).collect()),
643 (Moves::Normal, Stream::F32x3(v)) => Stream::F32x3(
644 v.iter()
645 .map(|x| map.normal(*x))
646 .collect::<Option<_>>()
647 .ok_or_else(|| format!("{sem:?}: the map turns no normal"))?,
648 ),
649 (Moves::Tangent, Stream::F32x4(v)) => Stream::F32x4(
650 v.iter()
651 .map(|x| map.tangent(*x))
652 .collect::<Option<_>>()
653 .ok_or_else(|| format!("{sem:?}: the map turns no tangent"))?,
654 ),
655 (moves, _) => return Err(format!("{sem:?} is not a stream that moves as a {moves:?}")),
656 };
657 moved.push((*sem, turned));
658 }
659 streams.extend(moved);
660 Ok(())
661}
662
663#[derive(Debug)]
681pub struct PrimitiveMut<'a>(&'a mut Primitive);
682
683impl std::ops::Deref for PrimitiveMut<'_> {
684 type Target = Primitive;
685 fn deref(&self) -> &Primitive {
686 self.0
687 }
688}
689
690impl PrimitiveMut<'_> {
691 pub fn topology_mut(&mut self) -> &mut Topology {
692 &mut self.0.topology
693 }
694 pub fn streams_mut(&mut self) -> &mut BTreeMap<Semantic, Stream> {
695 &mut self.0.streams
696 }
697 pub fn indices_mut(&mut self) -> &mut Option<Vec<u32>> {
698 &mut self.0.indices
699 }
700 pub fn material_mut(&mut self) -> &mut Option<Entity> {
701 &mut self.0.material
702 }
703 pub fn fields(&mut self) -> PrimitiveFields<'_> {
705 let Primitive {
706 topology,
707 streams,
708 indices,
709 material,
710 } = self.0;
711 PrimitiveFields {
712 topology,
713 streams,
714 indices,
715 material,
716 }
717 }
718}
719
720#[derive(Debug)]
722pub struct PrimitiveFields<'a> {
723 pub topology: &'a mut Topology,
724 pub streams: &'a mut BTreeMap<Semantic, Stream>,
725 pub indices: &'a mut Option<Vec<u32>>,
726 pub material: &'a mut Option<Entity>,
727}
728
729#[derive(Clone)]
742pub struct Mesh {
743 primitives: Vec<Primitive>,
744 id: u64,
745}
746static NEXT_MESH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
747impl Mesh {
748 pub fn new(primitives: Vec<Primitive>) -> Self {
749 Mesh {
750 primitives,
751 id: NEXT_MESH.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
752 }
753 }
754 pub(crate) fn id(&self) -> u64 {
756 self.id
757 }
758 pub(crate) fn keep_id(&mut self, id: u64) {
760 self.id = id;
761 }
762 pub fn primitives(&self) -> &[Primitive] {
763 &self.primitives
764 }
765 pub fn primitives_mut(&mut self) -> impl ExactSizeIterator<Item = PrimitiveMut<'_>> {
767 self.primitives.iter_mut().map(PrimitiveMut)
768 }
769 pub fn primitive_mut(&mut self, i: usize) -> Option<PrimitiveMut<'_>> {
771 self.primitives.get_mut(i).map(PrimitiveMut)
772 }
773 pub fn into_primitives(self) -> Vec<Primitive> {
774 self.primitives
775 }
776}
777impl Default for Mesh {
778 fn default() -> Self {
779 Mesh::new(Vec::new())
780 }
781}
782impl PartialEq for Mesh {
783 fn eq(&self, other: &Self) -> bool {
784 self.primitives == other.primitives
785 }
786}
787impl std::fmt::Debug for Mesh {
788 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789 f.debug_struct("Mesh")
790 .field("primitives", &self.primitives)
791 .finish()
792 }
793}
794impl Component for Mesh {
795 const NAME: &'static str = "core.mesh";
796 const VERSION: u32 = 2;
798 const GEOMETRY: crate::Geometry = crate::Geometry::None;
799 fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
802 let mut m = self.clone();
803 for p in &mut m.primitives {
804 if move_streams(&mut p.streams, l).is_err() {
806 return crate::world::Linearity::Unsupported;
807 }
808 if l.mirrors() && p.topology == Topology::Triangles {
809 let n = p.vertex_count() as u32;
810 for t in p
811 .indices
812 .get_or_insert_with(|| (0..n).collect())
813 .as_chunks_mut::<3>()
814 .0
815 {
816 t.swap(1, 2);
817 }
818 }
819 }
820 crate::world::Linearity::Applied(m)
821 }
822 fn references(&self, out: &mut Vec<Entity>) {
823 out.extend(self.primitives.iter().filter_map(|p| p.material));
824 }
825 fn payload_bytes(&self) -> u64 {
827 self.primitives
828 .iter()
829 .map(|p| {
830 p.streams.values().map(Stream::byte_len).sum::<u64>()
831 + p.indices.as_ref().map_or(0, |i| i.len() as u64 * 4)
832 })
833 .sum()
834 }
835 fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
840 if !w.has::<crate::Skin>(e)
841 && let Some(i) = self.primitives.iter().position(crate::skin::has_skin_streams)
842 {
843 return Err(format!(
844 "primitive {i}: joint or weight streams without a core.skin"
845 ));
846 }
847 for (i, p) in self.primitives.iter().enumerate() {
848 let n = match p.streams.get(&Semantic::Position) {
849 Some(Stream::F32x3(v)) => v.len(),
850 Some(_) => return Err(format!("primitive {i}: positions not 3 floats")),
851 None => return Err(format!("primitive {i}: no positions")),
852 };
853 if let Some((sem, _)) = p
854 .streams
855 .iter()
856 .find(|(sem, st)| sem.moves() == Moves::Point && !matches!(st, Stream::F32x3(_)))
857 {
858 return Err(format!("primitive {i}: {sem:?} points not 3 floats"));
859 }
860 if let Some((sem, st)) = p.streams.iter().find(|(_, st)| st.len() != n) {
861 return Err(format!(
862 "primitive {i}: {sem:?} has {} values for {n} vertices",
863 st.len()
864 ));
865 }
866 let corners = match &p.indices {
867 Some(idx) => {
868 if let Some(bad) = idx.iter().find(|&&v| usize::try_from(v).map_or(true, |v| v >= n)) {
869 return Err(format!("primitive {i}: index {bad} of {n} vertices"));
870 }
871 idx.len()
872 }
873 None => n,
874 };
875 let per = match p.topology {
876 Topology::Triangles => 3,
877 Topology::Lines => 2,
878 Topology::Points => 1,
879 };
880 if corners % per != 0 {
881 return Err(format!(
882 "primitive {i}: {corners} corners are not whole {:?}",
883 p.topology
884 ));
885 }
886 for (sem, st) in &p.streams {
887 let what = format!("primitive {i} {sem:?}");
888 match st {
889 Stream::F32x2(v) => finite(v.iter().flatten().copied(), &what)?,
890 Stream::F32x3(v) => finite(v.iter().flatten().copied(), &what)?,
891 Stream::F32x4(v) => finite(v.iter().flatten().copied(), &what)?,
892 Stream::U8x4(_) | Stream::U16x4(_) => {}
893 }
894 }
895 }
896 Ok(())
897 }
898 fn to_json(&self) -> Result<Option<Value>, JsonError> {
900 let prims: Vec<Value> = self
901 .primitives
902 .iter()
903 .map(|p| {
904 json!({
905 "topology": p.topology,
906 "vertices": p.vertex_count(),
907 "indices": p.indices.as_ref().map(Vec::len),
908 "streams": p.streams.keys().collect::<Vec<_>>(),
909 "material": p.material,
910 })
911 })
912 .collect();
913 Ok(Some(json!({ "primitives": prims })))
914 }
915}
916
917#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
919pub enum ColorSpace {
920 Srgb,
921 Linear,
922}
923
924#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
926pub enum TexRole {
927 Color,
928 Normal,
929 Mask,
931 Data,
936 Other,
937}
938
939#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
941pub enum NormalGreen {
942 Up,
944 Down,
946}
947
948#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
950pub enum NormalZ {
951 Stored,
953 Rebuild,
955}
956
957#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize)]
959pub enum NormalX {
960 #[default]
962 Red,
963 Alpha,
965}
966
967#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize)]
971pub enum NormalBasis {
972 #[default]
974 TangentBitangent,
975 BitangentTangent,
977}
978
979#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
981pub struct NormalConvention {
982 pub green: NormalGreen,
983 pub z: NormalZ,
984 pub x: NormalX,
985 pub basis: NormalBasis,
986}
987
988impl NormalConvention {
989 pub const GLTF: NormalConvention = NormalConvention {
991 green: NormalGreen::Up,
992 z: NormalZ::Stored,
993 x: NormalX::Red,
994 basis: NormalBasis::TangentBitangent,
995 };
996}
997
998#[derive(Clone, Debug, PartialEq)]
1000pub struct Texture {
1001 pub width: u32,
1002 pub height: u32,
1003 pub format: TexFormat,
1004 pub levels: Vec<Vec<u8>>,
1006 pub color_space: ColorSpace,
1007 pub role: TexRole,
1008 pub normal: Option<NormalConvention>,
1010}
1011impl Component for Texture {
1012 const NAME: &'static str = "core.texture";
1013 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1014 const VERSION: u32 = 3;
1016 fn payload_bytes(&self) -> u64 {
1018 self.levels.iter().map(|l| l.len() as u64).sum()
1019 }
1020 fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
1022 if self.width == 0 || self.height == 0 {
1023 return Err(format!("{} x {} pixels", self.width, self.height));
1024 }
1025 let most = self.width.max(self.height).ilog2() + 1;
1026 if self.levels.is_empty() || self.levels.len() > most as usize {
1027 return Err(format!(
1028 "{} levels for {} x {} (1 to {most})",
1029 self.levels.len(),
1030 self.width,
1031 self.height
1032 ));
1033 }
1034 for (i, level) in self.levels.iter().enumerate() {
1035 let shift = u32::try_from(i).unwrap_or(u32::MAX);
1036 let (w, h) = (
1037 self.width.checked_shr(shift).unwrap_or(0).max(1),
1038 self.height.checked_shr(shift).unwrap_or(0).max(1),
1039 );
1040 match self.format.checked_level_size(w, h) {
1041 Some(size) if size == level.len() => {}
1042 Some(size) => {
1043 return Err(format!(
1044 "level {i} ({w} x {h} {:?}): {} bytes, not {size}",
1045 self.format,
1046 level.len()
1047 ));
1048 }
1049 None => return Err(format!("level {i}: {w} x {h} doesn't fit in memory")),
1050 }
1051 }
1052 Ok(())
1053 }
1054 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1056 Ok(Some(json!({
1057 "width": self.width,
1058 "height": self.height,
1059 "format": format!("{:?}", self.format),
1060 "levels": self.levels.len(),
1061 "color_space": self.color_space,
1062 "role": self.role,
1063 "normal": self.normal,
1064 })))
1065 }
1066}
1067
1068#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1071pub enum LightKind {
1072 Point,
1073 Spot { inner_cone: f32, outer_cone: f32 },
1074 Directional,
1075}
1076
1077#[derive(Clone, Debug, PartialEq, Serialize)]
1085pub struct Light {
1086 pub kind: LightKind,
1087 pub color: [f32; 3],
1088 pub intensity: f32,
1089 pub range: Option<f32>,
1090 pub cast_shadows: Option<bool>,
1092 pub radius: Option<f32>,
1094 #[serde(skip_serializing_if = "Option::is_none")]
1096 pub specular: Option<LightSpecular>,
1097}
1098
1099#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1103pub struct LightSpecular {
1104 pub color: [f32; 3],
1105 pub intensity: f32,
1106}
1107
1108impl Component for Light {
1109 const NAME: &'static str = "core.light";
1110 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1111 const VERSION: u32 = 4;
1113 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1114 crate::json::to_value(self).map(Some)
1115 }
1116 fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
1119 finite(self.color.iter().copied().chain([self.intensity]), "light")?;
1120 if self.color.iter().any(|&c| c < 0.0) || self.intensity < 0.0 {
1121 return Err("a negative colour or intensity".into());
1122 }
1123 if let Some(s) = &self.specular {
1124 finite(s.color.iter().copied().chain([s.intensity]), "light specular")?;
1125 if s.color.iter().any(|&c| c < 0.0) || s.intensity < 0.0 {
1126 return Err("a negative specular colour or intensity".into());
1127 }
1128 }
1129 if self.range.is_some_and(|r| !(r.is_finite() && r > 0.0)) {
1130 return Err("a range that is not positive".into());
1131 }
1132 if self.radius.is_some_and(|r| !(r.is_finite() && r >= 0.0)) {
1133 return Err("a radius that is not finite and non-negative".into());
1134 }
1135 if let LightKind::Spot {
1136 inner_cone,
1137 outer_cone,
1138 } = self.kind
1139 && !(0.0 <= inner_cone && inner_cone <= outer_cone && outer_cone <= std::f32::consts::FRAC_PI_2)
1140 {
1141 return Err(format!(
1142 "spot cones {inner_cone} and {outer_cone} not 0 <= inner <= outer <= pi / 2"
1143 ));
1144 }
1145 Ok(())
1146 }
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Default)]
1157pub struct Collision {
1158 pub positions: Vec<[f32; 3]>,
1159 pub triangles: Vec<[u32; 3]>,
1160 pub surfaces: Vec<u16>,
1161}
1162
1163impl Collision {
1164 pub fn extend(
1166 &mut self,
1167 positions: &[[f32; 3]],
1168 triangles: &[[u32; 3]],
1169 surface: u16,
1170 f: impl Fn([f32; 3]) -> [f32; 3],
1171 ) {
1172 let base = self.positions.len() as u32;
1173 self.positions.extend(positions.iter().map(|&p| f(p)));
1174 self.triangles
1175 .extend(triangles.iter().map(|t| t.map(|i| i + base)));
1176 self.surfaces
1177 .extend(std::iter::repeat_n(surface, triangles.len()));
1178 }
1179}
1180
1181impl Component for Collision {
1182 const NAME: &'static str = "core.collision";
1183 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1184 fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
1186 let mut c = self.clone();
1187 c.positions.iter_mut().for_each(|p| *p = l.point(*p));
1188 if l.mirrors() {
1189 c.triangles.iter_mut().for_each(|t| t.swap(1, 2));
1190 }
1191 crate::world::Linearity::Applied(c)
1192 }
1193 fn payload_bytes(&self) -> u64 {
1195 (self.positions.len() as u64 + self.triangles.len() as u64) * 12 + self.surfaces.len() as u64 * 2
1196 }
1197 fn verify(&self, _e: Entity, w: &crate::World) -> Result<(), String> {
1199 if self.surfaces.len() != self.triangles.len() {
1200 return Err(format!(
1201 "{} surfaces for {} triangles",
1202 self.surfaces.len(),
1203 self.triangles.len()
1204 ));
1205 }
1206 let n = self.positions.len() as u32;
1207 if let Some(t) = self.triangles.iter().find(|t| t.iter().any(|&i| i >= n)) {
1208 return Err(format!("triangle {t:?} past the {n} positions"));
1209 }
1210 finite(self.positions.iter().flatten().copied(), "collision positions")?;
1211 let known = w.resource::<Surfaces>().map_or(0, |s| s.0.len());
1212 if let Some(&s) = self.surfaces.iter().find(|&&s| usize::from(s) >= known) {
1213 return Err(format!("surface {s} past the world's {known} surfaces"));
1214 }
1215 Ok(())
1216 }
1217 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1219 let mut per: BTreeMap<u16, u64> = BTreeMap::new();
1220 for &s in &self.surfaces {
1221 *per.entry(s).or_default() += 1;
1222 }
1223 Ok(Some(json!({
1224 "positions": self.positions.len(),
1225 "triangles": self.triangles.len(),
1226 "surfaces": per,
1227 })))
1228 }
1229}
1230
1231#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1236pub struct Surface {
1237 pub name: String,
1238 #[serde(skip_serializing_if = "Option::is_none")]
1240 pub class: Option<SurfaceClass>,
1241}
1242
1243impl Surface {
1244 pub fn new(name: impl Into<String>) -> Surface {
1246 Surface {
1247 name: name.into(),
1248 class: None,
1249 }
1250 }
1251}
1252
1253#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1261#[serde(rename_all = "snake_case")]
1262pub enum SurfaceClass {
1263 Concrete,
1264 Asphalt,
1265 Stone,
1266 Brick,
1267 Tile,
1268 Metal,
1269 Wood,
1270 Glass,
1271 Plastic,
1272 Rubber,
1273 Fabric,
1274 Grass,
1275 Dirt,
1276 Gravel,
1277 Sand,
1278 Mud,
1279 Snow,
1280 Ice,
1281 Water,
1283 Other,
1284}
1285
1286impl SurfaceClass {
1287 pub const ALL: [SurfaceClass; 20] = [
1288 SurfaceClass::Concrete,
1289 SurfaceClass::Asphalt,
1290 SurfaceClass::Stone,
1291 SurfaceClass::Brick,
1292 SurfaceClass::Tile,
1293 SurfaceClass::Metal,
1294 SurfaceClass::Wood,
1295 SurfaceClass::Glass,
1296 SurfaceClass::Plastic,
1297 SurfaceClass::Rubber,
1298 SurfaceClass::Fabric,
1299 SurfaceClass::Grass,
1300 SurfaceClass::Dirt,
1301 SurfaceClass::Gravel,
1302 SurfaceClass::Sand,
1303 SurfaceClass::Mud,
1304 SurfaceClass::Snow,
1305 SurfaceClass::Ice,
1306 SurfaceClass::Water,
1307 SurfaceClass::Other,
1308 ];
1309
1310 pub fn as_str(self) -> &'static str {
1312 match self {
1313 SurfaceClass::Concrete => "concrete",
1314 SurfaceClass::Asphalt => "asphalt",
1315 SurfaceClass::Stone => "stone",
1316 SurfaceClass::Brick => "brick",
1317 SurfaceClass::Tile => "tile",
1318 SurfaceClass::Metal => "metal",
1319 SurfaceClass::Wood => "wood",
1320 SurfaceClass::Glass => "glass",
1321 SurfaceClass::Plastic => "plastic",
1322 SurfaceClass::Rubber => "rubber",
1323 SurfaceClass::Fabric => "fabric",
1324 SurfaceClass::Grass => "grass",
1325 SurfaceClass::Dirt => "dirt",
1326 SurfaceClass::Gravel => "gravel",
1327 SurfaceClass::Sand => "sand",
1328 SurfaceClass::Mud => "mud",
1329 SurfaceClass::Snow => "snow",
1330 SurfaceClass::Ice => "ice",
1331 SurfaceClass::Water => "water",
1332 SurfaceClass::Other => "other",
1333 }
1334 }
1335
1336 pub fn parse(word: &str) -> Option<SurfaceClass> {
1337 SurfaceClass::ALL.into_iter().find(|c| c.as_str() == word)
1338 }
1339}
1340
1341#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
1343pub struct Surfaces(pub Vec<Surface>);
1344
1345#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1347#[error("surface index {index} exceeds u16")]
1348pub struct SurfaceIndexError {
1349 pub index: usize,
1350}
1351
1352impl Surfaces {
1353 pub fn index(&mut self, name: &str) -> Result<u16, SurfaceIndexError> {
1356 let index = self.0.iter().position(|s| s.name == name).unwrap_or(self.0.len());
1357 let id = u16::try_from(index).map_err(|_| SurfaceIndexError { index })?;
1358 if index == self.0.len() {
1359 self.0.push(Surface::new(name));
1360 }
1361 Ok(id)
1362 }
1363}
1364
1365impl crate::world::Resource for Surfaces {
1366 const NAME: &'static str = "core.surfaces";
1367 const VERSION: u32 = 2;
1369 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1370 crate::json::to_value(self).map(Some)
1371 }
1372}
1373
1374#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1377pub struct TextureFile {
1378 pub path: PathBuf,
1379}
1380impl Component for TextureFile {
1381 const NAME: &'static str = "core.texture_file";
1382 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1383 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1384 crate::json::to_value(self).map(Some)
1385 }
1386}
1387
1388#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1393pub struct TextureAbsent {
1394 pub reason: String,
1396}
1397impl Component for TextureAbsent {
1398 const NAME: &'static str = "core.texture_absent";
1399 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1400 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1401 crate::json::to_value(self).map(Some)
1402 }
1403}
1404
1405#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1409pub struct MeshFile {
1410 pub path: PathBuf,
1411 pub mesh: u32,
1413 pub materials: Vec<Option<Entity>>,
1416}
1417impl Component for MeshFile {
1418 const NAME: &'static str = "core.mesh_file";
1419 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1420 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1422 crate::world::Linearity::Drop
1423 }
1424 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1425 crate::json::to_value(self).map(Some)
1426 }
1427 fn references(&self, out: &mut Vec<Entity>) {
1428 out.extend(self.materials.iter().flatten());
1429 }
1430}
1431
1432#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
1435pub struct MaterialVariants(pub Vec<BTreeMap<u32, Entity>>);
1436impl Component for MaterialVariants {
1437 const NAME: &'static str = "core.material_variants";
1438 const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
1439 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1441 crate::world::Linearity::Invariant
1442 }
1443 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1444 crate::json::to_value(self).map(Some)
1445 }
1446 fn references(&self, out: &mut Vec<Entity>) {
1447 out.extend(self.0.iter().flat_map(|m| m.values().copied()));
1448 }
1449 fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
1450 Some(crate::world::remap_list(&self.0, from).map(MaterialVariants))
1451 }
1452}
1453
1454#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1456pub struct Tile {
1457 pub col: i32,
1458 pub row: i32,
1459}
1460
1461#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
1463#[error(
1464 "no tile at ground point {ground:?} on a grid of {size} metres: the point and the size must be finite, the size \
1465 positive and the tile within i32"
1466)]
1467pub struct TileError {
1468 pub ground: [f32; 2],
1469 pub size: f32,
1470}
1471
1472impl Tile {
1473 pub fn of(g: [f32; 2], size: f32) -> Result<Tile, TileError> {
1478 let bad = TileError { ground: g, size };
1479 if !(size.is_finite() && size > 0.0) {
1480 return Err(bad);
1481 }
1482 let axis = |v: f32| {
1483 let t = (v / size).floor();
1484 (t.is_finite() && t >= i32::MIN as f32 && t < i32::MAX as f32).then_some(t as i32)
1485 };
1486 match (axis(g[0]), axis(g[1])) {
1487 (Some(col), Some(row)) => Ok(Tile { col, row }),
1488 _ => Err(bad),
1489 }
1490 }
1491}
1492
1493impl Component for Tile {
1494 const NAME: &'static str = "core.tile";
1495 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1496 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1498 crate::world::Linearity::Invariant
1499 }
1500 fn to_json(&self) -> Result<Option<Value>, JsonError> {
1501 crate::json::to_value(self).map(Some)
1502 }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507 use super::*;
1508
1509 #[test]
1511 fn a_tile_of_a_point_that_has_none_is_an_error() {
1512 assert_eq!(Tile::of([-0.5, 399.9], 200.0), Ok(Tile { col: -1, row: 1 }));
1513 for (g, size) in [
1514 ([f32::NAN, 0.0], 200.0),
1515 ([0.0, f32::INFINITY], 200.0),
1516 ([1.0, 1.0], 0.0),
1517 ([1.0, 1.0], -5.0),
1518 ([1.0, 1.0], f32::NAN),
1519 ([1e30, 0.0], 200.0),
1520 ] {
1521 let e = Tile::of(g, size).unwrap_err();
1522 assert_eq!(
1523 (e.ground.map(f32::is_nan), e.size.is_nan()),
1524 (g.map(f32::is_nan), size.is_nan())
1525 );
1526 }
1527 }
1528
1529 #[test]
1530 fn payload_bytes_count_streams_indices_levels_and_soups() {
1531 let streams = BTreeMap::from([
1533 (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
1534 (Semantic::Normal, Stream::F32x3(vec![[0.0, 0.0, 1.0]; 3])),
1535 (Semantic::TexCoord(0), Stream::F32x2(vec![[0.0; 2]; 3])),
1536 ]);
1537 let mesh = Mesh::new(vec![Primitive {
1538 topology: Topology::Triangles,
1539 streams,
1540 indices: Some(vec![0, 1, 2]),
1541 material: None,
1542 }]);
1543 assert_eq!(mesh.payload_bytes(), 108);
1544 let texture = Texture {
1546 width: 4,
1547 height: 4,
1548 format: TexFormat::Rgba8Unorm,
1549 levels: vec![vec![0; 64], vec![0; 16], vec![0; 4]],
1550 color_space: ColorSpace::Srgb,
1551 role: TexRole::Color,
1552 normal: None,
1553 };
1554 assert_eq!(texture.payload_bytes(), 84);
1555 let soup = Collision {
1557 positions: vec![[0.0; 3]; 3],
1558 triangles: vec![[0, 1, 2]],
1559 surfaces: vec![0],
1560 };
1561 assert_eq!(soup.payload_bytes(), 50);
1562 let mut w = crate::World::new();
1563 let (a, b) = (w.spawn(), w.spawn());
1564 w.insert(a, mesh.clone());
1565 w.insert(b, mesh);
1566 w.insert(a, texture);
1567 w.insert(a, Name("small".into()));
1568 w.insert_opaque(
1569 b,
1570 "game.blob",
1571 crate::Opaque {
1572 version: 1,
1573 data: crate::OpaqueData::Bytes(vec![0; 7]),
1574 },
1575 )
1576 .unwrap();
1577 assert_eq!(
1578 w.payload_bytes(),
1579 BTreeMap::from([("core.mesh", 216), ("core.texture", 84), ("game.blob", 7)])
1580 );
1581 }
1582
1583 fn t(cols: [[f32; 3]; 4]) -> Transform {
1584 Transform { cols }
1585 }
1586
1587 #[test]
1588 fn surface_indices_check_the_u16_boundary_without_mutating_on_error() {
1589 let mut surfaces = Surfaces((0..u16::MAX).map(|i| Surface::new(i.to_string())).collect());
1590 assert_eq!(surfaces.index("last"), Ok(u16::MAX));
1591 assert_eq!(surfaces.index("0"), Ok(0));
1592 assert_eq!(surfaces.index("last"), Ok(u16::MAX));
1593 let before = surfaces.clone();
1594 assert_eq!(
1595 surfaces.index("overflow"),
1596 Err(SurfaceIndexError { index: 65536 })
1597 );
1598 assert_eq!(surfaces, before);
1599 surfaces.0.push(Surface::new("outside"));
1601 assert_eq!(surfaces.index("outside"), Err(SurfaceIndexError { index: 65536 }));
1602 assert_eq!(surfaces.index("0"), Ok(0));
1603 }
1604
1605 #[test]
1606 fn surface_classes_are_lower_case_words_shown_only_when_set() {
1607 for c in SurfaceClass::ALL {
1608 assert_eq!(SurfaceClass::parse(c.as_str()), Some(c));
1609 assert_eq!(serde_json::to_value(c).unwrap(), json!(c.as_str()));
1610 }
1611 assert_eq!(SurfaceClass::parse("Concrete"), None);
1612 let mut surfaces = Surfaces::default();
1613 surfaces.index("Snow").unwrap();
1614 surfaces.0.push(Surface {
1615 name: "CONCRETE_POTHOLE".into(),
1616 class: Some(SurfaceClass::Concrete),
1617 });
1618 assert_eq!(
1619 crate::world::Resource::to_json(&surfaces).unwrap(),
1620 Some(json!([{"name": "Snow"}, {"name": "CONCRETE_POTHOLE", "class": "concrete"}]))
1621 );
1622 }
1623
1624 #[test]
1625 fn json_views() {
1626 let mut w = crate::World::new();
1627 let mat = w.spawn();
1628 let p = w.spawn();
1629 w.insert(
1630 p,
1631 MeshFile {
1632 path: "prototypes/a.glb".into(),
1633 mesh: 1,
1634 materials: vec![Some(mat), None],
1635 },
1636 );
1637 w.insert(
1638 p,
1639 MaterialVariants(vec![BTreeMap::new(), BTreeMap::from([(3, mat)])]),
1640 );
1641 w.insert(p, Name("a".into()));
1642 w.insert(
1643 p,
1644 Mesh::new(vec![Primitive {
1645 topology: Topology::Triangles,
1646 streams: BTreeMap::from([(Semantic::Position, Stream::F32x3(vec![[0.; 3]; 3]))]),
1647 indices: None,
1648 material: Some(mat),
1649 }]),
1650 );
1651 let got: Vec<(&str, Option<Value>)> = w
1652 .components_json(p)
1653 .unwrap()
1654 .into_iter()
1655 .map(|c| (c.name, c.value))
1656 .collect();
1657 let names: Vec<&str> = got.iter().map(|(n, _)| *n).collect();
1658 assert_eq!(
1659 names,
1660 [
1661 "core.material_variants",
1662 "core.mesh",
1663 "core.mesh_file",
1664 "core.name"
1665 ]
1666 );
1667 assert_eq!(got[0].1, Some(json!([{}, {"3": 0}])));
1668 assert_eq!(
1669 got[1].1,
1670 Some(
1671 json!({"primitives": [{"topology": "Triangles", "vertices": 3, "indices": null,
1672 "streams": ["Position"], "material": 0}]})
1673 )
1674 );
1675 assert_eq!(
1676 got[2].1,
1677 Some(json!({"path": "prototypes/a.glb", "mesh": 1, "materials": [0, null]}))
1678 );
1679 assert_eq!(got[3].1, Some(json!("a")));
1680 }
1681
1682 #[test]
1685 fn the_verifier_refuses_structurally_broken_data() {
1686 let tri = |streams: BTreeMap<Semantic, Stream>, indices: Option<Vec<u32>>, topology| Primitive {
1687 topology,
1688 streams,
1689 indices,
1690 material: None,
1691 };
1692 let pos = |n: usize| (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; n]));
1693 let mesh_err = |p: Primitive| {
1694 let mut w = crate::World::new();
1695 let e = w.spawn();
1696 w.insert(e, Mesh::new(vec![p]));
1697 w.verify_components()
1698 .into_iter()
1699 .map(|(_, _, m)| m)
1700 .collect::<Vec<_>>()
1701 };
1702 assert!(
1703 mesh_err(tri(
1704 BTreeMap::from([pos(3)]),
1705 Some(vec![0, 1, 2]),
1706 Topology::Triangles
1707 ))
1708 .is_empty()
1709 );
1710 assert!(mesh_err(tri(BTreeMap::from([pos(4)]), Some(vec![0, 1]), Topology::Lines)).is_empty());
1711 for (p, words) in [
1712 (tri(BTreeMap::new(), None, Topology::Triangles), "no positions"),
1713 (
1714 tri(
1715 BTreeMap::from([(Semantic::Position, Stream::F32x2(vec![[0.0; 2]; 3]))]),
1716 None,
1717 Topology::Triangles,
1718 ),
1719 "not 3 floats",
1720 ),
1721 (
1722 tri(
1723 BTreeMap::from([pos(3), (Semantic::Normal, Stream::F32x3(vec![[0.0; 3]; 2]))]),
1724 None,
1725 Topology::Triangles,
1726 ),
1727 "2 values for 3 vertices",
1728 ),
1729 (
1730 tri(BTreeMap::from([pos(3)]), Some(vec![0, 1, 3]), Topology::Triangles),
1731 "index 3 of 3",
1732 ),
1733 (
1734 tri(
1735 BTreeMap::from([pos(3)]),
1736 Some(vec![0, 1, 2, 0]),
1737 Topology::Triangles,
1738 ),
1739 "4 corners",
1740 ),
1741 (tri(BTreeMap::from([pos(3)]), None, Topology::Lines), "3 corners"),
1742 ] {
1743 let got = mesh_err(p);
1744 assert!(got.len() == 1 && got[0].contains(words), "{words}: {got:?}");
1745 }
1746 let tex = |width, height, levels: Vec<usize>| {
1747 let mut w = crate::World::new();
1748 let e = w.spawn();
1749 w.insert(
1750 e,
1751 Texture {
1752 width,
1753 height,
1754 format: TexFormat::Rgba8Unorm,
1755 levels: levels.into_iter().map(|n| vec![0; n]).collect(),
1756 color_space: ColorSpace::Srgb,
1757 role: TexRole::Color,
1758 normal: None,
1759 },
1760 );
1761 w.verify_components()
1762 .into_iter()
1763 .map(|(_, _, m)| m)
1764 .collect::<Vec<_>>()
1765 };
1766 assert!(tex(4, 2, vec![32, 8, 4]).is_empty());
1767 assert!(tex(0, 2, vec![0])[0].contains("0 x 2"));
1768 assert!(tex(4, 4, vec![])[0].contains("0 levels"));
1769 assert!(tex(4, 4, vec![64, 16, 4, 4])[0].contains("4 levels"));
1770 assert!(tex(4, 4, vec![64, 15])[0].contains("level 1"));
1771 assert!(tex(u32::MAX, u32::MAX, vec![0])[0].contains("doesn't fit"));
1772 let mut w = crate::World::new();
1774 let (file, collision) = (w.spawn(), w.spawn());
1775 w.insert(
1776 file,
1777 MeshFile {
1778 path: "m.glb".into(),
1779 mesh: 0,
1780 materials: vec![None],
1781 },
1782 );
1783 w.insert(collision, Name("collision only".into()));
1784 let placed = w.spawn();
1785 w.insert(
1786 placed,
1787 Instance {
1788 prototype: file,
1789 variant: 0,
1790 },
1791 );
1792 let bare = w.spawn();
1793 w.insert(
1794 bare,
1795 Instance {
1796 prototype: collision,
1797 variant: 0,
1798 },
1799 );
1800 w.insert(placed, Transform::IDENTITY);
1801 let tf = w.spawn();
1802 w.insert(tf, TextureFile { path: "t.png".into() });
1803 assert_eq!(
1804 w.verify_components(),
1805 [(
1806 "core.instance",
1807 bare,
1808 "a placement without a core.transform".to_owned()
1809 )]
1810 );
1811 }
1812
1813 #[test]
1814 fn the_verifier_refuses_non_finite_geometry() {
1815 let mut w = crate::World::new();
1816 let e = w.spawn();
1817 w.insert(e, Transform::IDENTITY);
1818 let mut m = Transform::IDENTITY;
1819 m.cols[3][2] = f32::NAN;
1820 let e2 = w.spawn();
1821 w.insert(e2, m);
1822 w.insert(
1823 e2,
1824 Mesh::new(vec![Primitive {
1825 topology: Topology::Triangles,
1826 streams: BTreeMap::from([
1827 (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
1828 (
1829 Semantic::TexCoord(0),
1830 Stream::F32x2(vec![[0.0, f32::NEG_INFINITY], [0.0; 2], [0.0; 2]]),
1831 ),
1832 ]),
1833 indices: None,
1834 material: None,
1835 }]),
1836 );
1837 let got: Vec<(&str, Entity, String)> = w.verify_components();
1838 assert_eq!(
1839 got,
1840 [
1841 (
1842 "core.mesh",
1843 e2,
1844 "primitive 0 TexCoord(0): value 1 is -inf, not finite".to_owned()
1845 ),
1846 (
1847 "core.transform",
1848 e2,
1849 "transform: value 11 is NaN, not finite".to_owned()
1850 ),
1851 ]
1852 );
1853 assert!(
1855 w.components_json(e2)
1856 .unwrap_err()
1857 .to_string()
1858 .contains("core.transform on e1")
1859 );
1860 }
1861
1862 #[test]
1863 fn classify_follows_the_scene_interchange_rule() {
1864 assert_eq!(Transform::IDENTITY.classify(), TransformKind::Regular);
1865 assert_eq!(
1866 t([[2., 0., 0.], [0., 0., 3.], [0., -1., 0.], [5., 5., 5.]]).classify(),
1867 TransformKind::Regular
1868 );
1869 assert_eq!(
1870 t([[-1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0.; 3]]).classify(),
1871 TransformKind::Mirrored
1872 );
1873 assert_eq!(
1874 t([[1., 0., 0.], [0.1, 1., 0.], [0., 0., 1.], [0.; 3]]).classify(),
1875 TransformKind::Sheared
1876 );
1877 assert_eq!(
1878 t([[1., 0., 0.], [0., 1., 0.], [0., 0., 0.], [0.; 3]]).classify(),
1879 TransformKind::Degenerate
1880 );
1881 }
1882 #[test]
1885 fn a_four_by_four_is_affine_by_its_last_row() {
1886 let m = [
1887 [1., 0., 0., 0.],
1888 [0., 2., 0., 0.],
1889 [0., 0., 3., 0.],
1890 [4., 5., 6., 1.],
1891 ];
1892 let t = Transform::from_columns4(m, 0.0).unwrap();
1893 assert_eq!(t.cols, [[1., 0., 0.], [0., 2., 0.], [0., 0., 3.], [4., 5., 6.]]);
1894 let mut noisy = m;
1895 noisy[0][3] = 5e-5;
1896 assert!(Transform::from_columns4(noisy, 0.0).is_err());
1897 assert!(Transform::from_columns4(noisy, 1e-4).is_ok());
1898 let mut projective = m;
1899 projective[3][3] = 2.0;
1900 let e = Transform::from_columns4(projective, 1e-4).unwrap_err();
1901 assert!(e.contains("not affine"), "{e}");
1902 let mut nan = m;
1903 nan[2][3] = f32::NAN;
1904 assert!(Transform::from_columns4(nan, f32::INFINITY).is_err());
1905 }
1906
1907 #[test]
1910 fn composition_rounds_each_entry_once() {
1911 let outer = Transform {
1912 cols: [[1., 0., 0.], [1., 0., -1.], [1., 1., 0.], [10., 20., 30.]],
1913 };
1914 let tiny = 2f32.powi(-24);
1915 let inner = Transform {
1916 cols: [[1., tiny, tiny], [0., 1., 0.], [0., 0., 2.], [1., 2., 3.]],
1917 };
1918 let c = outer.compose(&inner);
1919 assert_eq!(c.cols[0], [1. + 2. * tiny, tiny, -tiny]);
1920 assert_eq!(c.cols[1], [1., 0., -1.]);
1921 assert_eq!(c.cols[2], [2., 2., 0.]);
1922 assert_eq!(c.cols[3], [16., 23., 28.]);
1923 let p = [0., 1., 1.];
1925 assert_eq!(c.apply(p), [19., 25., 27.]);
1926 assert_eq!(c.apply(p), outer.apply(inner.apply(p)));
1927 assert_eq!(Transform::IDENTITY.compose(&inner), inner);
1928 assert_eq!(inner.compose(&Transform::IDENTITY), inner);
1929 }
1930
1931 #[test]
1936 fn a_singular_map_turns_no_normal_or_tangent() {
1937 let near_flat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 1e-7]];
1938 for cols in [
1939 [[0.0; 3]; 3],
1940 [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
1941 near_flat,
1942 ] {
1943 let l = Linear::new(cols);
1944 let t = Transform {
1945 cols: [cols[0], cols[1], cols[2], [0.0; 3]],
1946 };
1947 assert!(l.singular(), "{cols:?}");
1948 assert_eq!(t.classify(), TransformKind::Degenerate, "{cols:?}");
1949 assert_eq!(l.normal([0.0, 0.0, 1.0]), None);
1950 assert_eq!(l.tangent([1.0, 0.0, 0.0, 1.0]), None);
1951 assert_eq!(l.inverse_columns(), None);
1952 }
1953 let flat = Linear::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]]);
1954 let mut streams = BTreeMap::new();
1955 streams.insert(Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3]));
1956 streams.insert(Semantic::Normal, Stream::F32x3(vec![[0.0, 0.0, 1.0]; 3]));
1957 let mesh = Mesh::new(vec![Primitive {
1958 topology: Topology::Triangles,
1959 streams,
1960 indices: None,
1961 material: None,
1962 }]);
1963 assert!(matches!(
1964 mesh.apply_linear(&flat),
1965 crate::world::Linearity::Unsupported
1966 ));
1967 let s = 0.00937f32;
1970 let small = Linear::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s]]);
1971 assert!(!small.singular());
1972 assert_eq!(small.normal([0.0, 1.0, 0.0]), Some([0.0, 1.0, 0.0]));
1973 assert_eq!(small.tangent([1.0, 0.0, 0.0, 1.0]), Some([1.0, 0.0, 0.0, 1.0]));
1974 assert!(small.inverse_columns().is_some());
1975 let squashed = Linear::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s * 1e-7]]);
1976 assert!(squashed.singular());
1977 let l = Linear::of_f64([[-2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]);
1979 assert!(!l.singular() && l.mirrors());
1980 assert_eq!(l.normal([1.0, 0.0, 0.0]), Some([-1.0, 0.0, 0.0]));
1981 assert_eq!(l.tangent([0.0, 3.0, 0.0, 1.0]), Some([0.0, 1.0, 0.0, -1.0]));
1982 }
1983
1984 #[test]
1987 fn a_point_stream_moves_as_the_positions_do() {
1988 let corners = vec![[1.0, 2.0, 3.0], [-4.0, 5.0, 0.5], [0.0, 0.0, -2.0]];
1989 let pivots = vec![[0.5, 1.0, 1.0]; 3];
1990 let primitive = |pivot: Stream| Primitive {
1991 topology: Topology::Triangles,
1992 streams: BTreeMap::from([
1993 (Semantic::Position, Stream::F32x3(corners.clone())),
1994 (Semantic::Point(3), pivot),
1995 (Semantic::TexCoord(0), Stream::F32x2(vec![[0.25, 0.75]; 3])),
1996 ]),
1997 indices: None,
1998 material: None,
1999 };
2000 let mesh = Mesh::new(vec![primitive(Stream::F32x3(pivots.clone()))]);
2001 let l = Linear::of_f64([[-2.0, 0.0, 0.0], [0.0, 0.0, 3.0], [0.0, -1.0, 0.0]]);
2002 assert!(l.mirrors());
2003 let crate::world::Linearity::Applied(moved) = mesh.apply_linear(&l) else {
2004 panic!("a mesh with a point stream follows an invertible map");
2005 };
2006 let p = &moved.primitives()[0];
2007 let points = |s: Semantic| match &p.streams[&s] {
2008 Stream::F32x3(v) => v.clone(),
2009 other => panic!("{other:?}"),
2010 };
2011 assert_eq!(
2012 points(Semantic::Position),
2013 corners.iter().map(|c| l.point(*c)).collect::<Vec<_>>()
2014 );
2015 assert_eq!(
2016 points(Semantic::Point(3)),
2017 pivots.iter().map(|c| l.point(*c)).collect::<Vec<_>>()
2018 );
2019 assert_eq!(
2020 p.streams[&Semantic::TexCoord(0)],
2021 Stream::F32x2(vec![[0.25, 0.75]; 3])
2022 );
2023 assert_eq!(p.indices, Some(vec![0, 2, 1]), "a mirror reverses the triangles");
2024 let wrong = Mesh::new(vec![primitive(Stream::F32x4(vec![[0.0; 4]; 3]))]);
2025 assert!(matches!(
2026 wrong.apply_linear(&l),
2027 crate::world::Linearity::Unsupported
2028 ));
2029 let mut w = crate::World::new();
2030 let e = w.spawn();
2031 let err = wrong.verify(e, &w).unwrap_err();
2032 assert!(err.contains("Point(3) points not 3 floats"), "{err}");
2033 assert_eq!(Semantic::Point(3).moves(), Moves::Point);
2034 assert_eq!(
2035 crate::shader::Attribute::from(Semantic::Point(3)),
2036 crate::shader::Attribute::Point(3)
2037 );
2038 }
2039
2040 #[test]
2041 fn normal_basis_has_explicit_json_and_default() {
2042 assert_eq!(NormalBasis::default(), NormalConvention::GLTF.basis);
2043 let convention = NormalConvention {
2044 basis: NormalBasis::BitangentTangent,
2045 ..NormalConvention::GLTF
2046 };
2047 let value = crate::json::to_value(&convention).expect("normal convention");
2048 assert_eq!(value["basis"], "BitangentTangent");
2049 }
2050
2051 #[test]
2052 fn collision_soups_and_their_surfaces() {
2053 let mut w = crate::World::new();
2054 let mut surfaces = Surfaces::default();
2055 let (snow, ice) = (surfaces.index("Snow").unwrap(), surfaces.index("Ice").unwrap());
2056 assert_eq!((snow, ice, surfaces.index("Snow").unwrap()), (0, 1, 0));
2057 w.insert_resource(surfaces);
2058 let mut c = Collision::default();
2059 let tri = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]];
2060 c.extend(&tri, &[[0, 1, 2]], snow, |p| p);
2061 c.extend(&tri, &[[0, 1, 2]], ice, |p| [p[0] + 2.0, p[1], p[2]]);
2062 assert_eq!(c.triangles, [[0, 1, 2], [3, 4, 5]]);
2063 assert_eq!(c.surfaces, [0, 1]);
2064 assert_eq!(c.positions[3], [2.0, 0.0, 0.0]);
2065 let e = w.spawn();
2066 assert_eq!(c.verify(e, &w), Ok(()));
2067 let mut bad = c.clone();
2068 bad.surfaces.pop();
2069 assert!(bad.verify(e, &w).is_err(), "a surface per triangle");
2070 let mut bad = c.clone();
2071 bad.triangles[1][2] = 6;
2072 assert!(bad.verify(e, &w).is_err(), "indices in range");
2073 let mut bad = c.clone();
2074 bad.surfaces[0] = 2;
2075 assert!(bad.verify(e, &w).is_err(), "surfaces the world knows");
2076 let mut bad = c.clone();
2077 bad.positions[0][1] = f32::NAN;
2078 assert!(bad.verify(e, &w).is_err(), "finite positions");
2079 assert_eq!(
2080 c.to_json().unwrap().unwrap(),
2081 json!({"positions": 6, "triangles": 2, "surfaces": {"0": 1, "1": 1}})
2082 );
2083 }
2084
2085 #[test]
2086 fn shadows_belong_to_drawn_entities_and_keep_stated_values() {
2087 let mut w = crate::World::new();
2088 let bare = w.spawn();
2089 let proto = w.spawn();
2090 w.insert(proto, Mesh::default());
2091 let place = w.spawn();
2092 w.insert(
2093 place,
2094 Instance {
2095 prototype: proto,
2096 variant: 0,
2097 },
2098 );
2099 let s = Shadows {
2100 cast: Some(ShadowCast::Off),
2101 receive: Some(false),
2102 };
2103 assert_eq!(s.verify(place, &w), Ok(()));
2104 assert_eq!(s.verify(proto, &w), Ok(()), "an entity drawing its own mesh");
2105 assert!(s.verify(bare, &w).is_err());
2106 assert_eq!(
2107 s.to_json().unwrap(),
2108 Some(serde_json::json!({"cast": "Off", "receive": false})),
2109 "stated off and false are kept"
2110 );
2111 assert_eq!(
2112 Shadows::default().to_json().unwrap(),
2113 Some(serde_json::json!({"cast": null, "receive": null}))
2114 );
2115 }
2116 #[test]
2117 fn lights_keep_to_khr_lights_punctual() {
2118 let mut w = crate::World::new();
2119 let e = w.spawn();
2120 let spot = |inner: f32, outer: f32| Light {
2121 kind: LightKind::Spot {
2122 inner_cone: inner,
2123 outer_cone: outer,
2124 },
2125 color: [1.0, 0.5, 0.25],
2126 intensity: 10.0,
2127 range: Some(5.0),
2128 cast_shadows: None,
2129 radius: None,
2130 specular: None,
2131 };
2132 assert_eq!(spot(0.2, 0.6).verify(e, &w), Ok(()));
2133 let sized = |r: f32| Light {
2134 radius: Some(r),
2135 ..spot(0.2, 0.6)
2136 };
2137 assert_eq!(sized(0.0).verify(e, &w), Ok(()), "a radius of 0 is a point");
2138 assert_eq!(sized(0.25).verify(e, &w), Ok(()));
2139 assert!(sized(-0.1).verify(e, &w).is_err(), "a negative radius");
2140 assert!(
2141 sized(f32::NAN).verify(e, &w).is_err(),
2142 "a radius that is not finite"
2143 );
2144 assert!(spot(0.7, 0.6).verify(e, &w).is_err(), "inner past outer");
2145 assert!(spot(0.2, 2.0).verify(e, &w).is_err(), "outer past pi / 2");
2146 let mut l = spot(0.2, 0.6);
2147 l.range = Some(0.0);
2148 assert!(l.verify(e, &w).is_err(), "a range that is not positive");
2149 l.range = None;
2150 l.intensity = -1.0;
2151 assert!(l.verify(e, &w).is_err(), "a negative intensity");
2152 l.intensity = f32::NAN;
2153 assert!(l.verify(e, &w).is_err(), "non-finite");
2154 assert_eq!(
2155 spot(0.25, 0.5).to_json().unwrap().unwrap()["kind"],
2156 serde_json::json!({"Spot": {"inner_cone": 0.25, "outer_cone": 0.5}})
2157 );
2158 }
2159
2160 #[test]
2161 fn closures_follow_declared_references_and_subsets_keep_ids() {
2162 use crate::pbr::{PbrMaterial, TextureRef};
2163 use crate::{Opaque, OpaqueData};
2164 let mut w = crate::World::new();
2165 let texture = w.spawn();
2166 w.insert(texture, Name("albedo".into()));
2167 let other_texture = w.spawn();
2168 let material = w.spawn();
2169 w.insert(
2170 material,
2171 PbrMaterial {
2172 base_color_texture: Some(TextureRef::new(texture)),
2173 ..PbrMaterial::default()
2174 },
2175 );
2176 let variant = w.spawn();
2177 w.insert(
2178 variant,
2179 PbrMaterial {
2180 emissive_texture: Some(TextureRef::new(other_texture)),
2181 ..PbrMaterial::default()
2182 },
2183 );
2184 let prototype = w.spawn();
2185 w.insert(
2186 prototype,
2187 MeshFile {
2188 path: "a.glb".into(),
2189 mesh: 0,
2190 materials: vec![Some(material)],
2191 },
2192 );
2193 w.insert(prototype, MaterialVariants(vec![BTreeMap::from([(1, variant)])]));
2194 let placement = w.spawn();
2195 w.insert(
2196 placement,
2197 Instance {
2198 prototype,
2199 variant: 1,
2200 },
2201 );
2202 w.insert(placement, Transform::IDENTITY);
2203 let unrelated = w.spawn();
2205 w.insert(unrelated, Name("elsewhere".into()));
2206 w.insert_opaque(
2207 placement,
2208 "game.link",
2209 Opaque {
2210 version: 1,
2211 data: OpaqueData::Json(json!(unrelated.index())),
2212 },
2213 )
2214 .unwrap();
2215 w.insert_resource(crate::Surfaces::default());
2216 let closure = w.closure([placement]);
2217 assert_eq!(
2218 closure,
2219 [texture, other_texture, material, variant, prototype, placement].into()
2220 );
2221 let mut sub = w.subset(&closure);
2222 assert!(!sub.is_alive(unrelated) && sub.get::<Name>(unrelated).is_none());
2223 assert_eq!(sub.get::<Name>(texture), w.get::<Name>(texture));
2224 assert_eq!(sub.get::<Instance>(placement), w.get::<Instance>(placement));
2225 assert!(
2226 sub.opaque_components()
2227 .any(|(n, c)| n == "game.link" && c.contains_key(&placement))
2228 );
2229 assert!(sub.resource::<crate::Surfaces>().is_some());
2230 assert_eq!(sub.verify_components(), Vec::new());
2231 assert!(sub.spawn().index() > unrelated.index());
2233 let mut kept = w.clone();
2235 kept.retain(&closure);
2236 assert_eq!(format!("{kept:?}"), format!("{:?}", w.subset(&closure)));
2237 assert!(!kept.is_alive(unrelated));
2238 }
2239
2240 #[test]
2241 fn a_split_off_part_moves_out_and_back() {
2242 let mut w = crate::World::new();
2243 let texture = w.spawn();
2244 w.insert(texture, Name("albedo".into()));
2245 let prototype = w.spawn();
2246 w.insert(prototype, Name("rock".into()));
2247 let placement = w.spawn();
2248 w.insert(
2249 placement,
2250 Instance {
2251 prototype,
2252 variant: 0,
2253 },
2254 );
2255 w.insert(placement, Transform::IDENTITY);
2256 let other = w.spawn();
2257 w.insert(other, Name("elsewhere".into()));
2258 let before = format!("{w:?}");
2259 let next = w.clone().spawn();
2260 let keep = w.closure([placement]);
2261 let mut part = w.split_off(&keep);
2262 assert!(!w.is_alive(placement) && w.get::<Name>(prototype).is_none());
2263 assert_eq!(w.get::<Name>(other), Some(&Name("elsewhere".into())));
2264 assert_eq!(
2265 part.get::<Instance>(placement).map(|i| i.prototype),
2266 Some(prototype)
2267 );
2268 assert!(!part.is_alive(other));
2269 let spawned = part.spawn();
2271 assert_eq!(spawned, next);
2272 part.insert(spawned, Name("chunk".into()));
2273 part.despawn(spawned);
2274 w.absorb(part).unwrap();
2275 assert_eq!(format!("{w:?}"), before);
2276 assert_eq!(w.get::<Name>(texture), Some(&Name("albedo".into())));
2277 assert_eq!(w.spawn(), next, "the next id is unchanged");
2278 }
2279}