GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
261 / 287
90.9%
Branches:
174 / 309
56.3%
Functions:
44 / 46, 1 excl
95.7%

python/scene.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file scene.cpp
3 /// @brief Python bindings of the scene: prims, attributes as Python values, arrays shared through
4 /// DLPack, JSON scene files and transforms. Values pass as scene files write them, so Python reads
5 /// and authors attributes by the same rules as JSON.
6 #include "ernie/python/scene.h"
7
8 #include "ernie/python/dlpack.h"
9 #include "ernie/scene/json.h"
10 #include "ernie/scene/json_value.h"
11 #include "ernie/scene/scene.h"
12 #include "ernie/scene/schema.h"
13 #include "ernie/scene/tokens.h"
14 #include "ernie/scene/xform.h"
15 #include "ernie/util/algorithm.h"
16
17 #include <nanobind/stl/array.h>
18 #include <nanobind/stl/filesystem.h>
19 #include <nanobind/stl/optional.h>
20 #include <nanobind/stl/string.h>
21 #include <nanobind/stl/string_view.h>
22 #include <nanobind/stl/tuple.h>
23 #include <nanobind/stl/variant.h>
24 #include <nanobind/stl/vector.h>
25
26 #include <cstdint>
27 #include <filesystem>
28 #include <functional>
29 #include <optional>
30 #include <string>
31 #include <string_view>
32 #include <variant>
33 #include <vector>
34
35 namespace nb = nanobind;
36
37 namespace ernie::python {
38 namespace {
39
40 /// A prim of a scene, as long as it is not removed; it keeps its scene alive.
41
1/2
✓ Branch 23 taken 5 times.
✗ Branch 24 not taken.
187 struct Prim {
42 PrimId id;
43 nb::object scene;
44
45 47 Scene& Owner() const {
46 47 return nb::cast<Scene&>(scene);
47 }
48 };
49
50 21 Prim MakePrim(Scene& scene, PrimId id) {
51 42 return {id, nb::find(&scene)};
52 21 }
53
54 /// A prim, or the path of one.
55 using Target = std::variant<Prim, std::string>;
56
57 56 PrimId Resolve(const Scene& scene, const Target& target) {
58
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 14 times.
56 if (const Prim* prim = std::get_if<Prim>(&target)) {
59
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 41 times.
42 if (&prim->Owner() != &scene) {
60
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw nb::value_error("The prim is another scene's");
61 }
62
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 40 times.
41 if (!scene.Store().Alive(prim->id)) {
63
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw nb::value_error("The prim has been removed");
64 }
65 40 return prim->id;
66 }
67
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
28 return scene.Require(Path(std::get<std::string>(target)));
68 }
69
70 /// @return A Python value as JSON: numbers, strings and None, and sequences of them nested.
71 112 Json ToJson(nb::handle value) {
72
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 112 times.
112 if (value.is_none()) {
73 ✗ return nullptr;
74 }
75
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 111 times.
112 if (nb::isinstance<nb::bool_>(value)) {
76 1 return nb::cast<bool>(value);
77 }
78
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 73 times.
111 if (nb::isinstance<nb::int_>(value)) {
79 38 return nb::cast<std::int64_t>(value);
80 }
81
2/2
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 32 times.
73 if (nb::isinstance<nb::float_>(value)) {
82 41 return nb::cast<double>(value);
83 }
84
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 25 times.
32 if (nb::isinstance<nb::str>(value)) {
85
1/2
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
14 return nb::cast<std::string>(value);
86 }
87
3/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
25 if (nb::isinstance<nb::list>(value) || nb::isinstance<nb::tuple>(value)) {
88 25 Json items = Json::array();
89
5/8
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 86 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 111 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 86 times.
✓ Branch 10 taken 25 times.
111 for (nb::handle item : value) {
90
3/6
✓ Branch 1 taken 86 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 86 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 86 times.
✗ Branch 9 not taken.
86 items.push_back(ToJson(item));
91 25 }
92 25 return items;
93 25 }
94 // NumPy's scalars, which are no Python numbers.
95
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (nb::hasattr(value, "__index__")) {
96
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ return nb::cast<std::int64_t>(value.attr("__index__")());
97 }
98
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (nb::hasattr(value, "__float__")) {
99
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ return nb::cast<double>(value.attr("__float__")());
100 }
101 ✗ throw nb::type_error(
102
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ ("A scene value cannot be a " + nb::cast<std::string>(value.type().attr("__name__")))
103
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ .c_str());
104 }
105
106 /// @return JSON as Python values: arrays as lists.
107 203 nb::object ToPython(const Json& json) {
108
5/7
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 150 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 47 times.
✗ Branch 6 not taken.
203 switch (json.type()) {
109 1 case Json::value_t::boolean:
110
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 return nb::bool_(json.get<bool>());
111 1 case Json::value_t::number_integer:
112 1 return nb::int_(json.get<std::int64_t>());
113 ✗ case Json::value_t::number_unsigned:
114 ✗ return nb::int_(json.get<std::uint64_t>());
115 150 case Json::value_t::number_float:
116 150 return nb::float_(json.get<double>());
117 4 case Json::value_t::string:
118 4 return nb::str(json.get_ref<const std::string&>().c_str());
119 47 case Json::value_t::array: {
120 47 nb::list items;
121
4/6
✓ Branch 4 taken 184 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 231 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 184 times.
✓ Branch 10 taken 47 times.
325 for (const Json& item : json) {
122
2/4
✓ Branch 1 taken 184 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 184 times.
✗ Branch 5 not taken.
368 items.append(ToPython(item));
123 }
124 47 return items;
125 47 }
126 ✗ default:
127 ✗ return nb::none();
128 }
129 }
130
131 /// Runs `edit`, naming the prim and attribute in what it throws.
132 34 void Attempt(const Scene& scene, PrimId prim, std::string_view attribute,
133 const std::function<void()>& edit) {
134 34 try {
135
2/2
✓ Branch 1 taken 32 times.
✓ Branch 2 taken 2 times.
34 edit();
136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 } catch (const std::exception& error) {
137
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 const std::string message =
138
4/8
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
6 "'" + scene.PrimPath(prim).String() + "' " + std::string(attribute) + ": ";
139
2/4
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
4 throw nb::value_error((message + error.what()).c_str());
140 2 }
141 32 }
142
143 /// @return Whether a value is an array to share: a NumPy array or another DLPack producer.
144 21 bool IsArray(nb::handle value) {
145 21 return nb::hasattr(value, "__dlpack__");
146 }
147
148 /// @return Whether DLPack places a tensor on the host or a GPU, with its device index.
149 ✗ std::tuple<int, int> DlpackDevice(const Tensor& tensor) {
150
0/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ const Device device = tensor.Location();
151
0/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ switch (device.kind) {
152 ✗ case DeviceKind::Cuda:
153 ✗ return {static_cast<int>(kDLCUDA), device.id};
154 ✗ case DeviceKind::Rocm:
155 ✗ return {static_cast<int>(kDLROCM), device.id};
156 case DeviceKind::Cpu:
157 break;
158 }
159 ✗ return {static_cast<int>(kDLCPU), 0};
160 }
161
162 /// An authored array, which NumPy, PyTorch and any DLPack library view in place.
163 12 struct SceneArray {
164 Tensor tensor;
165 };
166
167 8 nb::object TransformRows(const glm::dmat4& matrix) {
168
2/4
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 return ToPython(ernie::ToJson(Value(matrix)));
169 }
170
171 23 XformOps ReadXformArguments(const nb::object& translate, const nb::object& rotate,
172 const std::string& rotationOrder, const nb::object& orient,
173 const nb::object& scale) {
174 23 Json xform = Json::object();
175 115 const auto add = [&](const char* key, const nb::object& value) {
176
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 86 times.
92 if (!value.is_none()) {
177
1/2
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 xform[key] = ToJson(value);
178 }
179 92 };
180
1/2
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
23 add("translate", translate);
181
1/2
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
23 add("rotate", rotate);
182
1/2
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
23 add("orient", orient);
183
1/2
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
23 add("scale", scale);
184
2/4
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
23 xform["rotationOrder"] = rotationOrder;
185
2/2
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 1 time.
45 return ReadXform(xform);
186 23 }
187
188 /// Authors an attribute from a Python value: an array is shared in place, anything else read as
189 /// scene files read it.
190 21 void SetValue(Scene& scene, PrimId prim, const std::string& attribute, const nb::handle& value) {
191
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 20 times.
21 if (IsArray(value)) {
192
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
2 Tensor array = TakeTensor(nb::borrow(value), nb::none());
193
3/6
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
2 Attempt(scene, prim, attribute, [&] { scene.Write(prim, attribute, std::move(array)); });
194 1 return;
195 1 }
196 20 const Json json = ToJson(value);
197
3/4
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 19 times.
✓ Branch 5 taken 1 time.
41 Attempt(scene, prim, attribute, [&] { LoadAttribute(scene, prim, attribute, json); });
198 20 }
199
200 using Vector3 = std::array<double, 3>;
201
202 8 glm::dvec3 ToDvec3(const Vector3& v) {
203 8 return {v[0], v[1], v[2]};
204 }
205
206 /// The `define` keywords that author transform ops rather than attributes.
207 constexpr std::array<std::string_view, 5> kXformKeywords{"translate", "rotate", "rotation_order",
208 "orient", "scale"};
209
210 /// @return The transform ops `define`'s keywords author, or none when they author no op.
211 19 std::optional<XformOps> TakeXformKeywords(const nb::kwargs& keywords) {
212 114 const auto keyword = [&](const char* name) {
213
5/6
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 92 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 92 times.
98 return keywords.contains(name) ? nb::object(keywords[name]) : nb::none();
214 19 };
215 19 const nb::object order = keyword("rotation_order");
216 19 XformOps xform =
217
2/4
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
38 ReadXformArguments(keyword("translate"), keyword("rotate"),
218
4/6
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 18 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 19 times.
✗ Branch 7 not taken.
39 order.is_none() ? std::string("XYZ") : nb::cast<std::string>(order),
219
2/4
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
38 keyword("orient"), keyword("scale"));
220
5/8
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
19 if (!xform.translate && !xform.rotate && !xform.orient && !xform.scale) {
221 17 return std::nullopt;
222 }
223 2 return xform;
224 19 }
225
226 } // namespace
227
228 6 void BindScene(nb::module_& module) {
229 6 nb::class_<Prim>(module, "Prim", "A prim of a scene, valid until it is removed.")
230 6 .def("__eq__",
231 10 [](const Prim& self, const Prim& other) {
232
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 3 times.
8 return self.id == other.id && self.scene.is(other.scene);
233 })
234 6 .def("__hash__",
235 ✗ [](const Prim& self) {
236 ✗ return (static_cast<std::uint64_t>(self.id.generation) << 32) | self.id.slot;
237 })
238 6 .def("__repr__",
239 ✗ [](const Prim& self) {
240
0/4
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ return "Prim(" + std::to_string(self.id.slot) + ", " +
241 ✗ std::to_string(self.id.generation) + ")";
242 })
243 6 .def(
244 "set",
245 5 [](const Prim& self, const std::string& attribute, const nb::object& value) {
246 5 Scene& scene = self.Owner();
247
2/4
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 5 times.
✗ Branch 6 not taken.
5 SetValue(scene, Resolve(scene, self), attribute, value);
248 5 return self;
249 },
250 12 nb::arg("attribute"), nb::arg("value"),
251 "Authors an attribute as `Scene.set` does, returning the prim to set more.");
252
253 6 nb::class_<SceneArray>(module, "SceneArray",
254 "An array attribute's values, shared in place through DLPack.")
255 7 .def_prop_ro("shape", [](const SceneArray& self) { return self.tensor.Shape(); })
256 6 .def(
257 "__dlpack__",
258 2 [](const SceneArray& self, const nb::object&, const nb::object& maxVersion,
259 const nb::object&, const nb::object& copy) {
260
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
2 if (!copy.is_none() && nb::cast<bool>(copy)) {
261
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw nb::buffer_error("Scene arrays are shared in place, never copied");
262 }
263
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 const bool versioned =
264
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
4 !maxVersion.is_none() && nb::cast<std::tuple<int, int>>(maxVersion) >=
265 2 std::tuple<int, int>(DLPACK_MAJOR_VERSION, 0);
266 2 return Capsule(self.tensor.ToDlpack(), versioned);
267 },
268 12 nb::kw_only(), nb::arg("stream") = nb::none(), nb::arg("max_version") = nb::none(),
269 12 nb::arg("dl_device") = nb::none(), nb::arg("copy") = nb::none())
270 6 .def("__dlpack_device__", [](const SceneArray& self) { return DlpackDevice(self.tensor); });
271
272 6 nb::class_<Scene>(module, "Scene", "Typed prims at paths, their attributes in columns by type.")
273
1/2
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 .def(nb::init<>())
274 6 .def(
275 "define",
276 20 [](Scene& self, const std::string& path, const std::string& type,
277 const nb::kwargs& attributes) {
278
2/2
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 1 time.
21 const PrimId prim = self.Define(Path(path), type);
279
2/2
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 19 times.
25 for (const auto& [key, value] : attributes) {
280 6 const std::string name = nb::cast<std::string>(key);
281
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
6 if (std::ranges::find(kXformKeywords, name) == kXformKeywords.end()) {
282
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 SetValue(self, prim, name, value);
283 }
284 6 }
285
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 17 times.
19 if (const std::optional<XformOps> xform = TakeXformKeywords(attributes)) {
286
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 SetXform(self, Path(path), *xform);
287 }
288 19 return MakePrim(self, prim);
289 },
290
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
18 nb::arg("path"), nb::arg("type") = "", nb::arg("attributes"),
291 "Defines a prim of a concrete type, or none, with its missing ancestors. Keywords "
292 "author its transform ops (translate, rotate, rotation_order, orient, scale, as "
293 "`set_xform`) and attributes by name (radius=2.0), as `set` does.")
294 6 .def(
295
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 "remove", [](Scene& self, const std::string& path) { return self.Remove(Path(path)); },
296 6 nb::arg("path"), "Removes a prim and its descendants; whether there was one.")
297 6 .def(
298 "find",
299 4 [](Scene& self, const std::string& path) -> std::optional<Prim> {
300
1/2
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 const std::optional<PrimId> prim = self.Find(Path(path));
301
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 return prim ? std::optional<Prim>(MakePrim(self, *prim)) : std::nullopt;
302 },
303 6 nb::arg("path"), "The prim at a path, or None.")
304 6 .def("__contains__",
305 2 [](const Scene& self, const std::string& path) { return self.Has(Path(path)); })
306 6 .def(
307 "path",
308 1 [](const Scene& self, const Target& prim) {
309
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
2 return self.PrimPath(Resolve(self, prim)).String();
310 },
311 6 nb::arg("prim"), "A prim's path.")
312
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
6 .def(
313 "type_name",
314 4 [](const Scene& self, const Target& prim) {
315 4 return std::string(self.TypeName(Resolve(self, prim)));
316 },
317 6 nb::arg("prim"), "A prim's type; empty for none.")
318
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
6 .def(
319 "children",
320 7 [](const Scene& self, const std::string& path) {
321
2/4
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 time.
✗ Branch 6 not taken.
1 return Map(self.Children(Path(path)), &Path::String);
322 },
323
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
18 nb::arg("path") = "/", "The paths of a prim's children.")
324 6 .def(
325 "prims",
326 7 [](const Scene& self, const std::string& type) {
327
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 return Map(self.Prims(type), &Path::String);
328 },
329
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
18 nb::arg("type") = "", "The paths of every prim of a type or one deriving from it.")
330 6 .def(
331 "apply_api",
332 1 [](Scene& self, const Target& prim, const std::string& api) {
333 1 self.ApplyApi(Resolve(self, prim), api);
334 1 },
335 6 nb::arg("prim"), nb::arg("api"),
336 "Applies an API schema, or an instance of one, as 'CollectionAPI:lights'.")
337 6 .def(
338 "attributes",
339 1 [](const Scene& self, const Target& prim) {
340
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
2 return self.Attributes(self.PrimPath(Resolve(self, prim)));
341 },
342 6 nb::arg("prim"), "The authored attributes' names.")
343 6 .def(
344 "set",
345 14 [](Scene& self, const Target& target, const std::string& attribute,
346 const nb::object& value) {
347 14 SetValue(self, Resolve(self, target), attribute, value);
348 12 },
349 6 nb::arg("prim"), nb::arg("attribute"), nb::arg("value"),
350 "Authors an attribute: a number, string, sequence for a vector, quaternion "
351 "(w, x, y, z), matrix (4 rows) or array, list of strings for tokens or paths, or a "
352 "NumPy or DLPack array, shared in place.")
353 6 .def(
354 "get",
355 15 [](const Scene& self, const Target& target,
356 const std::string& attribute) -> nb::object {
357 15 const PrimId prim = Resolve(self, target);
358 14 const std::optional<Value> value = self.Get(prim, attribute);
359
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 11 times.
14 if (!value) {
360 3 return nb::none();
361 }
362 11 nb::object result;
363
2/4
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 6 taken 11 times.
✗ Branch 7 not taken.
22 Attempt(self, prim, attribute, [&] { result = ToPython(ernie::ToJson(*value)); });
364 11 return result;
365 25 },
366 6 nb::arg("prim"), nb::arg("attribute"),
367 "An authored value as scene files write it, or None; arrays are read with `read`.")
368 6 .def(
369 "write",
370 2 [](Scene& self, const Target& target, const std::string& attribute,
371 const nb::object& array, const nb::object& stream) {
372 2 const PrimId prim = Resolve(self, target);
373 2 Tensor tensor = TakeTensor(array, stream);
374
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
3 Attempt(self, prim, attribute,
375
3/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
4 [&] { self.Write(prim, attribute, std::move(tensor)); });
376 2 },
377 12 nb::arg("prim"), nb::arg("attribute"), nb::arg("array"), nb::arg("stream") = nb::none(),
378 "Authors an array attribute with a NumPy or DLPack array, shared, not copied.")
379 6 .def(
380 "read",
381 4 [](const Scene& self, const Target& target,
382 const std::string& attribute) -> std::optional<SceneArray> {
383 4 const Tensor* array = self.Read(Resolve(self, target), attribute);
384
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
7 return array != nullptr ? std::optional<SceneArray>(SceneArray{*array})
385 4 : std::nullopt;
386 },
387 6 nb::arg("prim"), nb::arg("attribute"),
388 "An authored array, shared in place through DLPack (numpy.from_dlpack), or None.")
389
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
6 .def(
390 "clear",
391 1 [](Scene& self, const Target& prim, const std::string& attribute) {
392 1 return self.Clear(Resolve(self, prim), attribute);
393 },
394 6 nb::arg("prim"), nb::arg("attribute"), "Unauthors an attribute; whether it was.")
395 6 .def(
396 "set_xform",
397 4 [](Scene& self, const Target& target, const nb::object& translate,
398 const nb::object& rotate, const std::string& rotationOrder, const nb::object& orient,
399 const nb::object& scale) {
400 4 const Path path = self.PrimPath(Resolve(self, target));
401
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 SetXform(self, path,
402
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
7 ReadXformArguments(translate, rotate, rotationOrder, orient, scale));
403 3 },
404 12 nb::arg("prim"), nb::kw_only(), nb::arg("translate") = nb::none(),
405
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
18 nb::arg("rotate") = nb::none(), nb::arg("rotation_order") = "XYZ",
406
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
12 nb::arg("orient") = nb::none(), nb::arg("scale") = nb::none(),
407 "Authors a prim's transform ops: translate, rotate (degrees, in `rotation_order`) or "
408 "orient (w, x, y, z), and scale.")
409 6 .def(
410 "local_transform",
411 2 [](const Scene& self, const Target& prim) {
412 2 return TransformRows(self.LocalTransform(Resolve(self, prim)));
413 },
414 6 nb::arg("prim"), "A prim's own transform, as 4 rows.")
415 6 .def(
416 "world_transform",
417 2 [](const Scene& self, const Target& prim) {
418 2 return TransformRows(self.WorldTransform(Resolve(self, prim)));
419 },
420 6 nb::arg("prim"),
421 "A prim's transform to world space, as 4 rows: column vectors, translation in the "
422 "last column.")
423 6 .def("update", &Scene::Update,
424 "Brings derived data, such as inherited attributes and extents, up to date.")
425 6 .def(
426 "load_json",
427 2 [](Scene& self, std::string_view text, const std::vector<nb::object>& arrays) {
428 2 const std::vector<Tensor> tensors = Map(
429
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
4 arrays, [](const nb::object& array) { return TakeTensor(array, nb::none()); });
430
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 LoadScene(self, text, tensors);
431 2 },
432
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
18 nb::arg("text"), nb::arg("arrays") = std::vector<nb::object>(),
433 "Builds the prims a scene file's text describes; {\"$array\": i} names arrays[i], "
434 "shared in place.")
435 6 .def("save_json", &SaveScene, "The scene as a scene file's text.")
436 6 .def(
437 "load",
438
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 [](Scene& self, const std::filesystem::path& path) { LoadSceneFile(self, path); },
439 6 nb::arg("path"), "Builds the prims a scene file describes.")
440 6 .def(
441 "save",
442
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 [](const Scene& self, const std::filesystem::path& path) { SaveSceneFile(self, path); },
443 12 nb::arg("path"), "Writes the scene to a scene file.");
444
445 // Each prim type by its name, for `scene.define(path, ernie.Sphere)`.
446 6 nb::list types;
447
3/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 390 times.
✓ Branch 4 taken 6 times.
396 for (const SchemaDef* schema : Schemas()) {
448
2/2
✓ Branch 0 taken 234 times.
✓ Branch 1 taken 156 times.
390 if (schema->kind == SchemaKind::Concrete) {
449
1/2
✓ Branch 1 taken 234 times.
✗ Branch 2 not taken.
234 const std::string name(schema->name);
450
1/2
✓ Branch 1 taken 234 times.
✗ Branch 2 not taken.
234 module.attr(name.c_str()) = name;
451
1/2
✓ Branch 1 taken 234 times.
✗ Branch 2 not taken.
234 types.append(name);
452 234 }
453 }
454
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 module.attr("PRIM_TYPES") = nb::tuple(types);
455
456 6 nb::module_ names = module.def_submodule("tokens", "Names the scene's code shares.");
457
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 names.attr("TRANSFORM") = std::string(tokens::kTransform);
458
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 names.attr("TRANSLATE") = std::string(tokens::kTranslate);
459
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 names.attr("ORIENT") = std::string(tokens::kOrient);
460
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 names.attr("SCALE") = std::string(tokens::kScale);
461
2/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
12 names.attr("XFORM_OP_ORDER") = std::string(tokens::kXformOpOrder);
462
463 6 module.def(
464 "look_at_transform",
465 8 [](const Vector3& eye, const Vector3& target, const Vector3& up) {
466
2/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 return TransformRows(LookAtTransform(ToDvec3(eye), ToDvec3(target), ToDvec3(up)));
467 },
468
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
12 nb::arg("eye"), nb::arg("target"), nb::arg("up") = Vector3{0.0, 0.0, 1.0},
469 "A camera's transform at `eye` looking at `target`, as 4 rows: cameras look down -z.");
470 6 module.def(
471 "distant_light_transform",
472 8 [](const Vector3& towardLight) {
473 2 return TransformRows(DistantLightTransform(ToDvec3(towardLight)));
474 },
475 6 nb::arg("toward_light"),
476 "A distant light's transform shining from the direction to its source, as 4 rows.");
477 6 }
478
479 } // namespace ernie::python
480