chrono_batch_render/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
177 / 233
76.0%
Branches:
91 / 189
48.1%
Functions:
23 / 32
71.9%

python/src/importer.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 #include <filesystem>
3 #include <map>
4 #include <memory>
5 #include <optional>
6 #include <string>
7 #include <string_view>
8 #include <utility>
9 #include <vector>
10
11 #include <fmt/format.h>
12 #include <nanobind/nanobind.h>
13 #include <nanobind/ndarray.h>
14 #include <nanobind/stl/filesystem.h>
15 #include <nanobind/stl/map.h>
16 #include <nanobind/stl/optional.h>
17 #include <nanobind/stl/shared_ptr.h>
18 #include <nanobind/stl/string.h>
19 #include <nanobind/stl/string_view.h>
20 #include <nanobind/stl/variant.h>
21 #include <nanobind/stl/vector.h>
22 #include <nanobind/trampoline.h>
23
24 #include "bindings.h"
25 #include "values.h"
26
27 namespace nb = nanobind;
28
29 namespace chrono::batch_render::python {
30 namespace {
31
32 class PyImporter : public Importer {
33 public:
34 NB_TRAMPOLINE(Importer);
35
36 16 std::string Name() const override {
37
2/5
✓ Branch 2 taken 16 times.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
32 NB_OVERRIDE_PURE_NAME("name", Name);
38 16 }
39
40 3 std::vector<std::string> Extensions() const override {
41
2/5
✓ Branch 2 taken 3 times.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
6 NB_OVERRIDE_PURE_NAME("extensions", Extensions);
42 3 }
43
44 3 std::vector<ImportOption> Options() const override {
45
4/6
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✓ Branch 4 taken 2 times.
✓ Branch 9 taken 1 time.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
5 NB_OVERRIDE_NAME("options", Options);
46 3 }
47
48 2 ImportPlan Read(std::string_view path, const ImportSource& source,
49 ImportContext& context) override {
50 2 nb::detail::ticket ticket(nb_trampoline, "read", nb::detail::str_hash("read"), true);
51
3/6
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
6 return nb::cast<ImportPlan>(nb_trampoline.base().attr(ticket.key)(
52
1/1
✓ Branch 1 taken 2 times.
6 path, source, nb::cast(context, nb::rv_policy::copy)));
53 2 }
54 };
55
56 13 std::shared_ptr<nb::object> Hold(nb::object object) {
57 13 return std::shared_ptr<nb::object>(new nb::object(std::move(object)), [](nb::object* held) {
58
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 if (Py_IsInitialized()) {
59 13 nb::gil_scoped_acquire gil;
60
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
26 delete held;
61 13 }
62 13 });
63 }
64
65 ✗ FileBytes ToFileBytes(const nb::bytes& bytes) {
66 ✗ const auto* data = static_cast<const std::byte*>(bytes.data());
67 ✗ return std::make_shared<const std::vector<std::byte>>(data, data + bytes.size());
68 }
69
70 1 nb::object ToPython(const FileBytes& bytes) {
71
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if (!bytes) {
72 ✗ return nb::none();
73 }
74 1 return nb::bytes(reinterpret_cast<const char*>(bytes->data()), bytes->size());
75 }
76
77 using OptionValues = std::optional<std::map<std::string, ImportValue>>;
78
79 9 ImportDesc CreateImportDesc(std::optional<std::shared_ptr<Importer>> importer,
80 std::optional<nb::callable> progress, bool synchronous,
81 std::optional<UpAxis> up, std::optional<float> metersPerUnit,
82 OptionValues options) {
83
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
9 ImportDesc desc{.synchronous = synchronous, .up = up, .metersPerUnit = metersPerUnit};
84
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
9 if (options) {
85
1/1
✓ Branch 1 taken 2 times.
2 desc.options.insert(options->begin(), options->end());
86 }
87
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (importer) {
88 ✗ desc.importer = std::move(*importer);
89 }
90
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 8 times.
9 if (progress) {
91
4/9
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 time.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
7 desc.progress = [callback = Hold(std::move(*progress))](const ImportProgress& report) {
92 3 nb::gil_scoped_acquire gil;
93
3/6
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
6 (*callback)(nb::cast(report, nb::rv_policy::copy));
94
1/1
✓ Branch 1 taken 1 time.
4 };
95 }
96 9 return desc;
97 ✗ }
98
99 2 void BindSceneImports() {
100 2 const nb::handle sceneType = nb::type<Scene>();
101 8 const auto import = [](Scene& scene, std::string_view path, const ImportSource& source,
102 std::optional<std::shared_ptr<Importer>> importer,
103 std::optional<nb::callable> progress, bool synchronous,
104 std::optional<UpAxis> up, std::optional<float> metersPerUnit,
105 OptionValues options) {
106 6 const ImportDesc desc =
107 CreateImportDesc(std::move(importer), std::move(progress), synchronous, up,
108
3/5
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✓ Branch 5 taken 6 times.
12 metersPerUnit, std::move(options));
109 6 nb::gil_scoped_release release;
110
1/1
✓ Branch 1 taken 4 times.
10 return scene.Import(path, source, desc);
111 6 };
112
1/1
✓ Branch 1 taken 2 times.
2 nb::cpp_function_def(
113 4 [import](Scene& scene, std::string_view path, const std::filesystem::path& file,
114 std::optional<std::shared_ptr<Importer>> importer,
115 std::optional<nb::callable> progress, bool synchronous, std::optional<UpAxis> up,
116 std::optional<float> metersPerUnit, OptionValues options) {
117
1/1
✓ Branch 1 taken 3 times.
5 return import(scene, path, ImportSource{.file = file}, std::move(importer),
118
4/6
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 6 taken 4 times.
✓ Branch 9 taken 3 times.
8 std::move(progress), synchronous, up, metersPerUnit, std::move(options));
119 },
120 4 nb::is_method(), nb::scope(sceneType), nb::name("import_file"), nb::arg("path"),
121 4 nb::arg("file"), nb::kw_only(), nb::arg("importer") = nb::none(),
122 4 nb::arg("progress") = nb::none(), nb::arg("synchronous") = false,
123
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("up") = nb::none(), nb::arg("meters_per_unit") = nb::none(),
124
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("options") = nb::none());
125
1/1
✓ Branch 1 taken 2 times.
4 nb::cpp_function_def(import, nb::is_method(), nb::scope(sceneType), nb::name("import_file"),
126 4 nb::arg("path"), nb::arg("source"), nb::kw_only(),
127 4 nb::arg("importer") = nb::none(), nb::arg("progress") = nb::none(),
128
1/1
✓ Branch 1 taken 2 times.
6 nb::arg("synchronous") = false, nb::arg("up") = nb::none(),
129
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("meters_per_unit") = nb::none(), nb::arg("options") = nb::none());
130
1/1
✓ Branch 1 taken 2 times.
2 nb::cpp_function_def(
131 3 [](Scene& scene, std::string_view path, nb::callable read, std::string name,
132 std::optional<nb::callable> progress, bool synchronous, std::optional<UpAxis> up,
133 std::optional<float> metersPerUnit, OptionValues options) {
134 3 ImportReader reader{.name = std::move(name),
135
3/7
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
18 .read = [held = Hold(std::move(read))](ImportContext& context) {
136 3 nb::gil_scoped_acquire gil;
137 3 return nb::cast<ImportPlan>(
138
4/6
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 2 times.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 time.
✗ Branch 11 not taken.
6 (*held)(nb::cast(context, nb::rv_policy::copy)));
139
2/3
✓ Branch 2 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
8 }};
140 3 const ImportDesc desc = CreateImportDesc(std::nullopt, std::move(progress), synchronous,
141
3/3
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 3 taken 3 times.
8 up, metersPerUnit, std::move(options));
142 3 nb::gil_scoped_release release;
143
1/1
✓ Branch 1 taken 2 times.
3 return scene.Import(path, reader, desc);
144 5 },
145 2 nb::is_method(), nb::scope(sceneType), nb::name("import_from"), nb::arg("path"),
146
1/1
✓ Branch 1 taken 2 times.
6 nb::arg("read"), nb::kw_only(), nb::arg("name") = "a Python reader",
147
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("progress") = nb::none(), nb::arg("synchronous") = false,
148
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("up") = nb::none(), nb::arg("meters_per_unit") = nb::none(),
149
1/1
✓ Branch 1 taken 2 times.
4 nb::arg("options") = nb::none());
150 2 nb::cpp_function_def(
151 ✗ [](Scene& scene, std::string path, const nb::bytes& bytes, std::optional<ScenePrim> owner) {
152
0/7
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ scene.AddFile(std::move(path), ToFileBytes(bytes),
153
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ owner ? std::optional<Prim>(owner->prim) : std::nullopt);
154 ✗ },
155 2 nb::is_method(), nb::scope(sceneType), nb::name("add_file"), nb::arg("path"),
156 4 nb::arg("bytes"), nb::kw_only(), nb::arg("owner") = nb::none());
157 2 nb::cpp_function_def(
158 ✗ [](Scene& scene, std::string path,
159 const nb::ndarray<nb::ro, nb::device::cpu, nb::c_contig>& array,
160 std::optional<ScenePrim> owner) {
161
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (array.ndim() != 2 && array.ndim() != 3) {
162
0/1
✗ Branch 2 not taken.
✗ throw nb::value_error("Textures are (rows, cols[, channels]) arrays");
163 }
164
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ TextureData data{.width = uint32_t(array.shape(1)),
165 ✗ .height = uint32_t(array.shape(0)),
166
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ .channels = array.ndim() == 3 ? uint32_t(array.shape(2)) : 1u};
167
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const auto* bytes = static_cast<const std::byte*>(array.data());
168 ✗ std::vector<float> converted;
169
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (array.dtype() == nb::dtype<uint8_t>()) {
170 ✗ data.type = TexelType::Uint8;
171 ✗ data.texels = Span<const std::byte>(bytes, array.nbytes());
172
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } else if (array.dtype() == nb::dtype<float>()) {
173 ✗ data.texels = Span<const std::byte>(bytes, array.nbytes());
174
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } else if (array.dtype() == nb::dtype<double>()) {
175 ✗ const auto* values = static_cast<const double*>(array.data());
176
0/1
✗ Branch 2 not taken.
✗ converted.resize(array.size());
177
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (size_t at = 0; at < converted.size(); ++at) {
178 ✗ converted[at] = float(values[at]);
179 }
180 ✗ data.texels =
181 ✗ Span<const std::byte>(reinterpret_cast<const std::byte*>(converted.data()),
182 ✗ converted.size() * sizeof(float));
183 } else {
184
0/1
✗ Branch 2 not taken.
✗ throw nb::type_error("Textures are uint8, float32 or float64 arrays");
185 }
186
0/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ scene.AddTexture(std::move(path), data,
187
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ owner ? std::optional<Prim>(owner->prim) : std::nullopt);
188 ✗ },
189 2 nb::is_method(), nb::scope(sceneType), nb::name("add_texture"), nb::arg("path"),
190 4 nb::arg("texels"), nb::kw_only(), nb::arg("owner") = nb::none());
191 2 nb::cpp_function_def(&Scene::RemoveFile, nb::is_method(), nb::scope(sceneType),
192 2 nb::name("remove_file"), nb::arg("path"));
193 2 }
194
195 }
196
197 2 void BindImport(nb::module_& module) {
198 2 nb::exception<CancelledError>(module, "CancelledError", PyExc_RuntimeError);
199
200 2 nb::class_<ImportProgress>(module, "ImportProgress")
201 2 .def_ro("stage", &ImportProgress::stage)
202 2 .def_ro("done", &ImportProgress::done)
203 2 .def_ro("total", &ImportProgress::total)
204 2 .def("__repr__", [](const ImportProgress& progress) {
205 ✗ return fmt::format("ImportProgress({}, {}/{})", progress.stage, progress.done,
206 ✗ progress.total);
207 });
208
209 2 nb::enum_<UpAxis>(module, "UpAxis").value("Y", UpAxis::Y).value("Z", UpAxis::Z);
210
211 2 nb::class_<ImportContext>(module, "ImportContext")
212 4 .def(nb::init<>())
213 2 .def("report", &ImportContext::Report, nb::arg("stage"), nb::arg("done"), nb::arg("total"))
214 2 .def_prop_ro("cancelled", &ImportContext::Cancelled)
215 2 .def_prop_ro("up", &ImportContext::Up)
216 4 .def_prop_ro("meters_per_unit", &ImportContext::MetersPerUnit)
217 4 .def("option", &ImportContext::Option, nb::arg("name"));
218
219 2 nb::class_<ImportOption>(module, "ImportOption")
220 2 .def(
221 "__init__",
222 6 [](ImportOption* option, std::string name, ImportValue value, std::string label,
223 std::string tip, std::vector<std::string> choices) {
224 6 new (option) ImportOption{.name = std::move(name),
225 .label = std::move(label),
226 .tip = std::move(tip),
227 .value = std::move(value),
228 6 .choices = std::move(choices)};
229 6 },
230
1/1
✓ Branch 1 taken 2 times.
6 nb::arg("name"), nb::arg("value"), nb::kw_only(), nb::arg("label") = "",
231
3/3
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
8 nb::arg("tip") = "", nb::arg("choices") = std::vector<std::string>{})
232 2 .def_rw("name", &ImportOption::name)
233 2 .def_rw("label", &ImportOption::label)
234 2 .def_rw("tip", &ImportOption::tip)
235 2 .def_rw("value", &ImportOption::value)
236 2 .def_rw("choices", &ImportOption::choices)
237 2 .def("__repr__", [](const ImportOption& option) {
238 ✗ return fmt::format("ImportOption({})", option.name);
239 });
240
241 2 nb::class_<ImportSource>(module, "ImportSource")
242 2 .def(
243 "__init__",
244 1 [](ImportSource* source, std::filesystem::path file,
245 std::optional<std::map<std::string, nb::bytes>> files) {
246
1/1
✓ Branch 2 taken 1 time.
1 new (source) ImportSource{.file = std::move(file)};
247
0/1
✗ Branch 1 not taken.
1 for (const auto& [name, bytes] :
248
2/4
✓ Branch 1 taken 1 time.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
1 files.value_or(std::map<std::string, nb::bytes>{})) {
249
0/4
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ source->files[name] = ToFileBytes(bytes);
250 1 }
251 1 },
252 4 nb::arg("file"), nb::arg("files") = nb::none())
253 2 .def_rw("file", &ImportSource::file)
254 2 .def_prop_ro("names",
255 ✗ [](const ImportSource& source) {
256 ✗ std::vector<std::string> names;
257
0/3
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ for (const auto& [name, bytes] : source.files) {
258
0/1
✗ Branch 1 not taken.
✗ names.push_back(name);
259 }
260 ✗ return names;
261 ✗ })
262 4 .def("locate", &ImportSource::Locate, nb::arg("name"))
263 2 .def(
264 "find",
265 ✗ [](const ImportSource& source, std::string_view name) {
266
0/1
✗ Branch 2 not taken.
✗ return ToPython(source.Find(name));
267 },
268 2 nb::arg("name"))
269 2 .def(
270 "read",
271 ✗ [](const ImportSource& source, std::string_view name) {
272
0/1
✗ Branch 2 not taken.
✗ return ToPython(source.Read(name));
273 },
274 2 nb::arg("name"))
275
1/1
✓ Branch 3 taken 1 time.
4 .def("read_entry", [](const ImportSource& source) { return ToPython(source.ReadEntry()); });
276
277 2 nb::class_<ImportPlan>(module, "ImportPlan")
278 4 .def(nb::init<>())
279 2 .def(
280 "define",
281 9 [](ImportPlan& plan, std::string path, std::string_view schema,
282 std::optional<nb::dict> values) {
283 9 const SchemaId id = FindSchema(schema);
284
2/2
✓ Branch 1 taken 9 times.
✓ Branch 5 taken 9 times.
18 plan.Add(std::move(path),
285
5/9
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 3 times.
✓ Branch 7 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
54 [id, held = Hold(values ? std::move(*values) : nb::dict())](
286 Scene& scene, std::string_view at) {
287 8 nb::gil_scoped_acquire gil;
288
4/7
✓ Branch 1 taken 8 times.
✓ Branch 4 taken 8 times.
✓ Branch 7 taken 8 times.
✓ Branch 10 taken 8 times.
✗ Branch 11 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
16 DefineSchema(scene, at, id, nb::cast<nb::dict>(*held), nb::dict());
289 8 });
290 9 },
291 4 nb::arg("path"), nb::arg("schema"), nb::arg("values") = nb::none())
292 2 .def(
293 "add_file",
294 ✗ [](ImportPlan& plan, std::string_view name, const nb::bytes& bytes) {
295
0/1
✗ Branch 2 not taken.
✗ return plan.AddFile(name, ToFileBytes(bytes));
296 },
297 2 nb::arg("name"), nb::arg("bytes"))
298 4 .def_prop_ro("prim_count", &ImportPlan::PrimCount);
299
300 2 nb::class_<Importer, PyImporter>(module, "Importer")
301 4 .def(nb::init<>())
302 2 .def("name", &Importer::Name)
303 2 .def("extensions", &Importer::Extensions)
304 2 .def("options", &Importer::Options)
305 2 .def(
306 "read",
307 ✗ [](Importer& importer, std::string_view path, const ImportSource& source,
308 std::optional<ImportContext> context) {
309
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ ImportContext used = context ? *context : ImportContext();
310
0/1
✗ Branch 1 not taken.
✗ return importer.Read(path, source, used);
311 ✗ },
312 4 nb::arg("path"), nb::arg("source"), nb::arg("context") = nb::none(),
313 nb::call_guard<nb::gil_scoped_release>());
314
315 2 nb::class_<ImportJob>(module, "ImportJob")
316 2 .def_prop_ro("ready", &ImportJob::Ready)
317 2 .def_prop_ro("progress", &ImportJob::Progress)
318 2 .def("cancel", &ImportJob::Cancel)
319 2 .def("wait", [](ImportJob& job) {
320 17 const Prim root = [&] {
321 6 nb::gil_scoped_release release;
322
1/1
✓ Branch 1 taken 5 times.
6 return job.Wait();
323 11 }();
324 5 return ScenePrim{Ref<Scene>(&root.GetScene()), root};
325 });
326
327 2 BindSceneImports();
328
329 2 module.def("register_importer", &RegisterImporter, nb::arg("importer"));
330 4 module.def("unregister_importer", &UnregisterImporter, nb::arg("extension"),
331 4 nb::arg("name") = "");
332 2 module.def("find_importer", &FindImporter, nb::arg("extension"));
333 2 module.def("find_importers", &FindImporters, nb::arg("extension"));
334 2 module.def("importer_extensions", &ImporterExtensions);
335
336
1/1
✓ Branch 3 taken 2 times.
4 nb::module_::import_("atexit").attr("register")(nb::cpp_function([] {
337
2/2
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 2 times.
16 for (const std::string& extension : ImporterExtensions()) {
338
3/3
✓ Branch 1 taken 14 times.
✓ Branch 3 taken 14 times.
✓ Branch 4 taken 14 times.
28 for (const std::shared_ptr<Importer>& importer : FindImporters(extension)) {
339
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (nb::find(importer.get()).is_valid()) {
340
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ UnregisterImporter(extension, importer->Name());
341 }
342 14 }
343 2 }
344 2 }));
345 2 }
346
347 }
348