differt-core 0.10.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {differt_core-0.10.0 → differt_core-0.12.0}/Cargo.lock +2 -1
- {differt_core-0.10.0 → differt_core-0.12.0}/PKG-INFO +1 -1
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/Cargo.toml +2 -1
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/src/geometry/graph.rs +1 -3
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/src/geometry/mesh.rs +94 -25
- differt_core-0.12.0/differt-core/src/geometry/scene.rs +361 -0
- {differt_core-0.10.0 → differt_core-0.12.0/differt-core}/src/geometry/sionna.rs +128 -9
- differt_core-0.12.0/differt-core/tests/geometry/test_core_scene.py +93 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/tests/geometry/test_graph.py +2 -29
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/_differt_core/__init__.pyi +0 -2
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/_differt_core/geometry/graph.pyi +0 -1
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/_differt_core/geometry/mesh.pyi +4 -3
- differt_core-0.12.0/python/differt_core/_differt_core/geometry/scene.pyi +9 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/_differt_core/geometry/sionna.pyi +3 -1
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/geometry/__init__.py +2 -4
- differt_core-0.12.0/python/differt_core/geometry/_mesh.py +6 -0
- differt_core-0.12.0/python/differt_core/geometry/_scene.py +5 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/src/geometry/graph.rs +1 -3
- {differt_core-0.10.0 → differt_core-0.12.0}/src/geometry/mesh.rs +94 -25
- differt_core-0.12.0/src/geometry/scene.rs +361 -0
- {differt_core-0.10.0/differt-core → differt_core-0.12.0}/src/geometry/sionna.rs +128 -9
- differt_core-0.10.0/differt-core/src/geometry/scene.rs +0 -83
- differt_core-0.10.0/differt-core/tests/geometry/test_core_mesh.py +0 -11
- differt_core-0.10.0/differt-core/tests/geometry/test_core_scene.py +0 -11
- differt_core-0.10.0/python/differt_core/_differt_core/geometry/scene.pyi +0 -7
- differt_core-0.10.0/python/differt_core/geometry/_mesh.py +0 -28
- differt_core-0.10.0/python/differt_core/geometry/_scene.py +0 -27
- differt_core-0.10.0/src/geometry/scene.rs +0 -83
- {differt_core-0.10.0 → differt_core-0.12.0}/Cargo.toml +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/LICENSE.md +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/README.md +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/LICENSE.md +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/README.md +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/benches/bench_main.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/benches/benchmarks/graph_iterators.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/benches/benchmarks/mod.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/src/geometry/mod.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/src/lib.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/tests/__init__.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/tests/geometry/__init__.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/differt-core/tests/test_version.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/pyproject.toml +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/__init__.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/geometry/_graph.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/geometry/_sionna.py +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/python/differt_core/py.typed +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/src/geometry/mod.rs +0 -0
- {differt_core-0.10.0 → differt_core-0.12.0}/src/lib.rs +0 -0
|
@@ -247,7 +247,7 @@ checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
|
|
247
247
|
|
|
248
248
|
[[package]]
|
|
249
249
|
name = "differt-core"
|
|
250
|
-
version = "0.
|
|
250
|
+
version = "0.12.0"
|
|
251
251
|
dependencies = [
|
|
252
252
|
"criterion",
|
|
253
253
|
"indexmap",
|
|
@@ -262,6 +262,7 @@ dependencies = [
|
|
|
262
262
|
"quick-xml",
|
|
263
263
|
"rstest",
|
|
264
264
|
"serde",
|
|
265
|
+
"tempfile",
|
|
265
266
|
"testing_logger",
|
|
266
267
|
]
|
|
267
268
|
|
|
@@ -19,6 +19,7 @@ serde = {version = "1.0", features = ["derive"]}
|
|
|
19
19
|
criterion = "0.5.1"
|
|
20
20
|
pyo3 = {version = "0.25", features = ["auto-initialize"]}
|
|
21
21
|
rstest = "0.18.2"
|
|
22
|
+
tempfile = "3.20"
|
|
22
23
|
testing_logger = "0.1.1"
|
|
23
24
|
|
|
24
25
|
[features]
|
|
@@ -37,5 +38,5 @@ unexpected_cfgs = {level = "warn", check-cfg = ['cfg(tarpaulin_include)']}
|
|
|
37
38
|
edition = "2021"
|
|
38
39
|
name = "differt-core"
|
|
39
40
|
rust-version = "1.78.0"
|
|
40
|
-
version = "0.
|
|
41
|
+
version = "0.12.0"
|
|
41
42
|
readme = "README.md"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#![allow(unknown_lints, non_local_definitions)]
|
|
2
|
-
|
|
3
1
|
use std::collections::VecDeque;
|
|
4
2
|
|
|
5
3
|
use numpy::{
|
|
@@ -156,7 +154,7 @@ pub mod complete {
|
|
|
156
154
|
///
|
|
157
155
|
/// Therefore, those iterators are equivalents:
|
|
158
156
|
///
|
|
159
|
-
/// >>> from differt_core.
|
|
157
|
+
/// >>> from differt_core.geometry import CompleteGraph, DiGraph
|
|
160
158
|
/// >>>
|
|
161
159
|
/// >>> num_nodes, depth = 100, 5
|
|
162
160
|
/// >>> complete_graph = CompleteGraph(num_nodes)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
use std::{
|
|
1
|
+
use std::{
|
|
2
|
+
fs::File,
|
|
3
|
+
io::BufReader,
|
|
4
|
+
path::{Path, PathBuf},
|
|
5
|
+
};
|
|
2
6
|
|
|
3
7
|
use indexmap::IndexMap;
|
|
4
8
|
use numpy::{PyArray1, PyArray2, ndarray::arr2};
|
|
@@ -207,7 +211,14 @@ impl Mesh {
|
|
|
207
211
|
other.set_face_material(None);
|
|
208
212
|
},
|
|
209
213
|
(None, Some(_)) => {
|
|
214
|
+
// 'self' has no material of its own yet: fill placeholder (-1)
|
|
215
|
+
// entries for its existing triangles (same as the 'None' arm
|
|
216
|
+
// of 'set_face_material'), then adopt 'other's material names
|
|
217
|
+
// outright, since 'self' had none. 'other's existing
|
|
218
|
+
// face-material indices already correctly reference the
|
|
219
|
+
// just-adopted names, so they need no remapping.
|
|
210
220
|
self.set_face_material(None);
|
|
221
|
+
self.material_names = std::mem::take(&mut other.material_names);
|
|
211
222
|
},
|
|
212
223
|
(Some(_), Some(_)) => {
|
|
213
224
|
// We need to possibly renumber material indices.
|
|
@@ -273,22 +284,48 @@ impl Mesh {
|
|
|
273
284
|
/// vertices.
|
|
274
285
|
///
|
|
275
286
|
/// Args:
|
|
276
|
-
/// file (str): The path to the Wavefront .obj file.
|
|
287
|
+
/// file (str | os.PathLike[str]): The path to the Wavefront .obj file.
|
|
277
288
|
///
|
|
278
289
|
/// Returns:
|
|
279
290
|
/// Mesh: The corresponding mesh containing only triangles.
|
|
280
291
|
#[classmethod]
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
#[pyo3(name = "load_obj")]
|
|
293
|
+
pub(crate) fn py_load_obj(_: &Bound<'_, PyType>, file: PathBuf) -> PyResult<Self> {
|
|
294
|
+
Self::load_obj(&file)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Load a triangle mesh from a Stanford PLY .ply file.
|
|
298
|
+
///
|
|
299
|
+
/// Currently, only vertices and triangles are loaded. Triangle normals
|
|
300
|
+
/// are ignored because they are computed with
|
|
301
|
+
/// :attr:`differt.geometry.Mesh.normals` using
|
|
302
|
+
/// JAX so that they can be differentiated with respect to triangle
|
|
303
|
+
/// vertices.
|
|
304
|
+
///
|
|
305
|
+
/// Args:
|
|
306
|
+
/// file (str | os.PathLike[str]): The path to the Stanford PLY .ply file.
|
|
307
|
+
///
|
|
308
|
+
/// Returns:
|
|
309
|
+
/// Mesh: The corresponding mesh containing only triangles.
|
|
310
|
+
#[classmethod]
|
|
311
|
+
#[pyo3(name = "load_ply")]
|
|
312
|
+
pub(crate) fn py_load_ply(_: &Bound<'_, PyType>, file: PathBuf) -> PyResult<Self> {
|
|
313
|
+
Self::load_ply(&file)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
impl Mesh {
|
|
318
|
+
pub(crate) fn load_obj(file: &Path) -> PyResult<Self> {
|
|
319
|
+
let input = BufReader::new(File::open(file)?);
|
|
283
320
|
let mut obj: RawObj = parse_obj(input).map_err(|err| {
|
|
284
321
|
PyValueError::new_err(format!(
|
|
285
|
-
"An error occurred while reading obj file {
|
|
322
|
+
"An error occurred while reading obj file {file:#?}: {}",
|
|
286
323
|
err
|
|
287
324
|
))
|
|
288
325
|
})?;
|
|
289
326
|
|
|
290
327
|
for material_file in obj.material_libraries.iter_mut() {
|
|
291
|
-
let mut path =
|
|
328
|
+
let mut path = file.to_path_buf();
|
|
292
329
|
path.set_file_name(&material_file);
|
|
293
330
|
if let Some(path_str) = path.to_str() {
|
|
294
331
|
*material_file = path_str.to_string();
|
|
@@ -298,29 +335,15 @@ impl Mesh {
|
|
|
298
335
|
Ok(obj.into())
|
|
299
336
|
}
|
|
300
337
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
/// Currently, only vertices and triangles are loaded. Triangle normals
|
|
304
|
-
/// are ignored because they are computed with
|
|
305
|
-
/// :attr:`differt.geometry.Mesh.normals` using
|
|
306
|
-
/// JAX so that they can be differentiated with respect to triangle
|
|
307
|
-
/// vertices.
|
|
308
|
-
///
|
|
309
|
-
/// Args:
|
|
310
|
-
/// file (str): The path to the Stanford PLY .ply file.
|
|
311
|
-
///
|
|
312
|
-
/// Returns:
|
|
313
|
-
/// Mesh: The corresponding mesh containing only triangles.
|
|
314
|
-
#[classmethod]
|
|
315
|
-
pub(crate) fn load_ply(_: &Bound<'_, PyType>, filename: &str) -> PyResult<Self> {
|
|
316
|
-
let mut input = BufReader::new(File::open(filename)?);
|
|
338
|
+
pub(crate) fn load_ply(file: &Path) -> PyResult<Self> {
|
|
339
|
+
let mut input = BufReader::new(File::open(file)?);
|
|
317
340
|
|
|
318
341
|
let vertex_parser = parser::Parser::<PlyVertex>::new();
|
|
319
342
|
let face_parser = parser::Parser::<PlyFace>::new();
|
|
320
343
|
|
|
321
344
|
let header = vertex_parser.read_header(&mut input).map_err(|err| {
|
|
322
345
|
PyValueError::new_err(format!(
|
|
323
|
-
"An error occurred while reading the header of ply file {
|
|
346
|
+
"An error occurred while reading the header of ply file {file:#?}: {}",
|
|
324
347
|
err
|
|
325
348
|
))
|
|
326
349
|
})?;
|
|
@@ -337,7 +360,7 @@ impl Mesh {
|
|
|
337
360
|
.map_err(|err| {
|
|
338
361
|
PyValueError::new_err(format!(
|
|
339
362
|
"An error occurred while reading the vertex elements of ply \
|
|
340
|
-
file {
|
|
363
|
+
file {file:#?}: {}",
|
|
341
364
|
err
|
|
342
365
|
))
|
|
343
366
|
})?
|
|
@@ -352,7 +375,7 @@ impl Mesh {
|
|
|
352
375
|
.map_err(|err| {
|
|
353
376
|
PyValueError::new_err(format!(
|
|
354
377
|
"An error occurred while reading the face elements of PLY \
|
|
355
|
-
file {
|
|
378
|
+
file {file:#?}: {}",
|
|
356
379
|
err
|
|
357
380
|
))
|
|
358
381
|
})?
|
|
@@ -507,3 +530,49 @@ pub(crate) fn mesh(m: Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
507
530
|
m.add_class::<Mesh>()?;
|
|
508
531
|
Ok(())
|
|
509
532
|
}
|
|
533
|
+
|
|
534
|
+
#[cfg(test)]
|
|
535
|
+
mod tests {
|
|
536
|
+
use super::*;
|
|
537
|
+
|
|
538
|
+
#[test]
|
|
539
|
+
fn append_backfills_placeholder_materials_and_adopts_other_material_names() {
|
|
540
|
+
// 'self' has no material info at all yet (no `set_face_material`
|
|
541
|
+
// call was ever made on it), while 'other' already has its single
|
|
542
|
+
// triangle assigned to material "foo".
|
|
543
|
+
let mut self_mesh = Mesh {
|
|
544
|
+
vertices: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
|
|
545
|
+
triangles: vec![[0, 1, 2]],
|
|
546
|
+
..Default::default()
|
|
547
|
+
};
|
|
548
|
+
assert!(self_mesh.face_materials.is_none());
|
|
549
|
+
assert!(self_mesh.material_names.is_empty());
|
|
550
|
+
|
|
551
|
+
let mut other_mesh = Mesh {
|
|
552
|
+
vertices: vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]],
|
|
553
|
+
triangles: vec![[0, 1, 2]],
|
|
554
|
+
..Default::default()
|
|
555
|
+
};
|
|
556
|
+
other_mesh.set_face_material(Some("foo".to_string()));
|
|
557
|
+
|
|
558
|
+
let other_material_names = other_mesh.material_names.clone();
|
|
559
|
+
assert_eq!(other_material_names, vec!["foo".to_string()]);
|
|
560
|
+
|
|
561
|
+
self_mesh.append(&mut other_mesh);
|
|
562
|
+
|
|
563
|
+
// 'self' adopted 'other's material names outright (moved via
|
|
564
|
+
// `std::mem::take`), since it previously had none of its own.
|
|
565
|
+
assert_eq!(self_mesh.material_names, other_material_names);
|
|
566
|
+
// 'other's material names were moved out, leaving it empty.
|
|
567
|
+
assert!(other_mesh.material_names.is_empty());
|
|
568
|
+
|
|
569
|
+
// 'self' now has face materials for both its own triangle
|
|
570
|
+
// (backfilled with the -1 placeholder) and 'other's triangle
|
|
571
|
+
// (unchanged, index 0, i.e., "foo").
|
|
572
|
+
let face_materials = self_mesh
|
|
573
|
+
.face_materials
|
|
574
|
+
.expect("face materials should be set after append");
|
|
575
|
+
assert_eq!(face_materials, vec![-1, 0]);
|
|
576
|
+
assert_eq!(self_mesh.triangles.len(), 2);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
collections::{HashMap, HashSet},
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use pyo3::{exceptions::PyValueError, prelude::*, types::PyType};
|
|
7
|
+
|
|
8
|
+
use super::sionna::SionnaScene;
|
|
9
|
+
use crate::geometry::mesh::Mesh;
|
|
10
|
+
|
|
11
|
+
/// Return the set of material names for which at least two materials
|
|
12
|
+
/// (i.e., two distinct XML ids) share that name but disagree on
|
|
13
|
+
/// `thickness`, meaning that name alone is not enough to tell them apart.
|
|
14
|
+
fn non_uniform_material_names(sionna: &SionnaScene) -> HashSet<String> {
|
|
15
|
+
let mut seen: HashMap<&str, Option<f32>> = HashMap::new();
|
|
16
|
+
let mut non_uniform = HashSet::new();
|
|
17
|
+
|
|
18
|
+
for mat in sionna.materials.values() {
|
|
19
|
+
match seen.get(mat.name.as_str()) {
|
|
20
|
+
Some(thickness) if *thickness != mat.thickness => {
|
|
21
|
+
non_uniform.insert(mat.name.clone());
|
|
22
|
+
},
|
|
23
|
+
Some(_) => {},
|
|
24
|
+
None => {
|
|
25
|
+
seen.insert(&mat.name, mat.thickness);
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
non_uniform
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A scene that contains one mesh, usually being the results of multiple call to :meth:`Mesh.append<differt_core.geometry.Mesh.append>`.
|
|
34
|
+
///
|
|
35
|
+
/// This class is only useful to provide a fast constructor for scenes
|
|
36
|
+
/// created using the Sionna file format.
|
|
37
|
+
#[derive(Clone)]
|
|
38
|
+
#[pyclass(subclass)]
|
|
39
|
+
struct Scene {
|
|
40
|
+
/// differt_core.geometry.Mesh: The scene mesh.
|
|
41
|
+
#[pyo3(get)]
|
|
42
|
+
mesh: Mesh,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[pymethods]
|
|
46
|
+
impl Scene {
|
|
47
|
+
/// Load a scene from a Sionna-compatible XML file.
|
|
48
|
+
///
|
|
49
|
+
/// Args:
|
|
50
|
+
/// file (str | os.PathLike[str]): The path to the XML file.
|
|
51
|
+
///
|
|
52
|
+
/// Returns:
|
|
53
|
+
/// Scene: The corresponding scene.
|
|
54
|
+
#[classmethod]
|
|
55
|
+
#[pyo3(name = "load_xml")]
|
|
56
|
+
fn py_load_xml(_cls: &Bound<'_, PyType>, file: PathBuf) -> PyResult<Self> {
|
|
57
|
+
Self::load_xml(&file)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
impl Scene {
|
|
61
|
+
fn load_xml(file: &Path) -> PyResult<Self> {
|
|
62
|
+
let sionna = SionnaScene::load_xml(file)?;
|
|
63
|
+
|
|
64
|
+
let folder = file.parent().ok_or_else(|| {
|
|
65
|
+
PyValueError::new_err(format!(
|
|
66
|
+
"Could not determine parent folder of file: {file:#?}",
|
|
67
|
+
))
|
|
68
|
+
})?;
|
|
69
|
+
|
|
70
|
+
let non_uniform_names = non_uniform_material_names(&sionna);
|
|
71
|
+
|
|
72
|
+
let mut mesh = Mesh::default();
|
|
73
|
+
|
|
74
|
+
for (_, shape) in sionna.shapes.into_iter() {
|
|
75
|
+
let mesh_file_path = folder.join(shape.file);
|
|
76
|
+
let mut other_mesh = match shape.r#type.as_str() {
|
|
77
|
+
"obj" => Mesh::load_obj(&mesh_file_path)?,
|
|
78
|
+
"ply" => Mesh::load_ply(&mesh_file_path)?,
|
|
79
|
+
ty => {
|
|
80
|
+
log::warn!("Unsupported shape type {ty}, skipping.");
|
|
81
|
+
continue;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let material = sionna.materials.get(&shape.material_id);
|
|
86
|
+
|
|
87
|
+
let color = material.map(|mat| mat.color);
|
|
88
|
+
|
|
89
|
+
// Materials whose name is shared by another, differently
|
|
90
|
+
// configured material (currently, only 'thickness' can differ)
|
|
91
|
+
// are kept under their unique XML id, so the two remain
|
|
92
|
+
// distinguishable; every other material still shares the
|
|
93
|
+
// generic, ITU-type-derived name, matching the previous
|
|
94
|
+
// behavior (and keeping it resolvable against the built-in ITU
|
|
95
|
+
// materials database, e.g. via `differt.em.materials_from_sionna`).
|
|
96
|
+
// A material with no 'thickness' override of its own is never
|
|
97
|
+
// renamed, even if its name collides with a differently
|
|
98
|
+
// configured one: 'differt.em._material._populate_materials'
|
|
99
|
+
// only ever creates a distinct, id-keyed entry for a material
|
|
100
|
+
// that actually carries a 'thickness' (see its own leading
|
|
101
|
+
// 'if mat.thickness is None: continue'), so keying a
|
|
102
|
+
// thickness-less shape by id here would leave it without any
|
|
103
|
+
// matching entry in the Python-side materials mapping.
|
|
104
|
+
let material_name = material.map(|mat| {
|
|
105
|
+
if mat.thickness.is_some() && non_uniform_names.contains(&mat.name) {
|
|
106
|
+
mat.id.clone()
|
|
107
|
+
} else {
|
|
108
|
+
mat.name.clone()
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
other_mesh.set_face_color(color.as_ref());
|
|
113
|
+
other_mesh.set_face_material(material_name);
|
|
114
|
+
|
|
115
|
+
mesh.append(&mut other_mesh);
|
|
116
|
+
}
|
|
117
|
+
Ok(Self { mesh })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[cfg(not(tarpaulin_include))]
|
|
122
|
+
#[pymodule(gil_used = false)]
|
|
123
|
+
pub(crate) fn scene(m: Bound<'_, PyModule>) -> PyResult<()> {
|
|
124
|
+
m.add_class::<Scene>()?;
|
|
125
|
+
Ok(())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[cfg(test)]
|
|
129
|
+
mod tests {
|
|
130
|
+
use std::fs;
|
|
131
|
+
|
|
132
|
+
use indexmap::IndexMap;
|
|
133
|
+
use pyo3::Python;
|
|
134
|
+
use tempfile::tempdir;
|
|
135
|
+
|
|
136
|
+
use super::*;
|
|
137
|
+
use crate::geometry::sionna::Material;
|
|
138
|
+
|
|
139
|
+
const TRIANGLE_OBJ: &str = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3\n";
|
|
140
|
+
|
|
141
|
+
fn shape_xml(id: &str) -> String {
|
|
142
|
+
format!(
|
|
143
|
+
r#"
|
|
144
|
+
<shape type="obj" id="shape-{id}">
|
|
145
|
+
<string name="filename" value="mesh.obj"/>
|
|
146
|
+
<ref id="{id}"/>
|
|
147
|
+
</shape>
|
|
148
|
+
"#
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn non_uniform_material_names_flags_same_name_different_thickness() {
|
|
154
|
+
let mut materials = IndexMap::new();
|
|
155
|
+
materials.insert(
|
|
156
|
+
"window1".to_string(),
|
|
157
|
+
Material {
|
|
158
|
+
name: "itu_glass".to_string(),
|
|
159
|
+
id: "window1".to_string(),
|
|
160
|
+
color: [0.168, 0.139, 0.509],
|
|
161
|
+
thickness: Some(0.01),
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
materials.insert(
|
|
165
|
+
"window2".to_string(),
|
|
166
|
+
Material {
|
|
167
|
+
name: "itu_glass".to_string(),
|
|
168
|
+
id: "window2".to_string(),
|
|
169
|
+
color: [0.168, 0.139, 0.509],
|
|
170
|
+
thickness: Some(0.05),
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
materials.insert(
|
|
174
|
+
"wall".to_string(),
|
|
175
|
+
Material {
|
|
176
|
+
name: "itu_concrete".to_string(),
|
|
177
|
+
id: "wall".to_string(),
|
|
178
|
+
color: [0.539, 0.539, 0.539],
|
|
179
|
+
thickness: None,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
materials.insert(
|
|
183
|
+
"wall2".to_string(),
|
|
184
|
+
Material {
|
|
185
|
+
name: "itu_concrete".to_string(),
|
|
186
|
+
id: "wall2".to_string(),
|
|
187
|
+
color: [0.539, 0.539, 0.539],
|
|
188
|
+
thickness: None,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
let sionna = SionnaScene {
|
|
193
|
+
materials,
|
|
194
|
+
shapes: IndexMap::new(),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let non_uniform = non_uniform_material_names(&sionna);
|
|
198
|
+
|
|
199
|
+
// Both "itu_glass" materials share a name but disagree on
|
|
200
|
+
// thickness, so the name alone cannot distinguish them.
|
|
201
|
+
assert!(non_uniform.contains("itu_glass"));
|
|
202
|
+
// Both "itu_concrete" materials agree (neither has a thickness
|
|
203
|
+
// override), so their shared name remains sufficient.
|
|
204
|
+
assert!(!non_uniform.contains("itu_concrete"));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn load_xml_keeps_ids_distinguishable_when_material_names_collide() {
|
|
209
|
+
let dir = tempdir().expect("failed to create temporary directory");
|
|
210
|
+
|
|
211
|
+
fs::write(dir.path().join("mesh.obj"), TRIANGLE_OBJ).expect("failed to write obj file");
|
|
212
|
+
|
|
213
|
+
let xml = format!(
|
|
214
|
+
r#"<scene version="2.1.0">
|
|
215
|
+
<bsdf type="itu-radio-material" id="window1">
|
|
216
|
+
<string name="type" value="glass"/>
|
|
217
|
+
<float name="thickness" value="0.01"/>
|
|
218
|
+
</bsdf>
|
|
219
|
+
<bsdf type="itu-radio-material" id="window2">
|
|
220
|
+
<string name="type" value="glass"/>
|
|
221
|
+
<float name="thickness" value="0.05"/>
|
|
222
|
+
</bsdf>
|
|
223
|
+
{}{}
|
|
224
|
+
</scene>"#,
|
|
225
|
+
shape_xml("window1"),
|
|
226
|
+
shape_xml("window2"),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
let scene_file = dir.path().join("scene.xml");
|
|
230
|
+
fs::write(&scene_file, xml).expect("failed to write scene file");
|
|
231
|
+
|
|
232
|
+
let material_names: Vec<String> = Python::with_gil(|py| {
|
|
233
|
+
let scene = Scene::load_xml(&scene_file).expect("scene should load");
|
|
234
|
+
let py_scene =
|
|
235
|
+
Bound::new(py, scene).expect("failed to wrap the scene in a Python object");
|
|
236
|
+
let py_mesh = py_scene
|
|
237
|
+
.getattr("mesh")
|
|
238
|
+
.expect("scene should have a `mesh` attribute");
|
|
239
|
+
py_mesh
|
|
240
|
+
.getattr("material_names")
|
|
241
|
+
.expect("mesh should have a `material_names` attribute")
|
|
242
|
+
.extract()
|
|
243
|
+
.expect("`material_names` should be extractable as Vec<String>")
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Since the two "glass" materials disagree on thickness, they must
|
|
247
|
+
// remain distinguishable under their own XML ids, rather than both
|
|
248
|
+
// collapsing to the generic "itu_glass" name (which would make them
|
|
249
|
+
// indistinguishable from one another).
|
|
250
|
+
assert_eq!(
|
|
251
|
+
material_names,
|
|
252
|
+
vec!["window1".to_string(), "window2".to_string()]
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn load_xml_keeps_generic_name_when_no_collision() {
|
|
258
|
+
let dir = tempdir().expect("failed to create temporary directory");
|
|
259
|
+
|
|
260
|
+
fs::write(dir.path().join("mesh.obj"), TRIANGLE_OBJ).expect("failed to write obj file");
|
|
261
|
+
|
|
262
|
+
let xml = format!(
|
|
263
|
+
r#"<scene version="2.1.0">
|
|
264
|
+
<bsdf type="itu-radio-material" id="window">
|
|
265
|
+
<string name="type" value="glass"/>
|
|
266
|
+
<float name="thickness" value="0.01"/>
|
|
267
|
+
</bsdf>
|
|
268
|
+
<bsdf type="itu-radio-material" id="wall">
|
|
269
|
+
<string name="type" value="concrete"/>
|
|
270
|
+
</bsdf>
|
|
271
|
+
{}{}
|
|
272
|
+
</scene>"#,
|
|
273
|
+
shape_xml("window"),
|
|
274
|
+
shape_xml("wall"),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
let scene_file = dir.path().join("scene.xml");
|
|
278
|
+
fs::write(&scene_file, xml).expect("failed to write scene file");
|
|
279
|
+
|
|
280
|
+
let material_names: Vec<String> = Python::with_gil(|py| {
|
|
281
|
+
let scene = Scene::load_xml(&scene_file).expect("scene should load");
|
|
282
|
+
let py_scene =
|
|
283
|
+
Bound::new(py, scene).expect("failed to wrap the scene in a Python object");
|
|
284
|
+
let py_mesh = py_scene
|
|
285
|
+
.getattr("mesh")
|
|
286
|
+
.expect("scene should have a `mesh` attribute");
|
|
287
|
+
py_mesh
|
|
288
|
+
.getattr("material_names")
|
|
289
|
+
.expect("mesh should have a `material_names` attribute")
|
|
290
|
+
.extract()
|
|
291
|
+
.expect("`material_names` should be extractable as Vec<String>")
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Neither material name collides with another, so both keep their
|
|
295
|
+
// generic, ITU-type-derived name (resolvable against the built-in
|
|
296
|
+
// ITU materials database), rather than being keyed by XML id.
|
|
297
|
+
assert_eq!(
|
|
298
|
+
material_names,
|
|
299
|
+
vec!["itu_glass".to_string(), "itu_concrete".to_string()]
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[test]
|
|
304
|
+
fn load_xml_keeps_generic_name_for_thickness_less_material_despite_collision() {
|
|
305
|
+
let dir = tempdir().expect("failed to create temporary directory");
|
|
306
|
+
|
|
307
|
+
fs::write(dir.path().join("mesh.obj"), TRIANGLE_OBJ).expect("failed to write obj file");
|
|
308
|
+
|
|
309
|
+
let xml = format!(
|
|
310
|
+
r#"<scene version="2.1.0">
|
|
311
|
+
<bsdf type="itu-radio-material" id="window1">
|
|
312
|
+
<string name="type" value="glass"/>
|
|
313
|
+
<float name="thickness" value="0.01"/>
|
|
314
|
+
</bsdf>
|
|
315
|
+
<bsdf type="itu-radio-material" id="window2">
|
|
316
|
+
<string name="type" value="glass"/>
|
|
317
|
+
<float name="thickness" value="0.05"/>
|
|
318
|
+
</bsdf>
|
|
319
|
+
<bsdf type="itu-radio-material" id="window3">
|
|
320
|
+
<string name="type" value="glass"/>
|
|
321
|
+
</bsdf>
|
|
322
|
+
{}{}{}
|
|
323
|
+
</scene>"#,
|
|
324
|
+
shape_xml("window1"),
|
|
325
|
+
shape_xml("window2"),
|
|
326
|
+
shape_xml("window3"),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
let scene_file = dir.path().join("scene.xml");
|
|
330
|
+
fs::write(&scene_file, xml).expect("failed to write scene file");
|
|
331
|
+
|
|
332
|
+
let material_names: Vec<String> = Python::with_gil(|py| {
|
|
333
|
+
let scene = Scene::load_xml(&scene_file).expect("scene should load");
|
|
334
|
+
let py_scene =
|
|
335
|
+
Bound::new(py, scene).expect("failed to wrap the scene in a Python object");
|
|
336
|
+
let py_mesh = py_scene
|
|
337
|
+
.getattr("mesh")
|
|
338
|
+
.expect("scene should have a `mesh` attribute");
|
|
339
|
+
py_mesh
|
|
340
|
+
.getattr("material_names")
|
|
341
|
+
.expect("mesh should have a `material_names` attribute")
|
|
342
|
+
.extract()
|
|
343
|
+
.expect("`material_names` should be extractable as Vec<String>")
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// 'window1'/'window2' disagree on thickness and are kept
|
|
347
|
+
// distinguishable under their own ids, but 'window3' has no
|
|
348
|
+
// 'thickness' override of its own: it must keep the shared generic
|
|
349
|
+
// name, since 'differt.em._material._populate_materials' never
|
|
350
|
+
// creates an id-keyed entry for a thickness-less material (it would
|
|
351
|
+
// otherwise have no matching entry on the Python side at all).
|
|
352
|
+
assert_eq!(
|
|
353
|
+
material_names,
|
|
354
|
+
vec![
|
|
355
|
+
"window1".to_string(),
|
|
356
|
+
"window2".to_string(),
|
|
357
|
+
"itu_glass".to_string(),
|
|
358
|
+
]
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|