203{
205
206
207
208
209 struct InternalRecord {
210 uint32_t id;
213 std::optional<glm::vec4> orientation;
217 std::optional<glm::vec3>
color;
218 std::optional<float>
size;
219 std::string influence_fn_name;
220 std::string perception_fn_name;
221
222 float entity_type_norm { 0.0F };
223 float trigger_kind { 0.0F };
224 float time_kind { 0.0F };
225
226 uint32_t sink_type { 0 };
227 uint32_t first_audio_channel { 0 };
228 };
229
230 std::vector<InternalRecord> records;
231
232 auto ids = fabric.all_ids();
233 std::ranges::sort(
ids);
234
235 for (uint32_t
id :
ids) {
236 const auto k = fabric.kind(id);
237 switch (k) {
239 auto e = fabric.get_emitter(id);
240 if (!e) {
241 continue;
242 }
243 if (e->fn() && e->fn_name().empty()) {
245 "StateEncoder: Emitter {} has a function but no fn_name", id);
246 }
247 auto& rec = records.emplace_back(InternalRecord {
248 .id = id,
249 .kind = k,
250 .position = e->position(),
252 .radius = e->radius(),
253 .color = e->color(),
254 .size = e->size(),
255 .influence_fn_name = e->fn_name(),
256 .entity_type_norm = 0.0F,
257 });
258 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
259 rec.sink_type = (e->audio_sinks().empty() ? 0
U : 1U)
260 | (e->render_sinks().empty() ? 0
U : 2U);
261 if (!e->audio_sinks().empty())
262 rec.first_audio_channel = e->audio_sinks().front().channel;
263 break;
264 }
266 auto s = fabric.get_sensor(id);
267 if (!s) {
268 continue;
269 }
270 if (s->fn() && s->fn_name().empty()) {
272 "StateEncoder: Sensor {} has a function but no fn_name", id);
273 }
274 auto& rec = records.emplace_back(InternalRecord {
275 .id = id,
276 .kind = k,
277 .position = s->position(),
279 .perception_fn_name = s->fn_name(),
280 .entity_type_norm = 0.333F,
281 });
282 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
283 break;
284 }
286 auto a = fabric.get_agent(
id);
288 continue;
289 }
290 if (
a->perception_fn() &&
a->perception_fn_name().empty()) {
292 "StateEncoder: Agent {} has a perception function but no perception_fn_name", id);
293 }
294 if (
a->influence_fn() &&
a->influence_fn_name().empty()) {
296 "StateEncoder: Agent {} has an influence function but no influence_fn_name", id);
297 }
298 auto& rec = records.emplace_back(InternalRecord {
299 .id = id,
300 .kind = k,
301 .position =
a->position(),
303 .radius =
a->radius(),
304 .query_radius =
a->query_radius(),
307 .influence_fn_name =
a->influence_fn_name(),
308 .perception_fn_name =
a->perception_fn_name(),
309 .entity_type_norm = 0.667F,
310 });
311 if (
const auto&
q =
a->orientation()) {
312 rec.orientation = glm::vec4(
q->x,
q->y,
q->z,
q->w);
313 }
314 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
315 rec.sink_type = (
a->audio_sinks().empty() ? 0
U : 1U)
316 | (
a->render_sinks().empty() ? 0
U : 2U);
317 if (!
a->audio_sinks().empty())
318 rec.first_audio_channel =
a->audio_sinks().front().channel;
319 break;
320 }
321 }
322 }
323
324 if (records.empty()) {
327 return false;
328 }
329
330
331
332
333 State::RangeSet rs;
334 bool init_pos_x = false, init_pos_y = false, init_pos_z = false;
335 bool init_intensity = false, init_radius = false, init_query_radius = false;
336 bool init_color_r = false, init_color_g = false, init_color_b = false;
337 bool init_size = false;
338
339 for (const auto& rec : records) {
340 if (rec.position) {
341 expand_range(rs.pos_x, rec.position->x, init_pos_x);
342 expand_range(rs.pos_y, rec.position->y, init_pos_y);
343 expand_range(rs.pos_z, rec.position->z, init_pos_z);
344 }
345
347 expand_range(rs.intensity, rec.intensity, init_intensity);
348 expand_range(rs.radius, rec.radius, init_radius);
349 if (rec.color) {
350 expand_range(rs.color_r, rec.color->r, init_color_r);
351 expand_range(rs.color_g, rec.color->g, init_color_g);
352 expand_range(rs.color_b, rec.color->b, init_color_b);
353 }
354 if (rec.size) {
355 expand_range(rs.size, *rec.size, init_size);
356 }
357 }
359 expand_range(rs.query_radius, rec.query_radius, init_query_radius);
360 }
361 }
362
363
364
365
366 const auto width =
static_cast<uint32_t
>(records.size());
367
372 image.format = Portal::Graphics::ImageFormat::RGBA32F;
373
375
376 for (size_t i = 0; i < records.size(); ++i) {
377 const auto& rec = records[i];
378
380 if (rec.position) {
384 }
386
388 if (rec.color) {
392 }
393 if (rec.size) {
395 }
396
400
402 pixels[row3 + 0] =
static_cast<float>(rec.id);
403 pixels[row3 + 1] = rec.entity_type_norm;
404 pixels[row3 + 2] = rec.trigger_kind;
405 pixels[row3 + 3] = rec.time_kind;
406
408 pixels[row4 + 0] =
static_cast<float>(rec.sink_type);
409 pixels[row4 + 1] =
static_cast<float>(rec.first_audio_channel);
410 }
411
413
414
415
416
417 const std::string exr_path = base_path + ".exr";
419 if (!writer) {
422 return false;
423 }
424
425 IO::ImageWriteOptions options;
426
427 if (!writer->write(exr_path,
image, options)) {
428 m_last_error =
"EXR write failed: " + writer->get_last_error();
430 return false;
431 }
432
433
434
435
436 State::FabricSchema schema;
438 schema.fabric_name = fabric.name();
439 schema.cell_size = fabric.m_cell_size;
440 schema.ranges = rs;
441 schema.entities.reserve(records.size());
442
443 for (const auto& rec : records) {
444 State::EntityRecord ent;
445 ent.id = rec.id;
447 ent.position = rec.position;
448 ent.orientation = rec.orientation;
449 ent.intensity = rec.intensity;
450 ent.radius = rec.radius;
451 ent.query_radius = rec.query_radius;
452 ent.color = rec.color;
453 ent.size = rec.size;
454 ent.influence_fn_name = rec.influence_fn_name;
455 ent.perception_fn_name = rec.perception_fn_name;
457
459 auto e = fabric.get_emitter(rec.id);
460 fill_sinks(ent, e->audio_sinks(), e->render_sinks());
462 auto a = fabric.get_agent(rec.id);
463 fill_sinks(ent,
a->audio_sinks(),
a->render_sinks());
464
465 if (
auto locus = std::dynamic_pointer_cast<Locus>(
a)) {
466 ent.subkind = "locus";
467 const auto& nav = locus->nav();
468 ent.locus_nav = State::LocusNavRecord {
469 .eye = nav.eye,
470 .target = nav.eye + glm::vec3 { std::cos(nav.pitch) * std::sin(nav.yaw), std::sin(nav.pitch), std::cos(nav.pitch) * std::cos(nav.yaw) },
471 .fov = nav.fov_radians,
472 .near_plane = nav.near_plane,
473 .far_plane = nav.far_plane,
474 .speed = nav.move_speed,
475 .mouse_sensitivity = nav.mouse_sensitivity,
476 .scroll_speed = nav.scroll_speed,
477 .aspect = locus->aspect(),
478 };
479 }
else if (
auto presence = std::dynamic_pointer_cast<Presence>(
a)) {
480 ent.subkind = "presence";
481 ent.radiate_fn_name = presence->radiate_fn_name();
482 ent.falloff_radius = presence->falloff_radius() != presence->query_radius()
483 ? std::optional<float>(presence->falloff_radius())
485 if (auto fc = presence->falloff_curve()) {
487 } else {
489 "StateEncoder: Presence {} has a custom falloff, which is not encoded", rec.id);
490 }
491 }
492 }
493
494 schema.entities.push_back(std::move(ent));
495 }
496
497 for (uint32_t xid : fabric.all_expanse_ids()) {
498 const auto x = fabric.get_expanse(xid);
499 if (!x || tapestry_owned.contains(x.get()))
500 continue;
501 State::ExpanseRecord record { .id = xid };
503 if (record.subkind.empty() && record.fn_name.empty() && !record.bounds_min) {
505 "StateEncoder: Expanse {} has neither fn_name nor bounds, skipping", xid);
506 continue;
507 }
508 schema.expanses.push_back(std::move(record));
509 }
510
511 IO::JSONSerializer ser;
512 const std::string json_path = base_path + ".json";
513 if (!ser.write(json_path, schema)) {
514 m_last_error =
"Failed to write schema: " + ser.last_error();
516 return false;
517 }
518
520 "StateEncoder: wrote {} entities to {} + {}",
521 records.size(), exr_path, json_path);
522
523 return true;
524}
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
std::optional< glm::vec3 > position
const std::vector< float > * pixels
std::vector< uint64_t > ids
static WriterRegistry & instance()
std::unique_ptr< Writer > create_writer(const std::string &filepath) const
static void fill_expanse(Record &record, const Expanse &expanse)
static State::WiringRecord build_wiring(const Fabric &fabric, uint32_t id)
Kakshya::ImageData ImageData
@ FileIO
Filesystem I/O operations.
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
void normalize(std::span< double > data, double target_min, double target_max) noexcept
Normalize to [target_min, target_max] in-place Single-pass min/max reduction followed by a single tra...
constexpr uint32_t k_schema_version
Current schema version written by StateEncoder and accepted by StateDecoder.
constexpr uint32_t k_exr_rows
RGBA32F EXR layout constants shared between encoder and decoder.
constexpr uint32_t k_channels
std::string kind_to_string(Fabric::Kind k)
Map Fabric::Kind to its lowercase JSON string token via magic_enum.
std::string enum_to_lowercase_string(EnumType value) noexcept
Universal enum to lowercase string converter using magic_enum.