20 bool version_supported(uint32_t version)
25 std::string version_error(uint32_t version)
27 return "Unsupported schema version: " + std::to_string(version)
32 void note_older_version(uint32_t version, std::vector<std::string>& warnings)
35 warnings.push_back(
"Schema version " + std::to_string(version)
37 +
": fields added since are absent and take their defaults");
41 float denormalize(
float norm,
const State::Range& r)
43 return r.min + norm * (r.max - r.min);
48 const std::vector<float>*
pixels {
nullptr };
52 std::optional<PixelView> load_exr(
53 const std::string& exr_path,
54 uint32_t expected_entity_count,
55 uint32_t expected_rows,
56 std::string& error_out)
60 error_out =
"Failed to load EXR: " + exr_path;
64 const auto*
pixels = image_opt->as_float();
66 error_out =
"EXR has no float pixel data: " + exr_path;
70 error_out =
"EXR channel count mismatch: expected "
71 + std::to_string(
State::k_channels) +
" got " + std::to_string(image_opt->channels);
74 if (image_opt->height != expected_rows) {
75 error_out =
"EXR row count mismatch: expected "
76 + std::to_string(expected_rows) +
" got " + std::to_string(image_opt->height);
79 if (image_opt->width != expected_entity_count) {
80 error_out =
"EXR width (" + std::to_string(image_opt->width)
81 +
") does not match entity count (" + std::to_string(expected_entity_count) +
")";
85 const uint32_t w = image_opt->width;
86 return PixelView { .image = std::move(*image_opt), .pixels =
nullptr, .width = w };
102 const State::EntityRecord& entry,
103 const std::vector<float>&
pixels,
106 const State::RangeSet& r)
108 const auto px = [&](
size_t row,
size_t channel) {
113 if (entry.position) {
114 f.position = glm::vec3 {
136 template <
typename Fn>
138 const std::shared_ptr<Fn>& resolved,
139 const std::string&
name,
140 std::string_view
label,
141 std::vector<std::string>& warnings)
146 if (!resolved || !*resolved) {
147 warnings.push_back(std::string(
label) +
": unknown '" +
name +
"', left empty");
155 const State::WiringRecord& rec,
156 std::string_view who,
157 std::vector<std::string>& warnings)
159 const auto fallback = [&](
const std::string& why) {
160 warnings.push_back(std::string(who) +
" wiring: " + why +
", commit_driven instead");
166 fallback(
"every has no interval");
169 wiring.every(*rec.interval,
172 wiring.for_duration(*rec.duration,
175 if (rec.times && *rec.times > 1) {
176 wiring.times(*rec.times);
178 if (!rec.position_fn_name.empty()) {
179 warnings.push_back(std::string(who) +
" wiring: position function '"
180 + rec.position_fn_name +
"' cannot be restored");
186 for (
const auto& s : *rec.steps) {
187 wiring.move_to(s.position, s.delay_seconds);
190 if (rec.times && *rec.times > 1) {
191 wiring.times(*rec.times);
196 fallback(
"scroll needs a live window");
199 fallback(
"a key trigger needs a live window");
202 fallback(
"a mouse trigger needs a live window");
205 fallback(
"a network trigger needs a live source");
208 fallback(
"a window event trigger needs a live source");
211 fallback(
"factory '" + rec.factory_name +
"' cannot be restored");
214 fallback(
"bind callables cannot be restored");
217 fallback(
"the saved wiring kind is unsupported");
229 void apply_locus_nav(Locus& locus,
const State::LocusNavRecord& nav)
231 auto& state = locus.nav();
233 state.fov_radians = nav.fov;
234 state.near_plane = nav.near_plane;
235 state.far_plane = nav.far_plane;
236 state.move_speed = nav.speed;
237 state.mouse_sensitivity = nav.mouse_sensitivity;
238 state.scroll_speed = nav.scroll_speed;
240 const glm::vec3 look = nav.target - nav.eye;
241 if (glm::dot(look, look) > 0.0F) {
242 const glm::vec3 dir = glm::normalize(look);
243 state.yaw = std::atan2(dir.x, dir.z);
244 state.pitch = std::asin(glm::clamp(dir.y, -1.0F, 1.0F));
246 locus.set_aspect(nav.aspect);
249 void apply_presence(Presence& presence,
const State::EntityRecord& entry)
251 if (!entry.falloff_curve_name.empty()) {
252 if (
auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name)) {
253 presence.set_falloff_curve(*fc);
256 if (entry.falloff_radius) {
257 presence.set_falloff_radius(*entry.falloff_radius);
261 void apply_emitter(Emitter& emitter,
const State::EntityRecord& entry,
const Fields& f)
264 emitter.set_position(*f.position);
266 emitter.clear_position();
268 emitter.set_intensity(f.intensity);
269 emitter.set_radius(f.radius);
271 emitter.set_color(f.color);
274 emitter.set_size(f.size);
278 void apply_sensor(Sensor& sensor,
const Fields& f)
281 sensor.set_position(*f.position);
283 sensor.clear_position();
285 sensor.set_query_radius(f.query_radius);
290 const State::EntityRecord& entry,
292 std::vector<std::string>& warnings)
295 agent.set_position(*f.position);
297 agent.clear_position();
299 if (entry.orientation) {
300 agent.set_orientation(glm::quat(
301 entry.orientation->w, entry.orientation->x, entry.orientation->y, entry.orientation->z));
303 agent.set_intensity(f.intensity);
304 agent.set_radius(f.radius);
305 agent.set_query_radius(f.query_radius);
307 agent.set_color(f.color);
310 agent.set_size(f.size);
313 if (entry.locus_nav) {
314 if (
auto* locus =
dynamic_cast<Locus*
>(&agent)) {
315 apply_locus_nav(*locus, *entry.locus_nav);
317 warnings.push_back(
"Agent " + std::to_string(entry.id)
318 +
" has locus_nav in schema but is not a Locus at runtime");
321 if (
auto* presence =
dynamic_cast<Presence*
>(&agent)) {
322 apply_presence(*presence, entry);
330 std::string who_is(
const State::EntityRecord& entry)
332 return entry.kind +
" " + std::to_string(entry.id);
335 void note_name_mismatch(
336 const State::EntityRecord& entry,
337 std::string_view what,
338 const std::string& saved,
339 const std::string& live,
340 std::vector<std::string>& warnings)
342 if (!saved.empty() && saved != live) {
343 warnings.push_back(who_is(entry) +
" " + std::string(what)
344 +
" mismatch: schema='" + saved +
"' live='" + live +
"'");
350 const State::EntityRecord& entry,
352 std::vector<std::string>& warnings)
356 auto e = fabric.get_emitter(entry.id);
360 note_name_mismatch(entry,
"fn_name", entry.influence_fn_name, e->fn_name(), warnings);
361 apply_emitter(*e, entry, f);
365 auto s = fabric.get_sensor(entry.id);
369 note_name_mismatch(entry,
"fn_name", entry.perception_fn_name, s->fn_name(), warnings);
374 auto a = fabric.get_agent(entry.id);
378 note_name_mismatch(entry,
"perception_fn_name", entry.perception_fn_name,
a->perception_fn_name(), warnings);
379 note_name_mismatch(entry,
"influence_fn_name", entry.influence_fn_name,
a->influence_fn_name(), warnings);
380 apply_agent(*
a, entry, f, warnings);
387 void note_runtime_id(
388 const State::EntityRecord& entry,
390 std::vector<std::string>& warnings)
392 if (runtime_id != entry.id) {
393 warnings.push_back(who_is(entry) +
" reconstructed as runtime_id=" + std::to_string(runtime_id));
397 std::shared_ptr<Agent> construct_agent(
399 const State::EntityRecord& entry,
401 std::vector<std::string>& warnings)
403 const std::string who = who_is(entry);
405 auto pfn = resolve_fn(fabric.resolve_perception_fn(entry.perception_fn_name),
406 entry.perception_fn_name, who +
" perception_fn", warnings);
407 auto ifn = resolve_fn(fabric.resolve_influence_fn(entry.influence_fn_name),
408 entry.influence_fn_name, who +
" influence_fn", warnings);
410 if (entry.subkind ==
"locus" && entry.locus_nav) {
411 const auto& nav = *entry.locus_nav;
412 const Kinesis::NavigationConfig config {
413 .initial_eye = nav.eye,
414 .initial_target = nav.target,
415 .fov_radians = nav.fov,
416 .near_plane = nav.near_plane,
417 .far_plane = nav.far_plane,
418 .move_speed = nav.speed,
419 .mouse_sensitivity = nav.mouse_sensitivity,
420 .scroll_speed = nav.scroll_speed,
422 warnings.push_back(who +
": view_targets must be reconnected by caller");
423 return std::make_shared<Locus>(config, f.query_radius,
424 entry.perception_fn_name, std::move(pfn),
425 entry.influence_fn_name, std::move(ifn));
428 if (entry.subkind ==
"presence") {
429 auto rfn = resolve_fn(fabric.resolve_radiate_fn(entry.radiate_fn_name),
430 entry.radiate_fn_name, who +
" radiate_fn", warnings);
431 return std::make_shared<Presence>(f.query_radius,
432 entry.perception_fn_name, std::move(pfn),
433 entry.influence_fn_name, std::move(ifn),
434 entry.radiate_fn_name, std::move(rfn));
437 if (entry.subkind ==
"locus") {
438 warnings.push_back(who +
": no locus_nav in schema, reconstructed as plain Agent");
439 }
else if (!entry.subkind.empty()) {
440 warnings.push_back(who +
": unknown subkind '" + entry.subkind
441 +
"', reconstructed as plain Agent");
444 if (entry.perception_fn_name.empty() && entry.influence_fn_name.empty()) {
445 return std::make_shared<Agent>(f.query_radius);
447 return std::make_shared<Agent>(f.query_radius,
448 entry.perception_fn_name, std::move(pfn),
449 entry.influence_fn_name, std::move(ifn));
452 void construct_entity(
454 const State::EntityRecord& entry,
456 std::vector<std::string>& warnings)
458 const std::string who = who_is(entry);
460 if (!entry.audio_sinks.empty() || !entry.render_sinks.empty()) {
461 warnings.push_back(who +
": sinks are not restored, they need a BufferManager and a window");
466 std::shared_ptr<Emitter> emitter;
467 if (entry.influence_fn_name.empty()) {
468 emitter = std::make_shared<Emitter>();
470 emitter = std::make_shared<Emitter>(entry.influence_fn_name,
471 resolve_fn(fabric.resolve_influence_fn(entry.influence_fn_name),
472 entry.influence_fn_name, who +
" influence_fn", warnings));
474 apply_emitter(*emitter, entry, f);
475 auto wiring = fabric.wire(emitter);
476 note_runtime_id(entry, emitter->id(), warnings);
477 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
481 std::shared_ptr<Sensor> sensor;
482 if (entry.perception_fn_name.empty()) {
483 sensor = std::make_shared<Sensor>(f.query_radius);
485 sensor = std::make_shared<Sensor>(f.query_radius, entry.perception_fn_name,
486 resolve_fn(fabric.resolve_perception_fn(entry.perception_fn_name),
487 entry.perception_fn_name, who +
" perception_fn", warnings));
489 apply_sensor(*sensor, f);
490 auto wiring = fabric.wire(sensor);
491 note_runtime_id(entry, sensor->id(), warnings);
492 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
496 auto agent = construct_agent(fabric, entry, f, warnings);
497 apply_agent(*agent, entry, f, warnings);
498 auto wiring = fabric.wire(agent);
499 note_runtime_id(entry, agent->id(), warnings);
500 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
510 enum class Restored : uint8_t {
516 struct ExpanseParts {
522 template <
typename Record>
523 void apply_bounds(Expanse& expanse,
const Record& rec)
525 if (rec.bounds_min && rec.bounds_max) {
526 expanse.set_bounds({ .min = *rec.bounds_min, .max = *rec.bounds_max });
536 template <
typename Record>
537 bool expanse_restorable(
const Record& rec,
const std::string&
label, std::vector<std::string>& warnings)
539 if (rec.subkind ==
"hull") {
540 warnings.push_back(
label +
": a Hull wraps a live buffer that is not encoded, skipping");
543 if (rec.subkind ==
"mantle") {
544 warnings.push_back(
label +
": a Mantle is restored as a plain boxed Expanse, its look must be made again");
545 }
else if (!rec.subkind.empty()) {
546 warnings.push_back(
label +
": unknown subkind '" + rec.subkind +
"', restored as a plain Expanse");
548 if (rec.fn_name.empty() && !(rec.bounds_min && rec.bounds_max)) {
549 warnings.push_back(
label +
": neither fn_name nor bounds, skipping");
555 template <
typename Record>
556 bool resolve_expanse_parts(
558 const std::vector<Fabric*>& fabrics,
559 const std::string&
label,
561 std::vector<std::string>& warnings)
563 if (!rec.fn_name.empty()) {
564 for (
const Fabric* fabric : fabrics) {
565 if (
auto ptr = fabric->resolve_expanse_fn(rec.fn_name);
ptr && *
ptr) {
566 parts.contains = *
ptr;
570 if (!parts.contains) {
571 warnings.push_back(
label +
": fn '" + rec.fn_name +
"' not in registry, skipping");
580 for (
const Fabric* fabric : fabrics) {
581 if (
auto ptr = fabric->resolve_crossing_fn(
name);
ptr && *
ptr) {
586 warnings.push_back(
label +
": crossing '" +
name +
"' not in registry, left empty");
588 resolve_crossing(rec.on_enter_fn_name, parts.on_enter);
589 resolve_crossing(rec.on_exit_fn_name, parts.on_exit);
599 std::shared_ptr<Expanse> find_equivalent(
const Fabric& fabric,
const State::ExpanseRecord& rec)
601 for (uint32_t
id : fabric.all_expanse_ids()) {
602 auto x = fabric.get_expanse(
id);
606 const Expanse& live = *x;
607 if (
typeid(live) !=
typeid(Expanse)) {
610 if (x->fn_name() != rec.fn_name
611 || x->on_enter_fn_name() != rec.on_enter_fn_name
612 || x->on_exit_fn_name() != rec.on_exit_fn_name) {
615 if (rec.fn_name.empty()
616 && (!x->bounds() || x->bounds()->min != *rec.bounds_min || x->bounds()->max != *rec.bounds_max)) {
624 Restored restore_expanse(
626 const State::ExpanseRecord& rec,
627 std::vector<std::string>& warnings)
629 const std::string
label =
"Expanse " + std::to_string(rec.id);
630 if (!expanse_restorable(rec,
label, warnings)) {
631 return Restored::Skipped;
634 if (
auto existing = find_equivalent(fabric, rec)) {
636 return Restored::Patched;
640 if (!resolve_expanse_parts(rec, { &fabric },
label, parts, warnings)) {
641 return Restored::Skipped;
644 auto expanse = std::make_shared<Expanse>(
646 rec.on_enter_fn_name,
648 std::move(parts.contains),
649 std::move(parts.on_enter),
650 std::move(parts.on_exit));
651 apply_bounds(*expanse, rec);
652 fabric.add_expanse(std::move(expanse));
653 return Restored::Constructed;
656 std::optional<float> read_cell_size(
const std::string& json_path)
658 IO::JSONSerializer ser;
659 auto schema = ser.read<State::FabricSchema>(json_path);
660 return schema ? schema->cell_size : std::nullopt;
663 void log_warnings(
const std::vector<std::string>& warnings)
665 for (
const auto& w : warnings) {
682 const std::string json_path = base_path +
".json";
683 const std::string exr_path = base_path +
".exr";
692 const auto& schema = *schema_opt;
694 if (!version_supported(schema.version)) {
700 if (schema.entities.empty()) {
701 m_last_error =
"Schema contains no entities: " + json_path;
711 const auto*
pixels = pv_opt->image.as_float();
712 const uint32_t
width = pv_opt->width;
714 std::vector<std::string> warnings;
715 note_older_version(schema.version, warnings);
717 for (
size_t i = 0; i < schema.entities.size(); ++i) {
718 const auto& entry = schema.entities[i];
721 warnings.push_back(
"unknown kind '" + entry.kind +
"' for id "
722 + std::to_string(entry.id) +
", skipping");
727 const Fields fields = read_fields(entry, *
pixels,
width, i, schema.ranges);
728 if (patch_entity(fabric, entry, fields, warnings)) {
731 warnings.push_back(
"id " + std::to_string(entry.id) +
" not found as "
732 + entry.kind +
", skipping");
737 for (
const auto& xrec : schema.expanses) {
738 restore_expanse(fabric, xrec, warnings);
741 log_warnings(warnings);
744 "StateDecoder: patched {} entities ({} missing) from {} + {}",
759 const std::string json_path = base_path +
".json";
760 const std::string exr_path = base_path +
".exr";
769 const auto& schema = *schema_opt;
771 if (!version_supported(schema.version)) {
777 if (schema.entities.empty()) {
778 m_last_error =
"Schema contains no entities: " + json_path;
788 const auto*
pixels = pv_opt->image.as_float();
789 const uint32_t
width = pv_opt->width;
791 note_older_version(schema.version, result.
warnings);
793 const auto existing_ids = fabric.
all_ids();
794 const std::unordered_set<uint32_t>
existing(existing_ids.begin(), existing_ids.end());
796 for (
size_t i = 0; i < schema.entities.size(); ++i) {
797 const auto& entry = schema.entities[i];
800 result.
warnings.push_back(
"Unknown kind '" + entry.kind
801 +
"' for id " + std::to_string(entry.id) +
", skipping");
806 const Fields fields = read_fields(entry, *
pixels,
width, i, schema.ranges);
809 if (patch_entity(fabric, entry, fields, result.
warnings)) {
815 construct_entity(fabric, entry, fields, result.
warnings);
820 for (
const auto& xrec : schema.expanses) {
821 switch (restore_expanse(fabric, xrec, result.
warnings)) {
822 case Restored::Constructed:
825 case Restored::Patched:
828 case Restored::Skipped:
835 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
842 Tapestry& tapestry,
const std::string& base_dir)
847 const std::string tapestry_path = base_dir +
"/tapestry.json";
855 const auto& schema = *schema_opt;
857 if (!version_supported(schema.version)) {
863 for (
const auto& ref : schema.fabrics) {
867 read_cell_size(ref.base_path +
".json").value_or(1.0F));
870 total.constructed += result.constructed;
871 total.patched += result.patched;
872 total.skipped += result.skipped;
873 for (
auto& w : result.warnings) {
874 total.warnings.push_back(ref.name +
": " + std::move(w));
878 for (
const auto& xrec : schema.expanses) {
879 const std::string
label =
"TapestryExpanse '" + xrec.name +
"'";
880 if (!expanse_restorable(xrec,
label,
total.warnings)) {
885 std::vector<Fabric*> fabrics;
886 for (
const auto& fname : xrec.fabric_names) {
887 if (
auto fabric = tapestry.
get_fabric(fname)) {
888 fabrics.push_back(fabric.get());
890 total.warnings.push_back(
label +
": fabric '" + fname +
"' not found");
894 std::shared_ptr<Expanse> expanse = tapestry.
get_expanse(xrec.name);
895 const bool existed = expanse !=
nullptr;
898 apply_bounds(*expanse, xrec);
901 if (!resolve_expanse_parts(xrec, fabrics,
label, parts,
total.warnings)) {
906 if (xrec.fn_name.empty()) {
910 std::move(parts.on_enter),
911 std::move(parts.on_exit));
915 std::move(parts.contains),
916 std::move(parts.on_enter),
917 std::move(parts.on_exit));
918 apply_bounds(*expanse, xrec);
925 expanse->set_fn_name(xrec.fn_name);
926 expanse->set_on_enter_fn_name(xrec.on_enter_fn_name);
927 expanse->set_on_exit_fn_name(xrec.on_exit_fn_name);
930 for (
Fabric* fabric : fabrics) {
931 fabric->add_expanse(expanse);
940 total.user_state = schema.user_state;
943 "StateDecoder::reconstruct(Tapestry): constructed={} patched={} skipped={} warnings={}",
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
Expanse::ContainsFn contains
std::optional< glm::vec3 > position
Expanse::CrossingFn on_enter
Expanse::CrossingFn on_exit
const std::vector< float > * pixels
std::vector< float > * out
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, std::optional< Kakshya::Region > region=std::nullopt)
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(uint32_t)> CrossingFn
std::function< bool(const glm::vec3 &)> ContainsFn
A defined region of space with a containment test and crossing actions.
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Orchestrates spatial indexing and scheduling for Nexus objects.
ReconstructionResult reconstruct(Fabric &fabric, const std::string &base_path)
Patch existing entities and construct missing ones from schema.
bool decode(Fabric &fabric, const std::string &base_path)
Decode and apply to fabric.
std::shared_ptr< Fabric > get_fabric(std::string_view name) const
Look up a named Fabric.
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.
std::shared_ptr< Expanse > get_expanse(std::string_view name) const
Look up a named Expanse.
std::shared_ptr< Fabric > create_fabric(float cell_size=1.0F)
create_fabric a new unnamed Fabric into the Tapestry.
Owner of one or more Fabrics and the shared state they rely on.
Kakshya::ImageData ImageData
@ 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].
constexpr uint32_t k_schema_version
Current schema version written by StateEncoder and accepted by StateDecoder.
bool kind_known(std::string_view s)
Return true if s maps to a known Fabric::Kind token.
Fabric::Kind parse_kind(std::string_view s)
Parse a JSON kind token to Fabric::Kind (case-insensitive).
constexpr uint32_t k_schema_min_version
Oldest schema version StateDecoder still reads.
constexpr uint32_t k_exr_rows
RGBA32F EXR layout constants shared between encoder and decoder.
constexpr uint32_t k_channels
@ SAMPLE_ACCURATE
Coroutine is sample-accurate.
Axis-aligned bounding box in 3D world space.
std::vector< std::string > warnings
Result of a reconstruct() call.