Cogs.Core
Editor.cpp
1#include "Editor.h"
2
3#include "EditorState.h"
4
5#include "imgui.h"
6#include "ImGuizmo.h"
7
8#include "../../../Cogs.Desktop/Source/ModalDialogs.h"
9
10#include "Context.h"
11#include "ViewContext.h"
12#include "Engine.h"
13#include "EntityStore.h"
14#include "Scene.h"
15#include "ExtensionRegistry.h"
16
17#include "Renderer/Renderer.h"
18
19#include "Renderer/InspectorGui/InspectorGuiRenderer.h"
20#include "Renderer/InspectorGui/InspectorGuiHelper.h"
21
22#include "Components/Core/AssetComponent.h"
23#include "Components/Core/SceneComponent.h"
24#include "Components/Core/TransformComponent.h"
25#include "Components/Core/ModelComponent.h"
26#include "Components/Core/StaticModelComponent.h"
27#include "Components/Core/PropertiesComponent.h"
28
29#include "Components/Behavior/OrbitingCameraController.h"
30#include "../Extensions/Potree/Source/PotreeComponent.h"
31#include "../Extensions/Image360/Source/Image360Component.h"
32
33#include "Utilities/CameraHelper.h"
34#include "Utilities/CameraUtils.h"
35
36#include "Resources/AssetManager.h"
37#include "Resources/ModelManager.h"
38#include "Resources/TextureManager.h"
39#include "Resources/MaterialManager.h"
40#include "Resources/Model.h"
41#include "Resources/ResourceStore.h"
42
43#include "Systems/Core/CameraSystem.h"
44#include "Systems/Core/ModelSystem.h"
45#include "Systems/Core/TransformSystem.h"
46
47#include "Services/DPIService.h"
48
49#include "Scene/RayPick.h"
50#include "Scene/GetBounds.h"
51
52#include "Input/InputManager.h"
53#include "Input/KeyboardMapping.h"
54
55#include "Renderer/RenderTexture.h"
56#include "Renderer/InspectorGui/Inspectors.h"
57
58#include "EntityEditor.h"
59#include "MaterialEditor.h"
60
61#include "Serialization/ModelWriter.h"
62#include "Serialization/ModelLoader.h"
63
64#include "Bridge/Bridge.h"
65#include "Bridge/RenderFunctions.h"
66#include "Bridge/ResourceFunctions.h"
67#include "Bridge/SceneFunctions.h"
68
69
70#include "Commands/ResourceCommands.h"
71#include "Commands/MergeCommand.h"
72#include "Commands/DumpStatsCommand.h"
73#include "Commands/ExportCommand.h"
74#include "Commands/ExportGltfCommand.h"
75#include "Commands/MeshOpCommands.h"
76#include "Commands/PackMeshCommand.h"
77#include "Commands/LoadRvmCommand.h"
78#include "Commands/ShowLicensesCommand.h"
79#include "Commands/AboutCommand.h"
80
81#include "Batch.h"
82
83#include "Foundation/Logging/Logger.h"
84#include "Foundation/Platform/Mouse.h"
85#include "Foundation/Platform/Keyboard.h"
86#include "Foundation/Platform/IO.h"
87
88#include <algorithm>
89#include <array>
90#include <cctype>
91#include <set>
92#include <unordered_set>
93
94// To stop warnings like
95// ../Extensions/Editor/Source/Editor.cpp:2686:3: warning: unused function 'operator~' [-Wunused-function]
96#if defined(__clang__)
97#pragma clang diagnostic ignored "-Wunused-function"
98#endif
99
100using namespace Cogs::Geometry;
101
102namespace
103{
104 const Cogs::Logging::Log logger = Cogs::Logging::getLogger("Editor");
105 Cogs::Core::EventBasedInput eventBasedInput;
106}
107
108namespace
109{
111 struct CameraSetupData {
112 glm::dvec3 position;
113 glm::quat rotation;
114 };
115
117 [[nodiscard]] Cogs::Core::EntityPtr getEditorCamera(const Cogs::Core::Context* context)
118 {
119 Cogs::Core::EntityPtr cameraEntity = context->store->findEntity("EditorCamera");
120 if (!cameraEntity) {
121 // Check for 'Camera'. Reading Scenes with clear can leave only saved Camera (normal name for Camera).
122 cameraEntity = context->store->findEntity("Camera");
123 }
124
125 // Auto-update ViewContext camera if changed.
126 // Check that really a Camera.
127 if (cameraEntity && cameraEntity != context->getDefaultView()->getCamera()) {
128 if (cameraEntity->getComponent<Cogs::Core::SceneComponent>()) {
129 context->getDefaultView()->setCamera(cameraEntity);
130 }
131 else {
132 cameraEntity = nullptr;
133 LOG_ERROR(logger, "Detected Camera without CameraComponent: %s", cameraEntity->getName().c_str());
134 }
135 }
136
137 if (!cameraEntity) {
138 LOG_FATAL(logger, "Crashing: Missing editor camera (Name: 'EditorCamera' or 'Camera') in scene");
139 abort();
140 }
141
142 return cameraEntity;
143 }
144
146 void utilSetRenderMode(Cogs::Core::Context* context, Cogs::Core::RenderMode mode)
147 {
148 ::setRenderMode(context, static_cast<int>(mode));
149 }
150
151 std::string toLower(const std::string & input)
152 {
153 std::string lowerCase;
154 lowerCase.resize(input.size());
155 std::transform(input.begin(), input.end(), lowerCase.begin(), [](const auto x) { return static_cast<char>(std::tolower(x)); });
156
157 return lowerCase;
158 }
159
160 bool is3dModelFileFormat(const std::string & extension)
161 {
162 const std::string ext = toLower(extension);
163 return ext == ".obj" || ext == ".3ds" || ext == ".fbx" || ext == ".dae" ||
164 ext == ".cogsbin" ||
165 ext == ".dxf" ||
166 ext == ".rvm" ||
167 ext == ".rvmdump" ||
168 ext == ".vue" ||
169 ext == ".gltf" ||
170 ext == ".glb" ||
171 ext == ".stp" ||
172 ext == ".ogex" || ext == ".iv" || ext == ".wrl";
173 }
174
176 template<typename T>
177 void setIfNotSet(Cogs::Core::Context* context, const Cogs::StringView& key, const T& value)
178 {
179 if (!context->variables->exist(key)) {
180 context->variables->set(key, value);
181 }
182 }
183
191 template<class Matcher>
192 void recurseEntity(const Cogs::ComponentModel::Entity& entity, Matcher matcherFunc)
193 {
194 bool recurse = matcherFunc(entity);
195 if (recurse) {
196 const auto* sceneComponent = entity.getComponent<Cogs::Core::SceneComponent>();
197 if (sceneComponent) {
198 for (const auto& child : sceneComponent->children) {
199 recurseEntity(*child, matcherFunc);
200 }
201 }
202 }
203 }
204
208 void addAssetTags(Cogs::Core::Context* context, Entity* asset, const std::string& fileName)
209 {
210 const size_t extLength = (::toLower(fileName).find(".asset.zst") != std::string::npos) ? 10u : 6u;
211 const std::string stem = fileName.substr(0, fileName.size() - extLength);
212 const std::string tagsPath = stem + ".tags.txt";
213
214 if (Cogs::IO::exists(tagsPath)) {
215 auto content = Cogs::IO::readFileSync(tagsPath);
216 if (content) {
217 Cogs::StringView data(static_cast<const char*>(content->data()), content->size);
218 const auto lines = data.split("\n");
219
220 // Store each tag.
222 if (propsComponent) {
223 size_t index = 0;
224 for (Cogs::StringView line : lines) {
225 // Ignore parent and children if tags with hierarchy.
226 const size_t comma = line.find(",");
227 if (comma != Cogs::StringView::NoPosition) {
228 line = line.substr(0, comma);
229 }
230 propsComponent->properties.addProperty(std::to_string(index), line);
231 index++;
232 }
233 }
234 LOG_DEBUG(logger, "Read Asset tags. Count: %zd", lines.size());
235 }
236 else {
237 LOG_ERROR(logger, "Reading Asset tags file failed: %s", tagsPath.c_str());
238 }
239 }
240 }
241
247 const Cogs::Core::Context* context,
248 size_t level = 0)
249 {
250 assert(level <= 2);
251 std::array<Cogs::ComponentModel::Entity*, 3> lastStack = { nullptr, nullptr, nullptr };
252 while (entity) {
253 lastStack[2] = lastStack[1];
254 lastStack[1] = lastStack[0];
255 lastStack[0] = entity;
256 entity = context->store->getEntityParent(entity);
257 }
258
259 return lastStack[level];
260 }
261
264 void setVisibility(Cogs::Core::SceneComponent* sceneComponent, bool visible, bool recurse = false)
265 {
266 if (sceneComponent && sceneComponent->visible != visible) {
267 sceneComponent->visible = visible;
268 sceneComponent->setChanged();
269 }
270
271 if (sceneComponent && recurse) {
272 for (const auto& child : sceneComponent->children) {
273 auto childSceneComponent = child->getComponent<Cogs::Core::SceneComponent>();
274 setVisibility(childSceneComponent, visible, recurse);
275 }
276 }
277 }
278
282 void setConsistentChildInvisibility(Cogs::ComponentModel::Entity* entity)
283 {
285 if (sceneComponent) {
286 if (sceneComponent->visible == false) {
287 setVisibility(sceneComponent, false, true);
288 }
289 else {
290 for (const auto& child : sceneComponent->children) {
291 setConsistentChildInvisibility(child.get());
292 }
293 }
294 }
295
296 }
297
299 void recurseAddFindPatternMatches(const Cogs::ComponentModel::Entity& entity, const std::regex& findRegex, Cogs::Core::EntityIds& ids)
300 {
301 recurseEntity(entity, [&](const Cogs::ComponentModel::Entity& entity) {
302 if (std::regex_search(entity.getName(), findRegex)) {
303 ids.push_back(entity.getId());
304 return false;
305 }
306 return true;
307 });
308 }
309
311 void addChildrenIds(const Cogs::ComponentModel::Entity* entity,
312 std::unordered_set<size_t>& entityIds)
313 {
314 entityIds.insert(entity->getId());
315 const auto* sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
316 if (sceneComponent) {
317 for (const auto& child : sceneComponent->children) {
318 addChildrenIds(child.get(), entityIds);
319 }
320 }
321 }
322
325 template<class T>
326 bool recursiveSetVisibility(Cogs::ComponentModel::Entity* entity, T show)
327 {
328 bool anyChildVisible = false;
329 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
330 if (sceneComponent) {
331 for (const auto& child : sceneComponent->children) {
332 anyChildVisible = recursiveSetVisibility(child.get(), show) || anyChildVisible;
333 }
334 }
335
336 // calculate visibility of node.
337 bool visible = show(entity, anyChildVisible);
338
339 if (sceneComponent && sceneComponent->visible != visible) {
340 sceneComponent->visible = visible;
341 sceneComponent->setChanged();
342 }
343
344 return visible;
345 }
346
347
350 template<class T>
351 bool recursiveGetVisibility(const Cogs::ComponentModel::Entity* entity, T isVisible)
352 {
353 bool anyChildVisible = false;
354 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
355 if (sceneComponent) {
356 for (const auto& child : sceneComponent->children) {
357 anyChildVisible = recursiveGetVisibility(child.get(), isVisible);
358 if (anyChildVisible) {
359 break;
360 }
361 }
362 }
363
364 if (!anyChildVisible) {
365 anyChildVisible = isVisible(entity);
366 }
367
368 return anyChildVisible;
369 }
370
372 void ensureEntityVisible(Cogs::Core::Context* context, Cogs::ComponentModel::Entity* entity)
373 {
374 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
375 if (sceneComponent && !sceneComponent->visible) {
376 sceneComponent->visible = true;
377 sceneComponent->setChanged();
378 }
379
380 auto parent = context->store->getEntityParent(entity);
381 if (parent) {
382 ensureEntityVisible(context, parent);
383 }
384 }
385
386 // Get all layers specified in the _rvmAllLayers property for the selected entity, after the following rules:
387 // if the current entity has a PropertiesComponent:
388 // if it has the _rvmAllLayers property, parse and return the layers
389 // else if not, search an ancestor that has PropertiesComponent and _rvmAllLayers and return those layers
390 // otherwise return no layers
391 // otherwise go through all children and find all that have a PropertiesComponent with the _rvmAllLayers property
392 // if any found, gather all layers and return
393 // otherwise return no layers
394 std::set<Cogs::StringView> getAllLayers(Cogs::ComponentModel::Entity* entity) {
395 if (entity == nullptr) return {};
396
398 if (propertiesComponent) {
399 if (propertiesComponent->properties.hasProperty("_rvmAllLayers")) {
400 Cogs::StringView::Vector allLayers = propertiesComponent->properties.getProperty("_rvmAllLayers", "").split("@");
401 std::set<Cogs::StringView> allLayersStrs = {};
402 for (Cogs::StringView layer : allLayers) {
403 allLayersStrs.insert(layer);
404 }
405 return allLayersStrs;
406 }
407 // Current entity has a PropertiesComponent but doesn't have a _rvmAllLayers property.
408 // Find the first parent that has and get the layers from it, or return no layers.
409 else {
411 if (sceneComponent) {
412 if (Component* parent = sceneComponent->parent.resolve(); parent && parent->getComponent<Cogs::Core::PropertiesComponent>()) {
413 return getAllLayers(parent->getContainer());
414 }
415 }
416 return {};
417 }
418 }
419 // Current entity doesn't have PropertiesComponent. Check the children and gather all layers from them, if any.
420 else {
422 if (!sceneComponent) return {};
423
424 std::set<Cogs::StringView> allLayers = {};
425 for (const Cogs::Core::EntityPtr& child : sceneComponent->children) {
426 std::set<Cogs::StringView> layers = getAllLayers(child.get());
427 allLayers.merge(layers);
428 }
429 return allLayers;
430 }
431 }
432
442 size_t searchEntity(size_t numFound, Cogs::Core::Editor& editor, const Entity* entity)
443 {
444 const std::string& name = entity->getName();
445 if (std::regex_search(name, editor.getState()->getFindRegex())) {
446 if (numFound == editor.getState()->findOffset) {
447 editor.selectOne(entity->getId());
448 editor.setStatusbarText("Found: " + entity->getName() + " Index:" + std::to_string(editor.getState()->findOffset + 1), false);
449 // editor.focusEntity();
450 return numFound + 1u;
451 }
452
453 numFound++;
454 }
455
456 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
457 if (sceneComponent) {
458 for (auto& child : sceneComponent->children) {
459 numFound = searchEntity(numFound, editor, child.get());
460 if (numFound > editor.getState()->findOffset) {
461 break;
462 }
463 }
464 }
465
466 return numFound;
467
468 }
469
485 CameraSetupData parseCameraPosition(Cogs::Core::Context* context, const std::string& path, bool fixCameraAlways, const Cogs::Core::EditorState* state)
486 {
487 bool fixCamera = fixCameraAlways;
488 glm::dvec3 pos = { 0.0, 0.0, 0.0 };
489 glm::quat orientation;
490
491 FILE* scannerPos = std::fopen(path.c_str(), "r");
492 if (scannerPos) {
493 std::array<char, 256> line = {};
494 std::fgets(line.data(), static_cast<int>(line.size()), scannerPos);
495 if (std::sscanf(line.data(), "%lf, %lf, %lf", &pos.x, &pos.y, &pos.z) == 3) {
496 fixCamera = true;
497 }
498 else if (std::sscanf(line.data(), "%lf %lf %lf", &pos.x, &pos.y, &pos.z) == 3) {
499 fixCamera = true;
500 }
501 else {
502 pos = { 0.0, 0.0, 0.0 };
503 }
504
505 // Read optional second line. Either Euler angles (3-values) or x,y,z,w.
506 if (fixCamera) {
507 line = {};
508 std::fgets(line.data(), static_cast<int>(line.size()), scannerPos);
509 glm::quat q;
510 int nScan = std::sscanf(line.data(), "%f %f %f %f", &q.x, &q.y, &q.z, &q.w);
511 if (nScan == 4) {
512 orientation = q;
513 }
514 else if (nScan == 3) {
515 orientation = glm::quat(glm::vec3(q.x, q.y, q.z));
516 }
517 else if (nScan != 0) {
518 LOG_ERROR(logger, "Parsing error second line. Expected x y z, OR x y z w File: %s", path.c_str());
519 }
520 }
521
522 std::fclose(scannerPos);
523 }
524
525 if (fixCamera) {
526 Cogs::Core::OrbitingCameraController* orbitingCameraController = state->editorCamera.lock()->getComponent<Cogs::Core::OrbitingCameraController>();
527 if (orbitingCameraController) {
528 Cogs::Core::TransformComponent* transformComponent = state->editorCamera.lock()->getComponent<Cogs::Core::TransformComponent>();
529 transformComponent->coordinates = pos;
530 transformComponent->position = {};
531 transformComponent->setChanged();
532 ::setOrigin(context, glm::value_ptr(pos));
533
534 orbitingCameraController->moveCamera = false;
535 orbitingCameraController->setChanged();
536 LOG_WARNING(logger, "OrbitingCameraComponent.moveCamera turned off (fixed camera rotation mode)");
537 LOG_WARNING(logger, "Camera position set to: [%.2f %.2f %.2f]", pos.x, pos.y, pos.z);
538 }
539 }
540
541 return { pos, orientation };
542 }
543}
544
545
546namespace Cogs::Core
547{
548 struct IconInfo
549 {
550 Icon icon;
551 const char * fileName;
552 };
553
554 const IconInfo icons[] = {
555 { Icon::None, nullptr },
556 { Icon::New, "IconNew.png" },
557 { Icon::Open, "IconOpen.png" },
558 { Icon::Save, "IconSave.png" },
559
560 { Icon::Undo, "IconUndo.png" },
561 { Icon::Redo, "IconRedo.png" },
562
563 { Icon::Select, "IconSelect.png" },
564 { Icon::Move, "IconMove.png" },
565 { Icon::Rotate, "IconRotate.png" },
566 { Icon::Scale, "IconScale.png" },
567
568 { Icon::Plus, "IconPlus.png" },
569 { Icon::Minus, "IconMinus.png" },
570 { Icon::Info, "IconInfo.png" },
571 { Icon::Gears, "IconGears.png" },
572
573 { Icon::Eye, "IconEye.png" },
574
575 { Icon::Solid, "IconSolid.png" },
576 { Icon::Wireframe, "IconWireframe.png" },
577 { Icon::BoundingBox, "IconBoundingBox.png" },
578
579 { Icon::PivotLocal, "IconPivotLocal.png" },
580 { Icon::PivotCenter, "IconPivotCenter.png" },
581
582 { Icon::ShowAll, "IconShowAll.png" },
583 { Icon::Hide, "IconHide.png" },
584 { Icon::HideUnselected, "IconHideUnselected.png" },
585 { Icon::Eye, "IconEye.png" },
586 { Icon::SelectFurthestAway, "IconFurthest.png" },
587 { Icon::IconInvertVisibility, "IconInvertVisibility.png" },
588 };
589}
590
591Cogs::Core::Editor::Editor(Context * context) :
592 context(context),
593 state(std::make_unique<EditorState>()),
594 materialEditor(std::make_unique<MaterialEditor>(context, this))
595{
596 state->editor = this;
597 state->context = context;
598}
599
600Cogs::Core::Editor::~Editor() = default;
601
602void Cogs::Core::Editor::registerExtensionCommand(const StringView & key, CommandCreator creator)
603{
604 auto & command = getExtensionCommands().emplace_back();
605
606 command.key = key.to_string();
607 command.creator = creator;
608}
609
610std::vector<Cogs::Core::ExtensionCommand> & Cogs::Core::Editor::getExtensionCommands()
611{
612 static std::vector<ExtensionCommand> commands;
613
614 return commands;
615}
616
617void Cogs::Core::Editor::registerExtensionItem(const StringView & key, const StringView & name)
618{
619 auto & item = getExtensionItems().emplace_back();
620
621 item.key = key.to_string();
622 item.name = name.to_string();
623}
624
625std::vector<Cogs::Core::ExtensionItem> & Cogs::Core::Editor::getExtensionItems()
626{
627 static std::vector<ExtensionItem> items;
628
629 return items;
630}
631
632
633void Cogs::Core::Editor::initialize()
634{
635 ::setIntVariable(context, "cogsbin.writeVersion", 4);
636
637 if (initialized) return;
638 for (auto & icon : icons) {
639 if (!icon.fileName) continue;
640
641 std::string path = "Textures/Gui/" + std::string(icon.fileName);
642
643 if (icon.icon == Icon::Select) {
644 state->icons[(int)icon.icon] = context->textureManager->loadTexture(path, NoResourceId, TextureLoadFlags::Flip);
645 }
646 else {
647 state->icons[(int)icon.icon] = context->textureManager->loadTexture(path, NoResourceId, TextureLoadFlags::ForceUnique);
648 }
649 }
650
651 ::setDoubleVariable(context, "resources.meshes.uploadBatchMaxTime", 0.2);
652 context->modelManager->registerLoader(new CogsModelLoader());
653
654 float scaleFactor = context->getDefaultView()->dpiService->getScaleFactor();
655
656 ImguiRenderer* guiRenderer = context->renderer->getGuiRenderer();
657 guiRenderer->fontRegistry.defaultFont->load(context, "Fonts/GoNotoCurrent.ttf", 16 * scaleFactor, ImGui::GetIO().Fonts->GetGlyphRangesDefault());
658
659 minWidth *= scaleFactor;
660 sidebarWidth *= scaleFactor;
661 inspectorWidth *= scaleFactor;
662 menuHeight *= scaleFactor;
663 toolbarHeight *= scaleFactor;
664 statusbarHeight *= scaleFactor;
665 buttonSize *= scaleFactor;
666
667 initialized = true;
668}
669
670void Cogs::Core::Editor::cleanup()
671{
672 state->icons.clear();
673 commands.clear();
674}
675
676namespace Cogs::Core
677{
678 struct MenuItem
679 {
680 static MenuItem Separator() {
681 return MenuItem{ nullptr, nullptr, nullptr, []() {} };
682 }
683
684 const char* getStatusbarText() const { return description ? description : name; }
685
686 const char * name;
687 const char * description;
688 const char * shortCut;
689 std::function<void(void)> action;
690 bool enabled = true;
691 bool * selected = nullptr;
692 };
693
694 struct Menu
695 {
696 const char * name;
697 std::vector<MenuItem> items;
698 bool enabled = true;
699 };
700}
701
702void Cogs::Core::Editor::show()
703{
704 if (!initialized) {
705 initialize();
706 }
707
708 eventBasedInput.updateState(context->getDefaultView()->refKeyboard(), context->getDefaultView()->refMouse());
709
710 ImGuizmo::BeginFrame();
711
712 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
713
714 const glm::vec2 size = context->renderer->getSize();
715 ImGui::SetNextWindowSize(ImVec2(size.x, size.y));
716 ImGui::SetNextWindowPos(ImVec2(0, 0));
717
718 processShortcuts();
719
720 bool selectedVisible = false;
721 if (state->getSelected()) {
722 auto* selectedSceneComponent = state->getSelected()->getComponent<Cogs::Core::SceneComponent>();
723 selectedVisible = selectedSceneComponent ? selectedSceneComponent->visible : false;
724 }
725
726 // Alt key temporarily shows the hidden menu
727 const Keyboard& keyboard = context->getDefaultView()->refKeyboard();
728 if ((showMenu || keyboard.isKeyDown(Key::Alt)) && ImGui::BeginMainMenuBar()) {
729
730 bool assetSystemFreeze = ::getBoolVariable(context, "resources.assets.lodFreeze");
731
732 Menu menus[] = {
733 { "File",
734 {
735 { "Clear All", "Clear any loaded files and reset the GUI", nullptr, [this]() { clear(); } },
736 { "Open", "Open a file for viewing in the GUI, add data to the root Entity level. Supported files are scenes, 3D models, assets, point clouds, 360 images", "Ctrl+O", [this]() { open("", true, false); }},
737 { "Open as Child of Selected", "Open a file for viewing, add data as a child to the selected Entity", nullptr, [this]() { open("", false, false); }, state->getSelected() != nullptr},
738 MenuItem::Separator(),
739 { nullptr, nullptr, nullptr, []() {} },
740 { "Save", "Save the contents in Cogs scene format", "Ctrl+S", [this]() { save(); }},
741 { "Save As...", "Save the contents in Cogs scene format to a new file", "Ctrl+Shift+S", [this]() { saveAs(); } },
742 { "Save Incremental", nullptr, "Ctrl+Alt+S", [this]() { saveIncremental(); } },
743 MenuItem::Separator(),
744 { "Import Static Models...", nullptr, nullptr, [this]() { importStatic(); } },
745 { "Export Selected As...", "Allows exporting selected entities in cogsbin/GLTF/GLB formats", "Ctrl+E", [this]() { exportEntities(); }},
746 { "Export Scene As...", "Allows exporting whole scene in cogsbin/GLTF/GLB formats", nullptr, [this]() { exportEntities(true); }},
747 MenuItem::Separator(),
748 { "Exit", "Exit the application", "Ctrl+Q", []() { exit(0); } },
749 }
750 },
751 { "Edit",
752 {
753 { "Undo", "Normal editor undo command", "Ctrl+Z", [this]() { undo(); }, modalCommand == nullptr && commandIndex != NoCommand},
754 { "Redo", "Normal editor redo command", "Ctrl+Y", [this]() { redo(); }, modalCommand == nullptr && commandIndex < (commands.size() - 1)},
755 { "Cut", "Cut (Copy and Delete) selected Entity", "Ctrl+X", [this]() { cut(); }, state->hasSelected()},
756 { "Copy", "Copy selected Entity", "Ctrl+C", [this]() { copy(); }, state->hasSelected() },
757 { "Paste", "Paste copied Entity below selected", "Ctrl+V", [this]() { paste(); }, state->copied.IsArray()},
758 { "Delete", "Delete selected Entity", "Ctrl+Del", [this]() { destroy(); }, state->hasSelected()},
759 { "Delete Permamently", "Delete selected Entity permanently, without ability to recover using Undo", "Shift+Del", [this]() { destroyPermanently(); }, state->hasSelected()},
760 MenuItem::Separator(),
761 { "Select All", "Select all entities", "Ctrl+A", [this]() { selectAll(); }},
762 { "Toggle Entity Search", "Turn Entity Search of or off", "Ctrl+F", [this]() { state->findEntities = !state->findEntities; }},
763 }
764 },
765 { "Debug",
766 {
767 { "No visualizer", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", ""); } },
768 { "Show normals", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", "Pipelines/DebugView/ShowNormals.pipeline"); } },
769 { "Show draw calls", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", "Pipelines/DebugView/ShowDrawCalls.pipeline"); } },
770 { "Show object IDs", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", "Pipelines/DebugView/ShowObjectIDs.pipeline"); } },
771 { "Show triangle size", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", "Pipelines/DebugView/ShowTriangleSizes.pipeline"); } },
772 { "Show shadow cascades", nullptr, nullptr, [this]() { ::setVariable(context, "renderer.pipeline.override", "Pipelines/DebugView/ShowShadowCascades.pipeline"); } },
773 MenuItem::Separator(),
774 { assetSystemFreeze ? "Disable asset lod freeze" : "Enable asset lod freeze", nullptr, nullptr, [&]() { ::setBoolVariable(context, "resources.assets.lodFreeze", !assetSystemFreeze); } }
775 }
776 },
777 { "Modes",
778 {
779 { "Select Mode", "Select objects by clicking on them", nullptr, [this]() { changeMode(EditingMode::Select); }},
780 { "Translate Mode", "Translate the object using the Gizmo tool", nullptr, [this]() { changeMode(EditingMode::Translate); }},
781 { "Rotate Mode", "Rotate the object using the Gizmo tool", nullptr, [this]() { changeMode(EditingMode::Rotate); }},
782 { "Scale Mode", "Scale the object using the Gizmo tool", nullptr, [this]() { changeMode(EditingMode::Scale); }},
783 MenuItem::Separator(),
784 { "Solid shading", nullptr, "0", [this]() { utilSetRenderMode(context, RenderMode::Normal); }},
785 { "Wireframe shading", nullptr, "1", [this]() { utilSetRenderMode(context, RenderMode::Wireframe); }},
786 { "Solid shading Bounding Boxes", nullptr, "3", [this]() { utilSetRenderMode(context, RenderMode::Box); }},
787 }
788 },
789 { "Commands",
790 {
791
792 }
793 },
794 { "Create",
795 {
796
797 }
798 },
799 { "Entity",
800 {
801 { "Destroy", "Destroy Entity", nullptr, [this]() { destroy(); }},
802 { "Destroy Permanently", "Destroy Entity permanently, without Undo possibility", nullptr, [this]() { destroyPermanently(); }},
803 MenuItem::Separator(),
804 }
805 },
806 { "View",
807 {
808 { "Focus on Selection", "Zooms to the selected item(s)", "f", [this]() { focusSelected(); }, state->hasSelected()},
809 { "Focus all", "Best fit all objects in the scene to the screen", "v", [this]() { focusAll(); }, true},
810 { nullptr, nullptr, nullptr, []() {} },
811 { "Show All", "Make all objects visible", nullptr, [this]() { showAll(); }, true},
812 { "Select All", "Select all objects", "Ctrl+A", [this]() { selectAll(); }, true},
813 { selectedVisible ? "Hide Selected" : "Show Selected", nullptr, nullptr, [this,selectedVisible]() { showHide(!selectedVisible); }, state->hasSelected() },
814 { "Hide Unselected", "Show only selected objects, hide all others", nullptr, [this]() { hideUnselected(); }, state->hasSelected()},
815 { "Invert Visibility", "Toggles visibility of all objects", nullptr, [this]() { invertVisibility(); }, state->hasSelected()},
816 { "Select Furthest Away", "Select object in tree furthest away from center of selected bounding box. Modifiers: Shift(X-axis), Control(Y-axis), Alt(Z-axis)", nullptr, [this]() { selectFurthestAway(); }, state->hasSelected()},
817 }
818 },
819 { "Help",
820 {
821 { "Cogs.Runtime Help", nullptr, nullptr, [this]() { onlineHelp(false); } },
822 { "Cogs Help", nullptr, nullptr, [this]() { onlineHelp(true); } },
823 { nullptr, nullptr, nullptr, []() {} },
824 { "About", nullptr, nullptr, [this]() { showAbout(); } },
825 { "Show Licenses", nullptr, nullptr, [this]() { showLicenses(); } }
826 },
827 },
828 };
829
830 auto findMenuEntry = [&menus](const Cogs::StringView & name) -> Menu& {
831 for (Menu& menu : menus) {
832 if (name == menu.name)
833 return menu;
834 }
835
836 return menus[0];
837 };
838
839 Menu& viewMenu = findMenuEntry("View");
840 viewMenu.items.push_back(MenuItem::Separator());
841 viewMenu.items.emplace_back(MenuItem{ "Show Menu", showMenu ? "Hide Menu (Alt key shows menu temporarily)" : "Show Menu", "F10", []() {}, true, &showMenu });
842 viewMenu.items.emplace_back(MenuItem{ "Show Toolbar", showToolbar ? "Hide Toolbar" : "Show Toolbar", "", []() {}, true, &showToolbar});
843 viewMenu.items.emplace_back(MenuItem{ "Show Entity sidebar", showSidebar ? "Hide Entity left sidebar" : "Show Entity left sidebar", "", []() {}, true, &showSidebar });
844 viewMenu.items.emplace_back(MenuItem{ "Show Inspector sidebar", showInspector ? "Hide Inspector right sidebar" : "Show Inspector right sidebar", "", []() {}, true, &showInspector });
845 viewMenu.items.emplace_back(MenuItem{ "Show Statusbar", showStatusbar ? "Hide Statusbar" : "Show Statusbar", "", []() {}, true, &showStatusbar });
846
847 auto & commandsMenu = findMenuEntry("Commands");
848 commandsMenu.enabled = state->hasSelected();
849
850 if (state->getSelected() != nullptr) {
851 commandsMenu.items.emplace_back(MenuItem{ "Scale to unit cube", nullptr, "", [this]() { apply<ScaleToUnitCubeCommand>(state->getSelectedId()); } });
852 commandsMenu.items.emplace_back(MenuItem{ "Merge Material", nullptr, "", [this]() { apply<MergeMaterialCommand>(state->getSelectedId()); } });
853 commandsMenu.items.emplace_back(MenuItem{ "Remap Material", nullptr, "", [this]() { apply<RemapMaterialCommand>(state->getSelectedId()); } });
854 commandsMenu.items.emplace_back(MenuItem{ "Merge Children", nullptr, "", [this]() { apply<MergeCommand>(state->selected); } });
855 commandsMenu.items.emplace_back(MenuItem{ "Merge Meshes", nullptr, "", [this]() { apply<MergeMeshCommand>(state->selected); } });
856 commandsMenu.items.emplace_back(MenuItem{ "Generate Normals", nullptr, "", [this]() { apply<GenerateNormalsCommand>(); } });
857 commandsMenu.items.emplace_back(MenuItem{ "Generate Tangents", nullptr, "", [this]() { apply<GenerateTangentsCommand>(state->getSelectedId()); } });
858 commandsMenu.items.emplace_back(MenuItem{ "Dump Entity Stats", nullptr, "", [this]() { apply<DumpStatsCommand>(state->getSelectedId()); } });
859 commandsMenu.items.emplace_back(MenuItem{ "Remove duplicate vertices", nullptr, "", [this]() { apply<UniqueVerticesCommand>(); } });
860 commandsMenu.items.emplace_back(MenuItem{ "Scale Mesh", nullptr, "", [this]() { apply<ScaleMeshCommand>(state->selected); } });
861
862 for (auto & item : getExtensionItems()) {
863 for (auto & ext : getExtensionCommands()) {
864 if (ext.key == item.key) {
865 commandsMenu.items.emplace_back(MenuItem{ item.name.c_str(), nullptr, nullptr,[&, creator = ext.creator]()
866 {
867 doApply(std::unique_ptr<EditorCommand>(creator(getState())));
868 }});
869 }
870 }
871 }
872
873 }
874
875 struct NameDescription
876 {
877 const char* name;
878 const char* description;
879 } defs[] = {
880 {"Empty", "Create Empty"},
881 {"Group", "Create Group"},
882 {"Lod", "Create Lod"},
883 {"Light", "Create Light"},
884 {nullptr, nullptr},
885 {"Plane", "Create Plane"},
886 {"Cube", "Create Cube"},
887 {"Sphere", "Create Sphere"},
888 {"Cylinder", "Create Cylinder"},
889 {"Cone", "Create Cone"},
890 {"SkyDome", "Create SkyDome"},
891 {"BasicOcean", "Create BasicOcean"},
892 {"ModelEntity", "Create ModelEntity"},
893 {"StaticModel", "Create StaticModel"},
894 {nullptr, nullptr},
895 {"PlaneMesh", "Create PlaneMesh"}
896 };
897
898 auto & createMenu = findMenuEntry("Create");
899
900 for (auto & def : defs) {
901 if (!def.name) {
902 createMenu.items.emplace_back(MenuItem::Separator());
903 } else {
904 createMenu.items.emplace_back(MenuItem{
905 def.name, def.description, nullptr, [&, def]() {
906 apply<CreateEntityCommand>(def.name, def.name, false);
907 }
908 });
909 }
910 }
911
912 const NameDescription comps[] = {
913 {"TransformComponent", "Create TransformComponent"},
914 {"SceneComponent", "Create SceneComponent"},
915 {"MeshComponent", "Create MeshComponent"},
916 {"MeshRenderComponent", "Create MeshRenderComponent"},
917 {"MeshGeneratorComponent", "Create MeshGeneratorComponent"},
918 {"ModelComponent", "Create ModelComponent"}
919 };
920
921 auto & entityMenu = findMenuEntry("Entity");
922
923 entityMenu.enabled = state->numSelected() == 1U;
924
925 if (entityMenu.enabled) {
926 for (const NameDescription& comp : comps) {
927 entityMenu.items.emplace_back(MenuItem{
928 comp.name, comp.description, nullptr, [this, comp]() {
929 apply<AddComponentCommand>(state->getSelectedId(), comp.name);
930 }
931 });
932 }
933 }
934
935 bool setStatus = false;
936 for (auto & menu : menus) {
937 if (!menu.enabled) continue;
938
939 if (ImGui::BeginMenu(menu.name, menu.enabled)) {
940 for (auto & item : menu.items) {
941 if (item.name) {
942 if (ImGui::MenuItem(item.name, item.shortCut, item.selected, item.enabled)) {
943 item.action();
944 }
945 if (ImGui::IsItemHovered(ImGuiHoveredFlags_None)) {
946 setStatusbarText(item.getStatusbarText(), true);
947 setStatus = true;
948 }
949 } else {
950 ImGui::Separator();
951 }
952 }
953
954 ImGui::EndMenu();
955 }
956 }
957
958 // No longer hovering over a GUI item remove.
959 // Don't remove if application status as always entering this code.
960 if (showingGuiStatusbarText() && !setStatus) {
961 setStatusbarText(std::string(), true);
962 }
963
964 ImGui::EndMainMenuBar();
965 }
966
967 auto staticWindowFlags = ImGuiWindowFlags_NoTitleBar |
968 ImGuiWindowFlags_NoResize |
969 ImGuiWindowFlags_NoMove |
970 ImGuiWindowFlags_NoCollapse |
971 ImGuiWindowFlags_NoSavedSettings |
972 ImGuiWindowFlags_NoFocusOnAppearing;
973
974 if (showToolbar) {
975
976 ImGui::SetNextWindowPos(ImVec2(0, getMenuHeight()));
977 ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize.x, getToolbarHeight()));
978
979 ImGui::Begin("Toolbar", nullptr, staticWindowFlags);
980
981 auto actionDummy = []() { };
982 auto highlightDummy = []() {return false;};
983
984 auto hightlightIfVisible = [selectedVisible]() {
985 return selectedVisible;
986 };
987
988 struct ToolbarButton
989 {
990 const char * name;
991 const char * description;
992 const Icon icon = Icon::None;
993 std::function<void(void)> action;
994 std::function<bool(void)> highlight;
995 };
996
997 const ToolbarButton buttons[] = {
998 { "Clear All", nullptr, Icon::New, [this]() { clear(); }, highlightDummy },
999 { "Open", nullptr, Icon::Open, [this]() { open("", true, false); }, highlightDummy },
1000 { "Save", nullptr, Icon::Save, [this]() { save(); }, highlightDummy },
1001
1002 { "Undo", nullptr, Icon::Undo, [this]() { undo(); }, highlightDummy },
1003 { "Redo", nullptr, Icon::Redo, [this]() { redo(); } , highlightDummy },
1004
1005 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1006
1007 { "Select Mode", nullptr, Icon::Select, [this]() { changeMode(EditingMode::Select); }, [this]() { return state->mode == EditingMode::Select; } },
1008 { "Translate Mode", nullptr, Icon::Move, [this]() { changeMode(EditingMode::Translate); }, [this]() { return state->mode == EditingMode::Translate; } },
1009 { "Rotate Mode", nullptr, Icon::Rotate, [this]() { changeMode(EditingMode::Rotate); }, [this]() { return state->mode == EditingMode::Rotate; } },
1010 { "Scale Mode", nullptr, Icon::Scale, [this]() { changeMode(EditingMode::Scale); }, [this]() { return state->mode == EditingMode::Scale; } },
1011
1012 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1013
1014 { "Variables", nullptr, Icon::Gears, [this]() { state->showSettings = !state->showSettings; }, highlightDummy },
1015
1016 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1017
1018 { "Focus", "Focus on Selection", Icon::Eye, [this]() { focusSelected(); }, highlightDummy },
1019
1020 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1021
1022 { "Solid", "Solid shading", Icon::Solid, [this]() { utilSetRenderMode(context, RenderMode::Normal); }, highlightDummy },
1023 { "Wireframe", "Wireframe shading", Icon::Wireframe, [this]() { utilSetRenderMode(context, RenderMode::Wireframe); }, highlightDummy },
1024 { "BoundingBox", "Solid shading Bounding Boxes", Icon::BoundingBox, [this]() { utilSetRenderMode(context, RenderMode::Box); }, highlightDummy },
1025
1026 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1027
1028 { "PivotLocal", "Use object local origin as pivot", Icon::PivotLocal, [this]() { state->pivot = EditingPivot::ObjectPivot; }, [this]() { return state->pivot == EditingPivot::ObjectPivot; } },
1029 { "PivotCenter", "Use object center as pivot", Icon::PivotCenter, [this]() { state->pivot = EditingPivot::ObjectCenter; }, [this]() { return state->pivot == EditingPivot::ObjectCenter; } },
1030
1031 { nullptr, nullptr, Icon::None, actionDummy, highlightDummy },
1032
1033 { "ShowAll", "Show All", Icon::ShowAll, [this]() { showAll(); }, highlightDummy },
1034 { "Hide", hightlightIfVisible() ? "Hide Selected" : "Show Selected", Icon::Hide, [this, selectedVisible]() { showHide(!selectedVisible); }, hightlightIfVisible },
1035 { "HideUnselected", "Hide Unselected", Icon::HideUnselected, [this]() { hideUnselected(); }, highlightDummy },
1036 { "InvertVisibility", "Invert Visibility", Icon::IconInvertVisibility, [this]() { invertVisibility(); }, highlightDummy },
1037 { "SelectFurthestAway", "Select Furthest Away", Icon::SelectFurthestAway, [this]() { selectFurthestAway(); }, highlightDummy },
1038 };
1039
1040 ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
1041
1042 for (const ToolbarButton& button : buttons) {
1043 if (!button.name) {
1044 ImVec2 cursor = ImGui::GetCursorScreenPos();
1045 const float xOffset = 6;
1046 ImGui::GetWindowDrawList()->AddLine(ImVec2(cursor.x + xOffset, cursor.y + 4), ImVec2(cursor.x + xOffset, cursor.y + 32), 0x33FFFFFF, 1.0f);
1047 ImGui::SameLine(cursor.x + 20);
1048 continue;
1049 }
1050
1051 if (button.icon != Icon::None) {
1052 auto texture = state->icons[(int)button.icon];
1053
1054 Renderer* renderer = dynamic_cast<Renderer*>(context->renderer);
1055 RenderTexture * renderTexture = renderer->getRenderResources().getRenderTexture(context->textureManager->generateHandle((Texture *)texture.resolve()));
1056
1057 if (renderTexture && HandleIsValid(renderTexture->textureHandle)) {
1058 auto textureId = renderTexture->textureHandle.handle;
1059
1060 float padding = 6;
1061 float textureSize = buttonSize.x - padding * 2;
1062
1063 if (ImGui::ImageButton(button.name, textureId, ImVec2(textureSize, textureSize), ImVec2(0, 1), ImVec2(1, 0))) {
1064 button.action();
1065 } else if (ImGui::IsItemHovered()) {
1066 ImGui::SetTooltip("%s", button.description ? button.description : button.name);
1067 setStatusbarText(std::string(), true);
1068 }
1069
1070 if (button.highlight) {
1071 const bool isHighlighted = button.highlight();
1072
1073 if (isHighlighted) {
1074 ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), 0x33FFFFFF, 0, 0);
1075 }
1076 }
1077 }
1078 } else {
1079 if (ImGui::Button(button.name, ImVec2(buttonSize.x, buttonSize.y))) {
1080 button.action();
1081 }
1082 }
1083
1084 ImGui::SameLine();
1085 }
1086
1087 ImGui::PopStyleColor();
1088
1089 ImGui::End();
1090 }
1091
1092 float w = ImGui::GetIO().DisplaySize.x;
1093 float thickness = 6.f;
1094
1095 if (showSidebar) {
1096
1097 if (getSidebarWidth() + getInspectorWidth() + 2 * thickness >= w) {
1098 // This can happen if user hid sidebars, shrunk the window and then enabled them again
1099 // Set left sidebar to 1/3 of the window, the right one will be checked/fixed afterwards
1100 sidebarWidth = w / 3;
1101 }
1102
1103 auto oldColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
1104 const float d = 0.85f;
1105 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(oldColor.x * d, oldColor.y * d, oldColor.z * d, 1));
1106
1107 ImGui::SetNextWindowPos(ImVec2(0, getMenuHeight() + getToolbarHeight()));
1108 ImGui::SetNextWindowSize(ImVec2(getSidebarWidth(), ImGui::GetIO().DisplaySize.y - getMenuHeight() - getToolbarHeight() - getStatusbarHeight()));
1109
1110 ImGui::Begin("EditorEntities", nullptr, staticWindowFlags | ImGuiWindowFlags_HorizontalScrollbar);
1111
1112 showEntities();
1113
1114 ImGui::End();
1115
1116 ImGui::PopStyleColor();
1117
1118 // Left Splitter window to resize sidebar
1119
1120 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Disable padding
1121 ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
1122 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
1123
1124 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.2f));
1125 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.f, 1.f, 1.f, 1.0f));
1126 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.8f));
1127
1128 ImGui::SetNextWindowPos(ImVec2(getSidebarWidth(), getMenuHeight() + getToolbarHeight()));
1129 ImGui::SetNextWindowSize(ImVec2(thickness, ImGui::GetIO().DisplaySize.y - getMenuHeight() - getToolbarHeight() - getStatusbarHeight()));
1130 ImGui::Begin("TransparentLWindow", nullptr, staticWindowFlags | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground);
1131
1132 // left splitter
1133 ImGui::SetCursorPosX(0);
1134 ImGui::Button("##LSplitter", ImVec2(thickness, -1));
1135 if (ImGui::IsItemActive()) {
1136 sidebarWidth += static_cast<int>(ImGui::GetIO().MouseDelta.x);
1137 if (sidebarWidth < minWidth) sidebarWidth = minWidth;
1138 if (w - 2 * thickness - getInspectorWidth() < sidebarWidth) sidebarWidth = w - 2 * thickness - getInspectorWidth();
1139 }
1140
1141 ImGui::End();
1142 ImGui::PopStyleColor(4);
1143 ImGui::PopStyleVar(2);
1144 }
1145
1146 if (showInspector) {
1147
1148 if (getSidebarWidth() + getInspectorWidth() + 2 * thickness >= w) {
1149 inspectorWidth = w / 3;
1150 }
1151
1152 ImGui::SetNextWindowPos(ImVec2(w - getInspectorWidth(), getMenuHeight() + getToolbarHeight()));
1153 ImGui::SetNextWindowSize(ImVec2(getInspectorWidth(), ImGui::GetIO().DisplaySize.y - getMenuHeight() - getToolbarHeight() - getStatusbarHeight()));
1154
1155 ImGui::Begin("Inspector", nullptr, staticWindowFlags | ImGuiWindowFlags_HorizontalScrollbar);
1156
1157 if (state->findEntities) {
1158 bool search = false;
1159 static std::array<char, 65> buffer;
1160
1161 float spacing = ImGui::GetStyle().ItemInnerSpacing.x;
1162 showText("Entity Search");
1163 ImGui::SameLine(0.0f, spacing);
1164 if (ImGui::Button("Prev")) {
1165 if (state->findOffset > 0) state->findOffset--;
1166 search = true;
1167 }
1168 ImGui::SameLine(0.0f, spacing);
1169 if (ImGui::Button("Next")) {
1170 state->findOffset++;
1171 search = true;
1172 }
1173
1174 if (ImGui::InputText("Pattern", buffer.data(), buffer.size(), ImGuiInputTextFlags_None)) {
1175 state->findOffset = 0u;
1176
1177 ImguiRenderer* guiRenderer = context->renderer->getGuiRenderer();
1178 if (guiRenderer) {
1179 guiRenderer->addTextToGlyphBuilder(buffer.data());
1180 }
1181
1182 state->setFindPattern(buffer.data());
1183 if (!state->getFindPattern().empty()) {
1184 if (state->isFindRegexValid()) {
1185 search = true;
1186 }
1187 else {
1188 setStatusbarText("Invalid regular expression", false);
1189 }
1190 }
1191 }
1192
1193 if (search) {
1194 setStatusbarText(std::string(), false);
1195 size_t numFound = 0;
1196 auto store = context->store;
1197 auto& entities = store->getEntities();
1198
1199 for (auto& e : entities) {
1200 const EntityPtr& entity = e.second;
1201 if (!entity) continue;
1202 const TransformComponent* transformComponent = entity->getComponent<TransformComponent>();
1203 if (transformComponent && !transformComponent->parent) {
1204 numFound = searchEntity(numFound, *this, entity.get());
1205 if (numFound > state->findOffset) {
1206 break;
1207 }
1208 }
1209 }
1210
1211 if (getStatusbarText().empty()) {
1212 setStatusbarText("Find - None Index: " + std::to_string(getState()->findOffset + 1), false);
1213 }
1214 else if (keyboard.isKeyDown(Key::Control)) {
1215 // Auto focus
1216 focusSelected();
1217 }
1218 }
1219 }
1220
1221 if (state->getSelected()) {
1222 const std::set<StringView> allLayers = getAllLayers(state->getSelected());
1223 if (!allLayers.empty()) {
1224 // Add Pull-down for individual layer visibility
1225 if (ImGui::CollapsingHeader("Layer Visibility"))
1226 {
1227 // Go to the first ancestor that doesn't have a PropertiesComponent and set the layer visibility down from there
1228 // to make sure all the nodes get toggled on/off based on the layer visibility.
1229 Entity* startingEntity = state->getSelected();
1230 for (SceneComponent* sceneComp = startingEntity->getComponent<SceneComponent>(); sceneComp; sceneComp = sceneComp->parent.resolveComponent<SceneComponent>()) {
1231 startingEntity = sceneComp->getContainer();
1232 }
1233
1234 ScopedIndent si;
1235 for (StringView layer : allLayers) {
1236 bool layerVisible = getLayerVisibility(startingEntity, layer);
1237 if (ImGui::Checkbox(layer.to_string().data(), &layerVisible)) {
1238
1239 setLayerVisibility(startingEntity, layer, layerVisible);
1240 }
1241 }
1242 }
1243 }
1244
1245 if (state->lastPickState.rootId != NoEntity &&
1246 (state->lastPickState.rootId != state->getSelected()->getId())) {
1247 showText("Pick Root Id: %zd", state->lastPickState.rootId);
1248 const Entity* entity = state->getSelected();
1250 if (propertiesComponent) {
1251 Cogs::StringView layerProperty = propertiesComponent->properties.getProperty("_rvmLayer", "");
1252 if (!layerProperty.empty()) {
1253 showText("Layer : %s", layerProperty.to_string().c_str());
1254 }
1255
1256 }
1257
1258 }
1259 if (!state->lastPickState.details.empty()) {
1260 showText("%s", state->lastPickState.details.c_str());
1261 }
1262 if (!state->lastPickState.assetRootGroupName.empty()) {
1263 showText("Asset Root : %s", state->lastPickState.assetRootGroupName.c_str());
1264 }
1265 if (!state->lastPickState.pickId.empty() && state->lastPickState.pickId != std::to_string(state->getSelected()->getId())) {
1266 showText("Asset PickId : %s", state->lastPickState.pickId.c_str());
1267 }
1268 if (!state->lastPickState.boundingBox.empty()) {
1269 showText("BBox : %s", state->lastPickState.boundingBox.c_str());
1270 }
1271
1272 showEntityDetails(state->getSelected());
1273 }
1274
1275 ImGui::End();
1276
1277 // Right Splitter window to resize inspector sidebar
1278
1279 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Disable padding
1280 ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
1281 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
1282
1283 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.2f));
1284 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.f, 1.f, 1.f, 1.0f));
1285 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.8f));
1286
1287 ImGui::SetNextWindowPos(ImVec2(w - getInspectorWidth() - thickness, getMenuHeight() + getToolbarHeight()));
1288 ImGui::SetNextWindowSize(ImVec2(thickness, ImGui::GetIO().DisplaySize.y - getMenuHeight() - getToolbarHeight() - getStatusbarHeight()));
1289 ImGui::Begin("TransparentRWindow", nullptr, staticWindowFlags | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground);
1290
1291 // right splitter
1292 ImGui::SetCursorPosX(0);
1293 ImGui::Button("##RSplitter", ImVec2(thickness, -1));
1294 if (ImGui::IsItemActive()) {
1295 inspectorWidth -= static_cast<int>(ImGui::GetIO().MouseDelta.x);
1296 if (inspectorWidth < minWidth) inspectorWidth = minWidth;
1297 if (w - 2 * thickness - getSidebarWidth() < inspectorWidth) inspectorWidth = w - 2 * thickness - getSidebarWidth();
1298 }
1299
1300 ImGui::End();
1301 ImGui::PopStyleColor(4);
1302 ImGui::PopStyleVar(2);
1303 }
1304
1305 if (showStatusbar) {
1306 ImGui::SetNextWindowPos(ImVec2(0, ImGui::GetIO().DisplaySize.y - getStatusbarHeight()));
1307 ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize.x, getStatusbarHeight()));
1308
1309 ImGui::Begin("Statusbar", nullptr, staticWindowFlags);
1310
1311 ImGui::SetCursorPos(ImVec2(5, 0));
1312 ImGui::TextUnformatted(getStatusbarText().c_str());
1313
1314 ImGui::End();
1315 }
1316
1317 showGizmo();
1318
1319 if (modalCommand) {
1320 modalCommand->showGui();
1321 }
1322
1323 ImGui::PopStyleVar(); // Window rounding
1324
1325 if (state->showSettings) {
1326 variableInspector(context, &state->showSettings);
1327
1328 }
1329}
1330
1331
1332void Cogs::Core::Editor::processShortcuts()
1333{
1334 if (modalCommand) return;
1335
1336 // TODO: Use input manager mappings after adding proper action modifier support.
1337 struct EditorShortcut
1338 {
1339 Key key = Key::None;
1340 KeyboardModifiers modifier = KeyboardModifiers::None;
1341 std::function<void(void)> action;
1342 };
1343
1344 const EditorShortcut shortCuts[] = {
1345 { Key::Q, KeyboardModifiers::None, [this]() { apply<ChangeEditingModeCommand>(EditingMode::Select); } },
1346 { Key::W, KeyboardModifiers::None, [this]() { apply<ChangeEditingModeCommand>(EditingMode::Translate); } },
1347 { Key::E, KeyboardModifiers::None, [this]() { apply<ChangeEditingModeCommand>(EditingMode::Rotate); } },
1348 { Key::R, KeyboardModifiers::None, [this]() { apply<ChangeEditingModeCommand>(EditingMode::Scale); } },
1349
1350 { Key::Zero, KeyboardModifiers::None, [this]() { utilSetRenderMode(context, RenderMode::Normal); } },
1351 { Key::One, KeyboardModifiers::None, [this]() { utilSetRenderMode(context, RenderMode::Wireframe); } },
1352 { Key::Three, KeyboardModifiers::None, [this]() { utilSetRenderMode(context, RenderMode::Box); } },
1353
1354 { Key::F, KeyboardModifiers::None, [this]() { focusSelected(); } },
1355 { Key::F, KeyboardModifiers::Control, [this]() { state->findEntities = !state->findEntities; } },
1356
1357 { Key::V, KeyboardModifiers::None, [this]() { focusAll(); } },
1358
1359 { Key::A, KeyboardModifiers::Control, [this]() { selectAll(); } },
1360
1361 { Key::Z, KeyboardModifiers::Control, [this]() { undo(); } },
1362 { Key::Y, KeyboardModifiers::Control, [this]() { redo(); } },
1363 { Key::Z, KeyboardModifiers::Control | KeyboardModifiers::Shift, [this]() { redo(); } },
1364
1365 { Key::C, KeyboardModifiers::Control, [this]() { copy(); } },
1366 { Key::V, KeyboardModifiers::Control, [this]() { paste(); } },
1367 { Key::X, KeyboardModifiers::Control, [this]() { cut(); } },
1368
1369 { Key::O, KeyboardModifiers::Control, [this]() { open("", true, false); } },
1370
1371 { Key::E, KeyboardModifiers::Control, [this]() { exportEntities(); } },
1372
1373 { Key::S, KeyboardModifiers::Control, [this]() { save(); } },
1374 { Key::S, KeyboardModifiers::Control | KeyboardModifiers::Shift, [this]() { saveAs(); } },
1375 { Key::S, KeyboardModifiers::Control | KeyboardModifiers::Alt, [this]() { saveIncremental(); } },
1376
1377 { Key::G, KeyboardModifiers::Control, [this]()
1378 {
1379 apply<GroupCommand>(state->selected);
1380 }
1381 },
1382
1383 // Avoid deleting entity to be edited pressing Delete editing component field in ImGui.
1384 // No keyboard focus in this GUI.
1385 { Key::Delete, KeyboardModifiers::Control, [this]() { destroy(); } },
1386 { Key::Delete, KeyboardModifiers::Shift, [this]() { destroyPermanently(); } },
1387 { Key::F10, KeyboardModifiers::None, [this]() { showMenu = !showMenu; } },
1388 };
1389
1390 const Keyboard& keyboard = context->getDefaultView()->refKeyboard();
1391
1392 KeyboardModifiers currentModifiers = KeyboardModifiers::None;
1393
1394 if (keyboard.isKeyDown(Key::Control)) currentModifiers |= KeyboardModifiers::Control;
1395 if (keyboard.isKeyDown(Key::Shift)) currentModifiers |= KeyboardModifiers::Shift;
1396 if (keyboard.isKeyDown(Key::Alt)) currentModifiers |= KeyboardModifiers::Alt;
1397
1398
1399 // Toggle debug GUI on 'g' if there are no GUI item is active (e.g. typing in a text box)
1400 if (!Cogs::Core::ImguiRenderer::isUsingKeyboard() && (currentModifiers == KeyboardModifiers::None) && keyboard.wasKeyPressed(Key::G)) {
1401 bool enabled = ::getBoolVariable(context, "gui.enabled");
1402 ::setBoolVariable(context, "gui.enabled", !enabled);
1403 }
1404
1405 for (const auto& event : eventBasedInput.events()) {
1406 switch (event.inputEventType) {
1408 if (!ImGui::GetIO().WantCaptureKeyboard) {
1409 for (const auto& s : shortCuts) {
1410 if (event.modifiers == s.modifier && event.keyEvent.key == s.key) {
1411 s.action();
1412 }
1413 }
1414 }
1415 break;
1416
1418 handleMouseEvent(event);
1419 break;
1420
1422 break;
1423 }
1424 }
1425
1427 (keyboard.wasKeyPressed(Key::Left) || keyboard.wasKeyPressed(Key::Right) || keyboard.wasKeyPressed(Key::Up) || keyboard.wasKeyPressed(Key::Down)))
1428 {
1429 if (!state->hasSelected()) {
1430
1431 if (!context->store->getEntities().empty()) {
1432 select({ context->store->getEntities().begin()->first }, SelectMode::Exclusive);
1433 }
1434 }
1435
1436 else if (state->numSelected() == 1)
1437 {
1438 const Entity* entity = state->getSelected();
1439 assert(entity);
1440
1441 if (keyboard.wasKeyPressed(Key::Right)) {
1442 if (SceneComponent* trc = entity->getComponent<SceneComponent>(); trc && !trc->children.empty()) {
1443 select({ trc->children[0]->getId() }, SelectMode::Exclusive);
1444 }
1445 }
1446
1447 else {
1448
1449 if (const TransformComponent* trc = entity->getComponent<TransformComponent>(); trc) {
1450
1451
1452 if (const Component* parent = trc->parent.resolve(); parent) {
1453
1454 if (keyboard.wasKeyPressed(Key::Left)) {
1455 select({ parent->getContainer()->getId() }, SelectMode::Exclusive);
1456 }
1457 else {
1458
1459 if (SceneComponent* scc = parent->getComponent<SceneComponent>(); scc) {
1460 size_t ix = 0;
1461 size_t childCount = scc->children.size();
1462 for (ix = 0; ix < childCount; ix++) {
1463 if (scc->children[ix].get() == entity) break;
1464 }
1465 if (keyboard.wasKeyPressed(Key::Up) && (0 < ix) && (ix < childCount)) {
1466 select({ scc->children[ix - 1]->getId() }, SelectMode::Exclusive);
1467 }
1468 else if (keyboard.wasKeyPressed(Key::Down) && (ix + 1 < childCount)) {
1469 select({ scc->children[ix + 1]->getId() }, SelectMode::Exclusive);
1470 }
1471 }
1472 }
1473 }
1474
1475 else {
1476
1477 std::vector<EntityId> roots;
1478 roots.reserve(context->store->getEntities().size());
1479 for (const auto& it : context->store->getEntities()) {
1480 roots.push_back(it.first);
1481 }
1482
1483 EntityId id = entity->getId();
1484 size_t ix = 0;
1485 size_t childCount = roots.size();
1486 for (ix = 0; ix < childCount; ix++) {
1487 if (roots[ix] == id) break;
1488 }
1489 if (keyboard.wasKeyPressed(Key::Up) && (0 < ix) && (ix < childCount)) {
1490 select({ roots[ix - 1] }, SelectMode::Exclusive);
1491 }
1492 else if (keyboard.wasKeyPressed(Key::Down) && (ix + 1 < childCount)) {
1493 select({ roots[ix + 1] }, SelectMode::Exclusive);
1494 }
1495
1496 }
1497 }
1498 }
1499
1500 }
1501 }
1502}
1503
1504void Cogs::Core::Editor::handleMouseEvent(const struct Cogs::Core::InputEvent& event)
1505{
1506 using Event = Cogs::Mouse::Event;
1507
1508 const glm::ivec2 mousePos = event.mouseEvent.currPosition;
1509
1510 switch (event.mouseEvent.type) {
1511 case Cogs::Mouse::Event::Type::Press:
1512 {
1513 Cogs::Mouse::Event::ButtonData data = std::get<Event::ButtonData>(event.mouseEvent.data);
1514 if (data.button == MouseButton::Left) {
1515 if (!state->lButtonDown) {
1516 state->lButtonDown = true;
1517
1518 state->downX = mousePos.x;
1519 state->downY = mousePos.y;
1520 }
1521 }
1522 }
1523 break;
1524
1525 case Cogs::Mouse::Event::Type::Release:
1526 {
1527 Cogs::Mouse::Event::ButtonData data = std::get<Event::ButtonData>(event.mouseEvent.data);
1528 if (data.button == MouseButton::Left) {
1529 const EntityPtr cameraEntity = getEditorCamera(context);
1530 ComponentHandle cameraHandle = cameraEntity->getComponentHandle<CameraComponent>();
1531 const CameraComponent* camera = cameraHandle.resolveComponent<CameraComponent>();
1532
1533 if (!ImGui::GetIO().WantCaptureMouse && !modalCommand && (mousePos.x == state->downX || mousePos.y == state->downY)) {
1534
1535 glm::vec2 pickPoint = glm::vec2(mousePos) - camera->viewportOrigin;
1536 pickPoint.y = camera->viewportSize.y - pickPoint.y;
1537
1538 if (pickPoint.x >= 0.f && pickPoint.y >= 0.f && pickPoint.x < camera->viewportSize.x && pickPoint.y < camera->viewportSize.y) {
1539
1540 glm::dvec3 coordsWorld;
1541 glm::vec2 textureCoords;
1542 EntityId rootId = NoEntity;
1543 EntityId entityId = NoEntity;
1544 const float pickingRadius = std::max(1.0f, context->variables->get("editor.pickingRadius", 4.0f));
1545 ::pickWithCameraRay(context, cameraEntity->getId(), pickPoint.x, pickPoint.y, std::numeric_limits<float>::max(), pickingRadius,
1547 1, &entityId, &rootId, glm::value_ptr(coordsWorld), glm::value_ptr(textureCoords));
1548 state->lastPickState.clear();
1549 int pickId = std::isnan(textureCoords.x) ? -1 : static_cast<int>(std::round(textureCoords.x));
1550 if (entityId != NoEntity) {
1551 LOG_DEBUG(logger, "Pick Global Coords: %f, %f, %f. ID: %d", coordsWorld.x, coordsWorld.y, coordsWorld.z, pickId);
1552 }
1553
1554
1555 if ((event.modifiers & KeyboardModifiers::Shift) == KeyboardModifiers::Shift) {
1556 apply<SelectCommand>(entityId, SelectMode::Add, pickId);
1557 }
1558 else if ((event.modifiers & KeyboardModifiers::Control) == KeyboardModifiers::Control) {
1559 apply<SelectCommand>(entityId, SelectMode::Toggle, pickId);
1560 }
1561 else {
1562 apply<SelectCommand>(entityId, SelectMode::Exclusive, pickId);
1563 }
1564 }
1565 }
1566
1567 state->lButtonDown = false;
1568
1569 }
1570
1571 // Release mode - complete current command
1572 if (commands.size()) {
1573 commands.back()->close();
1574 }
1575
1576 }
1577 break;
1578 default:
1579 break;
1580 }
1581}
1582
1583
1584void Cogs::Core::Editor::beginFrame()
1585{
1586 textElementId = 0;
1587 bool wasActive = active;
1588
1589 active = ::getBoolVariable(context, "editor.enabled");
1590
1591 const Cogs::StringView batch = ::getVariable(context, "editor.batch");
1592
1593 if (!batch.empty()) {
1594
1595 runBatch(context, this, batch);
1596
1597 ::setVariable(context, "editor.batch", "");
1598 }
1599
1600 std::string sceneFile;
1601 if (active && !wasActive) {
1602 sceneFile = Cogs::StringView(::getVariable(context, "editor.scene")).to_string();
1603 }
1604
1605 if (active) {
1606 if (!sceneFile.empty()) {
1607 std::string path = context->resourceStore->getResourcePath(sceneFile);
1608
1609 if (path.empty()) {
1610 LOG_ERROR(logger, "Could not resolve scene resource: %s.", sceneFile.data());
1611 }
1612
1613 ::loadScene(context, path.data(), int(AssetLoadFlags::ClearScene));
1614
1615 state->clearSelection();
1616 state->fileName = path;
1617 state->directory = IO::parentPath(path);
1618
1619 ::addSearchPath(context, IO::absolute(state->directory).data());
1620
1621 // TODO: Set title based on loaded scene.
1622 // Currently the extension doesn't have access to the window class.
1623 // auto title = "Editor - " + sceneFile;
1624 // Cogs::Desktop::setTitle(title);
1625
1626 ::setVariable(context, "editor.scene", "");
1627 }
1628
1629 // Set default scene before starting batch:
1630 if (!state->editorCamera.lock()) {
1631 LOG_INFO(logger, "No EditorCamera entity found. Resetting editor.");
1632
1633 bool ok = ::loadScene(context, "Scenes/Editor.scene", int(AssetLoadFlags::NoDefault | AssetLoadFlags::ClearScene));
1634 if (!ok) {
1635 LOG_FATAL(logger, "Failed loading Editor Scene file: Scenes/Editor.scene");
1636 abort();
1637 }
1638
1639 standardEntities.clear();
1640 for (const auto& entry : context->store->getEntities()) {
1641 standardEntities.push_back(entry.second);
1642 }
1643 }
1644
1645 const EntityPtr cameraEntity = getEditorCamera(context);
1646 if (cameraEntity != state->editorCamera.lock()) {
1647 state->editorCamera = cameraEntity;
1648 context->getDefaultView()->setCamera(cameraEntity);
1649 }
1650
1651 ComponentHandle cameraHandle = cameraEntity->getComponentHandle<CameraComponent>();
1652 CameraComponent* camera = cameraHandle.resolveComponent<CameraComponent>();
1653
1654 if (context->cameraSystem->getMainCamera() != camera) {
1655 context->cameraSystem->setMainCamera(cameraHandle);
1656 }
1657
1658 {
1659 const glm::vec2 rendererSize = context->renderer->getSize();
1660 float topUiHeight = getMenuHeight() + getToolbarHeight();
1661 const glm::vec2 newViewportSize(std::max(rendererSize.x - getSidebarWidth() - getInspectorWidth(), 1.0f), std::max(rendererSize.y - topUiHeight - getStatusbarHeight(), 1.0f));
1662 const glm::vec2 newViewportOrigin(getSidebarWidth(), topUiHeight);
1663
1664 if (camera->viewportSize != newViewportSize || camera->viewportOrigin != newViewportOrigin) {
1665 camera->viewportSize = newViewportSize;
1666 camera->viewportOrigin = newViewportOrigin;
1667 camera->setChanged();
1668 }
1669 }
1670
1671 if (modalCommand) {
1672 if (modalCommand->continueModal() == false) {
1673 modalCommand->close();
1674 modalCommand = nullptr;
1675 }
1676 }
1677 }
1678}
1679
1680namespace Cogs::Core
1681{
1682 Entity * showEntity(EditorState & editor, Entity * entity)
1683 {
1684 Entity * picked = nullptr;
1685
1686 bool descend = false;
1687 const SceneComponent* sceneComponent = entity->getComponent<SceneComponent>();
1688 ImguiRenderer* guiRenderer = editor.context->renderer->getGuiRenderer();
1689
1690 // Scope where static buf/header exists, do not recurse inside this scope.
1691 {
1692 static char buf[64] = {}; // Needs space to encode "Id: " + 64-bit decimal number + terminator.
1693 const char* header = nullptr;
1694
1695 // If Entity has a name, use that name
1696 if (const std::string& name = entity->getName(); !name.empty()) {
1697 header = name.c_str();
1698
1699 // Check for non-ASCII characters
1700 bool nonAscii = false;
1701 for (const char* p = header; *p != '\0'; p++) { nonAscii = nonAscii || (*p < 0); }
1702 if (nonAscii) {
1703 guiRenderer->addTextToGlyphBuilder(header);
1704 }
1705 }
1706
1707 // Otherwise use the entity id as name. This is always pure ascii.
1708 else {
1709 int rv = snprintf(buf, sizeof(buf), "Id: %zu", entity->getId());
1710 if (rv < 0) {
1711 return nullptr; // encoding error
1712 }
1713 assert(size_t(rv) < sizeof(buf));
1714 header = buf;
1715 }
1716
1717 ImGui::PushID(entity);
1718 const bool hasChildren = sceneComponent ? sceneComponent->children.size() != 0 : false;
1719 auto flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_NoAutoOpenOnLog | (hasChildren ? 0 : ImGuiTreeNodeFlags_Leaf);
1720
1721 if (editor.isSelected(entity->getId())) {
1722 flags |= ImGuiTreeNodeFlags_Selected;
1723
1724 if (!editor.scrolled) {
1725 ImGui::SetScrollHereY();
1726 editor.scrolled = true;
1727 }
1728 }
1729
1730 auto found = std::find(editor.selectedPath.begin(), editor.selectedPath.end(), entity);
1731
1732 if (found != editor.selectedPath.end()) {
1733 ImGui::SetNextItemOpen(true);
1734 }
1735
1736 bool descendChecked = false;
1737 if (ModelComponent* modelComponent = entity->getComponent<ModelComponent>()) {
1738 ModelData& modelData = editor.context->modelSystem->getData<ModelData>(modelComponent);
1739 if (!modelData.isResourcesLoaded()) {
1740 if (HandleIsValid(modelComponent->model)) {
1741 // ModelSystem does not inform that loading is completed. Assume loaded when fraction = 1.
1742 const float progressFrac = editor.context->modelSystem->getLoadProgress(modelComponent);
1743 if (progressFrac < 1.0f) {
1744 if (modelComponent->model->isLoaded()) {
1745 descend = ImGui::TreeNodeEx(header, flags, "%s (uploading %d%%)",
1746 header, static_cast<int>(100.f * progressFrac));
1747 }
1748 else {
1749 descend = ImGui::TreeNodeEx(header, flags, "%s (loading %d%%)",
1750 header, static_cast<int>(100.f * progressFrac));
1751 }
1752
1753 descendChecked = true;
1754 }
1755 }
1756 }
1757 }
1758
1759 if (!descendChecked) {
1760 descend = ImGui::TreeNodeEx(header, flags, "%s", header);
1761
1762 // Tree node context menu
1763 if (ImGui::BeginPopupContextItem(nullptr)) {
1764 // If current item was not selected, make it a selection
1765 if (!editor.isSelected(entity->getId())) {
1766 editor.setSelection(entity->getId());
1767 }
1768 if (ImGui::MenuItem("Focus", nullptr, nullptr, editor.hasSelected())) {
1769 editor.editor->focusSelected();
1770 }
1771 ImGui::Separator();
1772 if (ImGui::MenuItem("Copy", nullptr, nullptr, editor.hasSelected())) {
1773 editor.editor->copy();
1774 }
1775 if (ImGui::MenuItem("Paste as Child", nullptr, nullptr, editor.numSelected() == 1U && editor.copied.IsArray())) {
1776 editor.editor->paste();
1777 }
1778 ImGui::Separator();
1779 if (ImGui::MenuItem("Copy name", nullptr, nullptr, editor.numSelected() == 1U)) {
1780 ImGui::SetClipboardText(header);
1781 LOG_DEBUG(logger, "Copied \"%s\" to clipboard", header);
1782 }
1783 static std::array<char, 512> buffer;
1784 if (editor.numSelected() == 1U) {
1785 snprintf(buffer.data(), buffer.size(), "%s", entity->getName().c_str());
1786 ImGui::SetNextItemWidth(200);
1787 if (ImGui::InputText("Rename", buffer.data(), buffer.size(), ImGuiInputTextFlags_EnterReturnsTrue)) {
1788 entity->setName(buffer.data());
1789 }
1790 }
1791 else {
1792 ImGui::MenuItem("Rename", nullptr, nullptr, false);
1793 }
1794 ImGui::EndPopup();
1795 }
1796 }
1797 }
1798
1799
1800 if (descend) {
1801 if (ImGui::IsItemClicked()) {
1802 picked = entity;
1803 }
1804
1805 if (sceneComponent) {
1806 for (auto & child : sceneComponent->children) {
1807 auto p = showEntity(editor, child.get());
1808
1809 picked = p ? p : picked;
1810 }
1811 }
1812
1813 ImGui::TreePop();
1814 } else {
1815 if (ImGui::IsItemClicked()) {
1816 picked = entity;
1817 }
1818 }
1819
1820 ImGui::PopID();
1821
1822 return picked;
1823 }
1824}
1825
1826Cogs::Core::EditorState * Cogs::Core::Editor::getState()
1827{
1828 return state.get();
1829}
1830
1831void Cogs::Core::Editor::showText(const char* format, ...) const {
1832 // Using a read-only InputEdit instead of a Text element, so we can highlight and copy text from it.
1833 // Try to style it to get it looking like a normal label.
1834 char buff[256];
1835 va_list args;
1836 va_start(args, format);
1837 int ret = std::vsnprintf(buff, sizeof(buff), format, args);
1838 va_end(args);
1839 if (ret < 0) {
1840 LOG_ERROR_ONCE(logger, "Internal error: Format: %s", format);
1841 buff[0] = '\0';
1842 }
1843
1844 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f));
1845 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
1846 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
1847
1848 bool prevCursorBlink = ImGui::GetIO().ConfigInputTextCursorBlink;
1849 ImGui::GetIO().ConfigInputTextCursorBlink = false;
1850
1851 ImGui::PushID(textElementId++);
1852 ImGui::InputText("##", buff, 256, ImGuiInputTextFlags_ReadOnly);
1853 ImGui::PopID();
1854
1855 ImGui::GetIO().ConfigInputTextCursorBlink = prevCursorBlink;
1856
1857 ImGui::PopStyleColor();
1858 ImGui::PopStyleVar();
1859 ImGui::PopStyleVar();
1860}
1861
1862bool Cogs::Core::Editor::showModel(ModelHandle & modelHandle)
1863{
1864 std::vector<const char *> combos;
1865
1866 if (modelHandle) {
1867 ImguiRenderer* guiRenderer = context->renderer->getGuiRenderer();
1868 guiRenderer->addTextToGlyphBuilder(modelHandle->getName().to_string().c_str());
1869
1870 showText("%s", modelHandle->getName().to_string().c_str());
1871 } else {
1872 showText("Empty");
1873 ImGui::SameLine();
1874
1875 if (imageButton(Icon::Open, 26, 2)) {
1876 auto fileName = Cogs::Desktop::getOpenFileName(context->getDefaultView()->refWindowData(), "Model files (*.fbx;*.gltf;*.glb;*.obj;*.cogsbin;*.stp;*.dxf)\0*.fbx;*.gltf;*.glb;*.obj;*.cogsbin;*.stp;*.dxf\0All Files\0*.*\0");
1877
1878 if (fileName.size()) {
1879 ModelLoadInfo & loadInfo = *context->modelManager->createLoadInfo();
1880 loadInfo.resourceId = NoResourceId;
1881 loadInfo.resourcePath = std::move(fileName);
1882 loadInfo.resourceName = IO::fileName(loadInfo.resourcePath);
1883 loadInfo.loadFlags = (ResourceLoadFlags)ModelLoadFlags::None;
1884 loadInfo.modelFlags = ModelLoadFlags::None;
1885
1886 modelHandle = context->modelManager->loadResource(&loadInfo);
1887
1888 return true;
1889 }
1890 }
1891 }
1892
1893 return false;
1894}
1895
1896bool Cogs::Core::Editor::showTexture(const StringView & header, TextureHandle & textureHandle)
1897{
1898 TextureHandle oldValue = textureHandle;
1899
1900 showText("%s", header.to_string().c_str());
1901
1902 ImGui::SameLine();
1903
1904 if (textureHandle) {
1905 ImguiRenderer* guiRenderer = context->renderer->getGuiRenderer();
1906 guiRenderer->addTextToGlyphBuilder(textureHandle->getName().to_string().c_str());
1907
1908 showText("%s", textureHandle->getName().to_string().c_str());
1909 } else {
1910 showText("Empty");
1911 }
1912
1913 ImGui::SameLine();
1914
1915 ImGui::PushID(&textureHandle);
1916
1917 if (imageButton(Icon::Open, 26, 2)) {
1918 auto fileName = Desktop::getOpenFileName(context->getDefaultView()->refWindowData(), "Texture Files (*.png;*.jpeg;*.jpg;*.tga;*.dds)\0*.png;*.jpeg;*.jpg;*.tga;*.dds;*.bmp\0All Files\0*.*\0");
1919
1920 if (fileName.size()) {
1921 bool linear = fileName.find("norm") != StringView::NoPosition;
1922 linear |= fileName.find("Norm") != StringView::NoPosition;
1923 linear |= fileName.find("Spec") != StringView::NoPosition;
1924 linear |= fileName.find("spec") != StringView::NoPosition;
1925 linear |= fileName.find("tang") != StringView::NoPosition;
1926 linear |= fileName.find("Tang") != StringView::NoPosition;
1927 linear |= fileName.find("gloss") != StringView::NoPosition;
1928 linear |= fileName.find("Gloss") != StringView::NoPosition;
1929
1930 TextureLoadInfo & loadInfo = *context->textureManager->createLoadInfo();
1931 loadInfo.resourceId = NoResourceId;
1932 loadInfo.resourcePath = std::move(fileName);
1933 loadInfo.resourceName = IO::fileName(loadInfo.resourcePath);
1935
1936 textureHandle = context->textureManager->loadResource(&loadInfo);
1937
1938 context->scene->addResource(textureHandle);
1939 }
1940 }
1941
1942 ImGui::PopID();
1943
1944 return textureHandle != oldValue;
1945}
1946
1947bool Cogs::Core::Editor::showMaterialInstance(FieldInfo & fieldInfo, MaterialInstanceHandle & materialInstanceHandle)
1948{
1949 return materialEditor->showMaterialInstance(fieldInfo, materialInstanceHandle);
1950}
1951
1952void Cogs::Core::Editor::showEntities()
1953{
1954 auto store = context->store;
1955 auto & entities = store->getEntities();
1956
1957 Entity * picked = nullptr;
1958
1959 if (state->getSelected()) {
1960 state->selectedPath.clear();
1961
1962 auto parent = store->getEntityParent(state->getSelected());
1963
1964 while (parent) {
1965 state->selectedPath.push_back(parent);
1966
1967 parent = store->getEntityParent(parent);
1968 }
1969 }
1970
1971 for (auto & e : entities) {
1972 const EntityPtr & entity = e.second;
1973
1974 if (!entity) continue;
1975
1976 const TransformComponent* transformComponent = entity->getComponent<TransformComponent>();
1977
1978 if (!transformComponent || (transformComponent && !transformComponent->parent)) {
1979 Entity* p = showEntity(*state, entity.get());
1980
1981 picked = p ? p : picked;
1982 }
1983 }
1984
1985 const Keyboard& keyboard = context->getDefaultView()->refKeyboard();
1986 SelectMode mode = SelectMode::Exclusive;
1987
1988 if (keyboard.isKeyDown(Key::Shift)) {
1989 mode = SelectMode::Add;
1990 } else if (keyboard.isKeyDown(Key::Control)) {
1991 mode = SelectMode::Toggle;
1992 }
1993
1994 if (picked && (picked != state->getSelected() || mode == SelectMode::Toggle)) {
1995 apply<SelectCommand>(picked->getId(), mode);
1996 }
1997
1998 // Context menu for Entity tree viewer, when right clicked on the empty widget space
1999 if (ImGui::BeginPopupContextWindow(nullptr, ImGuiPopupFlags_MouseButtonRight + ImGuiPopupFlags_NoOpenOverItems)) {
2000 if (ImGui::MenuItem("Paste at Root level", nullptr, nullptr, state->copied.IsArray())) {
2001 state->clearSelection();
2002 paste();
2003 }
2004 if (ImGui::MenuItem("Clear selection", nullptr, nullptr, state->hasSelected())) {
2005 state->clearSelection();
2006 }
2007 ImGui::EndPopup();
2008 }
2009
2010 // Clear selection if left clicked on the empty Entity Editor widget space
2011 if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
2012 !picked && !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel))
2013 {
2014 state->clearSelection();
2015 }
2016}
2017
2018void Cogs::Core::Editor::showEntityDetails(Entity * entity)
2019{
2020 Cogs::Core::EntityEditor::showEntityEditor(context, this, entity);
2021}
2022
2023void Cogs::Core::Editor::showGizmo()
2024{
2025 Cogs::Core::EntityEditor::showEntityGizmo(context, this);
2026}
2027
2028void Cogs::Core::Editor::selectLayer(ComponentModel::Entity* entity, StringView layer)
2029{
2030 LOG_DEBUG(logger, "Selecting Layer: %s", layer.to_string().data());
2031 bool anyVisible = recursiveSetVisibility(entity,
2032 [&layer](Cogs::ComponentModel::Entity* entityPtr, bool anyChildVisible) {
2034 if (propertiesComponent) {
2035 Cogs::StringView layerProperty = propertiesComponent->properties.getProperty("_rvmLayer", "");
2036 if (layer == layerProperty) {
2037 return true;
2038 }
2039 }
2040
2041 return anyChildVisible;
2042 });
2043
2044 if (anyVisible) {
2045 ensureEntityVisible(context, entity);
2046 }
2047}
2048
2049void Cogs::Core::Editor::setLayerVisibility(ComponentModel::Entity* entity, StringView layer, bool visible)
2050{
2051 LOG_DEBUG(logger, "Set Layer: %s. Visible: %s", layer.to_string().data(), visible ? "true" : "false");
2052 bool anyVisible = recursiveSetVisibility(entity,
2053 [&layer, visible](Cogs::ComponentModel::Entity* entityPtr, bool /*anyChildVisible*/) {
2054 auto propertiesComponent = entityPtr->getComponent< Cogs::Core::PropertiesComponent>();
2055 if (propertiesComponent) {
2056 Cogs::StringView layerProperty = propertiesComponent->properties.getProperty("_rvmLayer", "");
2057 if (layer == layerProperty) {
2058 return visible;
2059 }
2060 }
2061
2062 auto sceneComponent = entityPtr->getComponent<Cogs::Core::SceneComponent>();
2063 return sceneComponent ? sceneComponent->visible : true;
2064 });
2065
2066 if (anyVisible) {
2067 ensureEntityVisible(context, entity);
2068 }
2069
2070}
2071
2073{
2074 bool anyVisible = recursiveGetVisibility(entity,
2075 [&layer](const Cogs::ComponentModel::Entity* entity) {
2076 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
2077 if (sceneComponent) {
2078 auto propertiesComponent = entity->getComponent< Cogs::Core::PropertiesComponent>();
2079 if (propertiesComponent) {
2080 Cogs::StringView layerProperty = propertiesComponent->properties.getProperty("_rvmLayer", "");
2081 if (layer == layerProperty) {
2082 return sceneComponent->visible;
2083 }
2084 }
2085 }
2086
2087 return false;
2088 });
2089
2090 return anyVisible;
2091}
2092
2094{
2095 for (const Cogs::Core::WeakEntityPtr& stdEntity : standardEntities) {
2096 if (stdEntity.lock().get() == entity) {
2097 return true;
2098 }
2099 }
2100
2101 auto cameraComponent = entity->getComponent< Cogs::Core::CameraComponent>();
2102 if (cameraComponent) {
2103 return true;
2104 }
2105
2106 return false;
2107}
2108
2110{
2111 if (isStandardEntity(entity)) {
2112 return false;
2113 }
2114
2115 if (context->store->getEntityParent(entity)) {
2116 return false;
2117 }
2118
2119 return true;
2120}
2121
2122void Cogs::Core::Editor::exportScene(const StringView & path)
2123{
2124 ExportCommand exportCommand(state.get(), NoEntity);
2125 auto & o = exportCommand.options.emplace_back();
2126 o.key = "fileName";
2127 o.value = path.to_string();
2128 o.type = ParsedDataType::String;
2129 exportCommand.apply();
2130}
2131
2132Cogs::Core::EntityIds Cogs::Core::Editor::getSelected() const
2133{
2134 return state->selected;
2135}
2136
2137void Cogs::Core::Editor::select(const EntityIds & ids, SelectMode mode)
2138{
2139 apply<SelectCommand>(ids, mode);
2140}
2141
2142Cogs::Core::EditorCommand * Cogs::Core::Editor::createCommand(const StringView & /*key*/)
2143{
2144 return nullptr;
2145}
2146
2147void Cogs::Core::Editor::createEntity(const StringView & type, const StringView & name)
2148{
2149 apply<CreateEntityCommand>(type, name, false);
2150}
2151
2152bool Cogs::Core::Editor::imageButton(Icon icon, int size, int padding)
2153{
2154 auto texture = state->icons[(int)icon];
2155
2156 Renderer* renderer = dynamic_cast<Renderer*>(context->renderer);
2157 RenderTexture* renderTexture = renderer->getRenderResources().getRenderTexture(context->textureManager->generateHandle((Texture *)texture.resolve()));
2158
2159 if (renderTexture && HandleIsValid(renderTexture->textureHandle)) {
2160 auto textureId = ImTextureID(renderTexture->textureHandle.handle);
2161
2162 int textureSize = size - padding * 2;
2163 if (ImGui::ImageButton("", textureId, ImVec2((float)textureSize, (float)textureSize), ImVec2(0, 1), ImVec2(1, 0))) {
2164 return true;
2165 }
2166 }
2167
2168 return false;
2169}
2170
2171void Cogs::Core::Editor::clear(bool interactive)
2172{
2173 const bool askSave = ::getBoolVariable(context, "editor.askSave");
2174 if (interactive && askSave && commands.size()) {
2175 auto result = Desktop::showMessageBox(context->getDefaultView()->refWindowData(), "Save changes before clearing?", "Save Changes", Desktop::MessageBoxType::YesNoCancel, Desktop::MessageBoxIcon::Question);
2176
2177 if (result == Desktop::MessageBoxResult::Cancel) return;
2178 else if (result == Desktop::MessageBoxResult::Yes) saveAs();
2179 }
2180
2181 state->clearSelection();
2182
2183 clearCommands();
2184
2185 context->scene->clear();
2186 context->scene->setup(true);
2187}
2188
2189void Cogs::Core::Editor::openInternal(const StringView& path, bool openInRoot, bool synchronous, bool interactive)
2190{
2191 if (path.empty()) {
2192 LOG_ERROR(logger, "Trying to open a file without a file path");
2193 return;
2194 }
2195
2196 std::string pathStr = path.to_string();
2197 std::string extension = ::toLower(IO::extension(pathStr));
2198 std::string name = IO::stem(pathStr);
2199 state->directory = IO::parentPath(pathStr);
2200
2201 if (extension == ".scene") {
2202 Cogs::Desktop::MessageBoxResult result = Desktop::MessageBoxResult::Yes;
2203 if (interactive) {
2204 result = Desktop::showMessageBox(context->getDefaultView()->refWindowData(), "Clear Scene before loading?", "Clear Scene", Desktop::MessageBoxType::YesNoCancel, Desktop::MessageBoxIcon::Question);
2205 }
2206
2207 // Parent Entity is selected if only one selected.
2208 EntityId parentId = openInRoot ? NoEntity : state->getSelectedId();
2209
2210 if (result == Desktop::MessageBoxResult::Yes) {
2211 clear(false);
2212 ::loadScene(context, pathStr.data(), int(AssetLoadFlags::ClearScene));
2213 state->editorCamera = getEditorCamera(context);
2214 }
2215 else if (result == Desktop::MessageBoxResult::No) {
2216 if (parentId == NoEntity) {
2217 ::loadScene(context, pathStr.data(), int(AssetLoadFlags::None));
2218 }
2219 else {
2220 ::loadAsset(context, pathStr.data(), parentId, int(AssetLoadFlags::None));
2221 }
2222 }
2223 }
2224 else if (extension == ".json" && name == "metadata") {
2225 std::string entityName = IO::stem(state->directory);
2226
2227 // Remove Twin full name, e.g. "XXX\Level 1\full\metadata.json" => "Level 1".
2228 if (entityName == "full") {
2229 entityName = IO::stem(IO::parentPath(state->directory));
2230 }
2231
2232 if (ExtensionRegistry::loadExtensionModule("Cogs.Core.Extensions.PoTree")) {
2233 auto createCommand = apply<CreateEntityCommand>("PotreeModel", entityName, openInRoot);
2234 createCommand->permanentUndo = true;
2235 Entity* potreeModel = state->getSelected();
2236 if (potreeModel) {
2237 auto potreeComponent = potreeModel->getComponent<PotreeComponent>();
2238 if (potreeComponent) {
2239 potreeComponent->source = pathStr;
2240 potreeComponent->coloring = PotreeColoring::Color;
2241 potreeComponent->outlineSize = 0.0f;
2242 potreeComponent->shading = PotreeShading::Flat;
2243 potreeComponent->pointShape = PotreePointShape::Disc;
2244 potreeComponent->setChanged();
2245
2246 if (context->store->getEntityParent(potreeModel) != nullptr) {
2247 // Auto origin sets TransformComponent coordinates which is ignored for child entities.
2248 LOG_INFO(logger, "Disabling PoTree auto origin when PoTree is not root entity");
2249 potreeComponent->originPolicy = PotreeOriginPolicy::None;
2250 potreeComponent->setChanged();
2251 }
2252 }
2253
2254 // Move and fix camera to PoTree camera position for single scans IF scanner position known.
2255 parseCameraPosition(context, IO::combine(state->directory, "ScannerPosition.pos"), false, state.get());
2256 }
2257 else {
2258 LOG_ERROR(logger, "Could not create Entity PotreeModel");
2259 }
2260 }
2261 else {
2262 LOG_ERROR(logger, "Could not load extension: Cogs.Core.Extensions.PoTree");
2263 }
2264 }
2265 else if (extension == ".json" && name == "360") {
2266 std::string entityName = IO::stem(state->directory);
2267
2268 if (ExtensionRegistry::loadExtensionModule("Cogs.Core.Extensions.Image360")) {
2269 auto createCommand = apply<CreateEntityCommand>("Image360", entityName, openInRoot);
2270 createCommand->permanentUndo = true;
2271 Entity* image360 = state->getSelected();
2272 if (image360) {
2273 auto image360Component = image360->getComponent<Image360Component>();
2274 if (image360Component) {
2275 image360Component->source = pathStr;
2276 image360Component->setChanged();
2277
2278 // Move and fix Camera Position from optional file. Move 360 image to fit position
2279 CameraSetupData pos = parseCameraPosition(context, IO::combine(state->directory, "ScannerPosition.pos"), true, state.get());
2280 TransformComponent* transformComponent = image360->getComponent<TransformComponent>();
2281
2282 if (context->store->getEntityParent(image360)) {
2283 transformComponent->position = pos.position;
2284 }
2285 else {
2286 transformComponent->coordinates = pos.position;
2287 }
2288
2289 transformComponent->rotation = pos.rotation;
2290 transformComponent->setChanged();
2291 }
2292 }
2293 else{
2294 LOG_ERROR(logger, "Could not create Entity Image360");
2295 }
2296 }
2297 else {
2298 LOG_ERROR(logger, "Could not load extension: Cogs.Core.Extensions.Image360");
2299 }
2300 }
2301 else if (extension == ".asset" || ::toLower(pathStr).find(".asset.zst") != std::string::npos) {
2302
2303 // Open an Asset with LOD support.
2304 auto createCommand = apply<CreateEntityCommand>("Asset", name, openInRoot);
2305 createCommand->permanentUndo = true;
2306
2307 Entity* selected = state->getSelected();
2308 if (selected) {
2309 auto assetComponent = selected->getComponent<AssetComponent>();
2311 assetComponent->asset = context->assetManager->loadAsset(pathStr, Core::NoResourceId, Core::AssetLoadFlags::None);
2312 assetComponent->setChanged();
2313
2314 addAssetTags(context, selected, pathStr);
2315 }
2316 else {
2317 LOG_ERROR(logger, "Could not create Entity Asset");
2318 }
2319 }
2320 else if (extension == ".rvm" || extension == ".rvmdump" || extension == ".vue") {
2321 if (interactive) {
2322 apply<LoadRvmCommand>(path, openInRoot, synchronous);
2323 }
2324 else {
2325 // Open the file without showing the dialog, non-modal
2326 LoadRvmCommand loadRvmCommand(state.get(), path, openInRoot, synchronous);
2327 loadRvmCommand.loadFile();
2328 }
2329 }
2330 else {
2331 auto createCommand = apply<CreateEntityCommand>("ModelEntity", name, openInRoot);
2332 createCommand->permanentUndo = true;
2333
2334 auto selected = state->getSelected();
2335
2336 if (selected) {
2337 auto modelComponent = selected->getComponent<ModelComponent>();
2338 modelComponent->model = context->modelManager->loadModel(pathStr, NoResourceId, ModelLoadFlags::DisableOptimization | (synchronous ? ModelLoadFlags::ForceSynchronous : ModelLoadFlags::None));
2339 modelComponent->waitForSubresources = false;
2340 modelComponent->setChanged();
2341 }
2342 else {
2343 LOG_ERROR(logger, "Could not create Entity ModelEntity");
2344 }
2345 }
2346}
2347
2348void Cogs::Core::Editor::open(const StringView& path, bool openInRoot, bool synchronous)
2349{
2350 if (path.empty()) {
2351 // Caled without a file path, ask user to select a file or files to open
2352 std::vector<std::string> files(Desktop::getOpenFileNames(context->getDefaultView()->refWindowData(), "Editor File (*.scene, *.asset, *.assbin, *.asset.zst, 360.json, metadata.json, *.obj, *.fbx, *.gltf, *.glb, *.rvm, *.rvmdump, *.cogsbin, *.vue, *.dxf)\0*.scene;*.asset;*.assbin;*.asset.zst;360.json;metadata.json;*.obj;*.fbx;*.gltf;*.glb;*.rvm;*.rvmdump;*.cogsbin;*.vue;*.dxf\0All Files\0*.*\0", nullptr));
2353
2354 // Alow further user interaction only if a single file has been selected
2355 bool interactive = (files.size() == 1);
2356
2357 // Reset selection for multi file import to import all on same level.
2358 EntityIds oldSelected = state->selected;
2359 EntityIds ids;
2360 ids.reserve(oldSelected.size());
2361 for (const std::string& file : files) {
2362 if (!file.empty()) {
2363 openInternal(file, openInRoot, synchronous, interactive);
2364 ids.insert(ids.end(), state->getAllSelectedIds().begin(), state->getAllSelectedIds().end());
2365 }
2366 if (!openInRoot && file != files.back()) {
2367 state->setSelection(oldSelected);
2368 }
2369 }
2370
2371 if (!ids.empty()) {
2372 state->setSelection(ids);
2373 }
2374 }
2375 else {
2376 // Called with a file path, e.g. from a command line. Do not show dialogs (interactive = false)
2377 EntityIds oldSelected = state->selected;
2378 openInternal(path, openInRoot, synchronous, false);
2379 oldSelected.push_back(state->getSelectedId());
2380 state->setSelection(oldSelected);
2381 }
2382}
2383
2384void Cogs::Core::Editor::save()
2385{
2386 if (state->fileName.size()) {
2387 const std::string out = state->fileName;
2388
2389 ::writeAsset(context, out.data(), NoEntity, int(AssetWriteFlags::None));
2390 } else {
2391 saveAs();
2392 }
2393}
2394
2395void Cogs::Core::Editor::saveAs()
2396{
2397 auto saveFile = Desktop::getSaveFileName(context->getDefaultView()->refWindowData(), state->fileName, "Scene Files (*.scene)\0*.scene\0All Files\0*.*\0");
2398
2399 if (saveFile.empty()) {
2400 return;
2401 }
2402
2403 state->fileName = saveFile;
2404
2405 if (IO::extension(state->fileName).empty()) {
2406 state->fileName += ".scene";
2407 }
2408
2409 ::writeAsset(context, state->fileName.data(), NoEntity, int(AssetWriteFlags::None | AssetWriteFlags::Hierarchy));
2410}
2411
2412void Cogs::Core::Editor::saveIncremental()
2413{
2414 if (state->fileName.empty()) {
2415 saveAs();
2416 return;
2417 }
2418
2419 auto fileName = IO::fileName(state->fileName);
2420 auto extension = IO::extension(fileName);
2421 auto extIt = fileName.rfind(extension);
2422
2423 auto name = fileName.substr(0, extIt);
2424
2425 auto firstDigit = std::find_if(name.begin(), name.end(), [](const char & c) { return ::isdigit(c); });
2426
2427 int number = 0;
2428
2429 if (firstDigit != name.end()) {
2430 auto digits = std::string(firstDigit, name.end());
2431 number = atoi(digits.c_str());
2432 } else {
2433 number = 0;
2434 }
2435
2436 ++number;
2437 char buffer[2048];
2438 snprintf(buffer, sizeof(buffer), "%03d", number);
2439
2440 name += std::string(buffer);
2441 name += extension;
2442
2443 auto path = IO::parentPath(state->fileName);
2444 path = IO::combine(path, name);
2445
2446 if (path.empty()) return;
2447
2448 state->fileName = path;
2449
2450 save();
2451}
2452
2453void Cogs::Core::Editor::importStatic()
2454{
2455 bool openInRoot = true;
2456 auto files = Desktop::getOpenFileNames(context->getDefaultView()->refWindowData(), "Editor File (*.obj, *.gltf, *.glb, *.fbx, *.cogsbin, *.dxf)\0*.obj;*.gltf;*.glb;*.fbx;*.cogsbin;*.dxf\0All Files\0*.*\0", nullptr);
2457
2458 if (files.empty()) return;
2459
2460 state->clearSelection();
2461 state->directory = IO::parentPath(files.front());
2462
2463 EntityIds ids;
2464
2465 for (const std::string & fileName : files) {
2466 std::string ext = IO::extension(fileName);
2467
2468 if (is3dModelFileFormat(ext)) {
2469 std::string name = IO::stem(fileName);
2470
2471 apply<CreateEntityCommand>("StaticModel", name, openInRoot);
2472
2473 const Entity* selected = state->getSelected();
2474 if (selected) {
2475 StaticModelComponent* modelComponent = selected->getComponent<StaticModelComponent>();
2476 modelComponent->model = context->modelManager->loadModel(fileName, NoResourceId, ModelLoadFlags::DisableOptimization);
2477 modelComponent->setChanged();
2478
2479 ids.push_back(selected->getId());
2480 }
2481 else {
2482 LOG_ERROR(logger, "Could not create Entity StaticModel");
2483 }
2484 }
2485 }
2486
2487 if (!ids.empty()) {
2488 state->setSelection(ids);
2489 }
2490}
2491
2492void Cogs::Core::Editor::exportEntities(bool exportScene)
2493{
2494 if (!exportScene && !state->hasSelected()) {
2495 LOG_WARNING(logger, "exportSelected: No entity selected.");
2496 return;
2497 }
2498
2499 auto [saveFile, filterIndex] = Desktop::getSaveFileNameExt(context->getDefaultView()->refWindowData(), state->fileName, "Cogsbin Files (*.cogsbin)\0*.cogsbin;\0GL Transmission Format (*.gltf)\0*.gltf;\0GL Binary (*.glb)\0*.glb\0");
2500
2501 if (saveFile.empty()) {
2502 LOG_WARNING(logger, "exportEntities: No filename");
2503 return;
2504 }
2505
2506 if (filterIndex == 1) {
2507 ExportCommand exportCommand(state.get(), state->getSelectedId(), exportScene);
2508 auto& option = exportCommand.options.emplace_back();
2509 option.key = "fileName";
2510 option.value = fs::path(saveFile).replace_extension(".cogsbin").string();
2511 exportCommand.apply();
2512 }
2513 else {
2514 auto path = IO::parentPath(saveFile);
2515 auto name = IO::stem(saveFile);
2516 if (filterIndex == 2) {
2517 apply<ExportGltfCommand>(path, name, false, exportScene);
2518 }
2519 else if (filterIndex == 3) {
2520 apply<ExportGltfCommand>(path, name, true, exportScene);
2521 }
2522 }
2523}
2524
2525void Cogs::Core::Editor::cut()
2526{
2527 if (!state->hasSelected()) return;
2528
2529 copy();
2530
2531 apply<DestroyCommand>(state->selected, true);
2532}
2533
2534void Cogs::Core::Editor::copy()
2535{
2536 if (!state->hasSelected()) return;
2537
2538 EntityIds ids;
2539 RemoveEntitiesWithAncestors(context, state->selected, ids);
2540
2541 Document d;
2542 d.SetArray();
2544
2545 for (const EntityId s : ids) {
2546 Value entityValue;
2547 entityValue.SetObject();
2548
2549 writeEntity(context, context->store->getEntityPtr(s), entityValue, d, nullptr, writeFlags);
2550
2551 d.PushBack(entityValue, d.GetAllocator());
2552 }
2553
2554 state->copied = std::move(d);
2555}
2556
2557void Cogs::Core::Editor::paste()
2558{
2559 if (!state->copied.IsArray()) return;
2560
2561 apply<PasteEntityCommand>();
2562}
2563
2564void Cogs::Core::Editor::invokeCommand(const StringView & /*key*/)
2565{
2566
2567}
2568
2569void Cogs::Core::Editor::destroy()
2570{
2571 if (!state->hasSelected()) return;
2572
2573 apply<DestroyCommand>(state->selected, true);
2574}
2575
2576void Cogs::Core::Editor::destroyPermanently()
2577{
2578 if (!state->hasSelected()) return;
2579
2580 apply<DestroyCommand>(state->selected, false);
2581}
2582
2583void Cogs::Core::Editor::changeMode(EditingMode mode)
2584{
2585 if (mode != state->mode) {
2586 apply<ChangeEditingModeCommand>(mode);
2587 }
2588}
2589
2590void Cogs::Core::Editor::focusSelected()
2591{
2592 if (!state->hasSelected()) return;
2593
2594 Cogs::Geometry::DBoundingBox bb;
2595
2596 for (EntityId id : state->getAllSelectedIds()) {
2597 Cogs::Geometry::DBoundingBox bbb;
2598 ::calculateBoundingBoxWorld(context, id, bbb.data());
2599
2600 bb += bbb;
2601 }
2602
2603 focusBoundingBox(bb);
2604}
2605
2606void Cogs::Core::Editor::focusAll()
2607{
2608 Geometry::DBoundingBox bb;
2609
2610 for (const auto& e : context->store->getEntities()) {
2611 const EntityPtr & entity = e.second;
2612
2613 if (!entity) continue;
2614
2615 const TransformComponent* transformComponent = entity->getComponent<TransformComponent>();
2616
2617 if (transformComponent && !transformComponent->parent) {
2618 Geometry::DBoundingBox bbb;
2619 ::calculateBoundingBoxWorld(context, entity->getId(), bbb.data());
2620
2621 bb += bbb;
2622 }
2623 }
2624
2625 if (isEmpty(bb)) {
2626 LOG_WARNING(logger, "Scene bounding box is empty.");
2627 }
2628 else {
2629 focusBoundingBox(bb);
2630 }
2631}
2632
2633bool Cogs::Core::Editor::focusBoundingBox(const Cogs::Geometry::DBoundingBox& boundingBox)
2634{
2635 if (isEmpty(boundingBox)) {
2636 LOG_DEBUG(logger, "focusBoundingBox bounds: Empty");
2637 }
2638 else {
2639 LOG_DEBUG(logger, "focusBoundingBox bounds: [%.2f %.2f %.2f] x [%.2f %.2f %.2f]",
2640 boundingBox.min.x, boundingBox.min.y, boundingBox.min.z,
2641 boundingBox.max.x, boundingBox.max.y, boundingBox.max.z);
2642 }
2643
2644 EntityPtr camera = state->editorCamera.lock();
2645 if (!camera) {
2646 return false;
2647 }
2648
2649 // Need extent (hasExtent(bbox) too strict requiring min<max for all XYZ).
2650 if (isEmpty(boundingBox) || Cogs::Utility::CameraHelper::getRadius(boundingBox) == 0.0f) {
2651 return false;
2652 }
2653
2654 // Calculate viewAll + update Components.
2655 // Not perfectly centered due to camera viewport offset used for imgui panels.
2656
2657 CameraComponent* cameraComponent = state->editorCamera.lock()->getComponent<CameraComponent>();
2658 TransformComponent* transformComponent = state->editorCamera.lock()->getComponent<TransformComponent>();
2659 OrbitingCameraController* orbitingCameraController = state->editorCamera.lock()->getComponent<OrbitingCameraController>();
2660 ProjectionMode projectionMode = cameraComponent->projectionMode;
2661 glm::vec2 viewport = cameraComponent->viewportSize;
2662
2663 // Update camera:
2664 const glm::dvec3 cameraPos = Cogs::Utility::CameraUtils::viewAll(cameraComponent, transformComponent, orbitingCameraController, projectionMode,
2665 boundingBox, viewport);
2666
2667 // Use camera Focal Point as new Origin:
2668 const glm::quat orientation = Cogs::Utility::CameraUtils::getCameraOrientation(transformComponent, camera->getComponent<OrbitingCameraController>());
2669 const glm::dvec3 focalPos = Cogs::Utility::CameraHelper::getFocalPosition(cameraPos, orientation, cameraComponent->focalDistance);
2670 ::setOrigin(context, glm::value_ptr(focalPos));
2671 LOG_DEBUG(logger, "Scene Origin set to: [%.2f %.2f %.2f]", focalPos.x, focalPos.y, focalPos.z);
2672 return true;
2673}
2674
2675void Cogs::Core::Editor::setStatusbarText(const std::string& text, bool guiStatus)
2676{
2677 statusbarText = text;
2678 guiStatusInStatusbar = guiStatus;
2679}
2680
2681
2682namespace {
2683 enum class FurthestFlags {
2684 None = 0,
2685 X = 1,
2686 Y = 2,
2687 Z = 4,
2688 All = 7
2689 };
2690 ENABLE_ENUM_FLAGS(FurthestFlags)
2691
2692
2693 void recurseSelectFurthest(Cogs::ComponentModel::Entity** furthest,
2694 double& distance,
2695 const Cogs::Geometry::DBoundingBox & selectedBbox,
2697 Cogs::Core::Context* context,
2698 FurthestFlags options)
2699 {
2700 // Only check leaf nodes
2701 auto sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
2702 if (sceneComponent && sceneComponent->visible) {
2703 if (sceneComponent->children.empty()) {
2704 Cogs::Geometry::DBoundingBox bb;
2705 ::calculateBoundingBoxWorld(context, entity->getId(), bb.data());
2706
2707 double xDist = 0;
2708 double yDist = 0;
2709 double zDist = 0;
2710 if (!isEmpty(bb)) {
2711 if ((options & FurthestFlags::X) != FurthestFlags::None) {
2712 xDist = std::max(xDist, selectedBbox.min.x - bb.max.x);
2713 xDist = std::max(xDist, bb.min.x - selectedBbox.max.x);
2714 }
2715 if ((options & FurthestFlags::Y) != FurthestFlags::None) {
2716 yDist = std::max(yDist, selectedBbox.min.y - bb.max.y);
2717 yDist = std::max(yDist, bb.min.y - selectedBbox.max.y);
2718 }
2719 if ((options & FurthestFlags::Z) != FurthestFlags::None) {
2720 zDist = std::max(zDist, selectedBbox.min.z - bb.max.z);
2721 zDist = std::max(zDist, bb.min.z - selectedBbox.max.z);
2722 }
2723 }
2724
2725 double sqrDist = double(xDist) * double(xDist) + double(yDist) * double(yDist) + double(zDist) * double(zDist);
2726 if (sqrDist > distance) {
2727 distance = sqrDist;
2728 *furthest = entity;
2729 }
2730 }
2731 else {
2732 for (const auto& child : sceneComponent->children) {
2733 recurseSelectFurthest(furthest, distance, selectedBbox, child.get(), context, options);
2734 }
2735 }
2736 }
2737 }
2738}
2739
2741{
2742 // Hide or show all selected entities.
2743 for (const EntityId id : state->selected) {
2744 auto entity = context->store->getEntityPtr(id);
2745 if (entity) {
2746 auto* sceneComponent = entity->getComponent<Cogs::Core::SceneComponent>();
2747 if (sceneComponent) {
2748 recursiveSetVisibility(entity,
2749 [visible](Cogs::ComponentModel::Entity* /*entity*/, bool /*anyChildVisible*/) {
2750 return visible;
2751 });
2752
2753 if (visible) {
2754 ensureEntityVisible(context, entity);
2755 }
2756 }
2757 }
2758 }
2759}
2760
2762{
2763 if (state->hasSelected()) {
2764 std::unordered_set<size_t> entityIds;
2765 std::vector<ComponentModel::Entity*> selectedEntities;
2766 std::unordered_set<size_t> selectedIds;
2767
2768 for (const EntityId id : state->selected) {
2769 auto ptr = context->store->getEntityPtr(id);
2770 if (ptr) {
2771 selectedIds.insert(ptr->getId());
2772 addChildrenIds(ptr, entityIds);
2773 }
2774 }
2775
2776 auto store = context->store;
2777 const auto& entities = store->getEntities();
2778
2779 for (auto& e : entities) {
2780 if (isUserRootEntity(e.second.get())) {
2781 recursiveSetVisibility(e.second.get(),
2782 [&entityIds](Cogs::ComponentModel::Entity* entity, bool anyChildVisible) {
2783 bool visible = entityIds.find(entity->getId()) != std::end(entityIds);
2784 return visible || anyChildVisible;
2785 });
2786 }
2787 }
2788 }
2789}
2790
2792{
2793 auto store = context->store;
2794 auto& entities = store->getEntities();
2795
2796 for (auto& e : entities) {
2797 auto& entity = e.second;
2798
2799 if (!entity) continue;
2800
2801 auto rootSceneComponent = entity->getComponent<SceneComponent>();
2802 if (rootSceneComponent) {
2803 setVisibility(rootSceneComponent, true, true);
2804 }
2805 }
2806}
2807
2808// Find entity furthest away from bounding box around selected entities.
2809// Control keys to limit search.
2810// Shift = Ignore X-distance
2811// Control = Ignore Y-distance
2812// Alt = Ignore Z-distance
2814{
2815 FurthestFlags flags = FurthestFlags::None;
2816 const Keyboard& keyboard = context->getDefaultView()->refKeyboard();
2817
2818 if (!keyboard.isKeyDown(Key::Shift)) {
2819 flags |= FurthestFlags::X;
2820 }
2821 if (!keyboard.isKeyDown(Key::Control)) {
2822 flags |= FurthestFlags::Y;
2823 }
2824 if (!keyboard.isKeyDown(Key::Alt)) {
2825 flags |= FurthestFlags::Z;
2826 }
2827
2828
2829 if (state->hasSelected()) {
2830 std::unordered_set<size_t> entityIds;
2831
2832 Geometry::DBoundingBox selectedBbox;
2833
2834 for (const EntityId id : state->selected) {
2835 auto entity = context->store->getEntityPtr(id);
2836 if (entity) {
2837 auto sceneComponent = entity->getComponent<SceneComponent>();
2838 if (sceneComponent && sceneComponent->visible) {
2839 Geometry::DBoundingBox bb;
2840 ::calculateBoundingBoxWorld(context, id, bb.data());
2841 selectedBbox += bb;
2842 }
2843 }
2844 }
2845
2846 if (isEmpty(selectedBbox)) {
2847 LOG_WARNING(logger, "selectFurthestAway: Selection bounding box is empty.");
2848 return;
2849 }
2850
2851 double distance = 0;
2852 Cogs::ComponentModel::Entity* furthest = nullptr;
2853 if (!isEmpty(selectedBbox)) {
2854 for (const EntityId id : state->selected) {
2855 auto entity = context->store->getEntityPtr(id);
2856 if (entity) {
2857 auto root = getRootEntity(entity, context);
2858 recurseSelectFurthest(&furthest, distance, selectedBbox, root, context, flags);
2859 }
2860 }
2861 }
2862
2863 if (distance > 0 && furthest) {
2864 LOG_DEBUG(logger, "Found Faraway land");
2865 EntityIds ids { furthest->getId() };
2866 apply<SelectCommand>(ids);
2867 }
2868 }
2869}
2870
2872{
2873 auto store = context->store;
2874 auto& entities = store->getEntities();
2875
2876 for (auto& e : entities) {
2877 if (isUserRootEntity(e.second.get())) {
2878 setConsistentChildInvisibility(e.second.get());
2879 recursiveSetVisibility(e.second.get(),
2880 [](Cogs::ComponentModel::Entity* entity, bool anyChildVisible) {
2881 auto sceneComponent = entity->getComponent< Cogs::Core::SceneComponent>();
2882 if (sceneComponent) {
2883 return anyChildVisible || !sceneComponent->visible;
2884 }
2885
2886 return anyChildVisible;
2887 });
2888 }
2889 }
2890}
2891
2892void Cogs::Core::Editor::selectAll()
2893{
2894 EntityIds ids;
2895
2896 for (auto& e : context->store->getEntities()) {
2897 const EntityPtr& entity = e.second;
2898 if (entity && !isStandardEntity(entity.get())) {
2899 if (state->getFindPattern().empty()) {
2900 ids.emplace_back(entity->getId());
2901 }
2902 else {
2903 recurseAddFindPatternMatches(*entity, state->getFindRegex(), ids);
2904 }
2905 }
2906 }
2907
2908 apply<SelectCommand>(ids);
2909 setStatusbarText("Selected entity count: " + std::to_string(ids.size()), false);
2910}
2911
2912void Cogs::Core::Editor::selectOne(EntityId id)
2913{
2914 EntityIds ids { id };
2915 apply<SelectCommand>(ids);
2916 setStatusbarText("Selected entity count: " + std::to_string(ids.size()), false);
2917}
2918
2919void Cogs::Core::Editor::clearCommands()
2920{
2921 commands.clear();
2922 commandIndex = NoCommand;
2923}
2924
2925void Cogs::Core::Editor::onlineHelp(bool generalHelp)
2926{
2927 if (generalHelp) {
2928 Cogs::Desktop::systemOpen("https://3dvstorage.blob.core.windows.net/$web/cogsdoc/core/index.html");
2929 }
2930 else {
2931 Cogs::Desktop::systemOpen("https://3dvstorage.blob.core.windows.net/$web/cogsdoc/core/md_Documentation_CogsCoreRuntime.html");
2932 }
2933}
2934
2935void Cogs::Core::Editor::showLicenses()
2936{
2937 apply<ShowLicensesCommand>();
2938}
2939
2940void Cogs::Core::Editor::showAbout()
2941{
2942 apply<AboutCommand>();
2943}
2944
2945void Cogs::Core::Editor::undo()
2946{
2947 if (commandIndex != NoCommand) {
2948 auto & command = commands[commandIndex--];
2949 command->undo();
2950 if (command->permanentUndo) {
2951 commands.resize(commandIndex + 1);
2952 }
2953 }
2954}
2955
2956void Cogs::Core::Editor::redo()
2957{
2958 ++commandIndex;
2959
2960 if (commandIndex >= commands.size()) {
2961 commandIndex = commands.size() - 1;
2962 return;
2963 }
2964
2965 modalCommand = dynamic_cast<ModalEditorCommand*>(commands[commandIndex].get());
2966
2967 commands[commandIndex]->redo();
2968}
2969
2971{
2972 lastPickState.clear();
2973 if (selected.size() == 1) {
2974 const EntityId pickEntityId = selected[0];
2975 Entity* selectedEntity = context->store->getEntityPtr(pickEntityId);
2976 if (!selectedEntity) {
2977 lastPickState.clear();
2978 return;
2979 }
2980
2981 const Entity* rootEntity = getRootEntity(selectedEntity, context);
2982 if (rootEntity) {
2983 lastPickState.rootId = rootEntity->getId();
2984 auto propsComponent = rootEntity->getComponent<Cogs::Core::PropertiesComponent>();
2985 if (propsComponent) {
2986 std::string tag;
2987 tag = propsComponent->properties.getProperty(std::to_string(this->pickId), std::string()).to_string();
2988 if (!tag.empty()) {
2989 lastPickState.details = "Tag: " + tag;
2990 }
2991 }
2992 }
2993
2994 if (pickId >= 0) {
2995 lastPickState.pickId = std::to_string(pickId);
2996 }
2997
2998 const Entity* assetGroupRoot = getRootEntity(selectedEntity, context, 2);
2999 const Entity* assetRoot = getRootEntity(selectedEntity, context, 1);
3000
3001 if (assetGroupRoot) {
3002 lastPickState.assetRootGroupName = assetGroupRoot->getName();
3003 }
3004
3005 Geometry::DBoundingBox globalBB;
3006 ::calculateBoundingBoxWorld(context, selectedEntity->getId(), globalBB.data());
3007
3008 if (assetRoot && !isEmpty(globalBB)) {
3009 auto propsComponent = assetRoot->getComponent<Cogs::Core::PropertiesComponent>();
3010 if (propsComponent) {
3011 const auto rvmOrigin = propsComponent->properties.getProperty("_rvmOrigin", std::span<const float>());
3012 if (rvmOrigin.size() == 3) {
3013 // Shift BBox by rvmOrigin
3014 globalBB.min += glm::dvec3(rvmOrigin[0], rvmOrigin[1], rvmOrigin[2]);
3015 globalBB.max += glm::dvec3(rvmOrigin[0], rvmOrigin[1], rvmOrigin[2]);
3016 }
3017 }
3018 }
3019
3020 if (!isEmpty(globalBB)) {
3021 char buffer[128] = {};
3022 snprintf(buffer, sizeof(buffer), "[%.2f %.2f %.2f] x [%.2f %.2f %.2f]",
3023 globalBB.min.x, globalBB.min.y, globalBB.min.z,
3024 globalBB.max.x, globalBB.max.y, globalBB.max.z);
3025 lastPickState.boundingBox = buffer;
3026 }
3027 }
3028}
3029
Base class for Component instances.
Definition: Component.h:143
void setChanged()
Sets the component to the ComponentFlags::Changed state with carry.
Definition: Component.h:202
ComponentType * getComponent() const
Definition: Component.h:159
Container for components, providing composition of dynamic entities.
Definition: Entity.h:18
T * getComponent() const
Get a pointer to the first component implementing the given type in the entity.
Definition: Entity.h:35
constexpr size_t getId() const noexcept
Get the unique identifier of this entity.
Definition: Entity.h:113
const std::string & getName() const noexcept
Get the name of this entity.
Definition: Entity.h:120
void setName(const StringView &name)
Definition: Entity.cpp:98
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
class IRenderer * renderer
Renderer.
Definition: Context.h:228
class EntityStore * store
Entity store.
Definition: Context.h:231
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
std::unique_ptr< class ResourceStore > resourceStore
ResourceStore service instance.
Definition: Context.h:210
std::unique_ptr< class Scene > scene
Scene structure.
Definition: Context.h:225
bool getLayerVisibility(const ComponentModel::Entity *entity, StringView layer)
Returns true if any Node in layer is visible.
Definition: Editor.cpp:2072
bool isUserRootEntity(const Cogs::ComponentModel::Entity *entity) const
Checks if the entity is.
Definition: Editor.cpp:2109
void showAll()
Make all entities visible.
Definition: Editor.cpp:2791
void showHide(bool visible)
Show or hide selected entity and all children.
Definition: Editor.cpp:2740
void showText(const char *format,...) const
Show the same type of label as ImGui::Text, but with the added ability to highlight and copy text....
Definition: Editor.cpp:1831
void selectFurthestAway()
Search and select visible entity furthest away from selection. Ie.e. find outliers.
Definition: Editor.cpp:2813
void invertVisibility()
Definition: Editor.cpp:2871
void open(const StringView &path, bool openInRoot, bool synchronous) override
Open file.
Definition: Editor.cpp:2348
void setStatusbarText(const std::string &text, bool guiStatus)
Definition: Editor.cpp:2675
void hideUnselected()
Make all non-selected entities invisible.
Definition: Editor.cpp:2761
bool isStandardEntity(const Cogs::ComponentModel::Entity *entity) const
Definition: Editor.cpp:2093
ComponentModel::Component * addComponent(ComponentModel::Entity *entity, Reflection::TypeId typeId)
Add a component of the given type to the entity.
ComponentModel::Entity * getEntityPtr(const EntityId entityId)
Get a raw pointer to the entity with the given id.
Cogs::ComponentModel::Entity * getEntityParent(const ComponentModel::Entity *entity) const
Gets the parent of the given entity.
EntityPtr findEntity(const StringView &name, const ComponentModel::Entity *root=nullptr, EntityFind findOptions=EntityFind::Default) const
Finds an entity with the given name.
const std::unordered_map< EntityId, EntityPtr > & getEntities() const
Return map of entities with global ownership.
Definition: EntityStore.h:268
Event based input handler.
static const void * loadExtensionModule(const std::string &path, void **modulehandle=nullptr, ExtensionModuleLoadResult *result=nullptr)
Load the extension module with the given name.
virtual glm::vec2 getSize() const =0
Get the output surface size of the renderer.
static bool isUsingKeyboard()
Tests whether any ImGui control is currently accepting text input.
Component that calculates position and orientation of the entity TransformComponent.
bool moveCamera
Set to false to disable controller moving the camera and only apply rotate and zoom....
Contains information on how the entity behaves in the scene.
std::vector< EntityPtr > children
Contains all child entities owned by this component.
bool visible
If the entity this component is a member of should be visible.
ComponentModel::ComponentHandle parent
Handle to the scene component of the parent entity.
Defines a 4x4 transformation matrix for the entity and a global offset for root entities.
glm::dvec3 coordinates
Global coordinates.
glm::vec3 position
Local position relative to the global coordinates, or the parent coordinate system if the parent fiel...
Cogs::Core::EntityPtr getCamera() const
Gets view camera. The camera must be valid when rendering a frame in the view.
void setCamera(const Cogs::Core::EntityPtr &renderCamera)
Sets or updates the camera of this ViewContext instance.
std::unique_ptr< class DPIService > dpiService
DPI service instance.
Definition: ViewContext.h:71
bool wasKeyPressed(Key key) const
Return true if the key was pressed in the last frame, false otherwise.
Definition: Keyboard.cpp:55
Log implementation class.
Definition: LogManager.h:139
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
constexpr bool empty() const noexcept
Check if the string is empty.
Definition: StringView.h:122
static constexpr size_t NoPosition
No position.
Definition: StringView.h:43
std::string to_string() const
String conversion method.
Definition: StringView.cpp:9
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
RenderMode
Defines global rendering modes that can be supported by the renderer.
Definition: IRenderer.h:37
@ Wireframe
Wireframe rendering.
@ Box
Bounding-box rendering over regular rendering.
@ Normal
Regular shaded rendering.
std::shared_ptr< ComponentModel::Entity > EntityPtr
Smart pointer for Entity access.
Definition: EntityPtr.h:12
bool HandleIsValid(const ResourceHandle_t< T > &handle)
Check if the given resource is valid, that is not equal to NoHandle or InvalidHandle.
@ Closest
Return just the closest hit.
std::weak_ptr< ComponentModel::Entity > WeakEntityPtr
Weak Smart pointer for Entity access.
Definition: EntityPtr.h:18
@ Disc
Each point is a flat disc.
KeyboardModifiers
Current keyboard modifiers.
@ ForceSynchronous
Force loading the resource synchronously.
@ ReturnChildEntity
Return ID if sub-entity picked, not set: return root parent entity.
@ FlatTexcoordInterpolation
Do not interpolate texture coordinates. Needed if integer IDs are encoded as texture coordinates.
@ PickSprites
Check picking for entities with SpriteRenderComponent. I.e. Text, Annotation, Billboard,...
@ AddInstanceTexcoords
If mesh has an offset to IDs encoded in a per-instance texcoords stream, add this to the result.
ProjectionMode
Defines projection modes for the camera.
ResourceLoadFlags
Flags for describing how to load a resource.
Definition: ResourceFlags.h:16
@ Reset
Input Focus lost event. Typically reset any cached mouse/keyboard state.
@ Keyboard
Keyboard event.
@ Flat
Use color directly as-is.
@ LinearColorSpace
For textures with RGBA format without color space information, mark the data as being in linear color...
@ ForceUnique
Force unique resource load when source resolves to existing resource.
@ Flip
Flip the texture data vertically before it is passed to the rendering backend.
AssetWriteFlags
Flags that control serialization of assets.
@ Hierarchy
Save all children, not only children in EntityStore.
@ Geometry
Store entity vector fields (vector<vec3>, vector<vec2>, vector<int>, vector<float>).
void COGSCORE_DLL_API writeEntity(Context *context, ComponentModel::Entity *entity, rapidjson::Value &object, rapidjson::Document &d, SerializationContext *sc=nullptr, const AssetWriteFlags flags=AssetWriteFlags::None)
Serialize entity adding entity to the given parent 'object'.
@ InstantiateOnDemand
Dynamically calculate lod levels and only instantiate what is visible.
@ RelativePaths
Paths in asset file is relative to file's path.
@ OverrideMaterial
Override any model's material with material member of asset component.
@ Color
Use color property from point set if present, fallback to basecolor.
@ None
Just use scale and offset as is.
@ NoDefault
Don't load the default scene. Highly recommended as not setting this flag cause extra scene parse.
@ ClearScene
Clear the scene before loading the asset. NOT allowed for Bridge: loadAsset and loadAssetFromString.
Contains geometry calculations and generation.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
COGSCORE_DLL_API glm::dvec3 getFocalPosition(const glm::dvec3 &position, const glm::quat &orientation, float focalDistance)
COGSCORE_DLL_API float getRadius(const Cogs::Geometry::DBoundingBox &boundingBox)
Gets the radius of the bounding sphere.
glm::dvec3 viewAll(const CameraComponent cameraComponent, const TransformComponent transformComponent, const OrbitingCameraController orbitingCameraController, const ProjectionMode projectionMode, const Cogs::Geometry::DBoundingBox &boundingBox, glm::vec2 viewport=glm::vec2(0.0f, 0.0f))
View all for given bounding box.
Definition: CameraUtils.h:56
glm::quat getCameraOrientation(const TransformComponent transformComponent, const OrbitingCameraController orbitingCameraController)
Definition: CameraUtils.h:29
STL namespace.
Handle to a Component instance.
Definition: Component.h:67
COGSFOUNDATION_API class Component * resolve() const
Resolve the handle, returning a pointer to the held Component instance.
Definition: Component.cpp:65
ComponentType * resolveComponent() const
Definition: Component.h:90
Base class for Cogs Editor commands.
Definition: EditorCommand.h:19
EntityId getSelectedId() const
Gets entity ID of the single selected entity.
Definition: EditorState.h:130
void updateSelectedInfo()
Update information in LastPickedState when selection changes.
Definition: Editor.cpp:2970
void setFindPattern(const std::string &pattern)
Entity search pattern.
Definition: EditorState.h:139
const std::regex & getFindRegex() const
Entity search pattern as Regexp for lookup.
Definition: EditorState.h:155
const std::string & getFindPattern() const
Gets Entity search pattern.
Definition: EditorState.h:153
std::span< const EntityId > getAllSelectedIds() const
Get Entity Ids of all selected entities.
Definition: EditorState.h:136
ComponentModel::Entity * getSelected() const
Gets entity Pointer to the single selected entity. Nullptr if not one selected or not found.
Definition: EditorState.h:133
WeakEntityPtr editorCamera
Editor scene camera.
Definition: EditorState.h:84
void setSelection(const EntityIds &ids)
Set new selected entities.
Definition: EditorState.h:100
Event input queue event. Contains either a keyboard or mouse event. Keyboard event:
KeyboardModifiers modifiers
Current keyboard modifiers.
Cogs::Mouse::Event mouseEvent
Mouse event data if mouse event.
EntityId rootId
Root entity ID.
Definition: EditorState.h:51
std::string assetRootGroupName
Asset root name (Name of root Group node from rvmparser - grandchild).
Definition: EditorState.h:60
std::string pickId
Pick details. Picked Texture ID.
Definition: EditorState.h:57
std::string details
Pick details. AssetTag.
Definition: EditorState.h:54
void clear()
Clear Picking information.
Definition: EditorState.h:66
StringView getName() const
Get the name of the resource.
Definition: ResourceBase.h:307
Type type
Event type.
Definition: Mouse.h:55
std::variant< ButtonData, MoveData, WheelData > data
Definition: Mouse.h:61
glm::ivec2 currPosition
Mouse position at the event.
Definition: Mouse.h:57