MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Decoder.cpp
Go to the documentation of this file.
1#include "Decoder.hpp"
2
4
7
9
11
12namespace MayaFlux::Nexus {
13
14namespace {
15
16 // -------------------------------------------------------------------------
17 // Helpers
18 // -------------------------------------------------------------------------
19
20 float denormalize(float norm, const State::Range& r)
21 {
22 return r.min + norm * (r.max - r.min);
23 }
24
25 void apply_wiring(Wiring wiring, const State::WiringRecord& rec, std::vector<std::string>& warnings)
26 {
27 switch (rec.kind) {
29 wiring.every(*rec.interval);
30 if (rec.duration)
31 wiring.for_duration(*rec.duration);
32 if (rec.times && *rec.times > 1)
33 wiring.times(*rec.times);
34 break;
35
37 if (rec.steps) {
38 for (const auto& s : *rec.steps)
39 wiring.move_to(s.position, s.delay_seconds);
40 }
41 if (rec.times && *rec.times > 1)
42 wiring.times(*rec.times);
43 break;
44
46 warnings.emplace_back("Scroll wiring cannot be reconstructed without a live window; falling back to commit_driven");
47 [[fallthrough]];
48
50 warnings.emplace_back("Unsupported wiring kind in schema; falling back to commit_driven");
51 [[fallthrough]];
52
54 break;
55 }
56 wiring.finalise();
57 }
58
59 // -------------------------------------------------------------------------
60 // EXR load + validation, shared by decode() and reconstruct()
61 // -------------------------------------------------------------------------
62
63 struct PixelView {
64 IO::ImageData image;
65 const std::vector<float>* pixels { nullptr };
66 uint32_t width { 0 };
67 };
68
69 std::optional<PixelView> load_exr(
70 const std::string& exr_path,
71 uint32_t expected_entity_count,
72 uint32_t expected_rows,
73 std::string& error_out)
74 {
75 auto image_opt = IO::ImageReader::load(exr_path, 0);
76 if (!image_opt) {
77 error_out = "Failed to load EXR: " + exr_path;
78 return std::nullopt;
79 }
80
81 const auto* pixels = image_opt->as_float();
82 if (!pixels || pixels->empty()) {
83 error_out = "EXR has no float pixel data: " + exr_path;
84 return std::nullopt;
85 }
86 if (image_opt->channels != State::k_channels) {
87 error_out = "EXR channel count mismatch: expected "
88 + std::to_string(State::k_channels) + " got " + std::to_string(image_opt->channels);
89 return std::nullopt;
90 }
91 if (image_opt->height != expected_rows) {
92 error_out = "EXR row count mismatch: expected "
93 + std::to_string(expected_rows) + " got " + std::to_string(image_opt->height);
94 return std::nullopt;
95 }
96 if (image_opt->width != expected_entity_count) {
97 error_out = "EXR width (" + std::to_string(image_opt->width)
98 + ") does not match entity count (" + std::to_string(expected_entity_count) + ")";
99 return std::nullopt;
100 }
101
102 const uint32_t w = image_opt->width;
103 return PixelView { .image = std::move(*image_opt), .pixels = nullptr, .width = w };
104 }
105
106 // =========================================================================
107 // Helper used by both decode() and reconstruct() to apply locus_nav to a
108 // live Locus. Returns true if the cast succeeded.
109 // =========================================================================
110
111 bool apply_locus_nav(const std::shared_ptr<Agent>& a, const State::LocusNavRecord& nav)
112 {
113 auto locus = std::dynamic_pointer_cast<Locus>(a);
114 if (!locus)
115 return false;
116 locus->nav().eye = nav.eye;
117 locus->nav().fov_radians = nav.fov;
118 locus->nav().near_plane = nav.near_plane;
119 locus->nav().far_plane = nav.far_plane;
120 locus->nav().move_speed = nav.speed;
121 // Derive yaw/pitch from the stored target direction.
122 const glm::vec3 dir = glm::normalize(nav.target - nav.eye);
123 locus->nav().yaw = std::atan2(dir.x, dir.z);
124 locus->nav().pitch = std::asin(glm::clamp(dir.y, -1.0F, 1.0F));
125 return true;
126 }
127
128} // namespace
129
130// -------------------------------------------------------------------------
131// decode()
132// -------------------------------------------------------------------------
133
134bool StateDecoder::decode(Fabric& fabric, const std::string& base_path)
135{
136 m_last_error.clear();
137 m_patched_count = 0;
138 m_missing_count = 0;
139
140 const std::string json_path = base_path + ".json";
141 const std::string exr_path = base_path + ".exr";
142
144 auto schema_opt = ser.read<State::FabricSchema>(json_path);
145 if (!schema_opt) {
146 m_last_error = "Failed to load schema: " + ser.last_error();
148 return false;
149 }
150 const auto& schema = *schema_opt;
151
152 if (schema.version != State::k_schema_version) {
153 m_last_error = "Unsupported schema version: " + std::to_string(schema.version)
154 + " (expected " + std::to_string(State::k_schema_version) + ")";
156 return false;
157 }
158
159 if (schema.entities.empty()) {
160 m_last_error = "Schema contains no entities: " + json_path;
162 return false;
163 }
164
165 const uint32_t expected_rows = State::k_exr_rows;
166 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
167 if (!pv_opt) {
169 return false;
170 }
171 auto& pv = *pv_opt;
172 const auto* pixels = pv.image.as_float();
173 const uint32_t width = pv.width;
174 const auto& r = schema.ranges;
175
176 for (size_t i = 0; i < schema.entities.size(); ++i) {
177 const auto& entry = schema.entities[i];
178
179 if (!State::kind_known(entry.kind)) {
181 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
183 continue;
184 }
185
186 const size_t row0 = (static_cast<size_t>(0) * width + i) * State::k_channels;
187 const size_t row1 = (static_cast<size_t>(1) * width + i) * State::k_channels;
188 const size_t row2 = (static_cast<size_t>(2) * width + i) * State::k_channels;
189
190 const glm::vec3 position {
191 denormalize((*pixels)[row0 + 0], r.pos_x),
192 denormalize((*pixels)[row0 + 1], r.pos_y),
193 denormalize((*pixels)[row0 + 2], r.pos_z),
194 };
195
196 switch (State::parse_kind(entry.kind)) {
198 auto e = fabric.get_emitter(entry.id);
199 if (!e) {
201 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
203 continue;
204 }
205 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
207 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
208 entry.id, entry.influence_fn_name, e->fn_name());
209 }
210 e->set_position(position);
211 e->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
212 if (entry.color) {
213 e->set_color(glm::vec3 {
214 denormalize((*pixels)[row1 + 0], r.color_r),
215 denormalize((*pixels)[row1 + 1], r.color_g),
216 denormalize((*pixels)[row1 + 2], r.color_b),
217 });
218 }
219 if (entry.size) {
220 e->set_size(denormalize((*pixels)[row1 + 3], r.size));
221 }
222 e->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
223 break;
224 }
226 auto s = fabric.get_sensor(entry.id);
227 if (!s) {
229 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
231 continue;
232 }
233 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
235 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
236 entry.id, entry.perception_fn_name, s->fn_name());
237 }
238 s->set_position(position);
239 s->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
240 break;
241 }
242 case Fabric::Kind::Agent: {
243 auto a = fabric.get_agent(entry.id);
244 if (!a) {
246 "StateDecoder: id {} not found as Agent, skipping", entry.id);
248 continue;
249 }
250 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
252 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
253 entry.id, entry.perception_fn_name, a->perception_fn_name());
254 }
255 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
257 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
258 entry.id, entry.influence_fn_name, a->influence_fn_name());
259 }
260 a->set_position(position);
261 a->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
262 if (entry.color) {
263 a->set_color(glm::vec3 {
264 denormalize((*pixels)[row1 + 0], r.color_r),
265 denormalize((*pixels)[row1 + 1], r.color_g),
266 denormalize((*pixels)[row1 + 2], r.color_b),
267 });
268 }
269 if (entry.size) {
270 a->set_size(denormalize((*pixels)[row1 + 3], r.size));
271 }
272 a->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
273 a->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
274
275 if (entry.locus_nav) {
276 if (!apply_locus_nav(a, *entry.locus_nav)) {
278 "StateDecoder: Agent {} has locus_nav in schema but is not a Locus at runtime",
279 entry.id);
280 }
281 }
282
283 if (auto presence = std::dynamic_pointer_cast<Presence>(a)) {
284 if (!entry.falloff_curve_name.empty()) {
285 if (auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
286 presence->set_falloff_curve(*fc);
287 }
288 if (entry.falloff_radius)
289 presence->set_falloff_radius(*entry.falloff_radius);
290 }
291 break;
292 }
293 }
294
296 }
297
298 for (const auto& xrec : schema.expanses) {
299 if (xrec.fn_name.empty()) {
301 "StateDecoder: Expanse {} has no fn_name, skipping", xrec.id);
302 continue;
303 }
304 auto contains_fn = fabric.resolve_expanse_fn(xrec.fn_name);
305 if (!contains_fn || !*contains_fn) {
307 "StateDecoder: Expanse {} fn '{}' not in registry, skipping",
308 xrec.id, xrec.fn_name);
309 continue;
310 }
311 auto on_enter_fn = xrec.on_enter_fn_name.empty()
313 : [ptr = fabric.resolve_crossing_fn(xrec.on_enter_fn_name)](uint32_t id) {
314 if (ptr && *ptr)
315 (*ptr)(id);
316 };
317 auto on_exit_fn = xrec.on_exit_fn_name.empty()
319 : [ptr = fabric.resolve_crossing_fn(xrec.on_exit_fn_name)](uint32_t id) {
320 if (ptr && *ptr)
321 (*ptr)(id);
322 };
323 auto expanse = std::make_shared<Expanse>(
324 xrec.fn_name,
325 xrec.on_enter_fn_name,
326 xrec.on_exit_fn_name,
327 *contains_fn,
328 std::move(on_enter_fn),
329 std::move(on_exit_fn));
330 fabric.add_expanse(std::move(expanse));
331 }
332
334 "StateDecoder: patched {} entities ({} missing) from {} + {}",
335 m_patched_count, m_missing_count, exr_path, json_path);
336
337 return true;
338}
339
340// -------------------------------------------------------------------------
341// reconstruct()
342// -------------------------------------------------------------------------
343
345{
347 m_last_error.clear();
348
349 const std::string json_path = base_path + ".json";
350 const std::string exr_path = base_path + ".exr";
351
353 auto schema_opt = ser.read<State::FabricSchema>(json_path);
354 if (!schema_opt) {
355 m_last_error = "Failed to load schema: " + ser.last_error();
357 return result;
358 }
359 const auto& schema = *schema_opt;
360
361 if (schema.version != State::k_schema_version) {
362 m_last_error = "Unsupported schema version: " + std::to_string(schema.version)
363 + " (expected " + std::to_string(State::k_schema_version) + ")";
365 return result;
366 }
367
368 if (schema.entities.empty()) {
369 m_last_error = "Schema contains no entities: " + json_path;
371 return result;
372 }
373
374 const uint32_t expected_rows = State::k_exr_rows;
375 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
376 if (!pv_opt) {
378 return result;
379 }
380 auto& pv = *pv_opt;
381 const auto* pixels = pv.image.as_float();
382 const uint32_t width = pv.width;
383 const auto& r = schema.ranges;
384
385 const auto existing_ids = fabric.all_ids();
386 const std::unordered_set<uint32_t> existing(existing_ids.begin(), existing_ids.end());
387
388 for (size_t i = 0; i < schema.entities.size(); ++i) {
389 const auto& entry = schema.entities[i];
390
391 if (!State::kind_known(entry.kind)) {
392 result.warnings.push_back("Unknown kind '" + entry.kind
393 + "' for id " + std::to_string(entry.id) + ", skipping");
394 ++result.skipped;
395 continue;
396 }
397
398 const size_t row0 = (static_cast<size_t>(0) * width + i) * State::k_channels;
399 const size_t row1 = (static_cast<size_t>(1) * width + i) * State::k_channels;
400 const size_t row2 = (static_cast<size_t>(2) * width + i) * State::k_channels;
401
402 const glm::vec3 position {
403 denormalize((*pixels)[row0 + 0], r.pos_x),
404 denormalize((*pixels)[row0 + 1], r.pos_y),
405 denormalize((*pixels)[row0 + 2], r.pos_z),
406 };
407 const float intensity = denormalize((*pixels)[row0 + 3], r.intensity);
408 const float radius = denormalize((*pixels)[row2 + 0], r.radius);
409 const float query_radius = denormalize((*pixels)[row2 + 1], r.query_radius);
410
411 auto read_color = [&]() -> glm::vec3 {
412 return {
413 denormalize((*pixels)[row1 + 0], r.color_r),
414 denormalize((*pixels)[row1 + 1], r.color_g),
415 denormalize((*pixels)[row1 + 2], r.color_b),
416 };
417 };
418 auto read_size = [&]() {
419 return denormalize((*pixels)[row1 + 3], r.size);
420 };
421
422 if (existing.count(entry.id)) {
423 // -----------------------------------------------------------------
424 // Patch existing entity.
425 // -----------------------------------------------------------------
426 switch (State::parse_kind(entry.kind)) {
428 auto e = fabric.get_emitter(entry.id);
429 if (!e) {
430 ++result.skipped;
431 continue;
432 }
433 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
434 result.warnings.push_back("Emitter " + std::to_string(entry.id)
435 + " fn_name mismatch: schema='" + entry.influence_fn_name
436 + "' live='" + e->fn_name() + "'");
437 }
438 e->set_position(position);
439 e->set_intensity(intensity);
440 if (entry.color) {
441 e->set_color(read_color());
442 }
443 if (entry.size) {
444 e->set_size(read_size());
445 }
446 e->set_radius(radius);
447 break;
448 }
450 auto s = fabric.get_sensor(entry.id);
451 if (!s) {
452 ++result.skipped;
453 continue;
454 }
455 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
456 result.warnings.push_back("Sensor " + std::to_string(entry.id)
457 + " fn_name mismatch: schema='" + entry.perception_fn_name
458 + "' live='" + s->fn_name() + "'");
459 }
460 s->set_position(position);
461 s->set_query_radius(query_radius);
462 break;
463 }
464 case Fabric::Kind::Agent: {
465 auto a = fabric.get_agent(entry.id);
466 if (!a) {
467 ++result.skipped;
468 continue;
469 }
470 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
471 result.warnings.push_back("Agent " + std::to_string(entry.id)
472 + " perception_fn mismatch: schema='" + entry.perception_fn_name + "'");
473 }
474 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
475 result.warnings.push_back("Agent " + std::to_string(entry.id)
476 + " influence_fn mismatch: schema='" + entry.influence_fn_name + "'");
477 }
478 a->set_position(position);
479 a->set_intensity(intensity);
480 if (entry.color) {
481 a->set_color(read_color());
482 }
483 if (entry.size) {
484 a->set_size(read_size());
485 }
486 a->set_radius(radius);
487 a->set_query_radius(query_radius);
488 break;
489 }
490 }
491 ++result.patched;
492
493 } else {
494 // -----------------------------------------------------------------
495 // Construct missing entity.
496 // -----------------------------------------------------------------
497 switch (State::parse_kind(entry.kind)) {
499 auto fn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
501 if (!fn_ptr || !*fn_ptr) {
502 result.warnings.push_back("Emitter: unknown influence_fn '"
503 + entry.influence_fn_name + "', using no-op");
504 fn = [](const InfluenceContext&) { };
505 } else {
506 fn = *fn_ptr;
507 }
508 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
509 emitter->set_position(position);
510 emitter->set_intensity(intensity);
511 emitter->set_radius(radius);
512 if (entry.color) {
513 emitter->set_color(read_color());
514 }
515 if (entry.size) {
516 emitter->set_size(read_size());
517 }
518 auto wiring = fabric.wire(emitter);
519 if (emitter->id() != entry.id) {
520 result.warnings.push_back("Emitter schema_id=" + std::to_string(entry.id)
521 + " reconstructed as runtime_id=" + std::to_string(emitter->id()));
522 }
523 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
524 break;
525 }
527 auto fn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
529 if (!fn_ptr || !*fn_ptr) {
530 result.warnings.push_back("Sensor: unknown perception_fn '"
531 + entry.perception_fn_name + "', using no-op");
532 fn = [](const PerceptionContext&) { };
533 } else {
534 fn = *fn_ptr;
535 }
536 auto sensor = std::make_shared<Sensor>(query_radius,
537 entry.perception_fn_name, std::move(fn));
538 sensor->set_position(position);
539 auto wiring = fabric.wire(sensor);
540 if (sensor->id() != entry.id) {
541 result.warnings.push_back("Sensor schema_id=" + std::to_string(entry.id)
542 + " reconstructed as runtime_id=" + std::to_string(sensor->id()));
543 }
544 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
545 break;
546 }
547 case Fabric::Kind::Agent: {
548 auto pfn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
550 if (!pfn_ptr || !*pfn_ptr) {
551 result.warnings.push_back("Agent: unknown perception_fn '"
552 + entry.perception_fn_name + "', using no-op");
553 pfn = [](const PerceptionContext&) { };
554 } else {
555 pfn = *pfn_ptr;
556 }
557 auto ifn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
559 if (!ifn_ptr || !*ifn_ptr) {
560 result.warnings.push_back("Agent: unknown influence_fn '"
561 + entry.influence_fn_name + "', using no-op");
562 ifn = [](const InfluenceContext&) { };
563 } else {
564 ifn = *ifn_ptr;
565 }
566 std::shared_ptr<Agent> agent;
567 if (entry.subkind == "locus" && entry.locus_nav) {
568 const auto& nav = *entry.locus_nav;
570 .initial_eye = nav.eye,
571 .initial_target = nav.target,
572 .fov_radians = nav.fov,
573 .near_plane = nav.near_plane,
574 .far_plane = nav.far_plane,
575 .move_speed = nav.speed,
576 };
577 agent = std::make_shared<Locus>(cfg, query_radius,
578 entry.perception_fn_name, std::move(pfn),
579 entry.influence_fn_name, std::move(ifn));
580 result.warnings.push_back("Locus " + std::to_string(entry.id)
581 + ": view_targets must be reconnected by caller");
582
583 } else if (entry.subkind == "presence") {
584 auto rfn_ptr = fabric.resolve_radiate_fn(entry.radiate_fn_name);
586 if (!rfn_ptr || !*rfn_ptr) {
587 result.warnings.push_back("Presence: unknown radiate_fn '"
588 + entry.radiate_fn_name + "', using no-op");
589 rfn = [](uint32_t, float) { };
590 } else {
591 rfn = *rfn_ptr;
592 }
593 auto presence = std::make_shared<Presence>(query_radius,
594 entry.perception_fn_name, std::move(pfn),
595 entry.influence_fn_name, std::move(ifn),
596 entry.radiate_fn_name, std::move(rfn));
597 if (!entry.falloff_curve_name.empty()) {
598 if (auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
599 presence->set_falloff_curve(*fc);
600 }
601 if (entry.falloff_radius)
602 presence->set_falloff_radius(*entry.falloff_radius);
603 agent = std::move(presence);
604
605 } else {
606 if (entry.subkind == "locus") {
607 result.warnings.push_back("Locus " + std::to_string(entry.id)
608 + ": no locus_nav in schema, reconstructed as plain Agent");
609 }
610 agent = std::make_shared<Agent>(query_radius,
611 entry.perception_fn_name, std::move(pfn),
612 entry.influence_fn_name, std::move(ifn));
613 }
614 agent->set_position(position);
615 agent->set_intensity(intensity);
616 agent->set_radius(radius);
617 agent->set_query_radius(query_radius);
618 if (entry.color) {
619 agent->set_color(read_color());
620 }
621 if (entry.size) {
622 agent->set_size(read_size());
623 }
624 auto wiring = fabric.wire(agent);
625 if (agent->id() != entry.id) {
626 result.warnings.push_back("Agent schema_id=" + std::to_string(entry.id)
627 + " reconstructed as runtime_id=" + std::to_string(agent->id()));
628 }
629 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
630 break;
631 }
632 }
633 ++result.constructed;
634 }
635 }
636
637 for (const auto& xrec : schema.expanses) {
638 if (xrec.fn_name.empty()) {
639 result.warnings.push_back("Expanse " + std::to_string(xrec.id)
640 + ": no fn_name, skipping");
641 continue;
642 }
643 auto contains_fn = fabric.resolve_expanse_fn(xrec.fn_name);
644 if (!contains_fn || !*contains_fn) {
645 result.warnings.push_back("Expanse " + std::to_string(xrec.id)
646 + ": fn '" + xrec.fn_name + "' not in registry, skipping");
647 continue;
648 }
649 auto on_enter_fn = xrec.on_enter_fn_name.empty()
651 : [ptr = fabric.resolve_crossing_fn(xrec.on_enter_fn_name)](uint32_t id) {
652 if (ptr && *ptr)
653 (*ptr)(id);
654 };
655 auto on_exit_fn = xrec.on_exit_fn_name.empty()
657 : [ptr = fabric.resolve_crossing_fn(xrec.on_exit_fn_name)](uint32_t id) {
658 if (ptr && *ptr)
659 (*ptr)(id);
660 };
661 auto expanse = std::make_shared<Expanse>(
662 xrec.fn_name,
663 xrec.on_enter_fn_name,
664 xrec.on_exit_fn_name,
665 *contains_fn,
666 std::move(on_enter_fn),
667 std::move(on_exit_fn));
668 fabric.add_expanse(std::move(expanse));
669 ++result.constructed;
670 }
671
673 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
674 result.constructed, result.patched, result.skipped, result.warnings.size());
675
676 return result;
677}
678
680 Tapestry& tapestry, const std::string& base_dir)
681{
683
684 const std::string tapestry_path = base_dir + "/tapestry.json";
686 auto schema_opt = ser.read<State::TapestrySchema>(tapestry_path);
687 if (!schema_opt) {
688 m_last_error = "Failed to load tapestry schema: " + ser.last_error();
690 return total;
691 }
692 const auto& schema = *schema_opt;
693
694 for (const auto& ref : schema.fabrics) {
695 auto fabric = tapestry.get_fabric(ref.name);
696 if (!fabric) {
697 fabric = tapestry.create_fabric(ref.name);
698 }
699 auto result = reconstruct(*fabric, ref.base_path);
700 total.constructed += result.constructed;
701 total.patched += result.patched;
702 total.skipped += result.skipped;
703 for (auto& w : result.warnings)
704 total.warnings.push_back(ref.name + ": " + std::move(w));
705 }
706
707 for (const auto& xrec : schema.expanses) {
708 if (xrec.fn_name.empty()) {
709 total.warnings.push_back("TapestryExpanse '" + xrec.name + "': no fn_name, skipping");
710 continue;
711 }
712 Expanse::ContainsFn contains_fn;
713 Expanse::CrossingFn on_enter_fn;
714 Expanse::CrossingFn on_exit_fn;
715
716 for (const auto& fname : xrec.fabric_names) {
717 auto fabric = tapestry.get_fabric(fname);
718 if (!fabric)
719 continue;
720 if (!contains_fn) {
721 if (auto ptr = fabric->resolve_expanse_fn(xrec.fn_name); ptr && *ptr)
722 contains_fn = *ptr;
723 }
724 if (!on_enter_fn && !xrec.on_enter_fn_name.empty()) {
725 if (auto ptr = fabric->resolve_crossing_fn(xrec.on_enter_fn_name); ptr && *ptr)
726 on_enter_fn = *ptr;
727 }
728 if (!on_exit_fn && !xrec.on_exit_fn_name.empty()) {
729 if (auto ptr = fabric->resolve_crossing_fn(xrec.on_exit_fn_name); ptr && *ptr)
730 on_exit_fn = *ptr;
731 }
732 }
733
734 if (!contains_fn) {
735 total.warnings.push_back("TapestryExpanse '" + xrec.name
736 + "': fn '" + xrec.fn_name + "' not resolved, skipping");
737 continue;
738 }
739
740 auto expanse = tapestry.create_expanse(
741 xrec.name,
742 std::move(contains_fn),
743 std::move(on_enter_fn),
744 std::move(on_exit_fn));
745
746 for (const auto& fname : xrec.fabric_names) {
747 if (auto fabric = tapestry.get_fabric(fname))
748 fabric->add_expanse(expanse);
749 }
750 ++total.constructed;
751 }
752
753 total.user_state = schema.user_state;
754
756 "StateDecoder::reconstruct(Tapestry): constructed={} patched={} skipped={} warnings={}",
757 total.constructed, total.patched, total.skipped, total.warnings.size());
758 return total;
759}
760
761} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
IO::ImageData image
Definition Decoder.cpp:64
uint32_t width
Definition Decoder.cpp:66
const std::vector< float > * pixels
Definition Decoder.cpp:65
size_t a
const uint8_t * ptr
uint32_t radius
Cycle Behavior: The for_cycles(N) configuration controls how many times the capture operation execute...
static std::optional< ImageData > load(const std::string &path, int desired_channels=4)
Load image from file (static utility)
std::optional< T > read(const std::string &path)
Read path and deserialize into T.
const std::string & last_error() const
Last error message, empty if no error.
Converts arbitrary C++ types to/from JSON strings and disk files.
std::function< void(const PerceptionContext &)> PerceptionFn
Definition Agent.hpp:31
std::function< void(const InfluenceContext &)> InfluenceFn
Definition Agent.hpp:30
std::function< void(const InfluenceContext &)> InfluenceFn
Definition Emitter.hpp:26
std::function< void(uint32_t)> CrossingFn
Definition Expanse.hpp:34
std::function< bool(const glm::vec3 &)> ContainsFn
Definition Expanse.hpp:33
std::shared_ptr< Sensor > get_sensor(uint32_t id) const
Get the Sensor registered under id.
Definition Fabric.cpp:150
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Definition Fabric.cpp:113
std::shared_ptr< Emitter::InfluenceFn > resolve_influence_fn(std::string_view name) const
Look up a registered influence function by name.
Definition Fabric.cpp:312
std::shared_ptr< Presence::RadiateFn > resolve_radiate_fn(std::string_view name) const
Look up a registered radiation function by name.
Definition Fabric.cpp:351
std::shared_ptr< Expanse::CrossingFn > resolve_crossing_fn(std::string_view name) const
Look up a registered Expanse crossing function by name.
Definition Fabric.cpp:330
std::shared_ptr< Sensor::PerceptionFn > resolve_perception_fn(std::string_view name) const
Look up a registered perception function by name.
Definition Fabric.cpp:318
uint32_t add_expanse(std::shared_ptr< Expanse > expanse)
Register an Expanse for per-commit crossing detection.
Definition Fabric.cpp:100
std::shared_ptr< Agent > get_agent(uint32_t id) const
Get the Agent registered under id.
Definition Fabric.cpp:161
std::shared_ptr< Emitter > get_emitter(uint32_t id) const
Get the Emitter registered under id.
Definition Fabric.cpp:139
Wiring wire(std::shared_ptr< Emitter > emitter)
Begin wiring an Emitter into the Fabric.
Definition Fabric.cpp:39
std::shared_ptr< Expanse::ContainsFn > resolve_expanse_fn(std::string_view name) const
Look up a registered Expanse containment function by name.
Definition Fabric.cpp:324
Orchestrates spatial indexing and scheduling for Nexus objects.
Definition Fabric.hpp:38
std::function< void(uint32_t id, float weight)> RadiateFn
Per-neighbor radiation callable.
Definition Presence.hpp:66
std::function< void(const PerceptionContext &)> PerceptionFn
Definition Sensor.hpp:21
ReconstructionResult reconstruct(Fabric &fabric, const std::string &base_path)
Patch existing entities and construct missing ones from schema.
Definition Decoder.cpp:344
bool decode(Fabric &fabric, const std::string &base_path)
Decode and apply to fabric.
Definition Decoder.cpp:134
std::shared_ptr< Fabric > get_fabric(std::string_view name) const
Look up a named Fabric.
Definition Tapestry.cpp:73
std::shared_ptr< Expanse > create_expanse(std::string name, Expanse::ContainsFn contains, Expanse::CrossingFn on_enter, Expanse::CrossingFn on_exit)
Create and register a named Expanse.
Definition Tapestry.cpp:84
std::shared_ptr< Fabric > create_fabric(float cell_size=1.0F)
create_fabric a new unnamed Fabric into the Tapestry.
Definition Tapestry.cpp:19
Owner of one or more Fabrics and the shared state they rely on.
Definition Tapestry.hpp:25
@ FileIO
Filesystem I/O operations.
@ Runtime
General runtime operations (default fallback)
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
constexpr T denormalize(T t, T lo, T hi) noexcept
Denormalize t from [0, 1] to [lo, hi].
Definition Scalar.hpp:61
constexpr uint32_t k_schema_version
Current schema version written by StateEncoder and accepted by StateDecoder.
Definition Schema.hpp:20
bool kind_known(std::string_view s)
Return true if s maps to a known Fabric::Kind token.
Definition Schema.hpp:368
Fabric::Kind parse_kind(std::string_view s)
Parse a JSON kind token to Fabric::Kind (case-insensitive).
Definition Schema.hpp:379
constexpr uint32_t k_exr_rows
RGBA32F EXR layout constants shared between encoder and decoder.
Definition Schema.hpp:31
constexpr uint32_t k_channels
Definition Schema.hpp:32
Tuning parameters for a first-person fly-navigation controller.
Data passed to an Emitter or Agent influence function on each commit.
Data passed to a Sensor or Agent perception function on each commit.