106 {.name =
"scene_create_primitives_batch",
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.",
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",
117 scene* scn =
nullptr;
121 return {.text =
error, .is_error =
true};
124 simdjson::dom::array items_arr;
125 if(args[
"items"].get(items_arr))
127 return {.text =
"Missing items array", .is_error =
true};
130 std::vector<primitive_batch_item>
items;
131 for(
auto el : items_arr)
133 simdjson::dom::object obj;
136 return {.text =
"Each item must be an object", .is_error =
true};
138 primitive_batch_item item{};
139 if(!
read_string(obj,
"primitive", item.primitive) || item.primitive.empty())
141 return {.text =
"Item missing primitive", .is_error =
true};
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)
149 item.material_index =
static_cast<uint32_t
>(mat_index);
152 if(
read_string(obj,
"space", space) && space ==
"world")
154 item.is_local =
false;
157 items.push_back(std::move(item));
161 return {.text =
"items array is empty", .is_error =
true};
164 std::vector<entt::handle> created;
167 "MCP Batch Create Primitives",
171 created.reserve(
items.size());
172 for(
const auto& item :
items)
179 if(!item.name.empty())
183 if(!item.parent_id.empty())
192 if(!item.material_key.empty())
194 apply_material_direct(ctx,
entity, item.material_key, item.material_index);
196 created.push_back(
entity);
201 std::string json =
"[";
202 for(
size_t i = 0; i < created.size(); ++i)
211 return {.text = fmt::format(R
"({{"created":{},"count":{},"requested":{}}})",
215 .is_error = created.empty()};
217 .mutates_scene = true});
220 {.name =
"scene_set_transforms_batch",
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.",
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",
229 scene* scn =
nullptr;
233 return {.text =
error, .is_error =
true};
236 simdjson::dom::array items_arr;
237 if(args[
"items"].get(items_arr))
239 return {.text =
"Missing items array", .is_error =
true};
251 for(
auto el : items_arr)
253 simdjson::dom::object obj;
256 return {.text =
"Each item must be an object", .is_error =
true};
258 std::string entity_id;
261 return {.text =
"Item missing entity_id", .is_error =
true};
266 return {.text =
"Entity not found or missing transform: " + entity_id, .is_error =
true};
271 if(
read_string(obj,
"space", space) && space ==
"local")
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;
290 "MCP Batch Set Transforms",
306 return {.text = fmt::format(R
"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
308 .mutates_scene =
true});
311 {.name =
"scene_set_model_materials_batch",
313 "Assign shared material assets to many model slots in one undoable action. Each item: "
314 "entity_id, material_key, optional index (default 0).",
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",
320 scene* scn =
nullptr;
324 return {.text =
error, .is_error =
true};
327 simdjson::dom::array items_arr;
328 if(args[
"items"].get(items_arr))
330 return {.text =
"Missing items array", .is_error =
true};
342 for(
auto el : items_arr)
344 simdjson::dom::object obj;
347 return {.text =
"Each item must be an object", .is_error =
true};
349 std::string entity_id;
353 return {.text =
"Item missing entity_id or material_key", .is_error =
true};
358 return {.text =
"Entity missing model_component: " + entity_id, .is_error =
true};
363 return {.text =
"Material not found: " +
material_key, .is_error =
true};
366 if(obj[
"index"].get(index_i))
376 e.index =
static_cast<uint32_t
>(index_i);
378 e.new_model = e.old_model;
379 e.new_model.set_material(mat_handle, e.index);
380 entries.push_back(std::move(e));
385 "MCP Batch Set Model Materials",
392 mc->set_model(e.new_model);
403 mc->set_model(e.old_model);
409 return {.text = fmt::format(R
"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
411 .mutates_scene =
true});
414 {.name =
"scene_get_bounds_batch",
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.",
419 R
"json({"type":"object","properties":{"entity_id":{"type":"string"},"entity_ids":{"type":"array","items":{"type":"string"}},"depth":{"type":"integer"}}})json",
423 scene* scn =
nullptr;
427 return {.text =
error, .is_error =
true};
430 std::vector<entt::handle> entities;
431 std::string entity_id;
432 if(
read_string(args,
"entity_id", entity_id) && !entity_id.empty())
437 return {.text =
"Entity not found: " + entity_id, .is_error =
true};
439 entities.push_back(
entity);
441 simdjson::dom::array ids;
442 if(!args[
"entity_ids"].get(ids))
446 std::string_view id_view;
449 return {.text =
"entity_ids must be strings", .is_error =
true};
454 return {.text =
"Entity not found: " + std::string(id_view), .is_error =
true};
456 entities.push_back(
entity);
461 return {.text =
"Provide entity_id or entity_ids", .is_error =
true};
465 if(args[
"depth"].get(depth))
472 for(
auto entity : entities)
486 return {.text = fmt::format(R
"({{"count":{},"bounds":{}}})", entities.size(), bbox_to_json(bounds)),
489 .mutates_scene =
false});
492 {.name =
"scene_find_entities_batch",
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).",
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",
501 scene* scn =
nullptr;
505 return {.text =
error, .is_error =
true};
508 std::string name_contains;
509 std::string name_exact;
512 if(name_contains.empty() && name_exact.empty())
514 return {.text =
"Provide name_contains and/or name_exact", .is_error =
true};
518 if(args[
"limit"].get(limit))
531 std::vector<entt::handle> matches;
538 return {.text =
"Parent not found: " +
parent_id, .is_error =
true};
540 collect_matching_entities(parent, name_contains, name_exact, matches,
static_cast<size_t>(limit));
545 [&](
auto entt_id,
auto&)
547 if(matches.size() >=
static_cast<size_t>(limit))
552 if(entity_matches_name(
entity, name_contains, name_exact))
554 matches.push_back(
entity);
559 std::string json =
"[";
560 for(
size_t i = 0; i < matches.size(); ++i)
569 return {.text = fmt::format(R
"({{"entities":{},"count":{},"limit":{}}})",
575 .mutates_scene =
false});
578 {.name =
"scene_duplicate_entities_batch",
580 "Duplicate entities (clone hierarchy) in one undoable action. Returns created entity summaries.",
582 R
"json({"type":"object","properties":{"entity_ids":{"type":"array","items":{"type":"string"}}},"required":["entity_ids"]})json",
586 scene* scn =
nullptr;
590 return {.text =
error, .is_error =
true};
593 simdjson::dom::array ids;
594 if(args[
"entity_ids"].get(ids))
596 return {.text =
"Missing entity_ids", .is_error =
true};
598 std::vector<entt::handle> sources;
601 std::string_view id_view;
604 return {.text =
"entity_ids must be strings", .is_error =
true};
609 return {.text =
"Entity not found: " + std::string(id_view), .is_error =
true};
611 sources.push_back(
entity);
614 std::vector<entt::handle> created;
617 "MCP Duplicate Entities",
621 for(
auto source : sources)
626 created.push_back(clone);
632 std::string json =
"[";
633 for(
size_t i = 0; i < created.size(); ++i)
642 return {.text = fmt::format(R
"({{"created":{},"count":{}}})", json, created.size()),
643 .is_error = created.empty()};
645 .mutates_scene = true});
static void mark_material_as_changed(entt::handle entity)
Marks material as changed in prefab override system.