Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_scene_batch.cpp
Go to the documentation of this file.
1#include "mcp_tools_common.h"
2
12#include <math/math.h>
13
14namespace unravel::mcp
15{
16namespace
17{
18
19struct primitive_batch_item
20{
21 std::string primitive;
22 std::string name;
23 std::string parent_id;
24 std::string material_key;
25 transform_snapshot pose{};
26 bool is_local{true};
27 uint32_t material_index{0};
28};
29
30auto apply_material_direct(rtti::context& ctx, entt::handle entity, const std::string& material_key, uint32_t index)
31 -> bool
32{
33 if(!entity || !entity.all_of<model_component>() || material_key.empty())
34 {
35 return false;
36 }
37 auto& am = ctx.get_cached<asset_manager>();
38 auto mat_handle = am.get_asset<::unravel::material>(material_key);
39 if(!mat_handle)
40 {
41 return false;
42 }
43 auto& model_comp = entity.get<model_component>();
44 auto model = model_comp.get_model();
45 model.set_material(mat_handle, index);
46 model_comp.set_model(model);
48 return true;
49}
50
51auto entity_matches_name(entt::handle entity, const std::string& name_contains, const std::string& name_exact) -> bool
52{
53 auto* tag = entity.try_get<tag_component>();
54 if(!tag)
55 {
56 return false;
57 }
58 if(!name_exact.empty())
59 {
60 return tag->name == name_exact;
61 }
62 if(!name_contains.empty())
63 {
64 return contains_ci(tag->name, name_contains);
65 }
66 return true;
67}
68
69void collect_matching_entities(entt::handle entity,
70 const std::string& name_contains,
71 const std::string& name_exact,
72 std::vector<entt::handle>& out,
73 size_t limit)
74{
75 if(out.size() >= limit)
76 {
77 return;
78 }
79 if(entity_matches_name(entity, name_contains, name_exact))
80 {
81 out.push_back(entity);
82 if(out.size() >= limit)
83 {
84 return;
85 }
86 }
87 if(!entity.all_of<transform_component>())
88 {
89 return;
90 }
91 for(auto child : entity.get<transform_component>().get_children())
92 {
93 collect_matching_entities(child, name_contains, name_exact, out, limit);
94 if(out.size() >= limit)
95 {
96 return;
97 }
98 }
99}
100
101} // namespace
102
104{
105 registry.add(
106 {.name = "scene_create_primitives_batch",
107 .description =
108 "Create many embedded mesh primitives in one undoable action. Each item: primitive, optional "
109 "name/parent_id/material_key/material_index, position/rotation_euler/scale, space "
110 "(local default|world). Axes: X-right, Y-up, Z-forward. Prefer space:\"local\" with "
111 "rotation_euler:[0,0,0] under rotated parents.",
112 .input_schema_json =
113 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"primitive":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":"string"},"material_key":{"type":"string"},"material_index":{"type":"integer","minimum":0},"space":{"type":"string","enum":["world","local"]},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"scale":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["primitive"]}}},"required":["items"]})json",
114 .handler =
115 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
116 {
117 scene* scn = nullptr;
118 std::string error;
119 if(!require_edit_scene(ctx, scn, error))
120 {
121 return {.text = error, .is_error = true};
122 }
123
124 simdjson::dom::array items_arr;
125 if(args["items"].get(items_arr))
126 {
127 return {.text = "Missing items array", .is_error = true};
128 }
129
130 std::vector<primitive_batch_item> items;
131 for(auto el : items_arr)
132 {
133 simdjson::dom::object obj;
134 if(el.get(obj))
135 {
136 return {.text = "Each item must be an object", .is_error = true};
137 }
138 primitive_batch_item item{};
139 if(!read_string(obj, "primitive", item.primitive) || item.primitive.empty())
140 {
141 return {.text = "Item missing primitive", .is_error = true};
142 }
143 read_string(obj, "name", item.name);
144 read_string(obj, "parent_id", item.parent_id);
145 read_string(obj, "material_key", item.material_key);
146 int64_t mat_index = 0;
147 if(!obj["material_index"].get(mat_index) && mat_index >= 0)
148 {
149 item.material_index = static_cast<uint32_t>(mat_index);
150 }
151 std::string space;
152 if(read_string(obj, "space", space) && space == "world")
153 {
154 item.is_local = false;
155 }
156 read_transform_snapshot(obj, item.pose);
157 items.push_back(std::move(item));
158 }
159 if(items.empty())
160 {
161 return {.text = "items array is empty", .is_error = true};
162 }
163
164 std::vector<entt::handle> created;
165 auto& em = ctx.get_cached<editing_manager>();
167 "MCP Batch Create Primitives",
168 [&]()
169 {
170 created.clear();
171 created.reserve(items.size());
172 for(const auto& item : items)
173 {
174 auto entity = defaults::create_embedded_mesh_entity(ctx, *scn, item.primitive);
175 if(!entity)
176 {
177 continue;
178 }
179 if(!item.name.empty())
180 {
181 entity.get<tag_component>().name = item.name;
182 }
183 if(!item.parent_id.empty())
184 {
185 auto parent = find_entity(*scn, item.parent_id);
186 if(parent)
187 {
188 entity.get<transform_component>().set_parent(parent, true);
189 }
190 }
191 apply_pose_direct(entity, item.pose, item.is_local);
192 if(!item.material_key.empty())
193 {
194 apply_material_direct(ctx, entity, item.material_key, item.material_index);
195 }
196 created.push_back(entity);
197 }
198 return created;
199 });
200
201 std::string json = "[";
202 for(size_t i = 0; i < created.size(); ++i)
203 {
204 if(i > 0)
205 {
206 json += ",";
207 }
208 json += entity_to_summary_json(created[i], 0, 0);
209 }
210 json += "]";
211 return {.text = fmt::format(R"({{"created":{},"count":{},"requested":{}}})",
212 json,
213 created.size(),
214 items.size()),
215 .is_error = created.empty()};
216 },
217 .mutates_scene = true});
218
219 registry.add(
220 {.name = "scene_set_transforms_batch",
221 .description =
222 "Set transforms on many entities in one undoable action. Each item: entity_id, optional "
223 "space/position/rotation_euler/scale. Axes: X-right, Y-up, Z-forward.",
224 .input_schema_json =
225 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"space":{"type":"string","enum":["world","local"]},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"scale":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["entity_id"]}}},"required":["items"]})json",
226 .handler =
227 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
228 {
229 scene* scn = nullptr;
230 std::string error;
231 if(!require_edit_scene(ctx, scn, error))
232 {
233 return {.text = error, .is_error = true};
234 }
235
236 simdjson::dom::array items_arr;
237 if(args["items"].get(items_arr))
238 {
239 return {.text = "Missing items array", .is_error = true};
240 }
241
242 struct entry
243 {
244 entt::handle entity{};
245 bool is_local{false};
247 transform_snapshot old_local{};
248 transform_snapshot old_world{};
249 };
250 std::vector<entry> entries;
251 for(auto el : items_arr)
252 {
253 simdjson::dom::object obj;
254 if(el.get(obj))
255 {
256 return {.text = "Each item must be an object", .is_error = true};
257 }
258 std::string entity_id;
259 if(!read_string(obj, "entity_id", entity_id))
260 {
261 return {.text = "Item missing entity_id", .is_error = true};
262 }
263 auto entity = find_entity(*scn, entity_id);
264 if(!entity || !entity.all_of<transform_component>())
265 {
266 return {.text = "Entity not found or missing transform: " + entity_id, .is_error = true};
267 }
268 entry e{};
269 e.entity = entity;
270 std::string space;
271 if(read_string(obj, "space", space) && space == "local")
272 {
273 e.is_local = true;
274 }
275 read_transform_snapshot(obj, e.pose);
276 auto& t = entity.get<transform_component>();
277 e.old_local.position = t.get_position_local();
278 e.old_local.rotation_euler = t.get_rotation_euler_local();
279 e.old_local.scale = t.get_scale_local();
280 e.old_local.has_position = e.old_local.has_rotation = e.old_local.has_scale = true;
281 e.old_world.position = t.get_position_global();
282 e.old_world.rotation_euler = t.get_rotation_euler_global();
283 e.old_world.scale = t.get_scale_global();
284 e.old_world.has_position = e.old_world.has_rotation = e.old_world.has_scale = true;
285 entries.push_back(e);
286 }
287
288 auto& em = ctx.get_cached<editing_manager>();
289 em.do_action(
290 "MCP Batch Set Transforms",
291 [entries]()
292 {
293 for(const auto& e : entries)
294 {
295 apply_pose_direct(e.entity, e.pose, e.is_local);
296 }
297 },
298 [entries]()
299 {
300 for(const auto& e : entries)
301 {
302 apply_pose_direct(e.entity, e.is_local ? e.old_local : e.old_world, e.is_local);
303 }
304 });
305
306 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
307 },
308 .mutates_scene = true});
309
310 registry.add(
311 {.name = "scene_set_model_materials_batch",
312 .description =
313 "Assign shared material assets to many model slots in one undoable action. Each item: "
314 "entity_id, material_key, optional index (default 0).",
315 .input_schema_json =
316 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"material_key":{"type":"string"},"index":{"type":"integer","minimum":0}},"required":["entity_id","material_key"]}}},"required":["items"]})json",
317 .handler =
318 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
319 {
320 scene* scn = nullptr;
321 std::string error;
322 if(!require_edit_scene(ctx, scn, error))
323 {
324 return {.text = error, .is_error = true};
325 }
326
327 simdjson::dom::array items_arr;
328 if(args["items"].get(items_arr))
329 {
330 return {.text = "Missing items array", .is_error = true};
331 }
332
333 struct entry
334 {
335 entt::handle entity{};
336 uint32_t index{0};
337 model old_model{};
338 model new_model{};
339 };
340 std::vector<entry> entries;
341 auto& am = ctx.get_cached<asset_manager>();
342 for(auto el : items_arr)
343 {
344 simdjson::dom::object obj;
345 if(el.get(obj))
346 {
347 return {.text = "Each item must be an object", .is_error = true};
348 }
349 std::string entity_id;
350 std::string material_key;
351 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "material_key", material_key))
352 {
353 return {.text = "Item missing entity_id or material_key", .is_error = true};
354 }
355 auto entity = find_entity(*scn, entity_id);
356 if(!entity || !entity.all_of<model_component>())
357 {
358 return {.text = "Entity missing model_component: " + entity_id, .is_error = true};
359 }
360 auto mat_handle = am.get_asset<::unravel::material>(material_key);
361 if(!mat_handle)
362 {
363 return {.text = "Material not found: " + material_key, .is_error = true};
364 }
365 int64_t index_i = 0;
366 if(obj["index"].get(index_i))
367 {
368 index_i = 0;
369 }
370 if(index_i < 0)
371 {
372 index_i = 0;
373 }
374 entry e{};
375 e.entity = entity;
376 e.index = static_cast<uint32_t>(index_i);
377 e.old_model = entity.get<model_component>().get_model();
378 e.new_model = e.old_model;
379 e.new_model.set_material(mat_handle, e.index);
380 entries.push_back(std::move(e));
381 }
382
383 auto& em = ctx.get_cached<editing_manager>();
384 em.do_action(
385 "MCP Batch Set Model Materials",
386 [entries]()
387 {
388 for(const auto& e : entries)
389 {
390 if(auto* mc = e.entity.try_get<model_component>())
391 {
392 mc->set_model(e.new_model);
394 }
395 }
396 },
397 [entries]()
398 {
399 for(const auto& e : entries)
400 {
401 if(auto* mc = e.entity.try_get<model_component>())
402 {
403 mc->set_model(e.old_model);
405 }
406 }
407 });
408
409 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
410 },
411 .mutates_scene = true});
412
413 registry.add(
414 {.name = "scene_get_bounds_batch",
415 .description =
416 "Get world-space AABB for one entity_id or many entity_ids (union). Optional depth "
417 "(-1 = full hierarchy). Axes: X-right, Y-up, Z-forward.",
418 .input_schema_json =
419 R"json({"type":"object","properties":{"entity_id":{"type":"string"},"entity_ids":{"type":"array","items":{"type":"string"}},"depth":{"type":"integer"}}})json",
420 .handler =
421 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
422 {
423 scene* scn = nullptr;
424 std::string error;
425 if(!require_edit_scene(ctx, scn, error))
426 {
427 return {.text = error, .is_error = true};
428 }
429
430 std::vector<entt::handle> entities;
431 std::string entity_id;
432 if(read_string(args, "entity_id", entity_id) && !entity_id.empty())
433 {
434 auto entity = find_entity(*scn, entity_id);
435 if(!entity)
436 {
437 return {.text = "Entity not found: " + entity_id, .is_error = true};
438 }
439 entities.push_back(entity);
440 }
441 simdjson::dom::array ids;
442 if(!args["entity_ids"].get(ids))
443 {
444 for(auto el : ids)
445 {
446 std::string_view id_view;
447 if(el.get(id_view))
448 {
449 return {.text = "entity_ids must be strings", .is_error = true};
450 }
451 auto entity = find_entity(*scn, std::string(id_view));
452 if(!entity)
453 {
454 return {.text = "Entity not found: " + std::string(id_view), .is_error = true};
455 }
456 entities.push_back(entity);
457 }
458 }
459 if(entities.empty())
460 {
461 return {.text = "Provide entity_id or entity_ids", .is_error = true};
462 }
463
464 int64_t depth = -1;
465 if(args["depth"].get(depth))
466 {
467 depth = -1;
468 }
469
470 math::bbox bounds;
471 bool first = true;
472 for(auto entity : entities)
473 {
474 auto eb = defaults::calc_bounds_global(entity, static_cast<int>(depth));
475 if(first)
476 {
477 bounds = eb;
478 first = false;
479 }
480 else
481 {
482 bounds.add_point(eb.min);
483 bounds.add_point(eb.max);
484 }
485 }
486 return {.text = fmt::format(R"({{"count":{},"bounds":{}}})", entities.size(), bbox_to_json(bounds)),
487 .is_error = false};
488 },
489 .mutates_scene = false});
490
491 registry.add(
492 {.name = "scene_find_entities_batch",
493 .description =
494 "Find entities by name_contains (case-insensitive) and/or name_exact. Optional parent_id "
495 "limits search to that subtree; omit to search whole scene. Optional limit (default 100).",
496 .input_schema_json =
497 R"json({"type":"object","properties":{"name_contains":{"type":"string"},"name_exact":{"type":"string"},"parent_id":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":5000}}})json",
498 .handler =
499 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
500 {
501 scene* scn = nullptr;
502 std::string error;
503 if(!require_edit_scene(ctx, scn, error))
504 {
505 return {.text = error, .is_error = true};
506 }
507
508 std::string name_contains;
509 std::string name_exact;
510 read_string(args, "name_contains", name_contains);
511 read_string(args, "name_exact", name_exact);
512 if(name_contains.empty() && name_exact.empty())
513 {
514 return {.text = "Provide name_contains and/or name_exact", .is_error = true};
515 }
516
517 int64_t limit = 100;
518 if(args["limit"].get(limit))
519 {
520 limit = 100;
521 }
522 if(limit < 1)
523 {
524 limit = 1;
525 }
526 if(limit > 5000)
527 {
528 limit = 5000;
529 }
530
531 std::vector<entt::handle> matches;
532 std::string parent_id;
533 if(read_string(args, "parent_id", parent_id) && !parent_id.empty())
534 {
535 auto parent = find_entity(*scn, parent_id);
536 if(!parent)
537 {
538 return {.text = "Parent not found: " + parent_id, .is_error = true};
539 }
540 collect_matching_entities(parent, name_contains, name_exact, matches, static_cast<size_t>(limit));
541 }
542 else
543 {
544 scn->registry->view<tag_component>().each(
545 [&](auto entt_id, auto&)
546 {
547 if(matches.size() >= static_cast<size_t>(limit))
548 {
549 return;
550 }
551 entt::handle entity(*scn->registry, entt_id);
552 if(entity_matches_name(entity, name_contains, name_exact))
553 {
554 matches.push_back(entity);
555 }
556 });
557 }
558
559 std::string json = "[";
560 for(size_t i = 0; i < matches.size(); ++i)
561 {
562 if(i > 0)
563 {
564 json += ",";
565 }
566 json += entity_to_summary_json(matches[i], 0, 0);
567 }
568 json += "]";
569 return {.text = fmt::format(R"({{"entities":{},"count":{},"limit":{}}})",
570 json,
571 matches.size(),
572 limit),
573 .is_error = false};
574 },
575 .mutates_scene = false});
576
577 registry.add(
578 {.name = "scene_duplicate_entities_batch",
579 .description =
580 "Duplicate entities (clone hierarchy) in one undoable action. Returns created entity summaries.",
581 .input_schema_json =
582 R"json({"type":"object","properties":{"entity_ids":{"type":"array","items":{"type":"string"}}},"required":["entity_ids"]})json",
583 .handler =
584 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
585 {
586 scene* scn = nullptr;
587 std::string error;
588 if(!require_edit_scene(ctx, scn, error))
589 {
590 return {.text = error, .is_error = true};
591 }
592
593 simdjson::dom::array ids;
594 if(args["entity_ids"].get(ids))
595 {
596 return {.text = "Missing entity_ids", .is_error = true};
597 }
598 std::vector<entt::handle> sources;
599 for(auto el : ids)
600 {
601 std::string_view id_view;
602 if(el.get(id_view))
603 {
604 return {.text = "entity_ids must be strings", .is_error = true};
605 }
606 auto entity = find_entity(*scn, std::string(id_view));
607 if(!entity)
608 {
609 return {.text = "Entity not found: " + std::string(id_view), .is_error = true};
610 }
611 sources.push_back(entity);
612 }
613
614 std::vector<entt::handle> created;
615 auto& em = ctx.get_cached<editing_manager>();
617 "MCP Duplicate Entities",
618 [&]()
619 {
620 created.clear();
621 for(auto source : sources)
622 {
623 auto clone = scn->clone_entity(source);
624 if(clone)
625 {
626 created.push_back(clone);
627 }
628 }
629 return created;
630 });
631
632 std::string json = "[";
633 for(size_t i = 0; i < created.size(); ++i)
634 {
635 if(i > 0)
636 {
637 json += ",";
638 }
639 json += entity_to_summary_json(created[i], 0, 0);
640 }
641 json += "]";
642 return {.text = fmt::format(R"({{"created":{},"count":{}}})", json, created.size()),
643 .is_error = created.empty()};
644 },
645 .mutates_scene = true});
646}
647
648} // namespace unravel::mcp
Manages assets, including loading, unloading, and storage.
Base class for materials used in rendering.
Definition material.h:44
Class that contains core data for meshes.
Structure describing a LOD group (set of meshes), LOD transitions, and their materials.
Definition model.h:275
Component that handles transformations (position, rotation, scale, etc.) in the ACE framework.
std::vector< render_pass_node_item > items
uint16_t index
std::vector< render_pass_entry > entries
std::string name
Definition hub.cpp:33
std::string tag
Definition hub.cpp:32
std::string error
Definition mcp_async.cpp:33
uint32_t material_index
std::string parent_id
std::string material_key
bool is_local
transform_snapshot pose
std::string primitive
auto find_entity(scene &scn, const std::string &id) -> entt::handle
auto require_edit_scene(rtti::context &ctx, scene *&out_scene, std::string &error) -> bool
void register_scene_batch_tools(mcp_tool_registry &registry)
void apply_pose_direct(entt::handle entity, const transform_snapshot &pose, bool is_local)
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
auto contains_ci(std::string_view haystack, std::string_view needle) -> bool
auto bbox_to_json(const math::bbox &bounds) -> std::string
auto read_transform_snapshot(const simdjson::dom::object &obj, transform_snapshot &out) -> void
auto entity_to_summary_json(entt::handle entity, int depth, int max_depth) -> std::string
JSON summary: transform, component pretty-names, optional children.
entt::handle entity
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
bbox & add_point(const vec3 &point)
Grows the bounding box based on the point passed.
Definition bbox.cpp:924
auto get_cached() -> T &
Definition context.hpp:49
Undoable create for entity subtrees: invokes a user-supplied factory on first execution,...
static auto create_embedded_mesh_entity(rtti::context &ctx, scene &scn, const std::string &name) -> entt::handle
Creates an embedded mesh entity.
Definition defaults.cpp:808
static auto calc_bounds_global(entt::handle entity, int depth=-1) -> math::bbox
Calculates the bounding box of an entity.
void do_action(const std::string &name, const std::function< void()> &action)
static void mark_material_as_changed(entt::handle entity)
Marks material as changed in prefab override system.
Represents a scene in the ACE framework, managing entities and their relationships.
Definition scene.h:70
auto clone_entity(entt::handle clone_from, bool keep_parent=true, bool call_callbacks=true) -> entt::handle
Clones an existing entity in the scene.
Definition scene.cpp:398
std::unique_ptr< entt::registry > registry
The registry that manages all entities in the scene.
Definition scene.h:187
Component that provides a tag (name or label) for an entity.
std::string name
The name of the entity.