Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_viewport.cpp
Go to the documentation of this file.
1#include "mcp_async.h"
2#include "mcp_tools_common.h"
3
4#include <editor/hub/hub.h>
8
11#include <engine/ecs/ecs.h>
14#include <seq/seq.h>
15
16#include <vector>
17
18namespace unravel::mcp
19{
20namespace
21{
22
23auto resolve_scene_panel(rtti::context& ctx) -> scene_panel&
24{
25 return ctx.get_cached<hub>().get_panels().get_scene_panel();
26}
27
28auto resolve_scene_camera(rtti::context& ctx, std::string& error) -> entt::handle
29{
30 auto camera = resolve_scene_panel(ctx).get_camera();
31 if(!camera || !camera.all_of<transform_component, camera_component>())
32 {
33 error = "Scene panel camera not available";
34 return {};
35 }
36 return camera;
37}
38
39auto resolve_scene_obuffer(rtti::context& ctx) -> gfx::frame_buffer::ptr
40{
41 auto& hub_sys = ctx.get_cached<hub>();
42 auto camera = hub_sys.get_panels().get_scene_panel().get_camera();
43 if(!camera)
44 {
45 return {};
46 }
47
48 auto* camera_comp = camera.try_get<camera_component>();
49 if(!camera_comp)
50 {
51 return {};
52 }
53
54 return camera_comp->get_render_view().fbo_safe_get("OBUFFER");
55}
56
57auto resolve_game_obuffer(rtti::context& ctx) -> gfx::frame_buffer::ptr
58{
59 // Prefer the active edit/play scene camera that owns an OBUFFER (same source as game_panel).
60 auto& em = ctx.get_cached<editing_manager>();
61 auto* scn = em.get_active_scene(ctx);
62 if(!scn || !scn->registry)
63 {
64 auto& ec = ctx.get_cached<ecs>();
65 scn = &ec.get_scene();
66 }
67 if(!scn || !scn->registry)
68 {
69 return {};
70 }
71
73 scn->registry->view<camera_component>().each(
74 [&](auto, auto&& camera_comp)
75 {
76 if(found)
77 {
78 return;
79 }
80 auto obuffer = camera_comp.get_render_view().fbo_safe_get("OBUFFER");
81 if(obuffer && obuffer->is_valid())
82 {
83 found = obuffer;
84 }
85 });
86 return found;
87}
88
89auto vec3_to_json(const math::vec3& v) -> std::string
90{
91 return fmt::format("[{:.6g},{:.6g},{:.6g}]", v.x, v.y, v.z);
92}
93
94auto camera_to_json(entt::handle camera) -> std::string
95{
96 auto& tc = camera.get<transform_component>();
97 auto& cc = camera.get<camera_component>();
98 return fmt::format(
99 R"({{"position":{},"rotation_euler":{},"forward":{},"up":{},"fov":{:.6g},"ortho_size":{:.6g}}})",
100 vec3_to_json(tc.get_position_global()),
101 vec3_to_json(tc.get_rotation_euler_global()),
102 vec3_to_json(tc.get_z_axis_global()),
103 vec3_to_json(tc.get_y_axis_global()),
104 cc.get_fov(),
105 cc.get_ortho_size());
106}
107
108auto cancel_camera_focus() -> void
109{
110 seq::scope::stop_all("camera_focus");
111}
112
113auto read_duration(const simdjson::dom::object& args, float default_duration = 0.4f) -> float
114{
115 double duration = default_duration;
116 (void)args["duration"].get(duration);
117 if(duration < 0.0)
118 {
119 duration = 0.0;
120 }
121 if(duration > 10.0)
122 {
123 duration = 10.0;
124 }
125 return static_cast<float>(duration);
126}
127
128auto resolve_focus_entities(rtti::context& ctx,
129 const simdjson::dom::object& args,
130 std::vector<entt::handle>& out,
131 std::string& error) -> bool
132{
133 scene* scn = nullptr;
134 if(!require_edit_scene(ctx, scn, error))
135 {
136 return false;
137 }
138
139 out.clear();
140
141 std::string single_id;
142 if(read_string(args, "entity_id", single_id) && !single_id.empty())
143 {
144 auto entity = find_entity(*scn, single_id);
145 if(!entity)
146 {
147 error = "Entity not found: " + single_id;
148 return false;
149 }
150 out.push_back(entity);
151 }
152
153 simdjson::dom::array ids;
154 if(!args["entity_ids"].get(ids))
155 {
156 for(auto el : ids)
157 {
158 std::string_view id_view;
159 if(el.get(id_view))
160 {
161 error = "entity_ids must be an array of strings";
162 return false;
163 }
164 auto entity = find_entity(*scn, std::string(id_view));
165 if(!entity)
166 {
167 error = "Entity not found: " + std::string(id_view);
168 return false;
169 }
170 out.push_back(entity);
171 }
172 }
173
174 if(out.empty())
175 {
176 error = "Provide entity_id or entity_ids";
177 return false;
178 }
179 return true;
180}
181
182} // namespace
183
185{
186 registry.add(
187 {.name = "viewport_screenshot_scene",
188 .description = "Capture a PNG screenshot of the Scene panel OBUFFER (editor viewport). "
189 "Blit + GPU readback of the offscreen color target (action + wait). "
190 "Returns an image content block plus metadata JSON.",
191 .input_schema_json =
192 R"({"type":"object","properties":{"wait_ms":{"type":"integer","minimum":100,"maximum":15000,"description":"Max time to wait for PNG after requesting capture (default 3000)."}}})",
193 .handler =
194 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
195 {
196 auto& mcp = ctx.get_cached<mcp_manager>();
197 return capture_fbo_screenshot(mcp, ctx, resolve_scene_obuffer, "scene", read_wait_ms(args, 3000));
198 },
199 .mutates_scene = false,
200 .requires_main_thread = false});
201
202 registry.add(
203 {.name = "viewport_screenshot_game",
204 .description = "Capture a PNG screenshot of the Game panel OBUFFER (active scene camera). "
205 "Blit + GPU readback of the offscreen color target (action + wait). "
206 "Returns an image content block plus metadata JSON.",
207 .input_schema_json =
208 R"({"type":"object","properties":{"wait_ms":{"type":"integer","minimum":100,"maximum":15000,"description":"Max time to wait for PNG after requesting capture (default 3000)."}}})",
209 .handler =
210 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
211 {
212 auto& mcp = ctx.get_cached<mcp_manager>();
213 return capture_fbo_screenshot(mcp, ctx, resolve_game_obuffer, "game", read_wait_ms(args, 3000));
214 },
215 .mutates_scene = false,
216 .requires_main_thread = false});
217
218 registry.add(
219 {.name = "viewport_get_camera",
220 .description =
221 "Get the Scene panel editor camera pose (position, rotation_euler degrees, forward/up, "
222 "fov, ortho_size). This is the viewport camera, not a scene Camera entity.",
223 .input_schema_json = empty_object_schema(),
224 .handler =
225 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
226 {
227 std::string error;
228 auto camera = resolve_scene_camera(ctx, error);
229 if(!camera)
230 {
231 return {.text = error, .is_error = true};
232 }
233 return {.text = camera_to_json(camera), .is_error = false};
234 },
235 .mutates_scene = false});
236
237 registry.add(
238 {.name = "viewport_set_camera",
239 .description =
240 "Set Scene panel camera position and/or rotation_euler (degrees). "
241 "Axes: X-right, Y-up, Z-forward. Cancels any in-flight "
242 "focus animation. Optional relative:true applies position as a local-space offset.",
243 .input_schema_json =
244 R"json({"type":"object","properties":{"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"relative":{"type":"boolean","description":"If true, position is added in camera local space (default false)"}}})json",
245 .handler =
246 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
247 {
248 std::string error;
249 auto camera = resolve_scene_camera(ctx, error);
250 if(!camera)
251 {
252 return {.text = error, .is_error = true};
253 }
254
255 math::vec3 position{};
256 math::vec3 rotation{};
257 const bool has_position = read_vec3(args, "position", position);
258 const bool has_rotation = read_vec3(args, "rotation_euler", rotation);
259 if(!has_position && !has_rotation)
260 {
261 return {.text = "Provide position and/or rotation_euler", .is_error = true};
262 }
263
264 bool relative = false;
265 read_bool(args, "relative", relative);
266
267 cancel_camera_focus();
268 auto& tc = camera.get<transform_component>();
269
270 if(has_position)
271 {
272 if(relative)
273 {
274 const auto world_delta = tc.get_x_axis_global() * position.x +
275 tc.get_y_axis_global() * position.y +
276 tc.get_z_axis_global() * position.z;
277 tc.set_position_global(tc.get_position_global() + world_delta);
278 }
279 else
280 {
281 tc.set_position_global(position);
282 }
283 }
284 if(has_rotation)
285 {
286 tc.set_rotation_euler_global(rotation);
287 }
288
289 return {.text = camera_to_json(camera), .is_error = false};
290 },
291 .mutates_scene = false});
292
293 registry.add(
294 {.name = "viewport_look_at",
295 .description =
296 "Aim the Scene panel camera at a world-space target point. Optional position moves the "
297 "camera first; optional up vector (default world up).",
298 .input_schema_json =
299 R"json({"type":"object","properties":{"target":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"up":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["target"]})json",
300 .handler =
301 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
302 {
303 std::string error;
304 auto camera = resolve_scene_camera(ctx, error);
305 if(!camera)
306 {
307 return {.text = error, .is_error = true};
308 }
309
310 math::vec3 target{};
311 if(!read_vec3(args, "target", target))
312 {
313 return {.text = "Missing target [x,y,z]", .is_error = true};
314 }
315
316 cancel_camera_focus();
317 auto& tc = camera.get<transform_component>();
318
319 math::vec3 position{};
320 if(read_vec3(args, "position", position))
321 {
322 tc.set_position_global(position);
323 }
324
325 math::vec3 up{};
326 if(read_vec3(args, "up", up))
327 {
328 tc.look_at(target, up);
329 }
330 else
331 {
332 tc.look_at(target);
333 }
334
335 return {.text = fmt::format(R"({{"ok":true,"target":{},"camera":{}}})",
336 vec3_to_json(target),
337 camera_to_json(camera)),
338 .is_error = false};
339 },
340 .mutates_scene = false});
341
342 registry.add(
343 {.name = "viewport_focus_entities_batch",
344 .description =
345 "Focus the Scene panel camera on one or more scene entities using "
346 "defaults::focus_camera_on_entities (same as hierarchy F / double-click). "
347 "Keeps current rotation unless aim:true (look at bounds center first). "
348 "duration default 0.4s; use 0 for instant.",
349 .input_schema_json =
350 R"json({"type":"object","properties":{"entity_id":{"type":"string"},"entity_ids":{"type":"array","items":{"type":"string"}},"duration":{"type":"number","minimum":0,"maximum":10},"aim":{"type":"boolean","description":"Look at the entities' bounds center before focusing (default false)"}}})json",
351 .handler =
352 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
353 {
354 std::string error;
355 auto camera = resolve_scene_camera(ctx, error);
356 if(!camera)
357 {
358 return {.text = error, .is_error = true};
359 }
360
361 std::vector<entt::handle> entities;
362 if(!resolve_focus_entities(ctx, args, entities, error))
363 {
364 return {.text = error, .is_error = true};
365 }
366
367 bool aim = false;
368 read_bool(args, "aim", aim);
369 const float duration = read_duration(args, 0.4f);
370
371 cancel_camera_focus();
372
373 defaults::focus_camera_on_entities(camera, hpp::span<const entt::handle>{entities}, duration);
374
375
376 return {.text = fmt::format(
377 R"({{"ok":true,"count":{},"duration":{:.3g},"aim":{},"camera":{}}})",
378 entities.size(),
379 duration,
380 aim ? "true" : "false",
381 camera_to_json(camera)),
382 .is_error = false};
383 },
384 .mutates_scene = false});
385
386 registry.add(
387 {.name = "viewport_focus_bounds",
388 .description =
389 "Focus the Scene panel camera on a world-space sphere (center+radius) or box "
390 "(min+max) via defaults::focus_camera_on_bounds. Optional aim:true looks at center first.",
391 .input_schema_json =
392 R"json({"type":"object","properties":{"center":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"radius":{"type":"number","minimum":0},"min":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"max":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"duration":{"type":"number","minimum":0,"maximum":10},"aim":{"type":"boolean"}}})json",
393 .handler =
394 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
395 {
396 std::string error;
397 auto camera = resolve_scene_camera(ctx, error);
398 if(!camera)
399 {
400 return {.text = error, .is_error = true};
401 }
402
403 const float duration = read_duration(args, 0.4f);
404 bool aim = false;
405 read_bool(args, "aim", aim);
406
407 math::vec3 center{};
408 math::vec3 min_v{};
409 math::vec3 max_v{};
410 const bool has_center = read_vec3(args, "center", center);
411 const bool has_min = read_vec3(args, "min", min_v);
412 const bool has_max = read_vec3(args, "max", max_v);
413
414 double radius = 0.0;
415 const bool has_radius = !args["radius"].get(radius);
416
417 cancel_camera_focus();
418
419 if(has_min && has_max)
420 {
422 box.add_point(min_v);
423 box.add_point(max_v);
424 if(aim)
425 {
426 camera.get<transform_component>().look_at(box.get_center());
427 }
429 }
430 else if(has_center && has_radius)
431 {
432 if(radius < 0.001)
433 {
434 radius = 0.001;
435 }
436 math::bsphere sphere{center, static_cast<float>(radius)};
437 if(aim)
438 {
439 camera.get<transform_component>().look_at(center);
440 }
442 }
443 else
444 {
445 return {.text = "Provide center+radius, or min+max", .is_error = true};
446 }
447
448 return {.text = fmt::format(R"({{"ok":true,"duration":{:.3g},"aim":{},"camera":{}}})",
449 duration,
450 aim ? "true" : "false",
451 camera_to_json(camera)),
452 .is_error = false};
453 },
454 .mutates_scene = false});
455
456 registry.add(
457 {.name = "viewport_orbit_camera",
458 .description =
459 "Orbit the Scene panel camera around a world pivot by yaw/pitch degrees (Y-up then "
460 "camera-right). Defaults pivot to current look target estimate from focus, or "
461 "explicit pivot. Keeps distance to pivot.",
462 .input_schema_json =
463 R"json({"type":"object","properties":{"yaw":{"type":"number","description":"Degrees around world up"},"pitch":{"type":"number","description":"Degrees around camera right"},"pivot":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"distance":{"type":"number","minimum":0.01,"description":"Optional override distance to pivot"}}})json",
464 .handler =
465 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
466 {
467 std::string error;
468 auto camera = resolve_scene_camera(ctx, error);
469 if(!camera)
470 {
471 return {.text = error, .is_error = true};
472 }
473
474 double yaw = 0.0;
475 double pitch = 0.0;
476 (void)args["yaw"].get(yaw);
477 (void)args["pitch"].get(pitch);
478 if(yaw == 0.0 && pitch == 0.0)
479 {
480 return {.text = "Provide yaw and/or pitch in degrees", .is_error = true};
481 }
482
483 cancel_camera_focus();
484 auto& tc = camera.get<transform_component>();
485 const auto position = tc.get_position_global();
486
487 math::vec3 pivot = position + tc.get_z_axis_global() * 5.0f;
488 read_vec3(args, "pivot", pivot);
489
490 double distance = 0.0;
491 if(args["distance"].get(distance) || distance <= 0.0)
492 {
493 distance = static_cast<double>(math::length(position - pivot));
494 if(distance < 0.01)
495 {
496 distance = 5.0;
497 }
498 }
499
500 if(yaw != 0.0)
501 {
502 tc.rotate_around_global(pivot, math::vec3{0.0f, 1.0f, 0.0f}, static_cast<float>(yaw));
503 }
504 if(pitch != 0.0)
505 {
506 tc.rotate_around_global(pivot, tc.get_x_axis_global(), static_cast<float>(pitch));
507 }
508
509 // Enforce distance after orbit (rotate_around keeps it; distance override may differ).
510 auto offset = tc.get_position_global() - pivot;
511 if(math::length(offset) > 1e-6f)
512 {
513 offset = math::normalize(offset) * static_cast<float>(distance);
514 tc.set_position_global(pivot + offset);
515 }
516 tc.look_at(pivot);
517
518 return {.text = fmt::format(R"({{"ok":true,"pivot":{},"yaw":{:.3g},"pitch":{:.3g},"camera":{}}})",
519 vec3_to_json(pivot),
520 yaw,
521 pitch,
522 camera_to_json(camera)),
523 .is_error = false};
524 },
525 .mutates_scene = false});
526
527 registry.add(
528 {.name = "viewport_reset_camera",
529 .description =
530 "Reset the Scene panel camera to the default editor pose (same as Scene panel "
531 "Reset Camera button).",
532 .input_schema_json = empty_object_schema(),
533 .handler =
534 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
535 {
536 cancel_camera_focus();
537 auto& panel = resolve_scene_panel(ctx);
538 panel.reset_camera(ctx);
539
540 std::string error;
541 auto camera = resolve_scene_camera(ctx, error);
542 if(!camera)
543 {
544 return {.text = error, .is_error = true};
545 }
546 return {.text = fmt::format(R"({{"ok":true,"camera":{}}})", camera_to_json(camera)),
547 .is_error = false};
548 },
549 .mutates_scene = false});
550}
551
552} // namespace unravel::mcp
Provides storage for common representation of spherical bounding volume, and wraps up common function...
Definition bsphere.h:18
Class representing a camera. Contains functionality for manipulating and updating a camera....
Definition camera.h:62
Component that handles transformations (position, rotation, scale, etc.) in the ACE framework.
auto get_position_global() const noexcept -> const math::vec3 &
TRANSLATION.
math::vec3 position
Definition defaults.cpp:52
std::string error
Definition mcp_async.cpp:33
const aiScene * scene
void stop_all(const std::string &scope)
Stops all actions within the specified scope.
Definition seq.cpp:160
auto read_vec3(const simdjson::dom::object &args, const char *key, math::vec3 &out) -> bool
auto empty_object_schema() -> std::string
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
auto read_bool(const simdjson::dom::object &args, const char *key, bool &out) -> bool
auto read_wait_ms(const simdjson::dom::object &args, int64_t default_ms=1000) -> std::chrono::milliseconds
auto capture_fbo_screenshot(mcp_manager &mcp, rtti::context &ctx, const std::function< gfx::frame_buffer::ptr(rtti::context &)> &resolve_fbo, const std::string &tag, std::chrono::milliseconds wait_timeout) -> tool_result
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
void register_viewport_tools(mcp_tool_registry &registry)
@ sphere
Sphere type reflection probe.
@ box
Box type reflection probe.
std::vector< math::quat > rotation
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
static void focus_camera_on_entities(entt::handle camera, hpp::span< const entt::handle > entities, float duration=0.0f)
Focuses a camera on a specified entity with a timed transition.
static void focus_camera_on_bounds(entt::handle camera, const math::bsphere &bounds, float duration=0.0f)
Focuses a camera on bounds with a timed transition.
Definition defaults.cpp:797