MayaFlux 0.4.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
8
9namespace MayaFlux::Nexus {
10
11namespace {
12
13 // -------------------------------------------------------------------------
14 // Helpers
15 // -------------------------------------------------------------------------
16
17 float denormalize(float norm, const State::Range& r)
18 {
19 return r.min + norm * (r.max - r.min);
20 }
21
22 void apply_wiring(Wiring wiring, const State::WiringRecord& rec, std::vector<std::string>& warnings)
23 {
24 switch (rec.kind) {
26 wiring.every(*rec.interval);
27 if (rec.duration)
28 wiring.for_duration(*rec.duration);
29 if (rec.times && *rec.times > 1)
30 wiring.times(*rec.times);
31 break;
32
34 if (rec.steps) {
35 for (const auto& s : *rec.steps)
36 wiring.move_to(s.position, s.delay_seconds);
37 }
38 if (rec.times && *rec.times > 1)
39 wiring.times(*rec.times);
40 break;
41
43 warnings.emplace_back("Unsupported wiring kind in schema; falling back to commit_driven");
44 [[fallthrough]];
45
47 break;
48 }
49 wiring.finalise();
50 }
51
52 // -------------------------------------------------------------------------
53 // EXR load + validation, shared by decode() and reconstruct()
54 // -------------------------------------------------------------------------
55
56 struct PixelView {
57 IO::ImageData image;
58 const std::vector<float>* pixels { nullptr };
59 uint32_t width { 0 };
60 };
61
62 std::optional<PixelView> load_exr(
63 const std::string& exr_path,
64 uint32_t expected_entity_count,
65 uint32_t expected_rows,
66 std::string& error_out)
67 {
68 auto image_opt = IO::ImageReader::load(exr_path, 0);
69 if (!image_opt) {
70 error_out = "Failed to load EXR: " + exr_path;
71 return std::nullopt;
72 }
73
74 const auto* pixels = image_opt->as_float();
75 if (!pixels || pixels->empty()) {
76 error_out = "EXR has no float pixel data: " + exr_path;
77 return std::nullopt;
78 }
79 if (image_opt->channels != State::k_channels) {
80 error_out = "EXR channel count mismatch: expected "
81 + std::to_string(State::k_channels) + " got " + std::to_string(image_opt->channels);
82 return std::nullopt;
83 }
84 if (image_opt->height != expected_rows) {
85 error_out = "EXR row count mismatch: expected "
86 + std::to_string(expected_rows) + " got " + std::to_string(image_opt->height);
87 return std::nullopt;
88 }
89 if (image_opt->width != expected_entity_count) {
90 error_out = "EXR width (" + std::to_string(image_opt->width)
91 + ") does not match entity count (" + std::to_string(expected_entity_count) + ")";
92 return std::nullopt;
93 }
94
95 const uint32_t w = image_opt->width;
96 return PixelView { .image = std::move(*image_opt), .pixels = nullptr, .width = w };
97 }
98
99 // =========================================================================
100 // Helper used by both decode() and reconstruct() to apply locus_nav to a
101 // live Locus. Returns true if the cast succeeded.
102 // =========================================================================
103
104 bool apply_locus_nav(const std::shared_ptr<Agent>& a, const State::LocusNavRecord& nav)
105 {
106 auto locus = std::dynamic_pointer_cast<Locus>(a);
107 if (!locus)
108 return false;
109 locus->nav().eye = nav.eye;
110 locus->nav().fov_radians = nav.fov;
111 locus->nav().near_plane = nav.near_plane;
112 locus->nav().far_plane = nav.far_plane;
113 locus->nav().move_speed = nav.speed;
114 // Derive yaw/pitch from the stored target direction.
115 const glm::vec3 dir = glm::normalize(nav.target - nav.eye);
116 locus->nav().yaw = std::atan2(dir.x, dir.z);
117 locus->nav().pitch = std::asin(glm::clamp(dir.y, -1.0F, 1.0F));
118 return true;
119 }
120
121} // namespace
122
123// -------------------------------------------------------------------------
124// decode()
125// -------------------------------------------------------------------------
126
127bool StateDecoder::decode(Fabric& fabric, const std::string& base_path)
128{
129 m_last_error.clear();
130 m_patched_count = 0;
131 m_missing_count = 0;
132
133 const std::string json_path = base_path + ".json";
134 const std::string exr_path = base_path + ".exr";
135
137 auto schema_opt = ser.read<State::FabricSchema>(json_path);
138 if (!schema_opt) {
139 m_last_error = "Failed to load schema: " + ser.last_error();
141 return false;
142 }
143 const auto& schema = *schema_opt;
144
145 if (schema.version != State::k_schema_version) {
146 m_last_error = "Unsupported schema version: " + std::to_string(schema.version)
147 + " (expected " + std::to_string(State::k_schema_version) + ")";
149 return false;
150 }
151
152 if (schema.entities.empty()) {
153 m_last_error = "Schema contains no entities: " + json_path;
155 return false;
156 }
157
158 const uint32_t expected_rows = State::k_exr_rows;
159 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
160 if (!pv_opt) {
162 return false;
163 }
164 auto& pv = *pv_opt;
165 const auto* pixels = pv.image.as_float();
166 const uint32_t width = pv.width;
167 const auto& r = schema.ranges;
168
169 for (size_t i = 0; i < schema.entities.size(); ++i) {
170 const auto& entry = schema.entities[i];
171
172 if (!State::kind_known(entry.kind)) {
174 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
176 continue;
177 }
178
179 const size_t row0 = (static_cast<size_t>(0) * width + i) * State::k_channels;
180 const size_t row1 = (static_cast<size_t>(1) * width + i) * State::k_channels;
181 const size_t row2 = (static_cast<size_t>(2) * width + i) * State::k_channels;
182
183 const glm::vec3 position {
184 denormalize((*pixels)[row0 + 0], r.pos_x),
185 denormalize((*pixels)[row0 + 1], r.pos_y),
186 denormalize((*pixels)[row0 + 2], r.pos_z),
187 };
188
189 switch (State::parse_kind(entry.kind)) {
191 auto e = fabric.get_emitter(entry.id);
192 if (!e) {
194 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
196 continue;
197 }
198 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
200 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
201 entry.id, entry.influence_fn_name, e->fn_name());
202 }
203 e->set_position(position);
204 e->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
205 if (entry.color) {
206 e->set_color(glm::vec3 {
207 denormalize((*pixels)[row1 + 0], r.color_r),
208 denormalize((*pixels)[row1 + 1], r.color_g),
209 denormalize((*pixels)[row1 + 2], r.color_b),
210 });
211 }
212 if (entry.size) {
213 e->set_size(denormalize((*pixels)[row1 + 3], r.size));
214 }
215 e->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
216 break;
217 }
219 auto s = fabric.get_sensor(entry.id);
220 if (!s) {
222 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
224 continue;
225 }
226 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
228 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
229 entry.id, entry.perception_fn_name, s->fn_name());
230 }
231 s->set_position(position);
232 s->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
233 break;
234 }
235 case Fabric::Kind::Agent: {
236 auto a = fabric.get_agent(entry.id);
237 if (!a) {
239 "StateDecoder: id {} not found as Agent, skipping", entry.id);
241 continue;
242 }
243 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
245 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
246 entry.id, entry.perception_fn_name, a->perception_fn_name());
247 }
248 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
250 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
251 entry.id, entry.influence_fn_name, a->influence_fn_name());
252 }
253 a->set_position(position);
254 a->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
255 if (entry.color) {
256 a->set_color(glm::vec3 {
257 denormalize((*pixels)[row1 + 0], r.color_r),
258 denormalize((*pixels)[row1 + 1], r.color_g),
259 denormalize((*pixels)[row1 + 2], r.color_b),
260 });
261 }
262 if (entry.size) {
263 a->set_size(denormalize((*pixels)[row1 + 3], r.size));
264 }
265 a->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
266 a->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
267
268 if (entry.locus_nav) {
269 if (!apply_locus_nav(a, *entry.locus_nav)) {
271 "StateDecoder: Agent {} has locus_nav in schema but is not a Locus at runtime",
272 entry.id);
273 }
274 }
275
276 if (auto presence = std::dynamic_pointer_cast<Presence>(a)) {
277 if (!entry.falloff_curve_name.empty()) {
278 if (auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
279 presence->set_falloff_curve(*fc);
280 }
281 if (entry.falloff_radius)
282 presence->set_falloff_radius(*entry.falloff_radius);
283 }
284 break;
285 }
286 }
287
289 }
290
291 for (const auto& xrec : schema.expanses) {
292 if (xrec.fn_name.empty()) {
294 "StateDecoder: Expanse {} has no fn_name, skipping", xrec.id);
295 continue;
296 }
297 auto contains_fn = fabric.resolve_expanse_fn(xrec.fn_name);
298 if (!contains_fn || !*contains_fn) {
300 "StateDecoder: Expanse {} fn '{}' not in registry, skipping",
301 xrec.id, xrec.fn_name);
302 continue;
303 }
304 auto on_enter_fn = xrec.on_enter_fn_name.empty()
306 : [ptr = fabric.resolve_crossing_fn(xrec.on_enter_fn_name)](uint32_t id) {
307 if (ptr && *ptr)
308 (*ptr)(id);
309 };
310 auto on_exit_fn = xrec.on_exit_fn_name.empty()
312 : [ptr = fabric.resolve_crossing_fn(xrec.on_exit_fn_name)](uint32_t id) {
313 if (ptr && *ptr)
314 (*ptr)(id);
315 };
316 auto expanse = std::make_shared<Expanse>(
317 xrec.fn_name,
318 xrec.on_enter_fn_name,
319 xrec.on_exit_fn_name,
320 *contains_fn,
321 std::move(on_enter_fn),
322 std::move(on_exit_fn));
323 fabric.add_expanse(std::move(expanse));
324 }
325
327 "StateDecoder: patched {} entities ({} missing) from {} + {}",
328 m_patched_count, m_missing_count, exr_path, json_path);
329
330 return true;
331}
332
333// -------------------------------------------------------------------------
334// reconstruct()
335// -------------------------------------------------------------------------
336
338{
340 m_last_error.clear();
341
342 const std::string json_path = base_path + ".json";
343 const std::string exr_path = base_path + ".exr";
344
346 auto schema_opt = ser.read<State::FabricSchema>(json_path);
347 if (!schema_opt) {
348 m_last_error = "Failed to load schema: " + ser.last_error();
350 return result;
351 }
352 const auto& schema = *schema_opt;
353
354 if (schema.version != State::k_schema_version) {
355 m_last_error = "Unsupported schema version: " + std::to_string(schema.version)
356 + " (expected " + std::to_string(State::k_schema_version) + ")";
358 return result;
359 }
360
361 if (schema.entities.empty()) {
362 m_last_error = "Schema contains no entities: " + json_path;
364 return result;
365 }
366
367 const uint32_t expected_rows = State::k_exr_rows;
368 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
369 if (!pv_opt) {
371 return result;
372 }
373 auto& pv = *pv_opt;
374 const auto* pixels = pv.image.as_float();
375 const uint32_t width = pv.width;
376 const auto& r = schema.ranges;
377
378 const auto existing_ids = fabric.all_ids();
379 const std::unordered_set<uint32_t> existing(existing_ids.begin(), existing_ids.end());
380
381 for (size_t i = 0; i < schema.entities.size(); ++i) {
382 const auto& entry = schema.entities[i];
383
384 if (!State::kind_known(entry.kind)) {
385 result.warnings.push_back("Unknown kind '" + entry.kind
386 + "' for id " + std::to_string(entry.id) + ", skipping");
387 ++result.skipped;
388 continue;
389 }
390
391 const size_t row0 = (static_cast<size_t>(0) * width + i) * State::k_channels;
392 const size_t row1 = (static_cast<size_t>(1) * width + i) * State::k_channels;
393 const size_t row2 = (static_cast<size_t>(2) * width + i) * State::k_channels;
394
395 const glm::vec3 position {
396 denormalize((*pixels)[row0 + 0], r.pos_x),
397 denormalize((*pixels)[row0 + 1], r.pos_y),
398 denormalize((*pixels)[row0 + 2], r.pos_z),
399 };
400 const float intensity = denormalize((*pixels)[row0 + 3], r.intensity);
401 const float radius = denormalize((*pixels)[row2 + 0], r.radius);
402 const float query_radius = denormalize((*pixels)[row2 + 1], r.query_radius);
403
404 auto read_color = [&]() -> glm::vec3 {
405 return {
406 denormalize((*pixels)[row1 + 0], r.color_r),
407 denormalize((*pixels)[row1 + 1], r.color_g),
408 denormalize((*pixels)[row1 + 2], r.color_b),
409 };
410 };
411 auto read_size = [&]() {
412 return denormalize((*pixels)[row1 + 3], r.size);
413 };
414
415 if (existing.count(entry.id)) {
416 // -----------------------------------------------------------------
417 // Patch existing entity.
418 // -----------------------------------------------------------------
419 switch (State::parse_kind(entry.kind)) {
421 auto e = fabric.get_emitter(entry.id);
422 if (!e) {
423 ++result.skipped;
424 continue;
425 }
426 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
427 result.warnings.push_back("Emitter " + std::to_string(entry.id)
428 + " fn_name mismatch: schema='" + entry.influence_fn_name
429 + "' live='" + e->fn_name() + "'");
430 }
431 e->set_position(position);
432 e->set_intensity(intensity);
433 if (entry.color) {
434 e->set_color(read_color());
435 }
436 if (entry.size) {
437 e->set_size(read_size());
438 }
439 e->set_radius(radius);
440 break;
441 }
443 auto s = fabric.get_sensor(entry.id);
444 if (!s) {
445 ++result.skipped;
446 continue;
447 }
448 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
449 result.warnings.push_back("Sensor " + std::to_string(entry.id)
450 + " fn_name mismatch: schema='" + entry.perception_fn_name
451 + "' live='" + s->fn_name() + "'");
452 }
453 s->set_position(position);
454 s->set_query_radius(query_radius);
455 break;
456 }
457 case Fabric::Kind::Agent: {
458 auto a = fabric.get_agent(entry.id);
459 if (!a) {
460 ++result.skipped;
461 continue;
462 }
463 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
464 result.warnings.push_back("Agent " + std::to_string(entry.id)
465 + " perception_fn mismatch: schema='" + entry.perception_fn_name + "'");
466 }
467 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
468 result.warnings.push_back("Agent " + std::to_string(entry.id)
469 + " influence_fn mismatch: schema='" + entry.influence_fn_name + "'");
470 }
471 a->set_position(position);
472 a->set_intensity(intensity);
473 if (entry.color) {
474 a->set_color(read_color());
475 }
476 if (entry.size) {
477 a->set_size(read_size());
478 }
479 a->set_radius(radius);
480 a->set_query_radius(query_radius);
481 break;
482 }
483 }
484 ++result.patched;
485
486 } else {
487 // -----------------------------------------------------------------
488 // Construct missing entity.
489 // -----------------------------------------------------------------
490 switch (State::parse_kind(entry.kind)) {
492 auto fn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
494 if (!fn_ptr || !*fn_ptr) {
495 result.warnings.push_back("Emitter: unknown influence_fn '"
496 + entry.influence_fn_name + "', using no-op");
497 fn = [](const InfluenceContext&) { };
498 } else {
499 fn = *fn_ptr;
500 }
501 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
502 emitter->set_position(position);
503 emitter->set_intensity(intensity);
504 emitter->set_radius(radius);
505 if (entry.color) {
506 emitter->set_color(read_color());
507 }
508 if (entry.size) {
509 emitter->set_size(read_size());
510 }
511 auto wiring = fabric.wire(emitter);
512 if (emitter->id() != entry.id) {
513 result.warnings.push_back("Emitter schema_id=" + std::to_string(entry.id)
514 + " reconstructed as runtime_id=" + std::to_string(emitter->id()));
515 }
516 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
517 break;
518 }
520 auto fn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
522 if (!fn_ptr || !*fn_ptr) {
523 result.warnings.push_back("Sensor: unknown perception_fn '"
524 + entry.perception_fn_name + "', using no-op");
525 fn = [](const PerceptionContext&) { };
526 } else {
527 fn = *fn_ptr;
528 }
529 auto sensor = std::make_shared<Sensor>(query_radius,
530 entry.perception_fn_name, std::move(fn));
531 sensor->set_position(position);
532 auto wiring = fabric.wire(sensor);
533 if (sensor->id() != entry.id) {
534 result.warnings.push_back("Sensor schema_id=" + std::to_string(entry.id)
535 + " reconstructed as runtime_id=" + std::to_string(sensor->id()));
536 }
537 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
538 break;
539 }
540 case Fabric::Kind::Agent: {
541 auto pfn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
543 if (!pfn_ptr || !*pfn_ptr) {
544 result.warnings.push_back("Agent: unknown perception_fn '"
545 + entry.perception_fn_name + "', using no-op");
546 pfn = [](const PerceptionContext&) { };
547 } else {
548 pfn = *pfn_ptr;
549 }
550 auto ifn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
552 if (!ifn_ptr || !*ifn_ptr) {
553 result.warnings.push_back("Agent: unknown influence_fn '"
554 + entry.influence_fn_name + "', using no-op");
555 ifn = [](const InfluenceContext&) { };
556 } else {
557 ifn = *ifn_ptr;
558 }
559 std::shared_ptr<Agent> agent;
560 if (entry.subkind == "locus" && entry.locus_nav) {
561 const auto& nav = *entry.locus_nav;
563 .initial_eye = nav.eye,
564 .initial_target = nav.target,
565 .fov_radians = nav.fov,
566 .near_plane = nav.near_plane,
567 .far_plane = nav.far_plane,
568 .move_speed = nav.speed,
569 };
570 agent = std::make_shared<Locus>(cfg, query_radius,
571 entry.perception_fn_name, std::move(pfn),
572 entry.influence_fn_name, std::move(ifn));
573 result.warnings.push_back("Locus " + std::to_string(entry.id)
574 + ": view_targets must be reconnected by caller");
575
576 } else if (entry.subkind == "presence") {
577 auto rfn_ptr = fabric.resolve_radiate_fn(entry.radiate_fn_name);
579 if (!rfn_ptr || !*rfn_ptr) {
580 result.warnings.push_back("Presence: unknown radiate_fn '"
581 + entry.radiate_fn_name + "', using no-op");
582 rfn = [](uint32_t, float) { };
583 } else {
584 rfn = *rfn_ptr;
585 }
586 auto presence = std::make_shared<Presence>(query_radius,
587 entry.perception_fn_name, std::move(pfn),
588 entry.influence_fn_name, std::move(ifn),
589 entry.radiate_fn_name, std::move(rfn));
590 if (!entry.falloff_curve_name.empty()) {
591 if (auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
592 presence->set_falloff_curve(*fc);
593 }
594 if (entry.falloff_radius)
595 presence->set_falloff_radius(*entry.falloff_radius);
596 agent = std::move(presence);
597
598 } else {
599 if (entry.subkind == "locus") {
600 result.warnings.push_back("Locus " + std::to_string(entry.id)
601 + ": no locus_nav in schema, reconstructed as plain Agent");
602 }
603 agent = std::make_shared<Agent>(query_radius,
604 entry.perception_fn_name, std::move(pfn),
605 entry.influence_fn_name, std::move(ifn));
606 }
607 agent->set_position(position);
608 agent->set_intensity(intensity);
609 agent->set_radius(radius);
610 agent->set_query_radius(query_radius);
611 if (entry.color) {
612 agent->set_color(read_color());
613 }
614 if (entry.size) {
615 agent->set_size(read_size());
616 }
617 auto wiring = fabric.wire(agent);
618 if (agent->id() != entry.id) {
619 result.warnings.push_back("Agent schema_id=" + std::to_string(entry.id)
620 + " reconstructed as runtime_id=" + std::to_string(agent->id()));
621 }
622 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
623 break;
624 }
625 }
626 ++result.constructed;
627 }
628 }
629
630 for (const auto& xrec : schema.expanses) {
631 if (xrec.fn_name.empty()) {
632 result.warnings.push_back("Expanse " + std::to_string(xrec.id)
633 + ": no fn_name, skipping");
634 continue;
635 }
636 auto contains_fn = fabric.resolve_expanse_fn(xrec.fn_name);
637 if (!contains_fn || !*contains_fn) {
638 result.warnings.push_back("Expanse " + std::to_string(xrec.id)
639 + ": fn '" + xrec.fn_name + "' not in registry, skipping");
640 continue;
641 }
642 auto on_enter_fn = xrec.on_enter_fn_name.empty()
644 : [ptr = fabric.resolve_crossing_fn(xrec.on_enter_fn_name)](uint32_t id) {
645 if (ptr && *ptr)
646 (*ptr)(id);
647 };
648 auto on_exit_fn = xrec.on_exit_fn_name.empty()
650 : [ptr = fabric.resolve_crossing_fn(xrec.on_exit_fn_name)](uint32_t id) {
651 if (ptr && *ptr)
652 (*ptr)(id);
653 };
654 auto expanse = std::make_shared<Expanse>(
655 xrec.fn_name,
656 xrec.on_enter_fn_name,
657 xrec.on_exit_fn_name,
658 *contains_fn,
659 std::move(on_enter_fn),
660 std::move(on_exit_fn));
661 fabric.add_expanse(std::move(expanse));
662 ++result.constructed;
663 }
664
666 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
667 result.constructed, result.patched, result.skipped, result.warnings.size());
668
669 return result;
670}
671
673 Tapestry& tapestry, const std::string& base_dir)
674{
676
677 const std::string tapestry_path = base_dir + "/tapestry.json";
679 auto schema_opt = ser.read<State::TapestrySchema>(tapestry_path);
680 if (!schema_opt) {
681 m_last_error = "Failed to load tapestry schema: " + ser.last_error();
683 return total;
684 }
685 const auto& schema = *schema_opt;
686
687 for (const auto& ref : schema.fabrics) {
688 auto fabric = tapestry.get_fabric(ref.name);
689 if (!fabric) {
690 fabric = tapestry.create_fabric(ref.name);
691 }
692 auto result = reconstruct(*fabric, ref.base_path);
693 total.constructed += result.constructed;
694 total.patched += result.patched;
695 total.skipped += result.skipped;
696 for (auto& w : result.warnings)
697 total.warnings.push_back(ref.name + ": " + std::move(w));
698 }
699
700 for (const auto& xrec : schema.expanses) {
701 if (xrec.fn_name.empty()) {
702 total.warnings.push_back("TapestryExpanse '" + xrec.name + "': no fn_name, skipping");
703 continue;
704 }
705 Expanse::ContainsFn contains_fn;
706 Expanse::CrossingFn on_enter_fn;
707 Expanse::CrossingFn on_exit_fn;
708
709 for (const auto& fname : xrec.fabric_names) {
710 auto fabric = tapestry.get_fabric(fname);
711 if (!fabric)
712 continue;
713 if (!contains_fn) {
714 if (auto ptr = fabric->resolve_expanse_fn(xrec.fn_name); ptr && *ptr)
715 contains_fn = *ptr;
716 }
717 if (!on_enter_fn && !xrec.on_enter_fn_name.empty()) {
718 if (auto ptr = fabric->resolve_crossing_fn(xrec.on_enter_fn_name); ptr && *ptr)
719 on_enter_fn = *ptr;
720 }
721 if (!on_exit_fn && !xrec.on_exit_fn_name.empty()) {
722 if (auto ptr = fabric->resolve_crossing_fn(xrec.on_exit_fn_name); ptr && *ptr)
723 on_exit_fn = *ptr;
724 }
725 }
726
727 if (!contains_fn) {
728 total.warnings.push_back("TapestryExpanse '" + xrec.name
729 + "': fn '" + xrec.fn_name + "' not resolved, skipping");
730 continue;
731 }
732
733 auto expanse = tapestry.create_expanse(
734 xrec.name,
735 std::move(contains_fn),
736 std::move(on_enter_fn),
737 std::move(on_exit_fn));
738
739 for (const auto& fname : xrec.fabric_names) {
740 if (auto fabric = tapestry.get_fabric(fname))
741 fabric->add_expanse(expanse);
742 }
743 ++total.constructed;
744 }
745
746 total.user_state = schema.user_state;
747
749 "StateDecoder::reconstruct(Tapestry): constructed={} patched={} skipped={} warnings={}",
750 total.constructed, total.patched, total.skipped, total.warnings.size());
751 return total;
752}
753
754} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
IO::ImageData image
Definition Decoder.cpp:57
uint32_t width
Definition Decoder.cpp:59
const std::vector< float > * pixels
Definition Decoder.cpp:58
size_t a
const uint8_t * ptr
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:337
bool decode(Fabric &fabric, const std::string &base_path)
Decode and apply to fabric.
Definition Decoder.cpp:127
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:367
Fabric::Kind parse_kind(std::string_view s)
Parse a JSON kind token to Fabric::Kind (case-insensitive).
Definition Schema.hpp:378
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.