11 constexpr uint32_t k_exr_rows = 3;
12 constexpr uint32_t k_channels = 4;
22 static constexpr auto describe()
24 return std::make_tuple(
38 static constexpr auto describe()
40 return std::make_tuple(
50 IO::member(
"query_radius", &RangeSet::query_radius));
58 static constexpr auto describe()
60 return std::make_tuple(
62 IO::member(
"delay", &WiringStep::delay_seconds));
67 std::string
kind {
"commit_driven" };
71 std::optional<std::vector<WiringStep>>
steps;
73 static constexpr auto describe()
75 return std::make_tuple(
92 std::optional<float>
size;
97 static constexpr auto describe()
99 return std::make_tuple(
102 IO::member(
"position", &EntityRecord::position),
103 IO::member(
"intensity", &EntityRecord::intensity),
105 IO::member(
"query_radius", &EntityRecord::query_radius),
108 IO::member(
"influence_fn_name", &EntityRecord::influence_fn_name),
109 IO::member(
"perception_fn_name", &EntityRecord::perception_fn_name),
114 struct FabricSchema {
120 static constexpr auto describe()
122 return std::make_tuple(
123 IO::member(
"version", &FabricSchema::version),
124 IO::member(
"fabric_name", &FabricSchema::fabric_name),
125 IO::member(
"entities", &FabricSchema::entities),
134 float denormalize(
float norm,
const Range& r)
136 return r.min + norm * (r.max - r.min);
150 bool kind_known(
const std::string& s)
152 return s ==
"emitter" || s ==
"sensor" || s ==
"agent";
155 void apply_wiring(Wiring
wiring,
const WiringRecord& rec, std::vector<std::string>& warnings)
157 if (rec.kind ==
"every") {
158 wiring.every(*rec.interval);
160 wiring.for_duration(*rec.duration);
162 if (rec.times && *rec.times > 1) {
165 }
else if (rec.kind ==
"move_to") {
167 for (
const auto& s : *rec.
steps) {
168 wiring.move_to(s.position, s.delay_seconds);
171 if (rec.times && *rec.times > 1) {
174 }
else if (rec.kind !=
"commit_driven") {
175 warnings.emplace_back(
"Unsupported wiring kind '" + rec.kind +
"'; falling back to commit_driven");
186 const std::vector<float>*
pixels {
nullptr };
190 std::optional<PixelView> load_exr(
191 const std::string& exr_path,
192 uint32_t expected_entity_count,
193 uint32_t expected_rows,
194 std::string& error_out)
198 error_out =
"Failed to load EXR: " + exr_path;
202 const auto*
pixels = image_opt->as_float();
204 error_out =
"EXR has no float pixel data: " + exr_path;
207 if (image_opt->channels != k_channels) {
208 error_out =
"EXR channel count mismatch: expected "
209 + std::to_string(k_channels) +
" got " + std::to_string(image_opt->channels);
212 if (image_opt->height != expected_rows) {
213 error_out =
"EXR row count mismatch: expected "
214 + std::to_string(expected_rows) +
" got " + std::to_string(image_opt->height);
217 if (image_opt->width != expected_entity_count) {
218 error_out =
"EXR width (" + std::to_string(image_opt->width)
219 +
") does not match entity count (" + std::to_string(expected_entity_count) +
")";
223 const uint32_t w = image_opt->width;
224 return PixelView { std::move(*image_opt),
nullptr, w };
239 const std::string json_path = base_path +
".json";
240 const std::string exr_path = base_path +
".exr";
243 auto schema_opt = ser.
read<FabricSchema>(json_path);
249 const auto& schema = *schema_opt;
251 if (schema.version < 2 || schema.version > 4) {
252 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version);
257 if (schema.entities.empty()) {
258 m_last_error =
"Schema contains no entities: " + json_path;
263 const uint32_t expected_rows = schema.version >= 4 ? 5 : k_exr_rows;
264 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
270 const auto*
pixels = pv.image.as_float();
271 const uint32_t
width = pv.width;
272 const auto& r = schema.ranges;
274 for (
size_t i = 0; i < schema.entities.size(); ++i) {
275 const auto& entry = schema.entities[i];
277 if (!kind_known(entry.kind)) {
279 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
284 const size_t row0 = (
static_cast<size_t>(0) *
width + i) * k_channels;
285 const size_t row1 = (
static_cast<size_t>(1) *
width + i) * k_channels;
286 const size_t row2 = (
static_cast<size_t>(2) *
width + i) * k_channels;
289 denormalize((*
pixels)[row0 + 0], r.pos_x),
290 denormalize((*
pixels)[row0 + 1], r.pos_y),
291 denormalize((*
pixels)[row0 + 2], r.pos_z),
294 switch (parse_kind(entry.kind)) {
299 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
303 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
305 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
306 entry.id, entry.influence_fn_name, e->fn_name());
309 e->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
311 e->set_color(glm::vec3 {
312 denormalize((*
pixels)[row1 + 0], r.color_r),
313 denormalize((*
pixels)[row1 + 1], r.color_g),
314 denormalize((*
pixels)[row1 + 2], r.color_b),
318 e->set_size(denormalize((*
pixels)[row1 + 3], r.size));
320 e->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
327 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
331 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
333 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
334 entry.id, entry.perception_fn_name, s->fn_name());
337 s->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
344 "StateDecoder: id {} not found as Agent, skipping", entry.id);
348 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
350 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
351 entry.id, entry.perception_fn_name,
a->perception_fn_name());
353 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
355 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
356 entry.id, entry.influence_fn_name,
a->influence_fn_name());
359 a->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
361 a->set_color(glm::vec3 {
362 denormalize((*
pixels)[row1 + 0], r.color_r),
363 denormalize((*
pixels)[row1 + 1], r.color_g),
364 denormalize((*
pixels)[row1 + 2], r.color_b),
368 a->set_size(denormalize((*
pixels)[row1 + 3], r.size));
370 a->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
371 a->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
380 "StateDecoder: patched {} entities ({} missing) from {} + {}",
395 const std::string json_path = base_path +
".json";
396 const std::string exr_path = base_path +
".exr";
399 auto schema_opt = ser.
read<FabricSchema>(json_path);
405 const auto& schema = *schema_opt;
407 if (schema.version < 2 || schema.version > 4) {
408 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version);
413 if (schema.entities.empty()) {
414 m_last_error =
"Schema contains no entities: " + json_path;
419 const uint32_t expected_rows = schema.version >= 4 ? 5 : k_exr_rows;
420 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
426 const auto*
pixels = pv.image.as_float();
427 const uint32_t
width = pv.width;
428 const auto& r = schema.ranges;
430 const auto existing_ids = fabric.
all_ids();
431 const std::unordered_set<uint32_t>
existing(existing_ids.begin(), existing_ids.end());
433 for (
size_t i = 0; i < schema.entities.size(); ++i) {
434 const auto& entry = schema.entities[i];
436 if (!kind_known(entry.kind)) {
437 result.
warnings.push_back(
"Unknown kind '" + entry.kind
438 +
"' for id " + std::to_string(entry.id) +
", skipping");
443 const size_t row0 = (
static_cast<size_t>(0) *
width + i) * k_channels;
444 const size_t row1 = (
static_cast<size_t>(1) *
width + i) * k_channels;
445 const size_t row2 = (
static_cast<size_t>(2) *
width + i) * k_channels;
448 denormalize((*
pixels)[row0 + 0], r.pos_x),
449 denormalize((*
pixels)[row0 + 1], r.pos_y),
450 denormalize((*
pixels)[row0 + 2], r.pos_z),
453 const float radius = denormalize((*
pixels)[row2 + 0], r.radius);
456 auto read_color = [&]() -> glm::vec3 {
458 denormalize((*
pixels)[row1 + 0], r.color_r),
459 denormalize((*
pixels)[row1 + 1], r.color_g),
460 denormalize((*
pixels)[row1 + 2], r.color_b),
463 auto read_size = [&]() {
464 return denormalize((*
pixels)[row1 + 3], r.size);
471 switch (parse_kind(entry.kind)) {
478 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
479 result.
warnings.push_back(
"Emitter " + std::to_string(entry.id)
480 +
" fn_name mismatch: schema='" + entry.influence_fn_name
481 +
"' live='" + e->fn_name() +
"'");
486 e->set_color(read_color());
489 e->set_size(read_size());
500 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
501 result.
warnings.push_back(
"Sensor " + std::to_string(entry.id)
502 +
" fn_name mismatch: schema='" + entry.perception_fn_name
503 +
"' live='" + s->fn_name() +
"'");
515 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
516 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
517 +
" perception_fn mismatch: schema='" + entry.perception_fn_name +
"'");
519 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
520 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
521 +
" influence_fn mismatch: schema='" + entry.influence_fn_name +
"'");
526 a->set_color(read_color());
529 a->set_size(read_size());
542 switch (parse_kind(entry.kind)) {
546 if (!fn_ptr || !*fn_ptr) {
547 result.
warnings.push_back(
"Emitter: unknown influence_fn '"
548 + entry.influence_fn_name +
"', using no-op");
553 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
556 emitter->set_radius(
radius);
558 emitter->set_color(read_color());
561 emitter->set_size(read_size());
564 if (emitter->id() != entry.id) {
565 result.
warnings.push_back(
"Emitter schema_id=" + std::to_string(entry.id)
566 +
" reconstructed as runtime_id=" + std::to_string(emitter->id()));
574 if (!fn_ptr || !*fn_ptr) {
575 result.
warnings.push_back(
"Sensor: unknown perception_fn '"
576 + entry.perception_fn_name +
"', using no-op");
582 entry.perception_fn_name, std::move(fn));
585 if (sensor->id() != entry.id) {
586 result.
warnings.push_back(
"Sensor schema_id=" + std::to_string(entry.id)
587 +
" reconstructed as runtime_id=" + std::to_string(sensor->id()));
595 if (!pfn_ptr || !*pfn_ptr) {
596 result.
warnings.push_back(
"Agent: unknown perception_fn '"
597 + entry.perception_fn_name +
"', using no-op");
604 if (!ifn_ptr || !*ifn_ptr) {
605 result.
warnings.push_back(
"Agent: unknown influence_fn '"
606 + entry.influence_fn_name +
"', using no-op");
612 entry.perception_fn_name, std::move(pfn),
613 entry.influence_fn_name, std::move(ifn));
616 agent->set_radius(
radius);
619 agent->set_color(read_color());
622 agent->set_size(read_size());
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()));
638 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
std::string perception_fn_name
std::vector< EntityRecord > entities
std::optional< double > duration
std::optional< double > interval
const std::vector< float > * pixels
std::optional< std::vector< WiringStep > > steps
std::optional< glm::vec3 > color
std::string influence_fn_name
std::optional< size_t > times
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
std::function< void(const InfluenceContext &)> InfluenceFn
std::function< void(const InfluenceContext &)> InfluenceFn
std::shared_ptr< Sensor > get_sensor(uint32_t id) const
Get the Sensor registered under id.
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
std::shared_ptr< Emitter::InfluenceFn > resolve_influence_fn(std::string_view name) const
Look up a registered influence function by name.
std::shared_ptr< Sensor::PerceptionFn > resolve_perception_fn(std::string_view name) const
Look up a registered perception function by name.
std::shared_ptr< Agent > get_agent(uint32_t id) const
Get the Agent registered under id.
std::shared_ptr< Emitter > get_emitter(uint32_t id) const
Get the Emitter registered under id.
Wiring wire(std::shared_ptr< Emitter > emitter)
Begin wiring an Emitter into the Fabric.
Orchestrates spatial indexing and scheduling for Nexus objects.
std::function< void(const PerceptionContext &)> PerceptionFn
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.
constexpr auto member(std::string_view key, T Class::*ptr)
constexpr auto opt_member(std::string_view key, std::optional< T > Class::*ptr)
@ FileIO
Filesystem I/O operations.
@ Runtime
General runtime operations (default fallback)
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
Data passed to an Emitter or Agent influence function on each commit.
Data passed to a Sensor or Agent perception function on each commit.
std::vector< std::string > warnings
Result of a reconstruct() call.