diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag index 71faf85..28ad70c 100644 --- a/shaders/gbuffer.frag +++ b/shaders/gbuffer.frag @@ -1,5 +1,6 @@ #version 450 #extension GL_GOOGLE_include_directive : require +#extension GL_EXT_buffer_reference : require #include "input_structures.glsl" layout(location = 0) in vec3 inNormal; @@ -11,6 +12,26 @@ layout(location = 4) in vec4 inTangent; layout(location = 0) out vec4 outPos; layout(location = 1) out vec4 outNorm; layout(location = 2) out vec4 outAlbedo; +layout(location = 3) out uint outObjectID; + +// Keep push constants layout in sync with mesh.vert / GPUDrawPushConstants +struct Vertex { + vec3 position; float uv_x; + vec3 normal; float uv_y; + vec4 color; + vec4 tangent; +}; + +layout(buffer_reference, std430) readonly buffer VertexBuffer{ + Vertex vertices[]; +}; + +layout(push_constant) uniform constants +{ + mat4 render_matrix; + VertexBuffer vertexBuffer; + uint objectID; +} PushConstants; void main() { // Apply baseColor texture and baseColorFactor once @@ -37,4 +58,5 @@ void main() { outPos = vec4(inWorldPos, 1.0); outNorm = vec4(Nw, roughness); outAlbedo = vec4(albedo, metallic); + outObjectID = PushConstants.objectID; } diff --git a/shaders/mesh.vert b/shaders/mesh.vert index 91bec7c..0d8afe3 100644 --- a/shaders/mesh.vert +++ b/shaders/mesh.vert @@ -25,11 +25,12 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{ Vertex vertices[]; }; -//push constants block -layout( push_constant ) uniform constants +//push constants block (must match GPUDrawPushConstants layout in C++) +layout(push_constant) uniform constants { mat4 render_matrix; VertexBuffer vertexBuffer; + uint objectID; } PushConstants; void main() diff --git a/shaders/shadow.vert b/shaders/shadow.vert index 84d12a8..d4639fb 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -19,6 +19,7 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{ layout(push_constant) uniform PushConsts { mat4 render_matrix; VertexBuffer vertexBuffer; + uint objectID; uint cascadeIndex; // which cascade this pass renders // pad to 16-byte boundary implicitly } PC; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6cda25d..b44b682 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -99,7 +99,7 @@ target_include_directories(vulkan_engine PUBLIC option(ENABLE_MIKKTS "Use MikkTSpace for tangent generation" ON) -target_link_libraries(vulkan_engine PUBLIC vma glm Vulkan::Vulkan fmt::fmt stb_image SDL2::SDL2 vkbootstrap imgui fastgltf::fastgltf) +target_link_libraries(vulkan_engine PUBLIC vma glm Vulkan::Vulkan fmt::fmt stb_image SDL2::SDL2 vkbootstrap imgui fastgltf::fastgltf ImGuizmo) if (ENABLE_MIKKTS) target_link_libraries(vulkan_engine PUBLIC mikktspace) target_compile_definitions(vulkan_engine PUBLIC MIKKTS_ENABLE=1) diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 25bd5a8..e9af8ae 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -14,10 +14,8 @@ // //> includes #include "vk_engine.h" -#include #include "SDL2/SDL.h" -#include "SDL2/SDL_vulkan.h" #include #include @@ -29,7 +27,6 @@ #include #include -#include "render/vk_pipelines.h" #include #include @@ -49,7 +46,6 @@ #include "vk_resource.h" #include "engine_context.h" #include "core/vk_pipeline_manager.h" -#include "core/config.h" #include "core/texture_cache.h" #include "core/ibl_manager.h" @@ -540,6 +536,44 @@ namespace { const DrawContext &dc = eng->_context->getMainDrawContext(); ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); + ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking); + ImGui::Separator(); + if (eng->_lastPick.valid) + { + const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : ""; + const char *sceneName = ""; + if (eng->_lastPick.scene && !eng->_lastPick.scene->debugName.empty()) + { + sceneName = eng->_lastPick.scene->debugName.c_str(); + } + ImGui::Text("Last pick scene: %s", sceneName); + ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex); + ImGui::Text("World pos: (%.3f, %.3f, %.3f)", + eng->_lastPick.worldPos.x, + eng->_lastPick.worldPos.y, + eng->_lastPick.worldPos.z); + ImGui::Text("Indices: first=%u count=%u", + eng->_lastPick.firstIndex, + eng->_lastPick.indexCount); + } + else + { + ImGui::TextUnformatted("Last pick: "); + } + ImGui::Separator(); + if (eng->_hoverPick.valid) + { + const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : ""; + ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex); + } + else + { + ImGui::TextUnformatted("Hover: "); + } + if (!eng->_dragSelection.empty()) + { + ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size()); + } } } // namespace @@ -708,7 +742,7 @@ void VulkanEngine::init() auto imguiPass = std::make_unique(); _renderPassManager->setImGuiPass(std::move(imguiPass)); - const std::string structurePath = _assetManager->modelPath("sponza_2/scene.gltf"); + const std::string structurePath = _assetManager->modelPath("mirage2000/scene.gltf"); const auto structureFile = _assetManager->loadGLTF(structurePath); assert(structureFile.has_value()); @@ -857,6 +891,13 @@ void VulkanEngine::cleanup() print_vma_stats(_deviceManager.get(), "after RTManager"); dump_vma_json(_deviceManager.get(), "after_RTManager"); + // Destroy pick readback buffer before resource manager cleanup + if (_pickReadbackBuffer.buffer) + { + _resourceManager->destroy_buffer(_pickReadbackBuffer); + _pickReadbackBuffer = {}; + } + _resourceManager->cleanup(); print_vma_stats(_deviceManager.get(), "after ResourceManager"); dump_vma_json(_deviceManager.get(), "after_ResourceManager"); @@ -884,11 +925,43 @@ void VulkanEngine::cleanup() void VulkanEngine::draw() { - _sceneManager->update_scene(); //> frame_clear //wait until the gpu has finished rendering the last frame. Timeout of 1 second VK_CHECK(vkWaitForFences(_deviceManager->device(), 1, &get_current_frame()._renderFence, true, 1000000000)); + // If we scheduled an ID-buffer readback in the previous frame, resolve it now. + if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager) + { + vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t)); + uint32_t pickedID = 0; + if (_pickReadbackBuffer.info.pMappedData) + { + pickedID = *reinterpret_cast(_pickReadbackBuffer.info.pMappedData); + } + + if (pickedID == 0) + { + // Do not override existing raycast pick when ID buffer reports "no object". + } + else + { + RenderObject picked{}; + if (_sceneManager->resolveObjectID(pickedID, picked)) + { + // Fallback hit position: object origin in world space (can refine later) + glm::vec3 fallbackPos = glm::vec3(picked.transform[3]); + _lastPick.mesh = picked.sourceMesh; + _lastPick.scene = picked.sourceScene; + _lastPick.worldPos = fallbackPos; + _lastPick.firstIndex = picked.firstIndex; + _lastPick.indexCount = picked.indexCount; + _lastPick.surfaceIndex = picked.surfaceIndex; + _lastPick.valid = true; + } + } + _pickResultPending = false; + } + get_current_frame()._deletionQueue.flush(); // Resolve last frame's pass timings before we clear and rebuild the graph if (_renderGraph) @@ -898,6 +971,29 @@ void VulkanEngine::draw() get_current_frame()._frameDescriptors.clear_pools(_deviceManager->device()); //< frame_clear + _sceneManager->update_scene(); + + // Per-frame hover raycast based on last mouse position. + if (_sceneManager && _mousePosPixels.x >= 0.0f && _mousePosPixels.y >= 0.0f) + { + RenderObject hoverObj{}; + glm::vec3 hoverPos{}; + if (_sceneManager->pick(_mousePosPixels, hoverObj, hoverPos)) + { + _hoverPick.mesh = hoverObj.sourceMesh; + _hoverPick.scene = hoverObj.sourceScene; + _hoverPick.worldPos = hoverPos; + _hoverPick.firstIndex = hoverObj.firstIndex; + _hoverPick.indexCount = hoverObj.indexCount; + _hoverPick.surfaceIndex = hoverObj.surfaceIndex; + _hoverPick.valid = true; + } + else + { + _hoverPick.valid = false; + } + } + uint32_t swapchainImageIndex; VkResult e = vkAcquireNextImageKHR(_deviceManager->device(), _swapchainManager->swapchain(), 1000000000, @@ -999,7 +1095,67 @@ void VulkanEngine::draw() } if (auto *geometry = _renderPassManager->getPass()) { - geometry->register_graph(_renderGraph.get(), hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hDepth); + RGImageHandle hID = _renderGraph->import_id_buffer(); + geometry->register_graph(_renderGraph.get(), hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hID, hDepth); + + // If ID-buffer picking is enabled and a pick was requested this frame, + // add a small transfer pass to read back 1 pixel from the ID buffer. + if (_useIdBufferPicking && _pendingPick.active && hID.valid() && _pickReadbackBuffer.buffer) + { + VkExtent2D swapExt = _swapchainManager->swapchainExtent(); + VkExtent2D drawExt = _drawExtent; + + float sx = _pendingPick.windowPos.x / float(std::max(1u, swapExt.width)); + float sy = _pendingPick.windowPos.y / float(std::max(1u, swapExt.height)); + + uint32_t idX = uint32_t(glm::clamp(sx * float(drawExt.width), 0.0f, float(drawExt.width - 1))); + uint32_t idY = uint32_t(glm::clamp(sy * float(drawExt.height), 0.0f, float(drawExt.height - 1))); + _pendingPick.idCoords = {idX, idY}; + + RGImportedBufferDesc bd{}; + bd.name = "pick.readback"; + bd.buffer = _pickReadbackBuffer.buffer; + bd.size = sizeof(uint32_t); + bd.currentStage = VK_PIPELINE_STAGE_2_NONE; + bd.currentAccess = 0; + RGBufferHandle hPickBuf = _renderGraph->import_buffer(bd); + + _renderGraph->add_pass( + "PickReadback", + RGPassType::Transfer, + [hID, hPickBuf](RGPassBuilder &builder, EngineContext *) + { + builder.read(hID, RGImageUsage::TransferSrc); + builder.write_buffer(hPickBuf, RGBufferUsage::TransferDst); + }, + [this, hID, hPickBuf](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *) + { + VkImage idImage = res.image(hID); + VkBuffer dst = res.buffer(hPickBuf); + if (idImage == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; + + VkBufferImageCopy region{}; + region.bufferOffset = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = { static_cast(_pendingPick.idCoords.x), + static_cast(_pendingPick.idCoords.y), + 0 }; + region.imageExtent = {1, 1, 1}; + + vkCmdCopyImageToBuffer(cmd, + idImage, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dst, + 1, + ®ion); + }); + + _pickResultPending = true; + _pendingPick.active = false; + } } if (auto *lighting = _renderPassManager->getPass()) { @@ -1104,6 +1260,95 @@ void VulkanEngine::run() freeze_rendering = false; } } + if (e.type == SDL_MOUSEMOTION) + { + _mousePosPixels = glm::vec2{static_cast(e.motion.x), + static_cast(e.motion.y)}; + if (_dragState.buttonDown) + { + _dragState.current = _mousePosPixels; + // Consider any motion as dragging for now; can add threshold if desired. + _dragState.dragging = true; + } + } + if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) + { + _dragState.buttonDown = true; + _dragState.dragging = false; + _dragState.start = glm::vec2{static_cast(e.button.x), + static_cast(e.button.y)}; + _dragState.current = _dragState.start; + } + if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) + { + glm::vec2 releasePos{static_cast(e.button.x), + static_cast(e.button.y)}; + _dragState.buttonDown = false; + + constexpr float clickThreshold = 3.0f; + glm::vec2 delta = releasePos - _dragState.start; + bool treatAsClick = !_dragState.dragging && + std::abs(delta.x) < clickThreshold && + std::abs(delta.y) < clickThreshold; + + if (treatAsClick) + { + // Raycast click selection + if (_sceneManager) + { + RenderObject hitObject{}; + glm::vec3 hitPos{}; + if (_sceneManager->pick(releasePos, hitObject, hitPos)) + { + _lastPick.mesh = hitObject.sourceMesh; + _lastPick.scene = hitObject.sourceScene; + _lastPick.worldPos = hitPos; + _lastPick.firstIndex = hitObject.firstIndex; + _lastPick.indexCount = hitObject.indexCount; + _lastPick.surfaceIndex = hitObject.surfaceIndex; + _lastPick.valid = true; + } + else + { + _lastPick.valid = false; + } + } + + // Optionally queue an ID-buffer pick at this position + if (_useIdBufferPicking) + { + _pendingPick.active = true; + _pendingPick.windowPos = releasePos; + } + } + else + { + // Drag selection completed; compute selection based on screen-space rectangle. + _dragSelection.clear(); + if (_sceneManager) + { + std::vector selected; + _sceneManager->selectRect(_dragState.start, releasePos, selected); + _dragSelection.reserve(selected.size()); + for (const RenderObject &obj : selected) + { + PickInfo info{}; + info.mesh = obj.sourceMesh; + info.scene = obj.sourceScene; + // Use bounds origin transformed to world as a representative point. + glm::vec3 centerWorld = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f)); + info.worldPos = centerWorld; + info.firstIndex = obj.firstIndex; + info.indexCount = obj.indexCount; + info.surfaceIndex = obj.surfaceIndex; + info.valid = true; + _dragSelection.push_back(info); + } + } + } + + _dragState.dragging = false; + } _sceneManager->getMainCamera().processSDLEvent(e); ImGui_ImplSDL2_ProcessEvent(&e); } @@ -1207,6 +1452,12 @@ void VulkanEngine::init_frame_resources() { _frames[i].init(_deviceManager.get(), frame_sizes); } + + // Allocate a small readback buffer for ID-buffer picking (single uint32 pixel) + _pickReadbackBuffer = _resourceManager->create_buffer( + sizeof(uint32_t), + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); } void VulkanEngine::init_pipelines() @@ -1218,8 +1469,16 @@ void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) { glm::mat4 nodeMatrix = topMatrix * worldTransform; - for (auto &s: mesh->surfaces) + if (!mesh) { + Node::Draw(topMatrix, ctx); + return; + } + + for (uint32_t i = 0; i < mesh->surfaces.size(); ++i) + { + const auto &s = mesh->surfaces[i]; + RenderObject def{}; def.indexCount = s.count; def.firstIndex = s.startIndex; @@ -1230,6 +1489,10 @@ void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) def.transform = nodeMatrix; def.vertexBufferAddress = mesh->meshBuffers.vertexBufferAddress; + def.sourceMesh = mesh.get(); + def.surfaceIndex = i; + def.objectID = ctx.nextID++; + def.sourceScene = scene; if (s.material->data.passType == MaterialPass::Transparent) { diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index b8706fc..7db1b82 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -49,9 +49,11 @@ struct RenderPass struct MeshNode : public Node { - std::shared_ptr mesh; + std::shared_ptr mesh; + // Owning glTF scene (for picking/debug); may be null for non-gltf meshes. + LoadedGLTF *scene = nullptr; - virtual void Draw(const glm::mat4 &topMatrix, DrawContext &ctx) override; + virtual void Draw(const glm::mat4 &topMatrix, DrawContext &ctx) override; }; class VulkanEngine @@ -114,6 +116,42 @@ public: // Debug helpers: track spawned IBL test meshes to remove them easily std::vector _iblTestNames; + struct PickInfo + { + MeshAsset *mesh = nullptr; + LoadedGLTF *scene = nullptr; + glm::vec3 worldPos{0.0f}; + uint32_t indexCount = 0; + uint32_t firstIndex = 0; + uint32_t surfaceIndex = 0; + bool valid = false; + } _lastPick; + + struct PickRequest + { + bool active = false; + glm::vec2 windowPos{0.0f}; + glm::uvec2 idCoords{0, 0}; + } _pendingPick; + bool _pickResultPending = false; + AllocatedBuffer _pickReadbackBuffer{}; + + // Hover and drag-selection state (raycast-based) + PickInfo _hoverPick{}; + glm::vec2 _mousePosPixels{-1.0f, -1.0f}; + struct DragState + { + bool dragging = false; + bool buttonDown = false; + glm::vec2 start{0.0f}; + glm::vec2 current{0.0f}; + } _dragState; + // Optional list of last drag-selected objects (for future editing UI) + std::vector _dragSelection; + + // Toggle to enable/disable ID-buffer picking in addition to raycast + bool _useIdBufferPicking = false; + // Debug: persistent pass enable overrides (by pass name) std::unordered_map _rgPassToggles; diff --git a/src/core/vk_swapchain.cpp b/src/core/vk_swapchain.cpp index d872e0c..c7cbca5 100644 --- a/src/core/vk_swapchain.cpp +++ b/src/core/vk_swapchain.cpp @@ -70,6 +70,10 @@ void SwapchainManager::init_swapchain() VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); _gBufferAlbedo = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + _idBuffer = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R32_UINT, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT); _deletionQueue.push_function([=]() { vkDestroyImageView(_deviceManager->device(), _drawImage.imageView, nullptr); @@ -81,6 +85,7 @@ void SwapchainManager::init_swapchain() _resourceManager->destroy_image(_gBufferPosition); _resourceManager->destroy_image(_gBufferNormal); _resourceManager->destroy_image(_gBufferAlbedo); + _resourceManager->destroy_image(_idBuffer); }); }; @@ -191,6 +196,10 @@ void SwapchainManager::resize_swapchain(struct SDL_Window *window) VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); _gBufferAlbedo = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + _idBuffer = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R32_UINT, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT); _deletionQueue.push_function([=]() { vkDestroyImageView(_deviceManager->device(), _drawImage.imageView, nullptr); @@ -202,6 +211,7 @@ void SwapchainManager::resize_swapchain(struct SDL_Window *window) _resourceManager->destroy_image(_gBufferPosition); _resourceManager->destroy_image(_gBufferNormal); _resourceManager->destroy_image(_gBufferAlbedo); + _resourceManager->destroy_image(_idBuffer); }); resize_requested = false; diff --git a/src/core/vk_swapchain.h b/src/core/vk_swapchain.h index 4b84d08..7fe4299 100644 --- a/src/core/vk_swapchain.h +++ b/src/core/vk_swapchain.h @@ -29,6 +29,7 @@ public: AllocatedImage gBufferPosition() const { return _gBufferPosition; } AllocatedImage gBufferNormal() const { return _gBufferNormal; } AllocatedImage gBufferAlbedo() const { return _gBufferAlbedo; } + AllocatedImage idBuffer() const { return _idBuffer; } VkExtent2D windowExtent() const { return _windowExtent; } bool resize_requested{false}; @@ -50,6 +51,7 @@ private: AllocatedImage _gBufferPosition = {}; AllocatedImage _gBufferNormal = {}; AllocatedImage _gBufferAlbedo = {}; + AllocatedImage _idBuffer = {}; DeletionQueue _deletionQueue; }; diff --git a/src/core/vk_types.h b/src/core/vk_types.h index 6ba7fbc..9187e59 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -129,6 +129,7 @@ struct GPUMeshBuffers { struct GPUDrawPushConstants { glm::mat4 worldMatrix; VkDeviceAddress vertexBuffer; + uint32_t objectID; }; struct DrawContext; diff --git a/src/render/rg_graph.cpp b/src/render/rg_graph.cpp index 9ce9e5d..22a1a72 100644 --- a/src/render/rg_graph.cpp +++ b/src/render/rg_graph.cpp @@ -947,6 +947,18 @@ RGImageHandle RenderGraph::import_gbuffer_albedo() return import_image(d); } +RGImageHandle RenderGraph::import_id_buffer() +{ + RGImportedImageDesc d{}; + d.name = "idBuffer.objectID"; + d.image = _context->getSwapchain()->idBuffer().image; + d.imageView = _context->getSwapchain()->idBuffer().imageView; + d.format = _context->getSwapchain()->idBuffer().imageFormat; + d.extent = _context->getDrawExtent(); + d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + return import_image(d); +} + RGImageHandle RenderGraph::import_swapchain_image(uint32_t index) { RGImportedImageDesc d{}; diff --git a/src/render/rg_graph.h b/src/render/rg_graph.h index dc9e5ea..d57e3ab 100644 --- a/src/render/rg_graph.h +++ b/src/render/rg_graph.h @@ -56,6 +56,7 @@ struct Pass; // fwd RGImageHandle import_gbuffer_position(); RGImageHandle import_gbuffer_normal(); RGImageHandle import_gbuffer_albedo(); + RGImageHandle import_id_buffer(); RGImageHandle import_swapchain_image(uint32_t index); void add_present_chain(RGImageHandle sourceDraw, RGImageHandle targetSwapchain, diff --git a/src/render/vk_materials.cpp b/src/render/vk_materials.cpp index d642827..42a3164 100644 --- a/src/render/vk_materials.cpp +++ b/src/render/vk_materials.cpp @@ -13,7 +13,7 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine) VkPushConstantRange matrixRange{}; matrixRange.offset = 0; matrixRange.size = sizeof(GPUDrawPushConstants); - matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; DescriptorLayoutBuilder layoutBuilder; layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); @@ -92,9 +92,10 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine) VkFormat gFormats[] = { engine->_swapchainManager->gBufferPosition().imageFormat, engine->_swapchainManager->gBufferNormal().imageFormat, - engine->_swapchainManager->gBufferAlbedo().imageFormat + engine->_swapchainManager->gBufferAlbedo().imageFormat, + engine->_swapchainManager->idBuffer().imageFormat }; - b.set_color_attachment_formats(std::span(gFormats, 3)); + b.set_color_attachment_formats(std::span(gFormats, 4)); b.set_depth_format(engine->_swapchainManager->depthImage().imageFormat); }; engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo); diff --git a/src/render/vk_renderpass_geometry.cpp b/src/render/vk_renderpass_geometry.cpp index b14f70a..d255c30 100644 --- a/src/render/vk_renderpass_geometry.cpp +++ b/src/render/vk_renderpass_geometry.cpp @@ -69,9 +69,11 @@ void GeometryPass::register_graph(RenderGraph *graph, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, + RGImageHandle idHandle, RGImageHandle depthHandle) { - if (!graph || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || !depthHandle.valid()) + if (!graph || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || + !idHandle.valid() || !depthHandle.valid()) { return; } @@ -79,7 +81,7 @@ void GeometryPass::register_graph(RenderGraph *graph, graph->add_pass( "Geometry", RGPassType::Graphics, - [gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](RGPassBuilder &builder, EngineContext *ctx) + [gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle](RGPassBuilder &builder, EngineContext *ctx) { VkClearValue clear{}; clear.color = {{0.f, 0.f, 0.f, 0.f}}; @@ -87,6 +89,9 @@ void GeometryPass::register_graph(RenderGraph *graph, builder.write_color(gbufferPosition, true, clear); builder.write_color(gbufferNormal, true, clear); builder.write_color(gbufferAlbedo, true, clear); + VkClearValue clearID{}; + clearID.color.uint32[0] = 0u; + builder.write_color(idHandle, true, clearID); // Reverse-Z: clear depth to 0.0 VkClearValue depthClear{}; @@ -118,11 +123,11 @@ void GeometryPass::register_graph(RenderGraph *graph, builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "geom.vertex"); } }, - [this, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](VkCommandBuffer cmd, - const RGPassResources &res, - EngineContext *ctx) + [this, gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle](VkCommandBuffer cmd, + const RGPassResources &res, + EngineContext *ctx) { - draw_geometry(cmd, ctx, res, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle); + draw_geometry(cmd, ctx, res, gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle); }); } @@ -132,6 +137,7 @@ void GeometryPass::draw_geometry(VkCommandBuffer cmd, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, + RGImageHandle /*idHandle*/, RGImageHandle depthHandle) const { EngineContext *ctxLocal = context ? context : _context; @@ -270,6 +276,7 @@ void GeometryPass::draw_geometry(VkCommandBuffer cmd, GPUDrawPushConstants push_constants{}; push_constants.worldMatrix = r.transform; push_constants.vertexBuffer = r.vertexBufferAddress; + push_constants.objectID = r.objectID; vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push_constants); diff --git a/src/render/vk_renderpass_geometry.h b/src/render/vk_renderpass_geometry.h index 78c2011..5b6933a 100644 --- a/src/render/vk_renderpass_geometry.h +++ b/src/render/vk_renderpass_geometry.h @@ -18,6 +18,7 @@ public: RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, + RGImageHandle idHandle, RGImageHandle depthHandle); private: @@ -29,5 +30,6 @@ private: RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, + RGImageHandle idHandle, RGImageHandle depthHandle) const; }; diff --git a/src/render/vk_renderpass_shadow.cpp b/src/render/vk_renderpass_shadow.cpp index c90f9a8..7b1d86f 100644 --- a/src/render/vk_renderpass_shadow.cpp +++ b/src/render/vk_renderpass_shadow.cpp @@ -29,7 +29,7 @@ void ShadowPass::init(EngineContext *context) // Keep push constants matching current shader layout for now VkPushConstantRange pc{}; pc.offset = 0; - // Push constants layout in shadow.vert is mat4 + device address + uint, rounded to 16 bytes + // Push constants layout in shadow.vert is GPUDrawPushConstants + cascade index, rounded to 16 bytes const uint32_t pcRaw = static_cast(sizeof(GPUDrawPushConstants) + sizeof(uint32_t)); const uint32_t pcAligned = (pcRaw + 15u) & ~15u; // 16-byte alignment to match std430 expectations pc.size = pcAligned; @@ -197,6 +197,7 @@ void ShadowPass::draw_shadow(VkCommandBuffer cmd, ShadowPC spc{}; spc.draw.worldMatrix = r.transform; spc.draw.vertexBuffer = r.vertexBufferAddress; + spc.draw.objectID = r.objectID; spc.cascadeIndex = cascadeIndex; vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ShadowPC), &spc); vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0); diff --git a/src/render/vk_renderpass_transparent.cpp b/src/render/vk_renderpass_transparent.cpp index f375d84..bdedc95 100644 --- a/src/render/vk_renderpass_transparent.cpp +++ b/src/render/vk_renderpass_transparent.cpp @@ -196,6 +196,7 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd, GPUDrawPushConstants push{}; push.worldMatrix = r.transform; push.vertexBuffer = r.vertexBufferAddress; + push.objectID = r.objectID; vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push); vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0); diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 91876a0..e8485bc 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -574,17 +574,31 @@ std::optional > loadGltf(VulkanEngine *engine, std:: newSurface.material = materials[0]; } - glm::vec3 minpos = vertices[initial_vtx].position; - glm::vec3 maxpos = vertices[initial_vtx].position; - for (int i = initial_vtx; i < vertices.size(); i++) + // Compute per-surface bounds using only the indices referenced by this primitive. + if (newSurface.count > 0) { - minpos = glm::min(minpos, vertices[i].position); - maxpos = glm::max(maxpos, vertices[i].position); + uint32_t firstIndex = newSurface.startIndex; + uint32_t lastIndex = newSurface.startIndex + newSurface.count; + uint32_t baseVertex = indices[firstIndex]; + glm::vec3 minpos = vertices[baseVertex].position; + glm::vec3 maxpos = vertices[baseVertex].position; + for (uint32_t i = firstIndex + 1; i < lastIndex; i++) + { + uint32_t vi = indices[i]; + const glm::vec3 &p = vertices[vi].position; + minpos = glm::min(minpos, p); + maxpos = glm::max(maxpos, p); + } + newSurface.bounds.origin = (maxpos + minpos) / 2.f; + newSurface.bounds.extents = (maxpos - minpos) / 2.f; + newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); + } + else + { + newSurface.bounds.origin = glm::vec3(0.0f); + newSurface.bounds.extents = glm::vec3(0.5f); + newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); } - - newSurface.bounds.origin = (maxpos + minpos) / 2.f; - newSurface.bounds.extents = (maxpos - minpos) / 2.f; - newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); newmesh->surfaces.push_back(newSurface); } @@ -616,8 +630,10 @@ std::optional > loadGltf(VulkanEngine *engine, std:: // find if the node has a mesh, and if it does hook it to the mesh pointer and allocate it with the meshnode class if (node.meshIndex.has_value()) { - newNode = std::make_shared(); - static_cast(newNode.get())->mesh = meshes[*node.meshIndex]; + auto meshNode = std::make_shared(); + meshNode->mesh = meshes[*node.meshIndex]; + meshNode->scene = &file; + newNode = meshNode; } else { diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index ba94ade..c9c0d39 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -84,6 +84,9 @@ struct LoadedGLTF : public IRenderable float animationTime = 0.f; bool animationLoop = true; + // Optional debug name (e.g., key used when loaded into SceneManager) + std::string debugName; + // Animation helpers void updateAnimation(float dt); void refreshAllTransforms(); diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index fd58e2e..eb1e126 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -9,12 +9,187 @@ #include "core/config.h" #include "glm/gtx/transform.hpp" #include - #include "glm/gtx/norm.inl" #include "glm/gtx/compatibility.hpp" #include +#include +#include #include "core/config.h" +namespace +{ + // Quick conservative ray / bounding-sphere test in world space. + // Returns false when the ray misses the sphere; on hit, outT is the + // closest positive intersection distance along the ray direction. + bool intersect_ray_sphere(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + float &outT) + { + // Sphere center is bounds.origin transformed to world. + glm::vec3 centerWorld = glm::vec3(worldTransform * glm::vec4(bounds.origin, 1.0f)); + + // Approximate world-space radius by scaling with the maximum axis scale. + glm::vec3 sx = glm::vec3(worldTransform[0]); + glm::vec3 sy = glm::vec3(worldTransform[1]); + glm::vec3 sz = glm::vec3(worldTransform[2]); + float maxScale = std::max({glm::length(sx), glm::length(sy), glm::length(sz)}); + float radiusWorld = bounds.sphereRadius * maxScale; + if (radiusWorld <= 0.0f) + { + return false; + } + + glm::vec3 oc = rayOrigin - centerWorld; + float b = glm::dot(oc, rayDir); + float c = glm::dot(oc, oc) - radiusWorld * radiusWorld; + float disc = b * b - c; + if (disc < 0.0f) + { + return false; + } + float s = std::sqrt(disc); + float t0 = -b - s; + float t1 = -b + s; + float t = t0 >= 0.0f ? t0 : t1; + if (t < 0.0f) + { + return false; + } + outT = t; + return true; + } + + // Ray / oriented-bounds intersection in world space using object-local AABB. + // Uses a quick sphere test first; on success refines with OBB slabs. + // Returns true when hit; outWorldHit is the closest hit point in world space. + bool intersect_ray_bounds(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + glm::vec3 &outWorldHit) + { + if (glm::length2(rayDir) < 1e-8f) + { + return false; + } + + // Early reject using bounding sphere in world space. + float sphereT = 0.0f; + if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) + { + return false; + } + + // Transform ray into local space of the bounds for precise box test. + glm::mat4 invM = glm::inverse(worldTransform); + glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f)); + glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f)); + + if (glm::length2(localDir) < 1e-8f) + { + return false; + } + localDir = glm::normalize(localDir); + + glm::vec3 minB = bounds.origin - bounds.extents; + glm::vec3 maxB = bounds.origin + bounds.extents; + + float tMin = 0.0f; + float tMax = std::numeric_limits::max(); + + for (int axis = 0; axis < 3; ++axis) + { + float o = localOrigin[axis]; + float d = localDir[axis]; + if (std::abs(d) < 1e-8f) + { + // Ray parallel to slab: must be inside to intersect. + if (o < minB[axis] || o > maxB[axis]) + { + return false; + } + } + else + { + float invD = 1.0f / d; + float t1 = (minB[axis] - o) * invD; + float t2 = (maxB[axis] - o) * invD; + if (t1 > t2) + { + std::swap(t1, t2); + } + + tMin = std::max(tMin, t1); + tMax = std::min(tMax, t2); + + if (tMax < tMin) + { + return false; + } + } + } + + if (tMax < 0.0f) + { + return false; + } + + float tHit = (tMin >= 0.0f) ? tMin : tMax; + glm::vec3 localHit = localOrigin + tHit * localDir; + glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f)); + + if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f) + { + return false; + } + + outWorldHit = worldHit; + return true; + } + + // Test whether the clip-space box corners of an object intersect a 2D NDC rectangle. + // ndcMin/ndcMax are in [-1,1]x[-1,1]. Returns true if any visible corner projects inside. + bool box_overlaps_ndc_rect(const RenderObject &obj, + const glm::mat4 &viewproj, + const glm::vec2 &ndcMin, + const glm::vec2 &ndcMax) + { + const glm::vec3 o = obj.bounds.origin; + const glm::vec3 e = obj.bounds.extents; + const glm::mat4 m = viewproj * obj.transform; // world -> clip + + const std::array corners{ + glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1}, + glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1}, + }; + + for (const glm::vec3 &c : corners) + { + glm::vec3 pLocal = o + c * e; + glm::vec4 clip = m * glm::vec4(pLocal, 1.f); + if (clip.w <= 0.0f) + { + continue; + } + float x = clip.x / clip.w; + float y = clip.y / clip.w; + float z = clip.z / clip.w; // Vulkan Z0: 0..1 + if (z < 0.0f || z > 1.0f) + { + continue; + } + if (x >= ndcMin.x && x <= ndcMax.x && + y >= ndcMin.y && y <= ndcMax.y) + { + return true; + } + } + return false; + } +} // namespace + void SceneManager::init(EngineContext *context) { _context = context; @@ -35,6 +210,7 @@ void SceneManager::update_scene() mainDrawContext.OpaqueSurfaces.clear(); mainDrawContext.TransparentSurfaces.clear(); + mainDrawContext.nextID = 1; mainCamera.update(); @@ -102,6 +278,7 @@ void SceneManager::update_scene() { const MeshInstance &inst = kv.second; if (!inst.mesh || inst.mesh->surfaces.empty()) continue; + uint32_t surfaceIndex = 0; for (const auto &surf: inst.mesh->surfaces) { RenderObject obj{}; @@ -113,6 +290,9 @@ void SceneManager::update_scene() obj.material = &surf.material->data; obj.bounds = surf.bounds; obj.transform = inst.transform; + obj.sourceMesh = inst.mesh.get(); + obj.surfaceIndex = surfaceIndex++; + obj.objectID = mainDrawContext.nextID++; if (obj.material->passType == MaterialPass::Transparent) { mainDrawContext.TransparentSurfaces.push_back(obj); @@ -229,8 +409,165 @@ void SceneManager::update_scene() stats.scene_update_time = elapsed.count() / 1000.f; } +bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos) +{ + if (_context == nullptr) + { + return false; + } + + SwapchainManager *swapchain = _context->getSwapchain(); + if (swapchain == nullptr) + { + return false; + } + + VkExtent2D extent = swapchain->windowExtent(); + if (extent.width == 0 || extent.height == 0) + { + return false; + } + + float width = static_cast(extent.width); + float height = static_cast(extent.height); + + // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. + float ndcX = (2.0f * mousePosPixels.x / width) - 1.0f; + float ndcY = 1.0f - (2.0f * mousePosPixels.y / height); + + float fovRad = glm::radians(mainCamera.fovDegrees); + float tanHalfFov = std::tan(fovRad * 0.5f); + float aspect = width / height; + + // Build ray in camera space using -Z forward convention. + glm::vec3 dirCamera(ndcX * aspect * tanHalfFov, + ndcY * tanHalfFov, + -1.0f); + dirCamera = glm::normalize(dirCamera); + + glm::vec3 rayOrigin = mainCamera.position; + glm::mat4 camRotation = mainCamera.getRotationMatrix(); + glm::vec3 rayDir = glm::normalize(glm::vec3(camRotation * glm::vec4(dirCamera, 0.0f))); + + bool anyHit = false; + float bestDist2 = std::numeric_limits::max(); + glm::vec3 bestHitPos{}; + + auto testList = [&](const std::vector &list) + { + for (const RenderObject &obj: list) + { + glm::vec3 hitPos{}; + if (!intersect_ray_bounds(rayOrigin, rayDir, obj.bounds, obj.transform, hitPos)) + { + continue; + } + + float d2 = glm::length2(hitPos - rayOrigin); + if (d2 < bestDist2) + { + bestDist2 = d2; + bestHitPos = hitPos; + outObject = obj; + anyHit = true; + } + } + }; + + testList(mainDrawContext.OpaqueSurfaces); + testList(mainDrawContext.TransparentSurfaces); + + if (anyHit) + { + outWorldPos = bestHitPos; + } + + return anyHit; +} + +bool SceneManager::resolveObjectID(uint32_t id, RenderObject &outObject) const +{ + if (id == 0) + { + return false; + } + + auto findIn = [&](const std::vector &list) -> bool + { + for (const RenderObject &obj : list) + { + if (obj.objectID == id) + { + outObject = obj; + return true; + } + } + return false; + }; + + if (findIn(mainDrawContext.OpaqueSurfaces)) + { + return true; + } + if (findIn(mainDrawContext.TransparentSurfaces)) + { + return true; + } + return false; +} + +void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector &outObjects) const +{ + if (!_context || !_context->getSwapchain()) + { + return; + } + + VkExtent2D extent = _context->getSwapchain()->windowExtent(); + if (extent.width == 0 || extent.height == 0) + { + return; + } + + float width = static_cast(extent.width); + float height = static_cast(extent.height); + + // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. + auto toNdc = [&](const glm::vec2 &p) -> glm::vec2 + { + float ndcX = (2.0f * p.x / width) - 1.0f; + float ndcY = 1.0f - (2.0f * p.y / height); + return glm::vec2{ndcX, ndcY}; + }; + + glm::vec2 ndc0 = toNdc(p0); + glm::vec2 ndc1 = toNdc(p1); + glm::vec2 ndcMin = glm::min(ndc0, ndc1); + glm::vec2 ndcMax = glm::max(ndc0, ndc1); + + const glm::mat4 vp = sceneData.viewproj; + + auto testList = [&](const std::vector &list) + { + for (const RenderObject &obj : list) + { + if (box_overlaps_ndc_rect(obj, vp, ndcMin, ndcMax)) + { + outObjects.push_back(obj); + } + } + }; + + testList(mainDrawContext.OpaqueSurfaces); + testList(mainDrawContext.TransparentSurfaces); +} + void SceneManager::loadScene(const std::string &name, std::shared_ptr scene) { + if (scene) + { + scene->debugName = name; + } loadedScenes[name] = std::move(scene); } diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index ef768a7..f2b1301 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -3,12 +3,14 @@ #include #include #include +#include #include "scene/vk_loader.h" class EngineContext; struct RenderObject { + // Geometry and material binding uint32_t indexCount; uint32_t firstIndex; VkBuffer indexBuffer; @@ -19,12 +21,22 @@ struct RenderObject glm::mat4 transform; VkDeviceAddress vertexBufferAddress; + + // Optional debug/source information (may be null/unused for some objects). + MeshAsset *sourceMesh = nullptr; + uint32_t surfaceIndex = 0; + // Unique per-draw identifier for ID-buffer picking (0 = none). + uint32_t objectID = 0; + // Optional owning glTF scene for this draw (null for procedural/dynamic meshes). + LoadedGLTF *sourceScene = nullptr; }; struct DrawContext { std::vector OpaqueSurfaces; std::vector TransparentSurfaces; + // Monotonic counter used to assign stable per-frame object IDs. + uint32_t nextID = 1; }; class SceneManager @@ -37,6 +49,20 @@ public: void update_scene(); Camera &getMainCamera() { return mainCamera; } + + // Ray-pick against current DrawContext using per-surface Bounds. + // mousePosPixels is in window coordinates (SDL), origin at top-left. + // Returns true if any object was hit, filling outObject and outWorldPos. + bool pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos); + + // Resolve an object ID (from ID buffer) back to the RenderObject for + // the most recently built DrawContext. Returns false if not found or id==0. + bool resolveObjectID(uint32_t id, RenderObject &outObject) const; + + // Select all objects whose projected bounds intersect the given screen-space + // rectangle (window coordinates, origin top-left). Results are appended to outObjects. + void selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector &outObjects) const; + const GPUSceneData &getSceneData() const { return sceneData; } DrawContext &getMainDrawContext() { return mainDrawContext; } diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 2245812..69b413a 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -49,6 +49,26 @@ target_sources(imgui PRIVATE target_link_libraries(imgui PUBLIC Vulkan::Vulkan SDL2::SDL2) +add_library(ImGuizmo STATIC + ImGuizmo/ImGuizmo.cpp + ImGuizmo/ImSequencer.cpp + ImGuizmo/ImCurveEdit.cpp + ImGuizmo/ImGradient.cpp + ImGuizmo/GraphEditor.cpp +) + +target_include_directories(ImGuizmo PUBLIC + ImGuizmo + imgui +) + +target_compile_definitions(ImGuizmo + PRIVATE + IMGUI_DEFINE_MATH_OPERATORS +) + +target_link_libraries(ImGuizmo PUBLIC imgui) + target_include_directories(stb_image INTERFACE stb_image) # MikkTSpace (optional) diff --git a/third_party/ImGuizmo/.editorconfig b/third_party/ImGuizmo/.editorconfig new file mode 100644 index 0000000..69cb70f --- /dev/null +++ b/third_party/ImGuizmo/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 3 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/third_party/ImGuizmo/GraphEditor.cpp b/third_party/ImGuizmo/GraphEditor.cpp new file mode 100644 index 0000000..a97f813 --- /dev/null +++ b/third_party/ImGuizmo/GraphEditor.cpp @@ -0,0 +1,1111 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" +#include "imgui_internal.h" +#include +#include +#include +#include +#include "GraphEditor.h" + +namespace GraphEditor { + +static inline float Distance(const ImVec2& a, const ImVec2& b) +{ + return sqrtf((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); +} + +static inline float sign(float v) +{ + return (v >= 0.f) ? 1.f : -1.f; +} + +static ImVec2 GetInputSlotPos(Delegate& delegate, const Node& node, SlotIndex slotIndex, float factor) +{ + ImVec2 Size = node.mRect.GetSize() * factor; + size_t InputsCount = delegate.GetTemplate(node.mTemplateIndex).mInputCount; + return ImVec2(node.mRect.Min.x * factor, + node.mRect.Min.y * factor + Size.y * ((float)slotIndex + 1) / ((float)InputsCount + 1) + 8.f); +} + +static ImVec2 GetOutputSlotPos(Delegate& delegate, const Node& node, SlotIndex slotIndex, float factor) +{ + ImVec2 Size = node.mRect.GetSize() * factor; + size_t OutputsCount = delegate.GetTemplate(node.mTemplateIndex).mOutputCount; + return ImVec2(node.mRect.Min.x * factor + Size.x, + node.mRect.Min.y * factor + Size.y * ((float)slotIndex + 1) / ((float)OutputsCount + 1) + 8.f); +} + +static ImRect GetNodeRect(const Node& node, float factor) +{ + ImVec2 Size = node.mRect.GetSize() * factor; + return ImRect(node.mRect.Min * factor, node.mRect.Min * factor + Size); +} + +static ImVec2 editingNodeSource; +static bool editingInput = false; +static ImVec2 captureOffset; + +enum NodeOperation +{ + NO_None, + NO_EditingLink, + NO_QuadSelecting, + NO_MovingNodes, + NO_EditInput, + NO_PanView, +}; +static NodeOperation nodeOperation = NO_None; + +static void HandleZoomScroll(ImRect regionRect, ViewState& viewState, const Options& options) +{ + ImGuiIO& io = ImGui::GetIO(); + + if (regionRect.Contains(io.MousePos)) + { + if (io.MouseWheel < -FLT_EPSILON) + { + viewState.mFactorTarget *= 1.f - options.mZoomRatio; + } + + if (io.MouseWheel > FLT_EPSILON) + { + viewState.mFactorTarget *= 1.0f + options.mZoomRatio; + } + } + + ImVec2 mouseWPosPre = (io.MousePos - ImGui::GetCursorScreenPos()) / viewState.mFactor; + viewState.mFactorTarget = ImClamp(viewState.mFactorTarget, options.mMinZoom, options.mMaxZoom); + viewState.mFactor = ImLerp(viewState.mFactor, viewState.mFactorTarget, options.mZoomLerpFactor); + ImVec2 mouseWPosPost = (io.MousePos - ImGui::GetCursorScreenPos()) / viewState.mFactor; + if (ImGui::IsMousePosValid()) + { + viewState.mPosition += mouseWPosPost - mouseWPosPre; + } +} + +void GraphEditorClear() +{ + nodeOperation = NO_None; +} + +static void FitNodes(Delegate& delegate, ViewState& viewState, const ImVec2 viewSize, bool selectedNodesOnly) +{ + const size_t nodeCount = delegate.GetNodeCount(); + + if (!nodeCount) + { + return; + } + + bool validNode = false; + ImVec2 min(FLT_MAX, FLT_MAX); + ImVec2 max(-FLT_MAX, -FLT_MAX); + for (NodeIndex nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + const Node& node = delegate.GetNode(nodeIndex); + + if (selectedNodesOnly && !node.mSelected) + { + continue; + } + + min = ImMin(min, node.mRect.Min); + min = ImMin(min, node.mRect.Max); + max = ImMax(max, node.mRect.Min); + max = ImMax(max, node.mRect.Max); + validNode = true; + } + + if (!validNode) + { + return; + } + + min -= viewSize * 0.05f; + max += viewSize * 0.05f; + ImVec2 nodesSize = max - min; + ImVec2 nodeCenter = (max + min) * 0.5f; + + float ratioY = viewSize.y / nodesSize.y; + float ratioX = viewSize.x / nodesSize.x; + + viewState.mFactor = viewState.mFactorTarget = ImMin(ImMin(ratioY, ratioX), 1.f); + viewState.mPosition = ImVec2(-nodeCenter.x, -nodeCenter.y) + (viewSize * 0.5f) / viewState.mFactorTarget; +} + +static void DisplayLinks(Delegate& delegate, + ImDrawList* drawList, + const ImVec2 offset, + const float factor, + const ImRect regionRect, + NodeIndex hoveredNode, + const Options& options) +{ + const size_t linkCount = delegate.GetLinkCount(); + for (LinkIndex linkIndex = 0; linkIndex < linkCount; linkIndex++) + { + const auto link = delegate.GetLink(linkIndex); + const auto nodeInput = delegate.GetNode(link.mInputNodeIndex); + const auto nodeOutput = delegate.GetNode(link.mOutputNodeIndex); + ImVec2 p1 = offset + GetOutputSlotPos(delegate, nodeInput, link.mInputSlotIndex, factor); + ImVec2 p2 = offset + GetInputSlotPos(delegate, nodeOutput, link.mOutputSlotIndex, factor); + + // con. view clipping + if ((p1.y < 0.f && p2.y < 0.f) || (p1.y > regionRect.Max.y && p2.y > regionRect.Max.y) || + (p1.x < 0.f && p2.x < 0.f) || (p1.x > regionRect.Max.x && p2.x > regionRect.Max.x)) + continue; + + bool highlightCons = hoveredNode == link.mInputNodeIndex || hoveredNode == link.mOutputNodeIndex; + uint32_t col = delegate.GetTemplate(nodeInput.mTemplateIndex).mHeaderColor | (highlightCons ? 0xF0F0F0 : 0); + if (options.mDisplayLinksAsCurves) + { + // curves + drawList->AddBezierCubic(p1, p1 + ImVec2(50, 0) * factor, p2 + ImVec2(-50, 0) * factor, p2, 0xFF000000, options.mLineThickness * 1.5f * factor); + drawList->AddBezierCubic(p1, p1 + ImVec2(50, 0) * factor, p2 + ImVec2(-50, 0) * factor, p2, col, options.mLineThickness * 1.5f * factor); + /* + ImVec2 p10 = p1 + ImVec2(20.f * factor, 0.f); + ImVec2 p20 = p2 - ImVec2(20.f * factor, 0.f); + + ImVec2 dif = p20 - p10; + ImVec2 p1a, p1b; + if (fabsf(dif.x) > fabsf(dif.y)) + { + p1a = p10 + ImVec2(fabsf(fabsf(dif.x) - fabsf(dif.y)) * 0.5 * sign(dif.x), 0.f); + p1b = p1a + ImVec2(fabsf(dif.y) * sign(dif.x) , dif.y); + } + else + { + p1a = p10 + ImVec2(0.f, fabsf(fabsf(dif.y) - fabsf(dif.x)) * 0.5 * sign(dif.y)); + p1b = p1a + ImVec2(dif.x, fabsf(dif.x) * sign(dif.y)); + } + drawList->AddLine(p1, p10, col, 3.f * factor); + drawList->AddLine(p10, p1a, col, 3.f * factor); + drawList->AddLine(p1a, p1b, col, 3.f * factor); + drawList->AddLine(p1b, p20, col, 3.f * factor); + drawList->AddLine(p20, p2, col, 3.f * factor); + */ + } + else + { + // straight lines + std::array pts; + int ptCount = 0; + ImVec2 dif = p2 - p1; + + ImVec2 p1a, p1b; + const float limitx = 12.f * factor; + if (dif.x < limitx) + { + ImVec2 p10 = p1 + ImVec2(limitx, 0.f); + ImVec2 p20 = p2 - ImVec2(limitx, 0.f); + + dif = p20 - p10; + p1a = p10 + ImVec2(0.f, dif.y * 0.5f); + p1b = p1a + ImVec2(dif.x, 0.f); + + pts = { p1, p10, p1a, p1b, p20, p2 }; + ptCount = 6; + } + else + { + if (fabsf(dif.y) < 1.f) + { + pts = { p1, (p1 + p2) * 0.5f, p2 }; + ptCount = 3; + } + else + { + if (fabsf(dif.y) < 10.f) + { + if (fabsf(dif.x) > fabsf(dif.y)) + { + p1a = p1 + ImVec2(fabsf(fabsf(dif.x) - fabsf(dif.y)) * 0.5f * sign(dif.x), 0.f); + p1b = p1a + ImVec2(fabsf(dif.y) * sign(dif.x), dif.y); + } + else + { + p1a = p1 + ImVec2(0.f, fabsf(fabsf(dif.y) - fabsf(dif.x)) * 0.5f * sign(dif.y)); + p1b = p1a + ImVec2(dif.x, fabsf(dif.x) * sign(dif.y)); + } + } + else + { + if (fabsf(dif.x) > fabsf(dif.y)) + { + float d = fabsf(dif.y) * sign(dif.x) * 0.5f; + p1a = p1 + ImVec2(d, dif.y * 0.5f); + p1b = p1a + ImVec2(fabsf(fabsf(dif.x) - fabsf(d) * 2.f) * sign(dif.x), 0.f); + } + else + { + float d = fabsf(dif.x) * sign(dif.y) * 0.5f; + p1a = p1 + ImVec2(dif.x * 0.5f, d); + p1b = p1a + ImVec2(0.f, fabsf(fabsf(dif.y) - fabsf(d) * 2.f) * sign(dif.y)); + } + } + pts = { p1, p1a, p1b, p2 }; + ptCount = 4; + } + } + float highLightFactor = factor * (highlightCons ? 2.0f : 1.f); + for (int pass = 0; pass < 2; pass++) + { + drawList->AddPolyline(pts.data(), ptCount, pass ? col : 0xFF000000, false, (pass ? options.mLineThickness : (options.mLineThickness * 1.5f)) * highLightFactor); + } + } + } +} + +static void HandleQuadSelection(Delegate& delegate, ImDrawList* drawList, const ImVec2 offset, const float factor, ImRect contentRect, const Options& options) +{ + if (!options.mAllowQuadSelection) + { + return; + } + ImGuiIO& io = ImGui::GetIO(); + static ImVec2 quadSelectPos; + //auto& nodes = delegate->GetNodes(); + auto nodeCount = delegate.GetNodeCount(); + + if (nodeOperation == NO_QuadSelecting && ImGui::IsWindowFocused()) + { + const ImVec2 bmin = ImMin(quadSelectPos, io.MousePos); + const ImVec2 bmax = ImMax(quadSelectPos, io.MousePos); + drawList->AddRectFilled(bmin, bmax, options.mQuadSelection, 1.f); + drawList->AddRect(bmin, bmax, options.mQuadSelectionBorder, 1.f); + if (!io.MouseDown[0]) + { + if (!io.KeyCtrl && !io.KeyShift) + { + for (size_t nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + delegate.SelectNode(nodeIndex, false); + } + } + + nodeOperation = NO_None; + ImRect selectionRect(bmin, bmax); + for (unsigned int nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + const auto node = delegate.GetNode(nodeIndex); + ImVec2 nodeRectangleMin = offset + node.mRect.Min * factor; + ImVec2 nodeRectangleMax = nodeRectangleMin + node.mRect.GetSize() * factor; + if (selectionRect.Overlaps(ImRect(nodeRectangleMin, nodeRectangleMax))) + { + if (io.KeyCtrl) + { + delegate.SelectNode(nodeIndex, false); + } + else + { + delegate.SelectNode(nodeIndex, true); + } + } + else + { + if (!io.KeyShift) + { + delegate.SelectNode(nodeIndex, false); + } + } + } + } + } + else if (nodeOperation == NO_None && io.MouseDown[0] && ImGui::IsWindowFocused() && + contentRect.Contains(io.MousePos)) + { + nodeOperation = NO_QuadSelecting; + quadSelectPos = io.MousePos; + } +} + +static bool HandleConnections(ImDrawList* drawList, + NodeIndex nodeIndex, + const ImVec2 offset, + const float factor, + Delegate& delegate, + const Options& options, + bool bDrawOnly, + SlotIndex& inputSlotOver, + SlotIndex& outputSlotOver, + const bool inMinimap) +{ + static NodeIndex editingNodeIndex; + static SlotIndex editingSlotIndex; + + ImGuiIO& io = ImGui::GetIO(); + const auto node = delegate.GetNode(nodeIndex); + const auto nodeTemplate = delegate.GetTemplate(node.mTemplateIndex); + const auto linkCount = delegate.GetLinkCount(); + + size_t InputsCount = nodeTemplate.mInputCount; + size_t OutputsCount = nodeTemplate.mOutputCount; + inputSlotOver = -1; + outputSlotOver = -1; + + // draw/use inputs/outputs + bool hoverSlot = false; + for (int i = 0; i < 2; i++) + { + float closestDistance = FLT_MAX; + SlotIndex closestConn = -1; + ImVec2 closestTextPos; + ImVec2 closestPos; + const size_t slotCount[2] = {InputsCount, OutputsCount}; + + for (SlotIndex slotIndex = 0; slotIndex < slotCount[i]; slotIndex++) + { + const char** con = i ? nodeTemplate.mOutputNames : nodeTemplate.mInputNames; + const char* conText = (con && con[slotIndex]) ? con[slotIndex] : ""; + + ImVec2 p = + offset + (i ? GetOutputSlotPos(delegate, node, slotIndex, factor) : GetInputSlotPos(delegate, node, slotIndex, factor)); + float distance = Distance(p, io.MousePos); + bool overCon = (nodeOperation == NO_None || nodeOperation == NO_EditingLink) && + (distance < options.mNodeSlotRadius * 2.f) && (distance < closestDistance); + + + ImVec2 textSize; + textSize = ImGui::CalcTextSize(conText); + ImVec2 textPos = + p + ImVec2(-options.mNodeSlotRadius * (i ? -1.f : 1.f) * (overCon ? 3.f : 2.f) - (i ? 0 : textSize.x), + -textSize.y / 2); + + ImRect nodeRect = GetNodeRect(node, factor); + if (!inMinimap && (overCon || (nodeRect.Contains(io.MousePos - offset) && closestConn == -1 && + (editingInput == (i != 0)) && nodeOperation == NO_EditingLink))) + { + closestDistance = distance; + closestConn = slotIndex; + closestTextPos = textPos; + closestPos = p; + + if (i) + { + outputSlotOver = slotIndex; + } + else + { + inputSlotOver = slotIndex; + } + } + else + { + const ImU32* slotColorSource = i ? nodeTemplate.mOutputColors : nodeTemplate.mInputColors; + const ImU32 slotColor = slotColorSource ? slotColorSource[slotIndex] : options.mDefaultSlotColor; + drawList->AddCircleFilled(p, options.mNodeSlotRadius, IM_COL32(0, 0, 0, 200)); + drawList->AddCircleFilled(p, options.mNodeSlotRadius * 0.75f, slotColor); + if (!options.mDrawIONameOnHover) + { + drawList->AddText(io.FontDefault, 14, textPos + ImVec2(2, 2), IM_COL32(0, 0, 0, 255), conText); + drawList->AddText(io.FontDefault, 14, textPos, IM_COL32(150, 150, 150, 255), conText); + } + } + } + + if (closestConn != -1) + { + const char** con = i ? nodeTemplate.mOutputNames : nodeTemplate.mInputNames; + const char* conText = (con && con[closestConn]) ? con[closestConn] : ""; + const ImU32* slotColorSource = i ? nodeTemplate.mOutputColors : nodeTemplate.mInputColors; + const ImU32 slotColor = slotColorSource ? slotColorSource[closestConn] : options.mDefaultSlotColor; + hoverSlot = true; + drawList->AddCircleFilled(closestPos, options.mNodeSlotRadius * options.mNodeSlotHoverFactor * 0.75f, IM_COL32(0, 0, 0, 200)); + drawList->AddCircleFilled(closestPos, options.mNodeSlotRadius * options.mNodeSlotHoverFactor, slotColor); + drawList->AddText(io.FontDefault, 16, closestTextPos + ImVec2(1, 1), IM_COL32(0, 0, 0, 255), conText); + drawList->AddText(io.FontDefault, 16, closestTextPos, IM_COL32(250, 250, 250, 255), conText); + bool inputToOutput = (!editingInput && !i) || (editingInput && i); + if (nodeOperation == NO_EditingLink && !io.MouseDown[0] && !bDrawOnly) + { + if (inputToOutput) + { + // check loopback + Link nl; + if (editingInput) + nl = Link{nodeIndex, closestConn, editingNodeIndex, editingSlotIndex}; + else + nl = Link{editingNodeIndex, editingSlotIndex, nodeIndex, closestConn}; + + if (!delegate.AllowedLink(nl.mOutputNodeIndex, nl.mInputNodeIndex)) + { + break; + } + bool alreadyExisting = false; + for (size_t linkIndex = 0; linkIndex < linkCount; linkIndex++) + { + const auto link = delegate.GetLink(linkIndex); + if (!memcmp(&link, &nl, sizeof(Link))) + { + alreadyExisting = true; + break; + } + } + + if (!alreadyExisting) + { + for (unsigned int linkIndex = 0; linkIndex < linkCount; linkIndex++) + { + const auto link = delegate.GetLink(linkIndex); + if (link.mOutputNodeIndex == nl.mOutputNodeIndex && link.mOutputSlotIndex == nl.mOutputSlotIndex) + { + delegate.DelLink(linkIndex); + + break; + } + } + + delegate.AddLink(nl.mInputNodeIndex, nl.mInputSlotIndex, nl.mOutputNodeIndex, nl.mOutputSlotIndex); + } + } + } + // when ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() is uncommented, one can't click the node + // input/output when mouse is over the node itself. + if (nodeOperation == NO_None && + /*ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() &&*/ io.MouseClicked[0] && !bDrawOnly) + { + nodeOperation = NO_EditingLink; + editingInput = i == 0; + editingNodeSource = closestPos; + editingNodeIndex = nodeIndex; + editingSlotIndex = closestConn; + if (editingInput) + { + // remove existing link + for (unsigned int linkIndex = 0; linkIndex < linkCount; linkIndex++) + { + const auto link = delegate.GetLink(linkIndex); + if (link.mOutputNodeIndex == nodeIndex && link.mOutputSlotIndex == closestConn) + { + delegate.DelLink(linkIndex); + break; + } + } + } + } + } + } + return hoverSlot; +} + +static void DrawGrid(ImDrawList* drawList, ImVec2 windowPos, const ViewState& viewState, const ImVec2 canvasSize, ImU32 gridColor, ImU32 gridColor2, float gridSize) +{ + float gridSpace = gridSize * viewState.mFactor; + int divx = static_cast(-viewState.mPosition.x / gridSize); + int divy = static_cast(-viewState.mPosition.y / gridSize); + for (float x = fmodf(viewState.mPosition.x * viewState.mFactor, gridSpace); x < canvasSize.x; x += gridSpace, divx ++) + { + bool tenth = !(divx % 10); + drawList->AddLine(ImVec2(x, 0.0f) + windowPos, ImVec2(x, canvasSize.y) + windowPos, tenth ? gridColor2 : gridColor); + } + for (float y = fmodf(viewState.mPosition.y * viewState.mFactor, gridSpace); y < canvasSize.y; y += gridSpace, divy ++) + { + bool tenth = !(divy % 10); + drawList->AddLine(ImVec2(0.0f, y) + windowPos, ImVec2(canvasSize.x, y) + windowPos, tenth ? gridColor2 : gridColor); + } +} + +// return true if node is hovered +static bool DrawNode(ImDrawList* drawList, + NodeIndex nodeIndex, + const ImVec2 offset, + const float factor, + Delegate& delegate, + bool overInput, + const Options& options, + const bool inMinimap, + const ImRect& viewPort) +{ + ImGuiIO& io = ImGui::GetIO(); + const auto node = delegate.GetNode(nodeIndex); + IM_ASSERT((node.mRect.GetWidth() != 0.f) && (node.mRect.GetHeight() != 0.f) && "Nodes must have a non-zero rect."); + const auto nodeTemplate = delegate.GetTemplate(node.mTemplateIndex); + const ImVec2 nodeRectangleMin = offset + node.mRect.Min * factor; + + const bool old_any_active = ImGui::IsAnyItemActive(); + ImGui::SetCursorScreenPos(nodeRectangleMin); + const ImVec2 nodeSize = node.mRect.GetSize() * factor; + + // test nested IO + drawList->ChannelsSetCurrent(1); // Background + const size_t InputsCount = nodeTemplate.mInputCount; + const size_t OutputsCount = nodeTemplate.mOutputCount; + + /* + for (int i = 0; i < 2; i++) + { + const size_t slotCount[2] = {InputsCount, OutputsCount}; + + for (size_t slotIndex = 0; slotIndex < slotCount[i]; slotIndex++) + { + const char* con = i ? nodeTemplate.mOutputNames[slotIndex] : nodeTemplate.mInputNames[slotIndex];//node.mOutputs[slot_idx] : node->mInputs[slot_idx]; + if (!delegate->IsIOPinned(nodeIndex, slot_idx, i == 1)) + { + + } + continue; + + ImVec2 p = offset + (i ? GetOutputSlotPos(delegate, node, slotIndex, factor) : GetInputSlotPos(delegate, node, slotIndex, factor)); + const float arc = 28.f * (float(i) * 0.3f + 1.0f) * (i ? 1.f : -1.f); + const float ofs = 0.f; + + ImVec2 pts[3] = {p + ImVec2(arc + ofs, 0.f), p + ImVec2(0.f + ofs, -arc), p + ImVec2(0.f + ofs, arc)}; + drawList->AddTriangleFilled(pts[0], pts[1], pts[2], i ? 0xFFAA5030 : 0xFF30AA50); + drawList->AddTriangle(pts[0], pts[1], pts[2], 0xFF000000, 2.f); + } + } + */ + + ImGui::SetCursorScreenPos(nodeRectangleMin); + float maxHeight = ImMin(viewPort.Max.y, nodeRectangleMin.y + nodeSize.y) - nodeRectangleMin.y; + float maxWidth = ImMin(viewPort.Max.x, nodeRectangleMin.x + nodeSize.x) - nodeRectangleMin.x; + ImGui::InvisibleButton("node", ImVec2(maxWidth, maxHeight)); + // must be called right after creating the control we want to be able to move + bool nodeMovingActive = ImGui::IsItemActive(); + + // Save the size of what we have emitted and whether any of the widgets are being used + bool nodeWidgetsActive = (!old_any_active && ImGui::IsAnyItemActive()); + ImVec2 nodeRectangleMax = nodeRectangleMin + nodeSize; + + bool nodeHovered = false; + if (ImGui::IsItemHovered() && nodeOperation == NO_None && !overInput) + { + nodeHovered = true; + } + + if (ImGui::IsWindowFocused()) + { + if ((nodeWidgetsActive || nodeMovingActive) && !inMinimap) + { + if (!node.mSelected) + { + if (!io.KeyShift) + { + const auto nodeCount = delegate.GetNodeCount(); + for (size_t i = 0; i < nodeCount; i++) + { + delegate.SelectNode(i, false); + } + } + delegate.SelectNode(nodeIndex, true); + } + } + } + if (nodeMovingActive && io.MouseDown[0] && nodeHovered && !inMinimap) + { + if (nodeOperation != NO_MovingNodes) + { + nodeOperation = NO_MovingNodes; + } + } + + const bool currentSelectedNode = node.mSelected; + const ImU32 node_bg_color = nodeHovered ? nodeTemplate.mBackgroundColorOver : nodeTemplate.mBackgroundColor; + + drawList->AddRect(nodeRectangleMin, + nodeRectangleMax, + currentSelectedNode ? options.mSelectedNodeBorderColor : options.mNodeBorderColor, + options.mRounding, + ImDrawFlags_RoundCornersAll, + currentSelectedNode ? options.mBorderSelectionThickness : options.mBorderThickness); + + ImVec2 imgPos = nodeRectangleMin + ImVec2(14, 25); + ImVec2 imgSize = nodeRectangleMax + ImVec2(-5, -5) - imgPos; + float imgSizeComp = std::min(imgSize.x, imgSize.y); + + drawList->AddRectFilled(nodeRectangleMin, nodeRectangleMax, node_bg_color, options.mRounding); + /*float progress = delegate->NodeProgress(nodeIndex); + if (progress > FLT_EPSILON && progress < 1.f - FLT_EPSILON) + { + ImVec2 progressLineA = nodeRectangleMax - ImVec2(nodeSize.x - 2.f, 3.f); + ImVec2 progressLineB = progressLineA + ImVec2(nodeSize.x * factor - 4.f, 0.f); + drawList->AddLine(progressLineA, progressLineB, 0xFF400000, 3.f); + drawList->AddLine(progressLineA, ImLerp(progressLineA, progressLineB, progress), 0xFFFF0000, 3.f); + }*/ + ImVec2 imgPosMax = imgPos + ImVec2(imgSizeComp, imgSizeComp); + + //ImVec2 imageSize = delegate->GetEvaluationSize(nodeIndex); + /*float imageRatio = 1.f; + if (imageSize.x > 0.f && imageSize.y > 0.f) + { + imageRatio = imageSize.y / imageSize.x; + } + ImVec2 quadSize = imgPosMax - imgPos; + ImVec2 marge(0.f, 0.f); + if (imageRatio > 1.f) + { + marge.x = (quadSize.x - quadSize.y / imageRatio) * 0.5f; + } + else + { + marge.y = (quadSize.y - quadSize.y * imageRatio) * 0.5f; + }*/ + + //delegate->DrawNodeImage(drawList, ImRect(imgPos, imgPosMax), marge, nodeIndex); + + drawList->AddRectFilled(nodeRectangleMin, + ImVec2(nodeRectangleMax.x, nodeRectangleMin.y + 20), + nodeTemplate.mHeaderColor, options.mRounding); + + drawList->PushClipRect(nodeRectangleMin, ImVec2(nodeRectangleMax.x, nodeRectangleMin.y + 20), true); + drawList->AddText(nodeRectangleMin + ImVec2(2, 2), IM_COL32(0, 0, 0, 255), node.mName); + drawList->PopClipRect(); + + ImRect customDrawRect(nodeRectangleMin + ImVec2(options.mRounding, 20 + options.mRounding), nodeRectangleMax - ImVec2(options.mRounding, options.mRounding)); + if (customDrawRect.Max.y > customDrawRect.Min.y && customDrawRect.Max.x > customDrawRect.Min.x) + { + delegate.CustomDraw(drawList, customDrawRect, nodeIndex); + } +/* + const ImTextureID bmpInfo = (ImTextureID)(uint64_t)delegate->GetBitmapInfo(nodeIndex).idx; + if (bmpInfo) + { + ImVec2 bmpInfoPos(nodeRectangleMax - ImVec2(26, 12)); + ImVec2 bmpInfoSize(20, 20); + if (delegate->NodeIsCompute(nodeIndex)) + { + drawList->AddImageQuad(bmpInfo, + bmpInfoPos, + bmpInfoPos + ImVec2(bmpInfoSize.x, 0.f), + bmpInfoPos + bmpInfoSize, + bmpInfoPos + ImVec2(0., bmpInfoSize.y)); + } + else if (delegate->NodeIs2D(nodeIndex)) + { + drawList->AddImageQuad(bmpInfo, + bmpInfoPos, + bmpInfoPos + ImVec2(bmpInfoSize.x, 0.f), + bmpInfoPos + bmpInfoSize, + bmpInfoPos + ImVec2(0., bmpInfoSize.y)); + } + else if (delegate->NodeIsCubemap(nodeIndex)) + { + drawList->AddImageQuad(bmpInfo, + bmpInfoPos + ImVec2(0., bmpInfoSize.y), + bmpInfoPos + bmpInfoSize, + bmpInfoPos + ImVec2(bmpInfoSize.x, 0.f), + bmpInfoPos); + } + }*/ + return nodeHovered; +} + +bool DrawMiniMap(ImDrawList* drawList, Delegate& delegate, ViewState& viewState, const Options& options, const ImVec2 windowPos, const ImVec2 canvasSize) +{ + if (Distance(options.mMinimap.Min, options.mMinimap.Max) <= FLT_EPSILON) + { + return false; + } + + const size_t nodeCount = delegate.GetNodeCount(); + + if (!nodeCount) + { + return false; + } + + ImVec2 min(FLT_MAX, FLT_MAX); + ImVec2 max(-FLT_MAX, -FLT_MAX); + const ImVec2 margin(50, 50); + for (NodeIndex nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + const Node& node = delegate.GetNode(nodeIndex); + min = ImMin(min, node.mRect.Min - margin); + min = ImMin(min, node.mRect.Max + margin); + max = ImMax(max, node.mRect.Min - margin); + max = ImMax(max, node.mRect.Max + margin); + } + + // add view in world space + const ImVec2 worldSizeView = canvasSize / viewState.mFactor; + const ImVec2 viewMin(-viewState.mPosition.x, -viewState.mPosition.y); + const ImVec2 viewMax = viewMin + worldSizeView; + min = ImMin(min, viewMin); + max = ImMax(max, viewMax); + const ImVec2 nodesSize = max - min; + const ImVec2 middleWorld = (min + max) * 0.5f; + const ImVec2 minScreen = windowPos + options.mMinimap.Min * canvasSize; + const ImVec2 maxScreen = windowPos + options.mMinimap.Max * canvasSize; + const ImVec2 viewSize = maxScreen - minScreen; + const ImVec2 middleScreen = (minScreen + maxScreen) * 0.5f; + const float ratioY = viewSize.y / nodesSize.y; + const float ratioX = viewSize.x / nodesSize.x; + const float factor = ImMin(ImMin(ratioY, ratioX), 1.f); + + drawList->AddRectFilled(minScreen, maxScreen, IM_COL32(30, 30, 30, 200), 3, ImDrawFlags_RoundCornersAll); + + for (NodeIndex nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + const Node& node = delegate.GetNode(nodeIndex); + const auto nodeTemplate = delegate.GetTemplate(node.mTemplateIndex); + + ImRect rect = node.mRect; + rect.Min -= middleWorld; + rect.Min *= factor; + rect.Min += middleScreen; + + rect.Max -= middleWorld; + rect.Max *= factor; + rect.Max += middleScreen; + + drawList->AddRectFilled(rect.Min, rect.Max, nodeTemplate.mBackgroundColor, 1, ImDrawFlags_RoundCornersAll); + if (node.mSelected) + { + drawList->AddRect(rect.Min, rect.Max, options.mSelectedNodeBorderColor, 1, ImDrawFlags_RoundCornersAll); + } + } + + // add view + ImVec2 viewMinScreen = (viewMin - middleWorld) * factor + middleScreen; + ImVec2 viewMaxScreen = (viewMax - middleWorld) * factor + middleScreen; + drawList->AddRectFilled(viewMinScreen, viewMaxScreen, IM_COL32(255, 255, 255, 32), 1, ImDrawFlags_RoundCornersAll); + drawList->AddRect(viewMinScreen, viewMaxScreen, IM_COL32(255, 255, 255, 128), 1, ImDrawFlags_RoundCornersAll); + + ImGuiIO& io = ImGui::GetIO(); + const bool mouseInMinimap = ImRect(minScreen, maxScreen).Contains(io.MousePos); + if (mouseInMinimap && io.MouseClicked[0]) + { + const ImVec2 clickedRatio = (io.MousePos - minScreen) / viewSize; + const ImVec2 worldPosCenter = ImVec2(ImLerp(min.x, max.x, clickedRatio.x), ImLerp(min.y, max.y, clickedRatio.y)); + + ImVec2 worldPosViewMin = worldPosCenter - worldSizeView * 0.5; + ImVec2 worldPosViewMax = worldPosCenter + worldSizeView * 0.5; + if (worldPosViewMin.x < min.x) + { + worldPosViewMin.x = min.x; + worldPosViewMax.x = worldPosViewMin.x + worldSizeView.x; + } + if (worldPosViewMin.y < min.y) + { + worldPosViewMin.y = min.y; + worldPosViewMax.y = worldPosViewMin.y + worldSizeView.y; + } + if (worldPosViewMax.x > max.x) + { + worldPosViewMax.x = max.x; + worldPosViewMin.x = worldPosViewMax.x - worldSizeView.x; + } + if (worldPosViewMax.y > max.y) + { + worldPosViewMax.y = max.y; + worldPosViewMin.y = worldPosViewMax.y - worldSizeView.y; + } + viewState.mPosition = ImVec2(-worldPosViewMin.x, -worldPosViewMin.y); + } + return mouseInMinimap; +} + +void Show(Delegate& delegate, const Options& options, ViewState& viewState, bool enabled, FitOnScreen* fit) +{ + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + + const ImVec2 windowPos = ImGui::GetCursorScreenPos(); + const ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + const ImVec2 scrollRegionLocalPos(0, 0); + + ImRect regionRect(windowPos, windowPos + canvasSize); + + HandleZoomScroll(regionRect, viewState, options); + ImVec2 offset = ImGui::GetCursorScreenPos() + viewState.mPosition * viewState.mFactor; + captureOffset = viewState.mPosition * viewState.mFactor; + + //ImGui::InvisibleButton("GraphEditorButton", canvasSize); + ImGui::BeginChild(71711, canvasSize, ImGuiChildFlags_FrameStyle); + + ImGui::SetCursorPos(windowPos); + ImGui::BeginGroup(); + + ImGuiIO& io = ImGui::GetIO(); + + // Create our child canvas + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(30, 30, 30, 200)); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImGui::PushClipRect(regionRect.Min, regionRect.Max, true); + drawList->AddRectFilled(windowPos, windowPos + canvasSize, options.mBackgroundColor); + + // Background or Display grid + if (options.mRenderGrid) + { + DrawGrid(drawList, windowPos, viewState, canvasSize, options.mGridColor, options.mGridColor2, options.mGridSize); + } + + // Fit view + if (fit && ((*fit == Fit_AllNodes) || (*fit == Fit_SelectedNodes))) + { + FitNodes(delegate, viewState, canvasSize, (*fit == Fit_SelectedNodes)); + } + + if (enabled) + { + static NodeIndex hoveredNode = -1; + // Display links + drawList->ChannelsSplit(3); + + // minimap + drawList->ChannelsSetCurrent(2); // minimap + const bool inMinimap = DrawMiniMap(drawList, delegate, viewState, options, windowPos, canvasSize); + + // Focus rectangle + if (ImGui::IsWindowFocused()) + { + drawList->AddRect(regionRect.Min, regionRect.Max, options.mFrameFocus, 1.f, 0, 2.f); + } + + drawList->ChannelsSetCurrent(1); // Background + + // Links + DisplayLinks(delegate, drawList, offset, viewState.mFactor, regionRect, hoveredNode, options); + + // edit node link + if (nodeOperation == NO_EditingLink) + { + ImVec2 p1 = editingNodeSource; + ImVec2 p2 = io.MousePos; + drawList->AddLine(p1, p2, IM_COL32(200, 200, 200, 255), 3.0f); + } + + // Display nodes + drawList->PushClipRect(regionRect.Min, regionRect.Max, true); + hoveredNode = -1; + + SlotIndex inputSlotOver = -1; + SlotIndex outputSlotOver = -1; + NodeIndex nodeOver = -1; + + const auto nodeCount = delegate.GetNodeCount(); + for (int i = 0; i < 2; i++) + { + for (NodeIndex nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) + { + //const auto* node = &nodes[nodeIndex]; + const auto node = delegate.GetNode(nodeIndex); + if (node.mSelected != (i != 0)) + { + continue; + } + + // node view clipping + ImRect nodeRect = GetNodeRect(node, viewState.mFactor); + nodeRect.Min += offset; + nodeRect.Max += offset; + if (!regionRect.Overlaps(nodeRect)) + { + continue; + } + + ImGui::PushID((int)nodeIndex); + SlotIndex inputSlot = -1; + SlotIndex outputSlot = -1; + + bool overInput = (!inMinimap) && HandleConnections(drawList, nodeIndex, offset, viewState.mFactor, delegate, options, false, inputSlot, outputSlot, inMinimap); + + // shadow + /* + ImVec2 shadowOffset = ImVec2(30, 30); + ImVec2 shadowPivot = (nodeRect.Min + nodeRect.Max) /2.f; + ImVec2 shadowPointMiddle = shadowPivot + shadowOffset; + ImVec2 shadowPointTop = ImVec2(shadowPivot.x, nodeRect.Min.y) + shadowOffset; + ImVec2 shadowPointBottom = ImVec2(shadowPivot.x, nodeRect.Max.y) + shadowOffset; + ImVec2 shadowPointLeft = ImVec2(nodeRect.Min.x, shadowPivot.y) + shadowOffset; + ImVec2 shadowPointRight = ImVec2(nodeRect.Max.x, shadowPivot.y) + shadowOffset; + + // top left + drawList->AddRectFilledMultiColor(nodeRect.Min + shadowOffset, shadowPointMiddle, IM_COL32(0 ,0, 0, 0), IM_COL32(0,0,0,0), IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 0)); + + // top right + drawList->AddRectFilledMultiColor(shadowPointTop, shadowPointRight, IM_COL32(0 ,0, 0, 0), IM_COL32(0,0,0,0), IM_COL32(0, 0, 0, 0), IM_COL32(0, 0, 0, 255)); + + // bottom left + drawList->AddRectFilledMultiColor(shadowPointLeft, shadowPointBottom, IM_COL32(0 ,0, 0, 0), IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 0), IM_COL32(0,0,0,0)); + + // bottom right + drawList->AddRectFilledMultiColor(shadowPointMiddle, nodeRect.Max + shadowOffset, IM_COL32(0, 0, 0, 255), IM_COL32(0 ,0, 0, 0), IM_COL32(0,0,0,0), IM_COL32(0, 0, 0, 0)); + */ + if (DrawNode(drawList, nodeIndex, offset, viewState.mFactor, delegate, overInput, options, inMinimap, regionRect)) + { + hoveredNode = nodeIndex; + } + + HandleConnections(drawList, nodeIndex, offset, viewState.mFactor, delegate, options, true, inputSlot, outputSlot, inMinimap); + if (inputSlot != -1 || outputSlot != -1) + { + inputSlotOver = inputSlot; + outputSlotOver = outputSlot; + nodeOver = nodeIndex; + } + + ImGui::PopID(); + } + } + + + + drawList->PopClipRect(); + + if (nodeOperation == NO_MovingNodes) + { + if (ImGui::IsMouseDragging(0, 1)) + { + ImVec2 delta = io.MouseDelta / viewState.mFactor; + if (fabsf(delta.x) >= 1.f || fabsf(delta.y) >= 1.f) + { + delegate.MoveSelectedNodes(delta); + } + } + } + + drawList->ChannelsSetCurrent(0); + + // quad selection + if (!inMinimap) + { + HandleQuadSelection(delegate, drawList, offset, viewState.mFactor, regionRect, options); + } + + drawList->ChannelsMerge(); + + // releasing mouse button means it's done in any operation + if (nodeOperation == NO_PanView) + { + if (!io.MouseDown[2]) + { + nodeOperation = NO_None; + } + } + else if (nodeOperation != NO_None && !io.MouseDown[0]) + { + nodeOperation = NO_None; + } + + // right click + if (!inMinimap && nodeOperation == NO_None && regionRect.Contains(io.MousePos) && + (ImGui::IsMouseClicked(1) /*|| (ImGui::IsWindowFocused() && ImGui::IsKeyPressedMap(ImGuiKey_Tab))*/)) + { + delegate.RightClick(nodeOver, inputSlotOver, outputSlotOver); + } + + // Scrolling + if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() && io.MouseClicked[2] && nodeOperation == NO_None) + { + nodeOperation = NO_PanView; + } + if (nodeOperation == NO_PanView) + { + viewState.mPosition += io.MouseDelta / viewState.mFactor; + } + } + + ImGui::PopClipRect(); + + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(2); + ImGui::EndGroup(); + ImGui::EndChild(); + + ImGui::PopStyleVar(3); + + // change fit to none + if (fit) + { + *fit = Fit_None; + } +} + +bool EditOptions(Options& options) +{ + bool updated = false; + if (ImGui::CollapsingHeader("Colors", nullptr)) + { + ImColor backgroundColor(options.mBackgroundColor); + ImColor gridColor(options.mGridColor); + ImColor selectedNodeBorderColor(options.mSelectedNodeBorderColor); + ImColor nodeBorderColor(options.mNodeBorderColor); + ImColor quadSelection(options.mQuadSelection); + ImColor quadSelectionBorder(options.mQuadSelectionBorder); + ImColor defaultSlotColor(options.mDefaultSlotColor); + ImColor frameFocus(options.mFrameFocus); + + updated |= ImGui::ColorEdit4("Background", (float*)&backgroundColor); + updated |= ImGui::ColorEdit4("Grid", (float*)&gridColor); + updated |= ImGui::ColorEdit4("Selected Node Border", (float*)&selectedNodeBorderColor); + updated |= ImGui::ColorEdit4("Node Border", (float*)&nodeBorderColor); + updated |= ImGui::ColorEdit4("Quad Selection", (float*)&quadSelection); + updated |= ImGui::ColorEdit4("Quad Selection Border", (float*)&quadSelectionBorder); + updated |= ImGui::ColorEdit4("Default Slot", (float*)&defaultSlotColor); + updated |= ImGui::ColorEdit4("Frame when has focus", (float*)&frameFocus); + + options.mBackgroundColor = backgroundColor; + options.mGridColor = gridColor; + options.mSelectedNodeBorderColor = selectedNodeBorderColor; + options.mNodeBorderColor = nodeBorderColor; + options.mQuadSelection = quadSelection; + options.mQuadSelectionBorder = quadSelectionBorder; + options.mDefaultSlotColor = defaultSlotColor; + options.mFrameFocus = frameFocus; + } + + if (ImGui::CollapsingHeader("Options", nullptr)) + { + updated |= ImGui::InputFloat4("Minimap", &options.mMinimap.Min.x); + updated |= ImGui::InputFloat("Line Thickness", &options.mLineThickness); + updated |= ImGui::InputFloat("Grid Size", &options.mGridSize); + updated |= ImGui::InputFloat("Rounding", &options.mRounding); + updated |= ImGui::InputFloat("Zoom Ratio", &options.mZoomRatio); + updated |= ImGui::InputFloat("Zoom Lerp Factor", &options.mZoomLerpFactor); + updated |= ImGui::InputFloat("Border Selection Thickness", &options.mBorderSelectionThickness); + updated |= ImGui::InputFloat("Border Thickness", &options.mBorderThickness); + updated |= ImGui::InputFloat("Slot Radius", &options.mNodeSlotRadius); + updated |= ImGui::InputFloat("Slot Hover Factor", &options.mNodeSlotHoverFactor); + updated |= ImGui::InputFloat2("Zoom min/max", &options.mMinZoom); + updated |= ImGui::InputFloat("Slot Hover Factor", &options.mSnap); + + if (ImGui::RadioButton("Curved Links", options.mDisplayLinksAsCurves)) + { + options.mDisplayLinksAsCurves = !options.mDisplayLinksAsCurves; + updated = true; + } + if (ImGui::RadioButton("Straight Links", !options.mDisplayLinksAsCurves)) + { + options.mDisplayLinksAsCurves = !options.mDisplayLinksAsCurves; + updated = true; + } + + updated |= ImGui::Checkbox("Allow Quad Selection", &options.mAllowQuadSelection); + updated |= ImGui::Checkbox("Render Grid", &options.mRenderGrid); + updated |= ImGui::Checkbox("Draw IO names on hover", &options.mDrawIONameOnHover); + } + + return updated; +} + +} // namespace diff --git a/third_party/ImGuizmo/GraphEditor.h b/third_party/ImGuizmo/GraphEditor.h new file mode 100644 index 0000000..3202ab1 --- /dev/null +++ b/third_party/ImGuizmo/GraphEditor.h @@ -0,0 +1,151 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#pragma once + +#include +#include +#include +#include "imgui.h" +#include "imgui_internal.h" + +namespace GraphEditor { + +typedef size_t NodeIndex; +typedef size_t SlotIndex; +typedef size_t LinkIndex; +typedef size_t TemplateIndex; + +// Force the view to be respositionned and zoom to fit nodes with Show function. +// Parameter value will be changed to Fit_None by the function. +enum FitOnScreen +{ + Fit_None, + Fit_AllNodes, + Fit_SelectedNodes +}; + +// Display options and colors +struct Options +{ + ImRect mMinimap{{0.75f, 0.8f, 0.99f, 0.99f}}; // rectangle coordinates of minimap + ImU32 mBackgroundColor{ IM_COL32(40, 40, 40, 255) }; // full background color + ImU32 mGridColor{ IM_COL32(0, 0, 0, 60) }; // grid lines color + ImU32 mGridColor2{ IM_COL32(0, 0, 0, 160) }; // grid lines color every 10th + ImU32 mSelectedNodeBorderColor{ IM_COL32(255, 130, 30, 255) }; // node border color when it's selected + ImU32 mNodeBorderColor{ IM_COL32(100, 100, 100, 0) }; // node border color when it's not selected + ImU32 mQuadSelection{ IM_COL32(255, 32, 32, 64) }; // quad selection inside color + ImU32 mQuadSelectionBorder{ IM_COL32(255, 32, 32, 255) }; // quad selection border color + ImU32 mDefaultSlotColor{ IM_COL32(128, 128, 128, 255) }; // when no color is provided in node template, use this value + ImU32 mFrameFocus{ IM_COL32(64, 128, 255, 255) }; // rectangle border when graph editor has focus + float mLineThickness{ 5 }; // links width in pixels when zoom value is 1 + float mGridSize{ 64.f }; // background grid size in pixels when zoom value is 1 + float mRounding{ 3.f }; // rounding at node corners + float mZoomRatio{ 0.1f }; // factor per mouse wheel delta + float mZoomLerpFactor{ 0.25f }; // the smaller, the smoother + float mBorderSelectionThickness{ 6.f }; // thickness of selection border around nodes + float mBorderThickness{ 6.f }; // thickness of selection border around nodes + float mNodeSlotRadius{ 8.f }; // circle radius for inputs and outputs + float mNodeSlotHoverFactor{ 1.2f }; // increase size when hovering + float mMinZoom{ 0.2f }, mMaxZoom { 1.1f }; + float mSnap{ 5.f }; + bool mDisplayLinksAsCurves{ true }; // false is straight and 45deg lines + bool mAllowQuadSelection{ true }; // multiple selection using drag and drop + bool mRenderGrid{ true }; // grid or nothing + bool mDrawIONameOnHover{ true }; // only draw node input/output when hovering +}; + +// View state: scroll position and zoom factor +struct ViewState +{ + ImVec2 mPosition{0.0f, 0.0f}; // scroll position + float mFactor{ 1.0f }; // current zoom factor + float mFactorTarget{ 1.0f }; // targeted zoom factor interpolated using Options.mZoomLerpFactor +}; + +struct Template +{ + ImU32 mHeaderColor; + ImU32 mBackgroundColor; + ImU32 mBackgroundColorOver; + ImU8 mInputCount; + const char** mInputNames; // can be nullptr. No text displayed. + ImU32* mInputColors; // can be nullptr, default slot color will be used. + ImU8 mOutputCount; + const char** mOutputNames; // can be nullptr. No text displayed. + ImU32* mOutputColors; // can be nullptr, default slot color will be used. +}; + +struct Node +{ + const char* mName; + TemplateIndex mTemplateIndex; + ImRect mRect; + bool mSelected{ false }; +}; + +struct Link +{ + NodeIndex mInputNodeIndex; + SlotIndex mInputSlotIndex; + NodeIndex mOutputNodeIndex; + SlotIndex mOutputSlotIndex; +}; + +struct Delegate +{ + virtual bool AllowedLink(NodeIndex from, NodeIndex to) = 0; + + virtual void SelectNode(NodeIndex nodeIndex, bool selected) = 0; + virtual void MoveSelectedNodes(const ImVec2 delta) = 0; + + virtual void AddLink(NodeIndex inputNodeIndex, SlotIndex inputSlotIndex, NodeIndex outputNodeIndex, SlotIndex outputSlotIndex) = 0; + virtual void DelLink(LinkIndex linkIndex) = 0; + + // user is responsible for clipping + virtual void CustomDraw(ImDrawList* drawList, ImRect rectangle, NodeIndex nodeIndex) = 0; + + // use mouse position to open context menu + // if nodeIndex != -1, right click happens on the specified node + virtual void RightClick(NodeIndex nodeIndex, SlotIndex slotIndexInput, SlotIndex slotIndexOutput) = 0; + + virtual const size_t GetTemplateCount() = 0; + virtual const Template GetTemplate(TemplateIndex index) = 0; + + virtual const size_t GetNodeCount() = 0; + virtual const Node GetNode(NodeIndex index) = 0; + + virtual const size_t GetLinkCount() = 0; + virtual const Link GetLink(LinkIndex index) = 0; + + virtual ~Delegate() = default; +}; + +void Show(Delegate& delegate, const Options& options, ViewState& viewState, bool enabled, FitOnScreen* fit = nullptr); +void GraphEditorClear(); + +bool EditOptions(Options& options); + +} // namespace diff --git a/third_party/ImGuizmo/ImCurveEdit.cpp b/third_party/ImGuizmo/ImCurveEdit.cpp new file mode 100644 index 0000000..3d69cf5 --- /dev/null +++ b/third_party/ImGuizmo/ImCurveEdit.cpp @@ -0,0 +1,458 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#include "ImCurveEdit.h" +#include "imgui.h" +#include "imgui_internal.h" +#include +#include +#include + +#if defined(_MSC_VER) || defined(__MINGW32__) +#include +#endif +#if !defined(_MSC_VER) && !defined(__MINGW64_VERSION_MAJOR) +#define _malloca(x) alloca(x) +#define _freea(x) +#endif + +namespace ImCurveEdit +{ + +#ifndef IMGUI_DEFINE_MATH_OPERATORS + static ImVec2 operator+(const ImVec2& a, const ImVec2& b) { + return ImVec2(a.x + b.x, a.y + b.y); + } + + static ImVec2 operator-(const ImVec2& a, const ImVec2& b) { + return ImVec2(a.x - b.x, a.y - b.y); + } + + static ImVec2 operator*(const ImVec2& a, const ImVec2& b) { + return ImVec2(a.x * b.x, a.y * b.y); + } + + static ImVec2 operator/(const ImVec2& a, const ImVec2& b) { + return ImVec2(a.x / b.x, a.y / b.y); + } + + static ImVec2 operator*(const ImVec2& a, const float b) { + return ImVec2(a.x * b, a.y * b); + } +#endif + + static float smoothstep(float edge0, float edge1, float x) + { + x = ImClamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return x * x * (3 - 2 * x); + } + + static float distance(float x, float y, float x1, float y1, float x2, float y2) + { + float A = x - x1; + float B = y - y1; + float C = x2 - x1; + float D = y2 - y1; + + float dot = A * C + B * D; + float len_sq = C * C + D * D; + float param = -1.f; + if (len_sq > FLT_EPSILON) + param = dot / len_sq; + + float xx, yy; + + if (param < 0.f) { + xx = x1; + yy = y1; + } + else if (param > 1.f) { + xx = x2; + yy = y2; + } + else { + xx = x1 + param * C; + yy = y1 + param * D; + } + + float dx = x - xx; + float dy = y - yy; + return sqrtf(dx * dx + dy * dy); + } + + static int DrawPoint(ImDrawList* draw_list, ImVec2 pos, const ImVec2 size, const ImVec2 offset, bool edited) + { + int ret = 0; + ImGuiIO& io = ImGui::GetIO(); + + static const ImVec2 localOffsets[4] = { ImVec2(1,0), ImVec2(0,1), ImVec2(-1,0), ImVec2(0,-1) }; + ImVec2 offsets[4]; + for (int i = 0; i < 4; i++) + { + offsets[i] = pos * size + localOffsets[i] * 4.5f + offset; + } + + const ImVec2 center = pos * size + offset; + const ImRect anchor(center - ImVec2(5, 5), center + ImVec2(5, 5)); + draw_list->AddConvexPolyFilled(offsets, 4, 0xFF000000); + if (anchor.Contains(io.MousePos)) + { + ret = 1; + if (io.MouseDown[0]) + ret = 2; + } + if (edited) + draw_list->AddPolyline(offsets, 4, 0xFFFFFFFF, true, 3.0f); + else if (ret) + draw_list->AddPolyline(offsets, 4, 0xFF80B0FF, true, 2.0f); + else + draw_list->AddPolyline(offsets, 4, 0xFF0080FF, true, 2.0f); + + return ret; + } + + int Edit(Delegate& delegate, const ImVec2& size, unsigned int id, const ImRect* clippingRect, ImVector* selectedPoints) + { + static bool selectingQuad = false; + static ImVec2 quadSelection; + static int overCurve = -1; + static int movingCurve = -1; + static bool scrollingV = false; + static std::set selection; + static bool overSelectedPoint = false; + + int ret = 0; + + ImGuiIO& io = ImGui::GetIO(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_Border, 0); + ImGui::BeginChild(id, size, ImGuiChildFlags_FrameStyle); + delegate.focused = ImGui::IsWindowFocused(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + if (clippingRect) + draw_list->PushClipRect(clippingRect->Min, clippingRect->Max, true); + + const ImVec2 offset = ImGui::GetCursorScreenPos() + ImVec2(0.f, size.y); + const ImVec2 ssize(size.x, -size.y); + const ImRect container(offset + ImVec2(0.f, ssize.y), offset + ImVec2(ssize.x, 0.f)); + ImVec2& min = delegate.GetMin(); + ImVec2& max = delegate.GetMax(); + + // handle zoom and VScroll + if (container.Contains(io.MousePos)) + { + if (fabsf(io.MouseWheel) > FLT_EPSILON) + { + const float r = (io.MousePos.y - offset.y) / ssize.y; + float ratioY = ImLerp(min.y, max.y, r); + auto scaleValue = [&](float v) { + v -= ratioY; + v *= (1.f - io.MouseWheel * 0.05f); + v += ratioY; + return v; + }; + min.y = scaleValue(min.y); + max.y = scaleValue(max.y); + } + if (!scrollingV && ImGui::IsMouseDown(2)) + { + scrollingV = true; + } + } + ImVec2 range = max - min + ImVec2(1.f, 0.f); // +1 because of inclusive last frame + + const ImVec2 viewSize(size.x, -size.y); + const ImVec2 sizeOfPixel = ImVec2(1.f, 1.f) / viewSize; + const size_t curveCount = delegate.GetCurveCount(); + + if (scrollingV) + { + float deltaH = io.MouseDelta.y * range.y * sizeOfPixel.y; + min.y -= deltaH; + max.y -= deltaH; + if (!ImGui::IsMouseDown(2)) + scrollingV = false; + } + + draw_list->AddRectFilled(offset, offset + ssize, delegate.GetBackgroundColor()); + + auto pointToRange = [&](ImVec2 pt) { return (pt - min) / range; }; + auto rangeToPoint = [&](ImVec2 pt) { return (pt * range) + min; }; + + draw_list->AddLine(ImVec2(-1.f, -min.y / range.y) * viewSize + offset, ImVec2(1.f, -min.y / range.y) * viewSize + offset, 0xFF000000, 1.5f); + bool overCurveOrPoint = false; + + int localOverCurve = -1; + // make sure highlighted curve is rendered last + int* curvesIndex = (int*)_malloca(sizeof(int) * curveCount); + for (size_t c = 0; c < curveCount; c++) + curvesIndex[c] = int(c); + int highLightedCurveIndex = -1; + if (overCurve != -1 && curveCount) + { + ImSwap(curvesIndex[overCurve], curvesIndex[curveCount - 1]); + highLightedCurveIndex = overCurve; + } + + for (size_t cur = 0; cur < curveCount; cur++) + { + int c = curvesIndex[cur]; + if (!delegate.IsVisible(c)) + continue; + const size_t ptCount = delegate.GetPointCount(c); + if (ptCount < 1) + continue; + CurveType curveType = delegate.GetCurveType(c); + if (curveType == CurveNone) + continue; + const ImVec2* pts = delegate.GetPoints(c); + uint32_t curveColor = delegate.GetCurveColor(c); + if ((c == highLightedCurveIndex && selection.empty() && !selectingQuad) || movingCurve == c) + curveColor = 0xFFFFFFFF; + + for (size_t p = 0; p < ptCount - 1; p++) + { + const ImVec2 p1 = pointToRange(pts[p]); + const ImVec2 p2 = pointToRange(pts[p + 1]); + + if (curveType == CurveSmooth || curveType == CurveLinear) + { + size_t subStepCount = (curveType == CurveSmooth) ? 20 : 2; + float step = 1.f / float(subStepCount - 1); + for (size_t substep = 0; substep < subStepCount - 1; substep++) + { + float t = float(substep) * step; + + const ImVec2 sp1 = ImLerp(p1, p2, t); + const ImVec2 sp2 = ImLerp(p1, p2, t + step); + + const float rt1 = smoothstep(p1.x, p2.x, sp1.x); + const float rt2 = smoothstep(p1.x, p2.x, sp2.x); + + const ImVec2 pos1 = ImVec2(sp1.x, ImLerp(p1.y, p2.y, rt1)) * viewSize + offset; + const ImVec2 pos2 = ImVec2(sp2.x, ImLerp(p1.y, p2.y, rt2)) * viewSize + offset; + + if (distance(io.MousePos.x, io.MousePos.y, pos1.x, pos1.y, pos2.x, pos2.y) < 8.f && !scrollingV) + { + localOverCurve = int(c); + overCurve = int(c); + overCurveOrPoint = true; + } + + draw_list->AddLine(pos1, pos2, curveColor, 1.3f); + } // substep + } + else if (curveType == CurveDiscrete) + { + ImVec2 dp1 = p1 * viewSize + offset; + ImVec2 dp2 = ImVec2(p2.x, p1.y) * viewSize + offset; + ImVec2 dp3 = p2 * viewSize + offset; + draw_list->AddLine(dp1, dp2, curveColor, 1.3f); + draw_list->AddLine(dp2, dp3, curveColor, 1.3f); + + if ((distance(io.MousePos.x, io.MousePos.y, dp1.x, dp1.y, dp3.x, dp1.y) < 8.f || + distance(io.MousePos.x, io.MousePos.y, dp3.x, dp1.y, dp3.x, dp3.y) < 8.f) + /*&& localOverCurve == -1*/) + { + localOverCurve = int(c); + overCurve = int(c); + overCurveOrPoint = true; + } + } + } // point loop + + for (size_t p = 0; p < ptCount; p++) + { + const int drawState = DrawPoint(draw_list, pointToRange(pts[p]), viewSize, offset, (selection.find({ int(c), int(p) }) != selection.end() && movingCurve == -1 && !scrollingV)); + if (drawState && movingCurve == -1 && !selectingQuad) + { + overCurveOrPoint = true; + overSelectedPoint = true; + overCurve = -1; + if (drawState == 2) + { + if (!io.KeyShift && selection.find({ int(c), int(p) }) == selection.end()) + selection.clear(); + selection.insert({ int(c), int(p) }); + } + } + } + } // curves loop + + if (localOverCurve == -1) + overCurve = -1; + + // move selection + static bool pointsMoved = false; + static ImVec2 mousePosOrigin; + static std::vector originalPoints; + if (overSelectedPoint && io.MouseDown[0]) + { + if ((fabsf(io.MouseDelta.x) > 0.f || fabsf(io.MouseDelta.y) > 0.f) && !selection.empty()) + { + if (!pointsMoved) + { + delegate.BeginEdit(0); + mousePosOrigin = io.MousePos; + originalPoints.resize(selection.size()); + int index = 0; + for (auto& sel : selection) + { + const ImVec2* pts = delegate.GetPoints(sel.curveIndex); + originalPoints[index++] = pts[sel.pointIndex]; + } + } + pointsMoved = true; + ret = 1; + auto prevSelection = selection; + int originalIndex = 0; + for (auto& sel : prevSelection) + { + const ImVec2 p = rangeToPoint(pointToRange(originalPoints[originalIndex]) + (io.MousePos - mousePosOrigin) * sizeOfPixel); + const int newIndex = delegate.EditPoint(sel.curveIndex, sel.pointIndex, p); + if (newIndex != sel.pointIndex) + { + selection.erase(sel); + selection.insert({ sel.curveIndex, newIndex }); + } + originalIndex++; + } + } + } + + if (overSelectedPoint && !io.MouseDown[0]) + { + overSelectedPoint = false; + if (pointsMoved) + { + pointsMoved = false; + delegate.EndEdit(); + } + } + + // add point + if (overCurve != -1 && io.MouseDoubleClicked[0]) + { + const ImVec2 np = rangeToPoint((io.MousePos - offset) / viewSize); + delegate.BeginEdit(overCurve); + delegate.AddPoint(overCurve, np); + delegate.EndEdit(); + ret = 1; + } + + // move curve + + if (movingCurve != -1) + { + const size_t ptCount = delegate.GetPointCount(movingCurve); + const ImVec2* pts = delegate.GetPoints(movingCurve); + if (!pointsMoved) + { + mousePosOrigin = io.MousePos; + pointsMoved = true; + originalPoints.resize(ptCount); + for (size_t index = 0; index < ptCount; index++) + { + originalPoints[index] = pts[index]; + } + } + if (ptCount >= 1) + { + for (size_t p = 0; p < ptCount; p++) + { + delegate.EditPoint(movingCurve, int(p), rangeToPoint(pointToRange(originalPoints[p]) + (io.MousePos - mousePosOrigin) * sizeOfPixel)); + } + ret = 1; + } + if (!io.MouseDown[0]) + { + movingCurve = -1; + pointsMoved = false; + delegate.EndEdit(); + } + } + if (movingCurve == -1 && overCurve != -1 && ImGui::IsMouseClicked(0) && selection.empty() && !selectingQuad) + { + movingCurve = overCurve; + delegate.BeginEdit(overCurve); + } + + // quad selection + if (selectingQuad) + { + const ImVec2 bmin = ImMin(quadSelection, io.MousePos); + const ImVec2 bmax = ImMax(quadSelection, io.MousePos); + draw_list->AddRectFilled(bmin, bmax, 0x40FF0000, 1.f); + draw_list->AddRect(bmin, bmax, 0xFFFF0000, 1.f); + const ImRect selectionQuad(bmin, bmax); + if (!io.MouseDown[0]) + { + if (!io.KeyShift) + selection.clear(); + // select everythnig is quad + for (size_t c = 0; c < curveCount; c++) + { + if (!delegate.IsVisible(c)) + continue; + + const size_t ptCount = delegate.GetPointCount(c); + if (ptCount < 1) + continue; + + const ImVec2* pts = delegate.GetPoints(c); + for (size_t p = 0; p < ptCount; p++) + { + const ImVec2 center = pointToRange(pts[p]) * viewSize + offset; + if (selectionQuad.Contains(center)) + selection.insert({ int(c), int(p) }); + } + } + // done + selectingQuad = false; + } + } + if (!overCurveOrPoint && ImGui::IsMouseClicked(0) && !selectingQuad && movingCurve == -1 && !overSelectedPoint && container.Contains(io.MousePos)) + { + selectingQuad = true; + quadSelection = io.MousePos; + } + if (clippingRect) + draw_list->PopClipRect(); + + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(1); + + if (selectedPoints) + { + selectedPoints->resize(int(selection.size())); + int index = 0; + for (auto& point : selection) + (*selectedPoints)[index++] = point; + } + _freea(curvesIndex); + return ret; + } +} diff --git a/third_party/ImGuizmo/ImCurveEdit.h b/third_party/ImGuizmo/ImCurveEdit.h new file mode 100644 index 0000000..3c0ebda --- /dev/null +++ b/third_party/ImGuizmo/ImCurveEdit.h @@ -0,0 +1,82 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#pragma once +#include +#include "imgui.h" + +struct ImRect; + +namespace ImCurveEdit +{ + enum CurveType + { + CurveNone, + CurveDiscrete, + CurveLinear, + CurveSmooth, + CurveBezier, + }; + + struct EditPoint + { + int curveIndex; + int pointIndex; + bool operator <(const EditPoint& other) const + { + if (curveIndex < other.curveIndex) + return true; + if (curveIndex > other.curveIndex) + return false; + + if (pointIndex < other.pointIndex) + return true; + return false; + } + }; + + struct Delegate + { + bool focused = false; + virtual size_t GetCurveCount() = 0; + virtual bool IsVisible(size_t /*curveIndex*/) { return true; } + virtual CurveType GetCurveType(size_t /*curveIndex*/) const { return CurveLinear; } + virtual ImVec2& GetMin() = 0; + virtual ImVec2& GetMax() = 0; + virtual size_t GetPointCount(size_t curveIndex) = 0; + virtual uint32_t GetCurveColor(size_t curveIndex) = 0; + virtual ImVec2* GetPoints(size_t curveIndex) = 0; + virtual int EditPoint(size_t curveIndex, int pointIndex, ImVec2 value) = 0; + virtual void AddPoint(size_t curveIndex, ImVec2 value) = 0; + virtual unsigned int GetBackgroundColor() { return 0xFF202020; } + // handle undo/redo thru this functions + virtual void BeginEdit(int /*index*/) {} + virtual void EndEdit() {} + + virtual ~Delegate() = default; + }; + + int Edit(Delegate& delegate, const ImVec2& size, unsigned int id, const ImRect* clippingRect = NULL, ImVector* selectedPoints = NULL); +} diff --git a/third_party/ImGuizmo/ImGradient.cpp b/third_party/ImGuizmo/ImGradient.cpp new file mode 100644 index 0000000..0363f9f --- /dev/null +++ b/third_party/ImGuizmo/ImGradient.cpp @@ -0,0 +1,116 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#include "ImGradient.h" +#include "imgui.h" +#include "imgui_internal.h" + +namespace ImGradient +{ +#ifndef IMGUI_DEFINE_MATH_OPERATORS + static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } + static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } + static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } + static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } + static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } + static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +#endif + + static int DrawPoint(ImDrawList* draw_list, ImVec4 color, const ImVec2 size, bool editing, ImVec2 pos) + { + ImGuiIO& io = ImGui::GetIO(); + + ImVec2 p1 = ImLerp(pos, ImVec2(pos + ImVec2(size.x - size.y, 0.f)), color.w) + ImVec2(3, 3); + ImVec2 p2 = ImLerp(pos + ImVec2(size.y, size.y), ImVec2(pos + size), color.w) - ImVec2(3, 3); + ImRect rc(p1, p2); + + color.w = 1.f; + draw_list->AddRectFilled(p1, p2, ImColor(color)); + if (editing) + draw_list->AddRect(p1, p2, 0xFFFFFFFF, 2.f, 15, 2.5f); + else + draw_list->AddRect(p1, p2, 0x80FFFFFF, 2.f, 15, 1.25f); + + if (rc.Contains(io.MousePos)) + { + if (io.MouseClicked[0]) + return 2; + return 1; + } + return 0; + } + + bool Edit(Delegate& delegate, const ImVec2& size, int& selection) + { + bool ret = false; + ImGuiIO& io = ImGui::GetIO(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::BeginChild(137, size, ImGuiChildFlags_FrameStyle); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 offset = ImGui::GetCursorScreenPos(); + + const ImVec4* pts = delegate.GetPoints(); + static int currentSelection = -1; + static int movingPt = -1; + if (currentSelection >= int(delegate.GetPointCount())) + currentSelection = -1; + if (movingPt != -1) + { + ImVec4 current = pts[movingPt]; + current.w += io.MouseDelta.x / size.x; + current.w = ImClamp(current.w, 0.f, 1.f); + delegate.EditPoint(movingPt, current); + ret = true; + if (!io.MouseDown[0]) + movingPt = -1; + } + for (size_t i = 0; i < delegate.GetPointCount(); i++) + { + int ptSel = DrawPoint(draw_list, pts[i], size, i == currentSelection, offset); + if (ptSel == 2) + { + currentSelection = int(i); + ret = true; + } + if (ptSel == 1 && io.MouseDown[0] && movingPt == -1) + { + movingPt = int(i); + } + } + ImRect rc(offset, offset + size); + if (rc.Contains(io.MousePos) && io.MouseDoubleClicked[0]) + { + float t = (io.MousePos.x - offset.x) / size.x; + delegate.AddPoint(delegate.GetPoint(t)); + ret = true; + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + selection = currentSelection; + return ret; + } +} diff --git a/third_party/ImGuizmo/ImGradient.h b/third_party/ImGuizmo/ImGradient.h new file mode 100644 index 0000000..1efff9f --- /dev/null +++ b/third_party/ImGuizmo/ImGradient.h @@ -0,0 +1,45 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#pragma once +#include + +struct ImVec4; +struct ImVec2; + +namespace ImGradient +{ + struct Delegate + { + virtual size_t GetPointCount() = 0; + virtual ImVec4* GetPoints() = 0; + virtual int EditPoint(int pointIndex, ImVec4 value) = 0; + virtual ImVec4 GetPoint(float t) = 0; + virtual void AddPoint(ImVec4 value) = 0; + virtual ~Delegate() = default; + }; + + bool Edit(Delegate& delegate, const ImVec2& size, int& selection); +} diff --git a/third_party/ImGuizmo/ImGuizmo.cpp b/third_party/ImGuizmo/ImGuizmo.cpp new file mode 100644 index 0000000..5c26297 --- /dev/null +++ b/third_party/ImGuizmo/ImGuizmo.cpp @@ -0,0 +1,3164 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui.h" +#include "imgui_internal.h" +#include "ImGuizmo.h" + +#if defined(_MSC_VER) || defined(__MINGW32__) +#include +#endif +#if !defined(_MSC_VER) && !defined(__MINGW64_VERSION_MAJOR) +#define _malloca(x) alloca(x) +#define _freea(x) +#endif + +// includes patches for multiview from +// https://github.com/CedricGuillemet/ImGuizmo/issues/15 + +namespace IMGUIZMO_NAMESPACE +{ + static const float ZPI = 3.14159265358979323846f; + static const float RAD2DEG = (180.f / ZPI); + static const float DEG2RAD = (ZPI / 180.f); + const float screenRotateSize = 0.06f; + // scale a bit so translate axis do not touch when in universal + const float rotationDisplayFactor = 1.2f; + + static OPERATION operator&(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) & static_cast(rhs)); + } + + static bool operator!=(OPERATION lhs, int rhs) + { + return static_cast(lhs) != rhs; + } + + static bool Intersects(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) != 0; + } + + // True if lhs contains rhs + static bool Contains(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) == rhs; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // utility and math + + void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) + { + r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; + r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; + r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; + r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; + + r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; + r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; + r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; + r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; + + r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; + r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; + r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; + r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; + + r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; + r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; + r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; + r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; + } + + void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) + { + float temp, temp2, temp3, temp4; + temp = 2.0f * znear; + temp2 = right - left; + temp3 = top - bottom; + temp4 = zfar - znear; + m16[0] = temp / temp2; + m16[1] = 0.0; + m16[2] = 0.0; + m16[3] = 0.0; + m16[4] = 0.0; + m16[5] = temp / temp3; + m16[6] = 0.0; + m16[7] = 0.0; + m16[8] = (right + left) / temp2; + m16[9] = (top + bottom) / temp3; + m16[10] = (-zfar - znear) / temp4; + m16[11] = -1.0f; + m16[12] = 0.0; + m16[13] = 0.0; + m16[14] = (-temp * zfar) / temp4; + m16[15] = 0.0; + } + + void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) + { + float ymax, xmax; + ymax = znear * tanf(fovyInDegrees * DEG2RAD); + xmax = ymax * aspectRatio; + Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); + } + + void Cross(const float* a, const float* b, float* r) + { + r[0] = a[1] * b[2] - a[2] * b[1]; + r[1] = a[2] * b[0] - a[0] * b[2]; + r[2] = a[0] * b[1] - a[1] * b[0]; + } + + float Dot(const float* a, const float* b) + { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + + void Normalize(const float* a, float* r) + { + float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); + r[0] = a[0] * il; + r[1] = a[1] * il; + r[2] = a[2] * il; + } + + void LookAt(const float* eye, const float* at, const float* up, float* m16) + { + float X[3], Y[3], Z[3], tmp[3]; + + tmp[0] = eye[0] - at[0]; + tmp[1] = eye[1] - at[1]; + tmp[2] = eye[2] - at[2]; + Normalize(tmp, Z); + Normalize(up, Y); + Cross(Y, Z, tmp); + Normalize(tmp, X); + Cross(Z, X, tmp); + Normalize(tmp, Y); + + m16[0] = X[0]; + m16[1] = Y[0]; + m16[2] = Z[0]; + m16[3] = 0.0f; + m16[4] = X[1]; + m16[5] = Y[1]; + m16[6] = Z[1]; + m16[7] = 0.0f; + m16[8] = X[2]; + m16[9] = Y[2]; + m16[10] = Z[2]; + m16[11] = 0.0f; + m16[12] = -Dot(X, eye); + m16[13] = -Dot(Y, eye); + m16[14] = -Dot(Z, eye); + m16[15] = 1.0f; + } + + template T Clamp(T x, T y, T z) { return ((x < y) ? y : ((x > z) ? z : x)); } + template T max(T x, T y) { return (x > y) ? x : y; } + template T min(T x, T y) { return (x < y) ? x : y; } + template bool IsWithin(T x, T y, T z) { return (x >= y) && (x <= z); } + + struct matrix_t; + struct vec_t + { + public: + float x, y, z, w; + + void Lerp(const vec_t& v, float t) + { + x += (v.x - x) * t; + y += (v.y - y) * t; + z += (v.z - z) * t; + w += (v.w - w) * t; + } + + void Set(float v) { x = y = z = w = v; } + void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) { x = _x; y = _y; z = _z; w = _w; } + + vec_t& operator -= (const vec_t& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } + vec_t& operator += (const vec_t& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } + vec_t& operator *= (const vec_t& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } + vec_t& operator *= (float v) { x *= v; y *= v; z *= v; w *= v; return *this; } + + vec_t operator * (float f) const; + vec_t operator - () const; + vec_t operator - (const vec_t& v) const; + vec_t operator + (const vec_t& v) const; + vec_t operator * (const vec_t& v) const; + + const vec_t& operator + () const { return (*this); } + float Length() const { return sqrtf(x * x + y * y + z * z); }; + float LengthSq() const { return (x * x + y * y + z * z); }; + vec_t Normalize() { (*this) *= (1.f / ( Length() > FLT_EPSILON ? Length() : FLT_EPSILON ) ); return (*this); } + vec_t Normalize(const vec_t& v) { this->Set(v.x, v.y, v.z, v.w); this->Normalize(); return (*this); } + vec_t Abs() const; + + void Cross(const vec_t& v) + { + vec_t res; + res.x = y * v.z - z * v.y; + res.y = z * v.x - x * v.z; + res.z = x * v.y - y * v.x; + + x = res.x; + y = res.y; + z = res.z; + w = 0.f; + } + + void Cross(const vec_t& v1, const vec_t& v2) + { + x = v1.y * v2.z - v1.z * v2.y; + y = v1.z * v2.x - v1.x * v2.z; + z = v1.x * v2.y - v1.y * v2.x; + w = 0.f; + } + + float Dot(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); + } + + float Dot3(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z); + } + + void Transform(const matrix_t& matrix); + void Transform(const vec_t& s, const matrix_t& matrix); + + void TransformVector(const matrix_t& matrix); + void TransformPoint(const matrix_t& matrix); + void TransformVector(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformVector(matrix); } + void TransformPoint(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformPoint(matrix); } + + float& operator [] (size_t index) { return ((float*)&x)[index]; } + const float& operator [] (size_t index) const { return ((float*)&x)[index]; } + bool operator!=(const vec_t& other) const { return memcmp(this, &other, sizeof(vec_t)) != 0; } + }; + + vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) { vec_t res; res.x = _x; res.y = _y; res.z = _z; res.w = _w; return res; } + vec_t makeVect(ImVec2 v) { vec_t res; res.x = v.x; res.y = v.y; res.z = 0.f; res.w = 0.f; return res; } + vec_t vec_t::operator * (float f) const { return makeVect(x * f, y * f, z * f, w * f); } + vec_t vec_t::operator - () const { return makeVect(-x, -y, -z, -w); } + vec_t vec_t::operator - (const vec_t& v) const { return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); } + vec_t vec_t::operator + (const vec_t& v) const { return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); } + vec_t vec_t::operator * (const vec_t& v) const { return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); } + vec_t vec_t::Abs() const { return makeVect(fabsf(x), fabsf(y), fabsf(z)); } + + vec_t Normalized(const vec_t& v) { vec_t res; res = v; res.Normalize(); return res; } + vec_t Cross(const vec_t& v1, const vec_t& v2) + { + vec_t res; + res.x = v1.y * v2.z - v1.z * v2.y; + res.y = v1.z * v2.x - v1.x * v2.z; + res.z = v1.x * v2.y - v1.y * v2.x; + res.w = 0.f; + return res; + } + + float Dot(const vec_t& v1, const vec_t& v2) + { + return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); + } + + vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) + { + vec_t normal, res; + normal.Normalize(p_normal); + res.w = normal.Dot(p_point1); + res.x = normal.x; + res.y = normal.y; + res.z = normal.z; + return res; + } + + struct matrix_t + { + public: + + union + { + float m[4][4]; + float m16[16]; + struct + { + vec_t right, up, dir, position; + } v; + vec_t component[4]; + }; + + operator float* () { return m16; } + operator const float* () const { return m16; } + void Translation(float _x, float _y, float _z) { this->Translation(makeVect(_x, _y, _z)); } + + void Translation(const vec_t& vt) + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(vt.x, vt.y, vt.z, 1.f); + } + + void Scale(float _x, float _y, float _z) + { + v.right.Set(_x, 0.f, 0.f, 0.f); + v.up.Set(0.f, _y, 0.f, 0.f); + v.dir.Set(0.f, 0.f, _z, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Scale(const vec_t& s) { Scale(s.x, s.y, s.z); } + + matrix_t& operator *= (const matrix_t& mat) + { + matrix_t tmpMat; + tmpMat = *this; + tmpMat.Multiply(mat); + *this = tmpMat; + return *this; + } + matrix_t operator * (const matrix_t& mat) const + { + matrix_t matT; + matT.Multiply(*this, mat); + return matT; + } + + void Multiply(const matrix_t& matrix) + { + matrix_t tmp; + tmp = *this; + + FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); + } + + void Multiply(const matrix_t& m1, const matrix_t& m2) + { + FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); + } + + float GetDeterminant() const + { + return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - + m[0][2] * m[1][1] * m[2][0] - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; + } + + float Inverse(const matrix_t& srcMatrix, bool affine = false); + void SetToIdentity() + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Transpose() + { + matrix_t tmpm; + for (int l = 0; l < 4; l++) + { + for (int c = 0; c < 4; c++) + { + tmpm.m[l][c] = m[c][l]; + } + } + (*this) = tmpm; + } + + void RotationAxis(const vec_t& axis, float angle); + + void OrthoNormalize() + { + v.right.Normalize(); + v.up.Normalize(); + v.dir.Normalize(); + } + }; + + void vec_t::Transform(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::Transform(const vec_t& s, const matrix_t& matrix) + { + *this = s; + Transform(matrix); + } + + void vec_t::TransformPoint(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::TransformVector(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) + { + float det = 0; + + if (affine) + { + det = GetDeterminant(); + float s = 1 / det; + m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; + m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; + m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; + m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; + m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; + m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; + m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; + m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; + m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; + m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); + m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); + m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); + } + else + { + // transpose matrix + float src[16]; + for (int i = 0; i < 4; ++i) + { + src[i] = srcMatrix.m16[i * 4]; + src[i + 4] = srcMatrix.m16[i * 4 + 1]; + src[i + 8] = srcMatrix.m16[i * 4 + 2]; + src[i + 12] = srcMatrix.m16[i * 4 + 3]; + } + + // calculate pairs for first 8 elements (cofactors) + float tmp[12]; // temp array for pairs + tmp[0] = src[10] * src[15]; + tmp[1] = src[11] * src[14]; + tmp[2] = src[9] * src[15]; + tmp[3] = src[11] * src[13]; + tmp[4] = src[9] * src[14]; + tmp[5] = src[10] * src[13]; + tmp[6] = src[8] * src[15]; + tmp[7] = src[11] * src[12]; + tmp[8] = src[8] * src[14]; + tmp[9] = src[10] * src[12]; + tmp[10] = src[8] * src[13]; + tmp[11] = src[9] * src[12]; + + // calculate first 8 elements (cofactors) + m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); + m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); + m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); + m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); + m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); + m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); + m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); + m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); + + // calculate pairs for second 8 elements (cofactors) + tmp[0] = src[2] * src[7]; + tmp[1] = src[3] * src[6]; + tmp[2] = src[1] * src[7]; + tmp[3] = src[3] * src[5]; + tmp[4] = src[1] * src[6]; + tmp[5] = src[2] * src[5]; + tmp[6] = src[0] * src[7]; + tmp[7] = src[3] * src[4]; + tmp[8] = src[0] * src[6]; + tmp[9] = src[2] * src[4]; + tmp[10] = src[0] * src[5]; + tmp[11] = src[1] * src[4]; + + // calculate second 8 elements (cofactors) + m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); + m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); + m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); + m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); + m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); + m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); + m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); + m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); + + // calculate determinant + det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; + + // calculate matrix inverse + float invdet = 1 / det; + for (int j = 0; j < 16; ++j) + { + m16[j] *= invdet; + } + } + + return det; + } + + void matrix_t::RotationAxis(const vec_t& axis, float angle) + { + float length2 = axis.LengthSq(); + if (length2 < FLT_EPSILON) + { + SetToIdentity(); + return; + } + + vec_t n = axis * (1.f / sqrtf(length2)); + float s = sinf(angle); + float c = cosf(angle); + float k = 1.f - c; + + float xx = n.x * n.x * k + c; + float yy = n.y * n.y * k + c; + float zz = n.z * n.z * k + c; + float xy = n.x * n.y * k; + float yz = n.y * n.z * k; + float zx = n.z * n.x * k; + float xs = n.x * s; + float ys = n.y * s; + float zs = n.z * s; + + m[0][0] = xx; + m[0][1] = xy + zs; + m[0][2] = zx - ys; + m[0][3] = 0.f; + m[1][0] = xy - zs; + m[1][1] = yy; + m[1][2] = yz + xs; + m[1][3] = 0.f; + m[2][0] = zx + ys; + m[2][1] = yz - xs; + m[2][2] = zz; + m[2][3] = 0.f; + m[3][0] = 0.f; + m[3][1] = 0.f; + m[3][2] = 0.f; + m[3][3] = 1.f; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + enum MOVETYPE + { + MT_NONE, + MT_MOVE_X, + MT_MOVE_Y, + MT_MOVE_Z, + MT_MOVE_YZ, + MT_MOVE_ZX, + MT_MOVE_XY, + MT_MOVE_SCREEN, + MT_ROTATE_X, + MT_ROTATE_Y, + MT_ROTATE_Z, + MT_ROTATE_SCREEN, + MT_SCALE_X, + MT_SCALE_Y, + MT_SCALE_Z, + MT_SCALE_XYZ + }; + + static bool IsTranslateType(int type) + { + return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; + } + + static bool IsRotateType(int type) + { + return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; + } + + static bool IsScaleType(int type) + { + return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; + } + + // Matches MT_MOVE_AB order + static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; + + Style::Style() + { + // default values + TranslationLineThickness = 3.0f; + TranslationLineArrowSize = 6.0f; + RotationLineThickness = 2.0f; + RotationOuterLineThickness = 3.0f; + ScaleLineThickness = 3.0f; + ScaleLineCircleSize = 6.0f; + HatchedAxisLineThickness = 6.0f; + CenterCircleSize = 6.0f; + + // initialize default colors + Colors[DIRECTION_X] = ImVec4(0.666f, 0.000f, 0.000f, 1.000f); + Colors[DIRECTION_Y] = ImVec4(0.000f, 0.666f, 0.000f, 1.000f); + Colors[DIRECTION_Z] = ImVec4(0.000f, 0.000f, 0.666f, 1.000f); + Colors[PLANE_X] = ImVec4(0.666f, 0.000f, 0.000f, 0.380f); + Colors[PLANE_Y] = ImVec4(0.000f, 0.666f, 0.000f, 0.380f); + Colors[PLANE_Z] = ImVec4(0.000f, 0.000f, 0.666f, 0.380f); + Colors[SELECTION] = ImVec4(1.000f, 0.500f, 0.062f, 0.541f); + Colors[INACTIVE] = ImVec4(0.600f, 0.600f, 0.600f, 0.600f); + Colors[TRANSLATION_LINE] = ImVec4(0.666f, 0.666f, 0.666f, 0.666f); + Colors[SCALE_LINE] = ImVec4(0.250f, 0.250f, 0.250f, 1.000f); + Colors[ROTATION_USING_BORDER] = ImVec4(1.000f, 0.500f, 0.062f, 1.000f); + Colors[ROTATION_USING_FILL] = ImVec4(1.000f, 0.500f, 0.062f, 0.500f); + Colors[HATCHED_AXIS_LINES] = ImVec4(0.000f, 0.000f, 0.000f, 0.500f); + Colors[TEXT] = ImVec4(1.000f, 1.000f, 1.000f, 1.000f); + Colors[TEXT_SHADOW] = ImVec4(0.000f, 0.000f, 0.000f, 1.000f); + } + + struct Context + { + Context() : mbUsing(false), mbUsingViewManipulate(false), mbEnable(true), mIsViewManipulatorHovered(false), mbUsingBounds(false) + { + } + + ImDrawList* mDrawList; + Style mStyle; + + MODE mMode; + matrix_t mViewMat; + matrix_t mProjectionMat; + matrix_t mModel; + matrix_t mModelLocal; // orthonormalized model + matrix_t mModelInverse; + matrix_t mModelSource; + matrix_t mModelSourceInverse; + matrix_t mMVP; + matrix_t mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition + matrix_t mViewProjection; + + vec_t mModelScaleOrigin; + vec_t mCameraEye; + vec_t mCameraRight; + vec_t mCameraDir; + vec_t mCameraUp; + vec_t mRayOrigin; + vec_t mRayVector; + + float mRadiusSquareCenter; + ImVec2 mScreenSquareCenter; + ImVec2 mScreenSquareMin; + ImVec2 mScreenSquareMax; + + float mScreenFactor; + vec_t mRelativeOrigin; + + bool mbUsing; + bool mbUsingViewManipulate; + bool mbEnable; + bool mbMouseOver; + bool mReversed; // reversed projection matrix + bool mIsViewManipulatorHovered; + + // translation + vec_t mTranslationPlan; + vec_t mTranslationPlanOrigin; + vec_t mMatrixOrigin; + vec_t mTranslationLastDelta; + + // rotation + vec_t mRotationVectorSource; + float mRotationAngle; + float mRotationAngleOrigin; + //vec_t mWorldToLocalAxis; + + // scale + vec_t mScale; + vec_t mScaleValueOrigin; + vec_t mScaleLast; + float mSaveMousePosx; + + // save axis factor when using gizmo + bool mBelowAxisLimit[3]; + int mAxisMask = 0; + bool mBelowPlaneLimit[3]; + float mAxisFactor[3]; + + float mAxisLimit=0.0025f; + float mPlaneLimit=0.02f; + + // bounds stretching + vec_t mBoundsPivot; + vec_t mBoundsAnchor; + vec_t mBoundsPlan; + vec_t mBoundsLocalPivot; + int mBoundsBestAxis; + int mBoundsAxis[2]; + bool mbUsingBounds; + matrix_t mBoundsMatrix; + + // + int mCurrentOperation; + + float mX = 0.f; + float mY = 0.f; + float mWidth = 0.f; + float mHeight = 0.f; + float mXMax = 0.f; + float mYMax = 0.f; + float mDisplayRatio = 1.f; + + bool mIsOrthographic = false; + // check to not have multiple gizmo highlighted at the same time + bool mbOverGizmoHotspot = false; + + ImGuiWindow* mAlternativeWindow = nullptr; + ImVector mIDStack; + ImGuiID mEditingID = -1; + OPERATION mOperation = OPERATION(-1); + + bool mAllowAxisFlip = true; + float mGizmoSizeClipSpace = 0.1f; + + inline ImGuiID GetCurrentID() + { + if (mIDStack.empty()) + { + mIDStack.push_back(-1); + } + return mIDStack.back(); + } + }; + + static Context gContext; + + static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; + static const char* translationInfoMask[] = { "X : %5.3f", "Y : %5.3f", "Z : %5.3f", + "Y : %5.3f Z : %5.3f", "X : %5.3f Z : %5.3f", "X : %5.3f Y : %5.3f", + "X : %5.3f Y : %5.3f Z : %5.3f" }; + static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; + static const char* rotationInfoMask[] = { "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" }; + static const int translationInfoIndex[] = { 0,0,0, 1,0,0, 2,0,0, 1,2,0, 0,2,0, 0,1,0, 0,1,2 }; + static const float quadMin = 0.5f; + static const float quadMax = 0.8f; + static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; + static const int halfCircleSegmentCount = 64; + static const float snapTension = 0.5f; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); + static int GetRotateType(OPERATION op); + static int GetScaleType(OPERATION op); + + Style& GetStyle() + { + return gContext.mStyle; + } + + static ImU32 GetColorU32(int idx) + { + IM_ASSERT(idx < COLOR::COUNT); + return ImGui::ColorConvertFloat4ToU32(gContext.mStyle.Colors[idx]); + } + + static ImVec2 worldToPos(const vec_t& worldPos, const matrix_t& mat, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + vec_t trans; + trans.TransformPoint(worldPos, mat); + trans *= 0.5f / trans.w; + trans += makeVect(0.5f, 0.5f); + trans.y = 1.f - trans.y; + trans.x *= size.x; + trans.y *= size.y; + trans.x += position.x; + trans.y += position.y; + return ImVec2(trans.x, trans.y); + } + + static void ComputeCameraRay(vec_t& rayOrigin, vec_t& rayDir, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + ImGuiIO& io = ImGui::GetIO(); + + matrix_t mViewProjInverse; + mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); + + const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; + const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; + + const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; + const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); + + rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); + rayOrigin *= 1.f / rayOrigin.w; + vec_t rayEnd; + rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); + rayEnd *= 1.f / rayEnd.w; + rayDir = Normalized(rayEnd - rayOrigin); + } + + static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) + { + vec_t startOfSegment = start; + const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP; + startOfSegment.TransformPoint(mvp); + if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + startOfSegment *= 1.f / startOfSegment.w; + } + + vec_t endOfSegment = end; + endOfSegment.TransformPoint(mvp); + if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + endOfSegment *= 1.f / endOfSegment.w; + } + + vec_t clipSpaceAxis = endOfSegment - startOfSegment; + if (gContext.mDisplayRatio < 1.0) + clipSpaceAxis.x *= gContext.mDisplayRatio; + else + clipSpaceAxis.y /= gContext.mDisplayRatio; + float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); + return segmentLengthInClipSpace; + } + + static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) + { + vec_t pts[] = { ptO, ptA, ptB }; + for (unsigned int i = 0; i < 3; i++) + { + pts[i].TransformPoint(gContext.mMVP); + if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction + { + pts[i] *= 1.f / pts[i].w; + } + } + vec_t segA = pts[1] - pts[0]; + vec_t segB = pts[2] - pts[0]; + segA.y /= gContext.mDisplayRatio; + segB.y /= gContext.mDisplayRatio; + vec_t segAOrtho = makeVect(-segA.y, segA.x); + segAOrtho.Normalize(); + float dt = segAOrtho.Dot3(segB); + float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); + return surface; + } + + inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) + { + vec_t c = point - vertPos1; + vec_t V; + + V.Normalize(vertPos2 - vertPos1); + float d = (vertPos2 - vertPos1).Length(); + float t = V.Dot3(c); + + if (t < 0.f) + { + return vertPos1; + } + + if (t > d) + { + return vertPos2; + } + + return vertPos1 + V * t; + } + + static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) + { + const float numer = plan.Dot3(rOrigin) - plan.w; + const float denom = plan.Dot3(rVector); + + if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect + { + return -1.0f; + } + + return -(numer / denom); + } + + static float DistanceToPlane(const vec_t& point, const vec_t& plan) + { + return plan.Dot3(point) + plan.w; + } + + static bool IsInContextRect(ImVec2 p) + { + return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); + } + + static bool IsHoveringWindow() + { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName); + if (g.HoveredWindow == window) // Mouse hovering drawlist window + return true; + if (gContext.mAlternativeWindow != nullptr && g.HoveredWindow == gContext.mAlternativeWindow) + return true; + if (g.HoveredWindow != NULL) // Any other window is hovered + return false; + if (ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max, false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows) + return true; + return false; + } + + void SetRect(float x, float y, float width, float height) + { + gContext.mX = x; + gContext.mY = y; + gContext.mWidth = width; + gContext.mHeight = height; + gContext.mXMax = gContext.mX + gContext.mWidth; + gContext.mYMax = gContext.mY + gContext.mXMax; + gContext.mDisplayRatio = width / height; + } + + void SetOrthographic(bool isOrthographic) + { + gContext.mIsOrthographic = isOrthographic; + } + + void SetDrawlist(ImDrawList* drawlist) + { + gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); + } + + void SetImGuiContext(ImGuiContext* ctx) + { + ImGui::SetCurrentContext(ctx); + } + + void BeginFrame() + { + const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; + +#ifdef IMGUI_HAS_VIEWPORT + ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); +#else + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::SetNextWindowPos(ImVec2(0, 0)); +#endif + + ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); + ImGui::PushStyleColor(ImGuiCol_Border, 0); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + ImGui::Begin("gizmo", NULL, flags); + gContext.mDrawList = ImGui::GetWindowDrawList(); + gContext.mbOverGizmoHotspot = false; + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + + bool IsUsing() + { + return (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) || gContext.mbUsingBounds; + } + + bool IsUsingViewManipulate() + { + return gContext.mbUsingViewManipulate; + } + + bool IsViewManipulateHovered() + { + return gContext.mIsViewManipulatorHovered; + } + + bool IsUsingAny() + { + return gContext.mbUsing || gContext.mbUsingBounds; + } + + bool IsOver() + { + return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || + (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || + (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); + } + + bool IsOver(OPERATION op) + { + if(IsUsing()) + { + return true; + } + if(Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) + { + return true; + } + if(Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) + { + return true; + } + if(Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) + { + return true; + } + return false; + } + + void Enable(bool enable) + { + gContext.mbEnable = enable; + if (!enable) + { + gContext.mbUsing = false; + gContext.mbUsingBounds = false; + } + } + + static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) + { + gContext.mMode = mode; + gContext.mViewMat = *(matrix_t*)view; + gContext.mProjectionMat = *(matrix_t*)projection; + gContext.mbMouseOver = IsHoveringWindow(); + + gContext.mModelLocal = *(matrix_t*)matrix; + gContext.mModelLocal.OrthoNormalize(); + + if (mode == LOCAL) + { + gContext.mModel = gContext.mModelLocal; + } + else + { + gContext.mModel.Translation(((matrix_t*)matrix)->v.position); + } + gContext.mModelSource = *(matrix_t*)matrix; + gContext.mModelScaleOrigin.Set(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + + gContext.mModelInverse.Inverse(gContext.mModel); + gContext.mModelSourceInverse.Inverse(gContext.mModelSource); + gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; + gContext.mMVP = gContext.mModel * gContext.mViewProjection; + gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection; + + matrix_t viewInverse; + viewInverse.Inverse(gContext.mViewMat); + gContext.mCameraDir = viewInverse.v.dir; + gContext.mCameraEye = viewInverse.v.position; + gContext.mCameraRight = viewInverse.v.right; + gContext.mCameraUp = viewInverse.v.up; + + // projection reverse + vec_t nearPos, farPos; + nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); + farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); + + gContext.mReversed = (nearPos.z/nearPos.w) > (farPos.z / farPos.w); + + // compute scale from the size of camera right vector projected on screen at the matrix position + vec_t pointRight = viewInverse.v.right; + pointRight.TransformPoint(gContext.mViewProjection); + + vec_t rightViewInverse = viewInverse.v.right; + rightViewInverse.TransformVector(gContext.mModelInverse); + float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); + gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; + + ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); + gContext.mScreenSquareCenter = centerSSpace; + gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); + gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); + + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); + } + + static void ComputeColors(ImU32* colors, int type, OPERATION operation) + { + if (gContext.mbEnable) + { + ImU32 selectionColor = GetColorU32(SELECTION); + + switch (operation) + { + case TRANSLATE: + colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : GetColorU32(PLANE_X + i); + colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; + } + break; + case ROTATE: + colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + } + break; + case SCALEU: + case SCALE: + colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + } + break; + // note: this internal function is only called with three possible values for operation + default: + break; + } + } + else + { + ImU32 inactiveColor = GetColorU32(INACTIVE); + for (int i = 0; i < 7; i++) + { + colors[i] = inactiveColor; + } + } + } + + static void ComputeTripodAxisAndVisibility(const int axisIndex, vec_t& dirAxis, vec_t& dirPlaneX, vec_t& dirPlaneY, bool& belowAxisLimit, bool& belowPlaneLimit, const bool localCoordinates = false) + { + dirAxis = directionUnary[axisIndex]; + dirPlaneX = directionUnary[(axisIndex + 1) % 3]; + dirPlaneY = directionUnary[(axisIndex + 2) % 3]; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + // when using, use stored factors so the gizmo doesn't flip when we translate + + // Apply axis mask to axes and planes + belowAxisLimit = gContext.mBelowAxisLimit[axisIndex] && ((1< FLT_EPSILON) ? -1.f : 1.f; + float mulAxisX = (allowFlip && lenDirPlaneX < lenDirMinusPlaneX&& fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisY = (allowFlip && lenDirPlaneY < lenDirMinusPlaneY&& fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; + dirAxis *= mulAxis; + dirPlaneX *= mulAxisX; + dirPlaneY *= mulAxisY; + + // for axis + float axisLengthInClipSpace = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates); + + float paraSurf = GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); + // Apply axis mask to axes and planes + belowPlaneLimit = (paraSurf > gContext.mAxisLimit) && (((1< gContext.mPlaneLimit) && !((1< (1.f - snapTension)) + { + *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); + } + } + static void ComputeSnap(vec_t& value, const float* snap) + { + for (int i = 0; i < 3; i++) + { + ComputeSnap(&value[i], snap[i]); + } + } + + static float ComputeAngleOnPlan() + { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); + + vec_t perpendicularVector; + perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); + perpendicularVector.Normalize(); + float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); + float angle = acosf(acosAngle); + angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; + return angle; + } + + static void DrawRotationGizmo(OPERATION op, int type) + { + if(!Intersects(op, ROTATE)) + { + return; + } + ImDrawList* drawList = gContext.mDrawList; + + bool isMultipleAxesMasked = (gContext.mAxisMask & (gContext.mAxisMask - 1)) != 0; + bool isNoAxesMasked = !gContext.mAxisMask; + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, ROTATE); + + vec_t cameraToModelNormalized; + if (gContext.mIsOrthographic) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); + cameraToModelNormalized = -viewInverse.v.dir; + } + else + { + cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + } + + cameraToModelNormalized.TransformVector(gContext.mModelInverse); + + gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; + + bool hasRSC = Intersects(op, ROTATE_SCREEN); + for (int axis = 0; axis < 3; axis++) + { + if(!Intersects(op, static_cast(ROTATE_Z >> axis))) + { + continue; + } + + bool isAxisMasked = ((1 << (2 - axis)) & gContext.mAxisMask) != 0; + + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis); + const int circleMul = (hasRSC && !usingAxis) ? 1 : 2; + + ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); + + float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; + + for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) + { + float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)(circleMul * halfCircleSegmentCount)); + vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); + vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * rotationDisplayFactor; + circlePos[i] = worldToPos(pos, gContext.mMVP); + } + if (!gContext.mbUsing || usingAxis) + { + drawList->AddPolyline(circlePos, circleMul* halfCircleSegmentCount + 1, colors[3 - axis], false, gContext.mStyle.RotationLineThickness); + } + + float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); + if (radiusAxis > gContext.mRadiusSquareCenter) + { + gContext.mRadiusSquareCenter = radiusAxis; + } + } + if(hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN) && (!isMultipleAxesMasked && isNoAxesMasked)) + { + drawList->AddCircle(worldToPos(gContext.mModel.v.position, gContext.mViewProjection), gContext.mRadiusSquareCenter, colors[0], 64, gContext.mStyle.RotationOuterLineThickness); + } + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(type)) + { + ImVec2 circlePos[halfCircleSegmentCount + 1]; + + circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + for (unsigned int i = 1; i < halfCircleSegmentCount + 1; i++) + { + float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); + matrix_t rotateVectorMatrix; + rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); + vec_t pos; + pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); + pos *= gContext.mScreenFactor * rotationDisplayFactor; + circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); + } + drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_FILL)); + drawList->AddPolyline(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_BORDER), true, gContext.mStyle.RotationLineThickness); + + ImVec2 destinationPosOnScreen = circlePos[1]; + char tmps[512]; + ImFormatString(tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawHatchedAxis(const vec_t& axis) + { + if (gContext.mStyle.HatchedAxisLineThickness <= 0.0f) + { + return; + } + + for (int j = 1; j < 10; j++) + { + ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); + gContext.mDrawList->AddLine(baseSSpace2, worldDirSSpace2, GetColorU32(HATCHED_AXIS_LINES), gContext.mStyle.HatchedAxisLineThickness); + } + } + + static void DrawScaleGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if(!Intersects(op, SCALE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALE); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if(!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + ImU32 scaleLineColor = GetColorU32(SCALE_LINE); + drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, scaleLineColor, gContext.mStyle.ScaleLineThickness); + drawList->AddCircleFilled(worldDirSSpaceNoScale, gContext.mStyle.ScaleLineCircleSize, scaleLineColor); + } + + if (!hasTranslateOnAxis || gContext.mbUsing) + { + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.ScaleLineThickness); + } + drawList->AddCircleFilled(worldDirSSpace, gContext.mStyle.ScaleLineCircleSize, colors[i + 1]); + + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis * scaleDisplay[i]); + } + } + } + } + + // draw screen cirle + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + + static void DrawScaleUniveralGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if (!Intersects(op, SCALEU)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALEU); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal); + +#if 0 + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, IM_COL32(0x40, 0x40, 0x40, 0xFF), 3.f); + drawList->AddCircleFilled(worldDirSSpaceNoScale, 6.f, IM_COL32(0x40, 0x40, 0x40, 0xFF)); + } + /* + if (!hasTranslateOnAxis || gContext.mbUsing) + { + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], 3.f); + } + */ +#endif + drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]); + } + } + } + + // draw screen cirle + drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, gContext.mStyle.CenterCircleSize); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawTranslationGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + if (!drawList) + { + return; + } + + if(!Intersects(op, TRANSLATE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, TRANSLATE); + + const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + + // draw + bool belowAxisLimit = false; + bool belowPlaneLimit = false; + for (int i = 0; i < 3; ++i) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i)) + { + // draw axis + if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) + { + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); + + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.TranslationLineThickness); + + // Arrow head begin + ImVec2 dir(origin - worldDirSSpace); + + float d = sqrtf(ImLengthSqr(dir)); + dir /= d; // Normalize + dir *= gContext.mStyle.TranslationLineArrowSize; + + ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector + ImVec2 a(worldDirSSpace + dir); + drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); + // Arrow head end + + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis); + } + } + } + // draw plane + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i)) + { + if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) + { + ImVec2 screenQuadPts[4]; + for (int j = 0; j < 4; ++j) + { + vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; + screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); + } + drawList->AddPolyline(screenQuadPts, 4, GetColorU32(DIRECTION_X + i), true, 1.0f); + drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); + } + } + } + + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(type)) + { + ImU32 translationLineColor = GetColorU32(TRANSLATION_LINE); + + ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + + char tmps[512]; + vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_MOVE_X) * 3; + ImFormatString(tmps, sizeof(tmps), translationInfoMask[type - MT_MOVE_X], deltaInfo[translationInfoIndex[componentInfoIndex]], deltaInfo[translationInfoIndex[componentInfoIndex + 1]], deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static bool CanActivate() + { + if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) + { + return true; + } + return false; + } + + static bool HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) + { + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* drawList = gContext.mDrawList; + + bool manipulated = false; + + // compute best projection axis + vec_t axesWorldDirections[3]; + vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; + int axes[3]; + unsigned int numAxes = 1; + axes[0] = gContext.mBoundsBestAxis; + int bestAxis = axes[0]; + if (!gContext.mbUsingBounds) + { + numAxes = 0; + float bestDot = 0.f; + for (int i = 0; i < 3; i++) + { + vec_t dirPlaneNormalWorld; + dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); + dirPlaneNormalWorld.Normalize(); + + float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); + if (dt >= bestDot) + { + bestDot = dt; + bestAxis = i; + bestAxisWorldDirection = dirPlaneNormalWorld; + } + + if (dt >= 0.1f) + { + axes[numAxes] = i; + axesWorldDirections[numAxes] = dirPlaneNormalWorld; + ++numAxes; + } + } + } + + if (numAxes == 0) + { + axes[0] = bestAxis; + axesWorldDirections[0] = bestAxisWorldDirection; + numAxes = 1; + } + + else if (bestAxis != axes[0]) + { + unsigned int bestIndex = 0; + for (unsigned int i = 0; i < numAxes; i++) + { + if (axes[i] == bestAxis) + { + bestIndex = i; + break; + } + } + int tempAxis = axes[0]; + axes[0] = axes[bestIndex]; + axes[bestIndex] = tempAxis; + vec_t tempDirection = axesWorldDirections[0]; + axesWorldDirections[0] = axesWorldDirections[bestIndex]; + axesWorldDirections[bestIndex] = tempDirection; + } + + for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) + { + bestAxis = axes[axisIndex]; + bestAxisWorldDirection = axesWorldDirections[axisIndex]; + + // corners + vec_t aabb[4]; + + int secondAxis = (bestAxis + 1) % 3; + int thirdAxis = (bestAxis + 2) % 3; + + for (int i = 0; i < 4; i++) + { + aabb[i][3] = aabb[i][bestAxis] = 0.f; + aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; + aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; + } + + // draw bounds + unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); + + matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; + for (int i = 0; i < 4; i++) + { + ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); + ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); + if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) + { + continue; + } + float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); + int stepCount = (int)(boundDistance / 10.f); + stepCount = min(stepCount, 1000); + for (int j = 0; j < stepCount; j++) + { + float stepLength = 1.f / (float)stepCount; + float t1 = (float)j * stepLength; + float t2 = (float)j * stepLength + stepLength * 0.5f; + ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); + ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); + //drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); + drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); + } + vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; + ImVec2 midBound = worldToPos(midPoint, boundsMVP); + static const float AnchorBigRadius = 8.f; + static const float AnchorSmallRadius = 6.f; + bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + + int type = MT_NONE; + vec_t gizmoHitProportion; + + if(Intersects(operation, TRANSLATE)) + { + type = GetMoveType(operation, &gizmoHitProportion); + } + if(Intersects(operation, ROTATE) && type == MT_NONE) + { + type = GetRotateType(operation); + } + if(Intersects(operation, SCALE) && type == MT_NONE) + { + type = GetScaleType(operation); + } + + if (type != MT_NONE) + { + overBigAnchor = false; + overSmallAnchor = false; + } + + ImU32 selectionColor = GetColorU32(SELECTION); + + unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + + drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); + + drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); + int oppositeIndex = (i + 2) % 4; + // big anchor on corners + if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) + { + gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + gContext.mBoundsAxis[0] = secondAxis; + gContext.mBoundsAxis[1] = thirdAxis; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; + gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } + // small anchor on middle of segment + if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) + { + vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; + gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + int indices[] = { secondAxis , thirdAxis }; + gContext.mBoundsAxis[0] = indices[i % 2]; + gContext.mBoundsAxis[1] = -1; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = aabb[oppositeIndex][indices[i % 2]];// bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } + } + + if (gContext.mbUsingBounds && (gContext.GetCurrentID() == gContext.mEditingID)) + { + matrix_t scale; + scale.SetToIdentity(); + + // compute projected mouse position on plan + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute a reference and delta vectors base on mouse move + vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); + vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); + + // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length + for (int i = 0; i < 2; i++) + { + int axisIndex1 = gContext.mBoundsAxis[i]; + if (axisIndex1 == -1) + { + continue; + } + + float ratioAxis = 1.f; + vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); + + float dtAxis = axisDir.Dot(referenceVector); + float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; + if (dtAxis > FLT_EPSILON) + { + ratioAxis = axisDir.Dot(deltaVector) / dtAxis; + } + + if (snapValues) + { + float length = boundSize * ratioAxis; + ComputeSnap(&length, snapValues[axisIndex1]); + if (boundSize > FLT_EPSILON) + { + ratioAxis = length / boundSize; + } + } + scale.component[axisIndex1] *= ratioAxis; + + if (fabsf(ratioAxis - 1.0f) > FLT_EPSILON) { + manipulated = true; + } + } + + // transform matrix + matrix_t preScale, postScale; + preScale.Translation(-gContext.mBoundsLocalPivot); + postScale.Translation(gContext.mBoundsLocalPivot); + matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; + *matrix = res; + + // info text + char tmps[512]; + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + ImFormatString(tmps, sizeof(tmps), "X: %.2f Y: %.2f Z: %.2f" + , (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length() + , (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length() + , (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length() + ); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + + if (!io.MouseDown[0]) { + gContext.mbUsingBounds = false; + gContext.mEditingID = -1; + } + if (gContext.mbUsingBounds) + { + break; + } + } + + return manipulated; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + static int GetScaleType(OPERATION op) + { + if (gContext.mbUsing) + { + return MT_NONE; + } + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, SCALE)) + { + type = MT_SCALE_XYZ; + } + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if(!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + bool isAxisMasked = ((1 << i) & gContext.mAxisMask) != 0; + + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + dirAxis.TransformVector(gContext.mModelLocal); + dirPlaneX.TransformVector(gContext.mModelLocal); + dirPlaneY.TransformVector(gContext.mModelLocal); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; + const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; + const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); + + vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + + if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size + { + if (!isAxisMasked) + type = MT_SCALE_X + i; + } + } + + // universal + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f) + { + type = MT_SCALE_XYZ; + } + + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; + } + + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal); + + float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos)); + if (distance < 12.f) + { + type = MT_SCALE_X + i; + } + } + } + return type; + } + + static int GetRotateType(OPERATION op) + { + if (gContext.mbUsing) + { + return MT_NONE; + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = (gContext.mAxisMask & (gContext.mAxisMask - 1)) != 0; + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f)) + { + if (!isNoAxesMasked) + return MT_NONE; + type = MT_ROTATE_SCREEN; + } + + const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; + + vec_t modelViewPos; + modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); + + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if(!Intersects(op, static_cast(ROTATE_X << i))) + { + continue; + } + bool isAxisMasked = ((1 << i) & gContext.mAxisMask) != 0; + // pickup plan + vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); + const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t intersectViewPos; + intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); + + if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) + { + continue; + } + + const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; + vec_t idealPosOnCircle = Normalized(localPos); + idealPosOnCircle.TransformVector(gContext.mModelInverse); + const ImVec2 idealPosOnCircleScreen = worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP); + + //gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); + const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; + + const float distance = makeVect(distanceOnScreen).Length(); + if (distance < 8.f) // pixel size + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_ROTATE_X + i; + } + } + + return type; + } + + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) + { + if(!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver) + { + return MT_NONE; + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = (gContext.mAxisMask & (gContext.mAxisMask - 1)) != 0; + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, TRANSLATE)) + { + type = MT_MOVE_SCREEN; + } + + const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + bool isAxisMasked = ((1 << i) & gContext.mAxisMask) != 0; + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + dirAxis.TransformVector(gContext.mModel); + dirPlaneX.TransformVector(gContext.mModel); + dirPlaneY.TransformVector(gContext.mModel); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + + vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + if ((closestPointOnAxis - screenCoord).Length() < 12.f && Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size + { + if (isAxisMasked) + break; + type = MT_MOVE_X + i; + } + + const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && Contains(op, TRANSLATE_PLANS[i])) + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_MOVE_YZ + i; + } + + if (gizmoHitProportion) + { + *gizmoHitProportion = makeVect(dx, dy, 0.f); + } + } + return type; + } + + static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if(!Intersects(op, TRANSLATE) || type != MT_NONE) + { + return false; + } + const ImGuiIO& io = ImGui::GetIO(); + const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; + bool modified = false; + + // move + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + const float len = fabsf(signedLength); // near plan + const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute delta + const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModel.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) + { + const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; + const float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + } + + // snap + if (snap) + { + vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; + if (applyRotationLocaly) + { + matrix_t modelSourceNormalized = gContext.mModelSource; + modelSourceNormalized.OrthoNormalize(); + matrix_t modelSourceNormalizedInverse; + modelSourceNormalizedInverse.Inverse(modelSourceNormalized); + cumulativeDelta.TransformVector(modelSourceNormalizedInverse); + ComputeSnap(cumulativeDelta, snap); + cumulativeDelta.TransformVector(modelSourceNormalized); + } + else + { + ComputeSnap(cumulativeDelta, snap); + } + delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; + + } + + if (delta != gContext.mTranslationLastDelta) + { + modified = true; + } + gContext.mTranslationLastDelta = delta; + + // compute matrix & delta + matrix_t deltaMatrixTranslation; + deltaMatrixTranslation.Translation(delta); + if (deltaMatrix) + { + memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); + } + + const matrix_t res = gContext.mModelSource * deltaMatrixTranslation; + *(matrix_t*)matrix = res; + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + } + + type = gContext.mCurrentOperation; + } + else + { + // find new possible way to move + vec_t gizmoHitProportion; + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetMoveType(op, &gizmoHitProportion); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + if (type != MT_NONE) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + -gContext.mCameraDir }; + + vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + for (unsigned int i = 0; i < 3; i++) + { + vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); + movePlanNormal[i].Cross(orthoVector); + movePlanNormal[i].Normalize(); + } + // pickup plan + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModel.v.position; + + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); + } + } + return modified; + } + + static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool modified = false; + + if (!gContext.mbUsing) + { + // find new possible way to scale + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetScaleType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + + if (type != MT_NONE) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t movePlanNormal[] = { gContext.mModelLocal.v.up, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.right, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.up, gContext.mModelLocal.v.right, -gContext.mCameraDir }; + // pickup plan + + gContext.mTranslationPlan = BuildPlan(gContext.mModelLocal.v.position, movePlanNormal[type - MT_SCALE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModelLocal.v.position; + gContext.mScale.Set(1.f, 1.f, 1.f); + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position) * (1.f / gContext.mScreenFactor); + gContext.mScaleValueOrigin = makeVect(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + gContext.mSaveMousePosx = io.MousePos.x; + } + } + // scale + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModelLocal.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) + { + int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex]; + float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + + vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position; + float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); + + gContext.mScale[axisIndex] = max(ratio, 0.001f); + } + else + { + float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; + gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); + } + + // snap + if (snap) + { + float scaleSnap[] = { snap[0], snap[0], snap[0] }; + ComputeSnap(gContext.mScale, scaleSnap); + } + + // no 0 allowed + for (int i = 0; i < 3; i++) + gContext.mScale[i] = max(gContext.mScale[i], 0.001f); + + if (gContext.mScaleLast != gContext.mScale) + { + modified = true; + } + gContext.mScaleLast = gContext.mScale; + + // compute matrix & delta + matrix_t deltaMatrixScale; + deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); + + matrix_t res = deltaMatrixScale * gContext.mModelLocal; + *(matrix_t*)matrix = res; + + if (deltaMatrix) + { + vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; + + vec_t originalScaleDivider; + originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; + originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; + originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; + + deltaScale = deltaScale * originalScaleDivider; + + deltaMatrixScale.Scale(deltaScale); + memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mScale.Set(1.f, 1.f, 1.f); + } + + type = gContext.mCurrentOperation; + } + return modified; + } + + static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if(!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool applyRotationLocaly = gContext.mMode == LOCAL; + bool modified = false; + + if (!gContext.mbUsing) + { + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetRotateType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + + if (type != MT_NONE) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + } + + if (type == MT_ROTATE_SCREEN) + { + applyRotationLocaly = true; + } + + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t rotatePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; + // pickup plan + if (applyRotationLocaly) + { + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); + } + else + { + gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); + } + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; + gContext.mRotationVectorSource = Normalized(localPos); + gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); + } + } + + // rotation + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + gContext.mRotationAngle = ComputeAngleOnPlan(); + if (snap) + { + float snapInRadian = snap[0] * DEG2RAD; + ComputeSnap(&gContext.mRotationAngle, snapInRadian); + } + vec_t rotationAxisLocalSpace; + + rotationAxisLocalSpace.TransformVector(makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), gContext.mModelInverse); + rotationAxisLocalSpace.Normalize(); + + matrix_t deltaRotation; + deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); + if (gContext.mRotationAngle != gContext.mRotationAngleOrigin) + { + modified = true; + } + gContext.mRotationAngleOrigin = gContext.mRotationAngle; + + matrix_t scaleOrigin; + scaleOrigin.Scale(gContext.mModelScaleOrigin); + + if (applyRotationLocaly) + { + *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal; + } + else + { + matrix_t res = gContext.mModelSource; + res.v.position.Set(0.f); + + *(matrix_t*)matrix = res * deltaRotation; + ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; + } + + if (deltaMatrix) + { + *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mEditingID = -1; + } + type = gContext.mCurrentOperation; + } + return modified; + } + + void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) + { + matrix_t mat = *(matrix_t*)matrix; + + scale[0] = mat.v.right.Length(); + scale[1] = mat.v.up.Length(); + scale[2] = mat.v.dir.Length(); + + mat.OrthoNormalize(); + + rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); + rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); + rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); + + translation[0] = mat.v.position.x; + translation[1] = mat.v.position.y; + translation[2] = mat.v.position.z; + } + + void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) + { + matrix_t& mat = *(matrix_t*)matrix; + + matrix_t rot[3]; + for (int i = 0; i < 3; i++) + { + rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); + } + + mat = rot[0] * rot[1] * rot[2]; + + float validScale[3]; + for (int i = 0; i < 3; i++) + { + if (fabsf(scale[i]) < FLT_EPSILON) + { + validScale[i] = 0.001f; + } + else + { + validScale[i] = scale[i]; + } + } + mat.v.right *= validScale[0]; + mat.v.up *= validScale[1]; + mat.v.dir *= validScale[2]; + mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); + } + + void SetAlternativeWindow(ImGuiWindow* window) + { + gContext.mAlternativeWindow = window; + } + + void SetID(int id) + { + if (gContext.mIDStack.empty()) + { + gContext.mIDStack.push_back(-1); + } + gContext.mIDStack.back() = id; + } + + ImGuiID GetID(const char* str, const char* str_end) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + return id; + } + + ImGuiID GetID(const char* str) + { + return GetID(str, nullptr); + } + + ImGuiID GetID(const void* ptr) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); + return id; + } + + ImGuiID GetID(int n) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); + return id; + } + + void PushID(const char* str_id) + { + ImGuiID id = GetID(str_id); + gContext.mIDStack.push_back(id); + } + + void PushID(const char* str_id_begin, const char* str_id_end) + { + ImGuiID id = GetID(str_id_begin, str_id_end); + gContext.mIDStack.push_back(id); + } + + void PushID(const void* ptr_id) + { + ImGuiID id = GetID(ptr_id); + gContext.mIDStack.push_back(id); + } + + void PushID(int int_id) + { + ImGuiID id = GetID(int_id); + gContext.mIDStack.push_back(id); + } + + void PopID() + { + IM_ASSERT(gContext.mIDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + gContext.mIDStack.pop_back(); + if (gContext.mIDStack.empty()) + { + gContext.mIDStack.clear(); + } + } + + void AllowAxisFlip(bool value) + { + gContext.mAllowAxisFlip = value; + } + + void SetAxisLimit(float value) + { + gContext.mAxisLimit=value; + } + + void SetAxisMask(bool x, bool y, bool z) + { + gContext.mAxisMask = (x ? 1 : 0) + (y ? 2 : 0) + (z ? 4 : 0); + } + + void SetPlaneLimit(float value) + { + gContext.mPlaneLimit = value; + } + + bool IsOver(float* position, float pixelRadius) + { + const ImGuiIO& io = ImGui::GetIO(); + + float radius = sqrtf((ImLengthSqr(worldToPos({ position[0], position[1], position[2], 0.0f }, gContext.mViewProjection) - io.MousePos))); + return radius < pixelRadius; + } + + bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix, const float* snap, const float* localBounds, const float* boundsSnap) + { + gContext.mDrawList->PushClipRect (ImVec2 (gContext.mX, gContext.mY), ImVec2 (gContext.mX + gContext.mWidth, gContext.mY + gContext.mHeight), false); + + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + + // set delta to identity + if (deltaMatrix) + { + ((matrix_t*)deltaMatrix)->SetToIdentity(); + } + + // behind camera + vec_t camSpacePosition; + camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); + if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f && !gContext.mbUsing) + { + return false; + } + + // -- + int type = MT_NONE; + bool manipulated = false; + if (gContext.mbEnable) + { + if (!gContext.mbUsingBounds) + { + manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || + HandleScale(matrix, deltaMatrix, operation, type, snap) || + HandleRotation(matrix, deltaMatrix, operation, type, snap); + } + } + + if (localBounds && !gContext.mbUsing) + { + manipulated |= HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); + } + + gContext.mOperation = operation; + if (!gContext.mbUsingBounds) + { + DrawRotationGizmo(operation, type); + DrawTranslationGizmo(operation, type); + DrawScaleGizmo(operation, type); + DrawScaleUniveralGizmo(operation, type); + } + + gContext.mDrawList->PopClipRect (); + return manipulated; + } + + void SetGizmoSizeClipSpace(float value) + { + gContext.mGizmoSizeClipSpace = value; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + void ComputeFrustumPlanes(vec_t* frustum, const float* clip) + { + frustum[0].x = clip[3] - clip[0]; + frustum[0].y = clip[7] - clip[4]; + frustum[0].z = clip[11] - clip[8]; + frustum[0].w = clip[15] - clip[12]; + + frustum[1].x = clip[3] + clip[0]; + frustum[1].y = clip[7] + clip[4]; + frustum[1].z = clip[11] + clip[8]; + frustum[1].w = clip[15] + clip[12]; + + frustum[2].x = clip[3] + clip[1]; + frustum[2].y = clip[7] + clip[5]; + frustum[2].z = clip[11] + clip[9]; + frustum[2].w = clip[15] + clip[13]; + + frustum[3].x = clip[3] - clip[1]; + frustum[3].y = clip[7] - clip[5]; + frustum[3].z = clip[11] - clip[9]; + frustum[3].w = clip[15] - clip[13]; + + frustum[4].x = clip[3] - clip[2]; + frustum[4].y = clip[7] - clip[6]; + frustum[4].z = clip[11] - clip[10]; + frustum[4].w = clip[15] - clip[14]; + + frustum[5].x = clip[3] + clip[2]; + frustum[5].y = clip[7] + clip[6]; + frustum[5].z = clip[11] + clip[10]; + frustum[5].w = clip[15] + clip[14]; + + for (int i = 0; i < 6; i++) + { + frustum[i].Normalize(); + } + } + + void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + struct CubeFace + { + float z; + ImVec2 faceCoordsScreen[4]; + ImU32 color; + }; + CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); + + if (!faces) + { + return; + } + + vec_t frustum[6]; + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + ComputeFrustumPlanes(frustum, viewProjection.m16); + + int cubeFaceCount = 0; + for (int cube = 0; cube < matrixCount; cube++) + { + const float* matrix = &matrices[cube * 16]; + + matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; + + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + + const vec_t faceCoords[4] = { directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], + directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], + }; + + // clipping + /* + bool skipFace = false; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + vec_t camSpacePosition; + camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); + if (camSpacePosition.z < 0.001f) + { + skipFace = true; + break; + } + } + if (skipFace) + { + continue; + } + */ + vec_t centerPosition, centerPositionVP; + centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); + centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); + + bool inFrustum = true; + for (int iFrustum = 0; iFrustum < 6; iFrustum++) + { + float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); + if (dist < 0.f) + { + inFrustum = false; + break; + } + } + + if (!inFrustum) + { + continue; + } + CubeFace& cubeFace = faces[cubeFaceCount]; + + // 3D->2D + //ImVec2 faceCoordsScreen[4]; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); + } + + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + cubeFace.color = directionColor | IM_COL32(0x80, 0x80, 0x80, 0); + + cubeFace.z = centerPositionVP.z / centerPositionVP.w; + cubeFaceCount++; + } + } + qsort(faces, cubeFaceCount, sizeof(CubeFace), [](void const* _a, void const* _b) { + CubeFace* a = (CubeFace*)_a; + CubeFace* b = (CubeFace*)_b; + if (a->z < b->z) + { + return 1; + } + return -1; + }); + // draw face with lighter color + for (int iFace = 0; iFace < cubeFaceCount; iFace++) + { + const CubeFace& cubeFace = faces[iFace]; + gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); + } + + _freea(faces); + } + + void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) + { + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + vec_t frustum[6]; + ComputeFrustumPlanes(frustum, viewProjection.m16); + matrix_t res = *(matrix_t*)matrix * viewProjection; + + for (float f = -gridSize; f <= gridSize; f += 1.f) + { + for (int dir = 0; dir < 2; dir++) + { + vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); + vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); + bool visible = true; + for (int i = 0; i < 6; i++) + { + float dA = DistanceToPlane(ptA, frustum[i]); + float dB = DistanceToPlane(ptB, frustum[i]); + if (dA < 0.f && dB < 0.f) + { + visible = false; + break; + } + if (dA > 0.f && dB > 0.f) + { + continue; + } + if (dA < 0.f) + { + float len = fabsf(dA - dB); + float t = fabsf(dA) / len; + ptA.Lerp(ptB, t); + } + if (dB < 0.f) + { + float len = fabsf(dB - dA); + float t = fabsf(dB) / len; + ptB.Lerp(ptA, t); + } + } + if (visible) + { + ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); + col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; + col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF): col; + + float thickness = 1.f; + thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; + thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; + + gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); + } + } + } + } + + void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + ViewManipulate(view, length, position, size, backgroundColor); + } + + void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + static bool isDraging = false; + static bool isClicking = false; + static vec_t interpolationUp; + static vec_t interpolationDir; + static int interpolationFrames = 0; + const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); + + matrix_t svgView, svgProjection; + svgView = gContext.mViewMat; + svgProjection = gContext.mProjectionMat; + + ImGuiIO& io = ImGui::GetIO(); + gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; + + // view/projection matrices + const float distance = 3.f; + matrix_t cubeProjection, cubeView; + float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; + Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); + + vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); + vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); + vec_t eye = dir * distance; + vec_t zero = makeVect(0.f, 0.f); + LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); + + // set context + gContext.mViewMat = cubeView; + gContext.mProjectionMat = cubeProjection; + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); + + const matrix_t res = cubeView * cubeProjection; + + // panels + static const ImVec2 panelPosition[9] = { ImVec2(0.75f,0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), + ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), + ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; + + static const ImVec2 panelSize[9] = { ImVec2(0.25f,0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), + ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), + ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; + + // tag faces + bool boxes[27]{}; + static int overBox = -1; + for (int iPass = 0; iPass < 2; iPass++) + { + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + const vec_t indexVectorX = directionUnary[perpXIndex] * invert; + const vec_t indexVectorY = directionUnary[perpYIndex] * invert; + const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; + + // plan local space + const vec_t n = directionUnary[normalIndex] * invert; + vec_t viewSpaceNormal = n; + vec_t viewSpacePoint = n * 0.5f; + viewSpaceNormal.TransformVector(cubeView); + viewSpaceNormal.Normalize(); + viewSpacePoint.TransformPoint(cubeView); + const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); + + // back face culling + if (viewSpaceFacePlan.w > 0.f) + { + continue; + } + + const vec_t facePlan = BuildPlan(n * 0.5f, n); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); + + float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; + float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; + + // panels + const vec_t dx = directionUnary[perpXIndex]; + const vec_t dy = directionUnary[perpYIndex]; + const vec_t origin = directionUnary[normalIndex] - dx - dy; + for (int iPanel = 0; iPanel < 9; iPanel++) + { + vec_t boxCoord = boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); + const ImVec2 p = panelPosition[iPanel] * 2.f; + const ImVec2 s = panelSize[iPanel] * 2.f; + ImVec2 faceCoordsScreen[4]; + vec_t panelPos[4] = { dx * p.x + dy * p.y, + dx * p.x + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * p.y }; + + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); + } + + const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; + bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && localy < panelCorners[1].y; + int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); + IM_ASSERT(boxCoordInt < 27); + boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver; + + // draw face with lighter color + if (iPass) + { + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, (directionColor | IM_COL32(0x80, 0x80, 0x80, 0x80)) | (gContext.mIsViewManipulatorHovered ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); + if (boxes[boxCoordInt]) + { + ImU32 selectionColor = GetColorU32(SELECTION); + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, selectionColor); + + if (io.MouseDown[0] && !isClicking && !isDraging && GImGui->ActiveId == 0) { + overBox = boxCoordInt; + isClicking = true; + isDraging = true; + } + } + } + } + } + } + if (interpolationFrames) + { + interpolationFrames--; + vec_t newDir = viewInverse.v.dir; + newDir.Lerp(interpolationDir, 0.2f); + newDir.Normalize(); + + vec_t newUp = viewInverse.v.up; + newUp.Lerp(interpolationUp, 0.3f); + newUp.Normalize(); + newUp = interpolationUp; + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &newUp.x, view); + } + gContext.mIsViewManipulatorHovered = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos); + + if (io.MouseDown[0] && (fabsf(io.MouseDelta[0]) || fabsf(io.MouseDelta[1])) && isClicking) + { + isClicking = false; + } + + if (!io.MouseDown[0]) + { + if (isClicking) + { + // apply new view direction + int cx = overBox / 9; + int cy = (overBox - cx * 9) / 3; + int cz = overBox % 3; + interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz); + interpolationDir.Normalize(); + + if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) + { + vec_t right = viewInverse.v.right; + if (fabsf(right.x) > fabsf(right.z)) + { + right.z = 0.f; + } + else + { + right.x = 0.f; + } + right.Normalize(); + interpolationUp = Cross(interpolationDir, right); + interpolationUp.Normalize(); + } + else + { + interpolationUp = referenceUp; + } + interpolationFrames = 40; + + } + isClicking = false; + isDraging = false; + } + + + if (isDraging) + { + matrix_t rx, ry, roll; + + rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); + ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); + + roll = rx * ry; + + vec_t newDir = viewInverse.v.dir; + newDir.TransformVector(roll); + newDir.Normalize(); + + // clamp + vec_t planDir = Cross(viewInverse.v.right, referenceUp); + planDir.y = 0.f; + planDir.Normalize(); + float dt = Dot(planDir, newDir); + if (dt < 0.0f) + { + newDir += planDir * dt; + newDir.Normalize(); + } + + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); + } + + gContext.mbUsingViewManipulate = (interpolationFrames != 0) || isDraging; + if (isClicking || gContext.mbUsingViewManipulate || gContext.mIsViewManipulatorHovered) { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + } + + // restore view/projection because it was used to compute ray + ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); + } +}; diff --git a/third_party/ImGuizmo/ImGuizmo.h b/third_party/ImGuizmo/ImGuizmo.h new file mode 100644 index 0000000..158e0fe --- /dev/null +++ b/third_party/ImGuizmo/ImGuizmo.h @@ -0,0 +1,306 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// ------------------------------------------------------------------------------------------- +// History : +// 2019/11/03 View gizmo +// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation fixed. Display message is incorrect (X: ... Y:...) in local mode. +// 2016/09/09 Hatched negative axis. Snapping. Documentation update. +// 2016/09/04 Axis switch and translation plan autohiding. Scale transform stability improved +// 2016/09/01 Mogwai changed to Manipulate. Draw debug cube. Fixed inverted scale. Mixing scale and translation/rotation gives bad results. +// 2016/08/31 First version +// +// ------------------------------------------------------------------------------------------- +// Future (no order): +// +// - Multi view +// - display rotation/translation/scale infos in local/world space and not only local +// - finish local/world matrix application +// - OPERATION as bitmask +// +// ------------------------------------------------------------------------------------------- +// Example +#if 0 +void EditTransform(const Camera& camera, matrix_t& matrix) +{ + static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::ROTATE); + static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::WORLD); + if (ImGui::IsKeyPressed(90)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + if (ImGui::IsKeyPressed(69)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + if (ImGui::IsKeyPressed(82)) // r Key + mCurrentGizmoOperation = ImGuizmo::SCALE; + if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) + mCurrentGizmoOperation = ImGuizmo::SCALE; + float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + ImGuizmo::DecomposeMatrixToComponents(matrix.m16, matrixTranslation, matrixRotation, matrixScale); + ImGui::InputFloat3("Tr", matrixTranslation, 3); + ImGui::InputFloat3("Rt", matrixRotation, 3); + ImGui::InputFloat3("Sc", matrixScale, 3); + ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix.m16); + + if (mCurrentGizmoOperation != ImGuizmo::SCALE) + { + if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) + mCurrentGizmoMode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) + mCurrentGizmoMode = ImGuizmo::WORLD; + } + static bool useSnap(false); + if (ImGui::IsKeyPressed(83)) + useSnap = !useSnap; + ImGui::Checkbox("", &useSnap); + ImGui::SameLine(); + vec_t snap; + switch (mCurrentGizmoOperation) + { + case ImGuizmo::TRANSLATE: + snap = config.mSnapTranslation; + ImGui::InputFloat3("Snap", &snap.x); + break; + case ImGuizmo::ROTATE: + snap = config.mSnapRotation; + ImGui::InputFloat("Angle Snap", &snap.x); + break; + case ImGuizmo::SCALE: + snap = config.mSnapScale; + ImGui::InputFloat("Scale Snap", &snap.x); + break; + } + ImGuiIO& io = ImGui::GetIO(); + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::Manipulate(camera.mView.m16, camera.mProjection.m16, mCurrentGizmoOperation, mCurrentGizmoMode, matrix.m16, NULL, useSnap ? &snap.x : NULL); +} +#endif +#pragma once + +#ifdef USE_IMGUI_API +#include "imconfig.h" +#endif +#ifndef IMGUI_API +#define IMGUI_API +#endif + +#ifndef IMGUIZMO_NAMESPACE +#define IMGUIZMO_NAMESPACE ImGuizmo +#endif + +struct ImGuiWindow; + +namespace IMGUIZMO_NAMESPACE +{ + // call inside your own window and before Manipulate() in order to draw gizmo to that window. + // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). + IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); + + // call BeginFrame right after ImGui_XXXX_NewFrame(); + IMGUI_API void BeginFrame(); + + // this is necessary because when imguizmo is compiled into a dll, and imgui into another + // globals are not shared between them. + // More details at https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam + // expose method to set imgui context + IMGUI_API void SetImGuiContext(ImGuiContext* ctx); + + // return true if mouse cursor is over any gizmo control (axis, plan or screen component) + IMGUI_API bool IsOver(); + + // return true if mouse IsOver or if the gizmo is in moving state + IMGUI_API bool IsUsing(); + + // return true if the view gizmo is in moving state + IMGUI_API bool IsUsingViewManipulate(); + // only check if your mouse is over the view manipulator - no matter whether it's active or not + IMGUI_API bool IsViewManipulateHovered(); + + // return true if any gizmo is in moving state + IMGUI_API bool IsUsingAny(); + + // enable/disable the gizmo. Stay in the state until next call to Enable. + // gizmo is rendered with gray half transparent color when disabled + IMGUI_API void Enable(bool enable); + + // helper functions for manualy editing translation/rotation/scale with an input float + // translation, rotation and scale float points to 3 floats each + // Angles are in degrees (more suitable for human editing) + // example: + // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); + // ImGui::InputFloat3("Tr", matrixTranslation, 3); + // ImGui::InputFloat3("Rt", matrixRotation, 3); + // ImGui::InputFloat3("Sc", matrixScale, 3); + // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); + // + // These functions have some numerical stability issues for now. Use with caution. + IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); + IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); + + IMGUI_API void SetRect(float x, float y, float width, float height); + // default is false + IMGUI_API void SetOrthographic(bool isOrthographic); + + // Render a cube with face color corresponding to face normal. Usefull for debug/tests + IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); + IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); + + // call it when you want a gizmo + // Needs view and projection matrices. + // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional + // translation is applied in world space + enum OPERATION + { + TRANSLATE_X = (1u << 0), + TRANSLATE_Y = (1u << 1), + TRANSLATE_Z = (1u << 2), + ROTATE_X = (1u << 3), + ROTATE_Y = (1u << 4), + ROTATE_Z = (1u << 5), + ROTATE_SCREEN = (1u << 6), + SCALE_X = (1u << 7), + SCALE_Y = (1u << 8), + SCALE_Z = (1u << 9), + BOUNDS = (1u << 10), + SCALE_XU = (1u << 11), + SCALE_YU = (1u << 12), + SCALE_ZU = (1u << 13), + + TRANSLATE = TRANSLATE_X | TRANSLATE_Y | TRANSLATE_Z, + ROTATE = ROTATE_X | ROTATE_Y | ROTATE_Z | ROTATE_SCREEN, + SCALE = SCALE_X | SCALE_Y | SCALE_Z, + SCALEU = SCALE_XU | SCALE_YU | SCALE_ZU, // universal + UNIVERSAL = TRANSLATE | ROTATE | SCALEU + }; + + inline OPERATION operator|(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) | static_cast(rhs)); + } + + enum MODE + { + LOCAL, + WORLD + }; + + IMGUI_API bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix = NULL, const float* snap = NULL, const float* localBounds = NULL, const float* boundsSnap = NULL); + // + // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en + // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as + // other software are using the same mechanics. But just in case, you are now warned! + // + IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + + // use this version if you did not call Manipulate before and you are just using ViewManipulate + IMGUI_API void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + + IMGUI_API void SetAlternativeWindow(ImGuiWindow* window); + + [[deprecated("Use PushID/PopID instead.")]] + IMGUI_API void SetID(int id); + + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + + // return true if the cursor is over the operation's gizmo + IMGUI_API bool IsOver(OPERATION op); + IMGUI_API void SetGizmoSizeClipSpace(float value); + + // Allow axis to flip + // When true (default), the guizmo axis flip for better visibility + // When false, they always stay along the positive world/local axis + IMGUI_API void AllowAxisFlip(bool value); + + // Configure the limit where axis are hidden + IMGUI_API void SetAxisLimit(float value); + // Set an axis mask to permanently hide a given axis (true -> hidden, false -> shown) + IMGUI_API void SetAxisMask(bool x, bool y, bool z); + // Configure the limit where planes are hiden + IMGUI_API void SetPlaneLimit(float value); + // from a x,y,z point in space and using Manipulation view/projection matrix, check if mouse is in pixel radius distance of that projected point + IMGUI_API bool IsOver(float* position, float pixelRadius); + + enum COLOR + { + DIRECTION_X, // directionColor[0] + DIRECTION_Y, // directionColor[1] + DIRECTION_Z, // directionColor[2] + PLANE_X, // planeColor[0] + PLANE_Y, // planeColor[1] + PLANE_Z, // planeColor[2] + SELECTION, // selectionColor + INACTIVE, // inactiveColor + TRANSLATION_LINE, // translationLineColor + SCALE_LINE, + ROTATION_USING_BORDER, + ROTATION_USING_FILL, + HATCHED_AXIS_LINES, + TEXT, + TEXT_SHADOW, + COUNT + }; + + struct Style + { + IMGUI_API Style(); + + float TranslationLineThickness; // Thickness of lines for translation gizmo + float TranslationLineArrowSize; // Size of arrow at the end of lines for translation gizmo + float RotationLineThickness; // Thickness of lines for rotation gizmo + float RotationOuterLineThickness; // Thickness of line surrounding the rotation gizmo + float ScaleLineThickness; // Thickness of lines for scale gizmo + float ScaleLineCircleSize; // Size of circle at the end of lines for scale gizmo + float HatchedAxisLineThickness; // Thickness of hatched axis lines + float CenterCircleSize; // Size of circle at the center of the translate/scale gizmo + + ImVec4 Colors[COLOR::COUNT]; + }; + + IMGUI_API Style& GetStyle(); +} diff --git a/third_party/ImGuizmo/ImSequencer.cpp b/third_party/ImGuizmo/ImSequencer.cpp new file mode 100644 index 0000000..178b813 --- /dev/null +++ b/third_party/ImGuizmo/ImSequencer.cpp @@ -0,0 +1,695 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#include "ImSequencer.h" +#include "imgui.h" +#include "imgui_internal.h" +#include + +namespace ImSequencer +{ +#ifndef IMGUI_DEFINE_MATH_OPERATORS + static ImVec2 operator+(const ImVec2& a, const ImVec2& b) { + return ImVec2(a.x + b.x, a.y + b.y); + } +#endif + static bool SequencerAddDelButton(ImDrawList* draw_list, ImVec2 pos, bool add = true) + { + ImGuiIO& io = ImGui::GetIO(); + ImRect btnRect(pos, ImVec2(pos.x + 16, pos.y + 16)); + bool overBtn = btnRect.Contains(io.MousePos); + bool containedClick = overBtn && btnRect.Contains(io.MouseClickedPos[0]); + bool clickedBtn = containedClick && io.MouseReleased[0]; + int btnColor = overBtn ? 0xAAEAFFAA : 0x77A3B2AA; + if (containedClick && io.MouseDownDuration[0] > 0) + btnRect.Expand(2.0f); + + float midy = pos.y + 16 / 2 - 0.5f; + float midx = pos.x + 16 / 2 - 0.5f; + draw_list->AddRect(btnRect.Min, btnRect.Max, btnColor, 4); + draw_list->AddLine(ImVec2(btnRect.Min.x + 3, midy), ImVec2(btnRect.Max.x - 3, midy), btnColor, 2); + if (add) + draw_list->AddLine(ImVec2(midx, btnRect.Min.y + 3), ImVec2(midx, btnRect.Max.y - 3), btnColor, 2); + return clickedBtn; + } + + bool Sequencer(SequenceInterface* sequence, int* currentFrame, bool* expanded, int* selectedEntry, int* firstFrame, int sequenceOptions) + { + bool ret = false; + ImGuiIO& io = ImGui::GetIO(); + int cx = (int)(io.MousePos.x); + int cy = (int)(io.MousePos.y); + static float framePixelWidth = 10.f; + static float framePixelWidthTarget = 10.f; + int legendWidth = 200; + + static int movingEntry = -1; + static int movingPos = -1; + static int movingPart = -1; + int delEntry = -1; + int dupEntry = -1; + int ItemHeight = 20; + + bool popupOpened = false; + int sequenceCount = sequence->GetItemCount(); + if (!sequenceCount) + return false; + ImGui::BeginGroup(); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); // ImDrawList API uses screen coordinates! + ImVec2 canvas_size = ImGui::GetContentRegionAvail(); // Resize canvas to what's available + int firstFrameUsed = firstFrame ? *firstFrame : 0; + + + int controlHeight = sequenceCount * ItemHeight; + for (int i = 0; i < sequenceCount; i++) + controlHeight += int(sequence->GetCustomHeight(i)); + int frameCount = ImMax(sequence->GetFrameMax() - sequence->GetFrameMin(), 1); + + static bool MovingScrollBar = false; + static bool MovingCurrentFrame = false; + struct CustomDraw + { + int index; + ImRect customRect; + ImRect legendRect; + ImRect clippingRect; + ImRect legendClippingRect; + }; + ImVector customDraws; + ImVector compactCustomDraws; + // zoom in/out + const int visibleFrameCount = (int)floorf((canvas_size.x - legendWidth) / framePixelWidth); + const float barWidthRatio = ImMin(visibleFrameCount / (float)frameCount, 1.f); + const float barWidthInPixels = barWidthRatio * (canvas_size.x - legendWidth); + + ImRect regionRect(canvas_pos, canvas_pos + canvas_size); + + static bool panningView = false; + static ImVec2 panningViewSource; + static int panningViewFrame; + if (ImGui::IsWindowFocused() && io.KeyAlt && io.MouseDown[2]) + { + if (!panningView) + { + panningViewSource = io.MousePos; + panningView = true; + panningViewFrame = *firstFrame; + } + *firstFrame = panningViewFrame - int((io.MousePos.x - panningViewSource.x) / framePixelWidth); + *firstFrame = ImClamp(*firstFrame, sequence->GetFrameMin(), sequence->GetFrameMax() - visibleFrameCount); + } + if (panningView && !io.MouseDown[2]) + { + panningView = false; + } + framePixelWidthTarget = ImClamp(framePixelWidthTarget, 0.1f, 50.f); + + framePixelWidth = ImLerp(framePixelWidth, framePixelWidthTarget, 0.33f); + + frameCount = sequence->GetFrameMax() - sequence->GetFrameMin(); + if (visibleFrameCount >= frameCount && firstFrame) + *firstFrame = sequence->GetFrameMin(); + + + // -- + if (expanded && !*expanded) + { + ImGui::InvisibleButton("canvas", ImVec2(canvas_size.x - canvas_pos.x, (float)ItemHeight)); + draw_list->AddRectFilled(canvas_pos, ImVec2(canvas_size.x + canvas_pos.x, canvas_pos.y + ItemHeight), 0xFF3D3837, 0); + char tmps[512]; + ImFormatString(tmps, IM_ARRAYSIZE(tmps), sequence->GetCollapseFmt(), frameCount, sequenceCount); + draw_list->AddText(ImVec2(canvas_pos.x + 26, canvas_pos.y + 2), 0xFFFFFFFF, tmps); + } + else + { + bool hasScrollBar(true); + /* + int framesPixelWidth = int(frameCount * framePixelWidth); + if ((framesPixelWidth + legendWidth) >= canvas_size.x) + { + hasScrollBar = true; + } + */ + // test scroll area + ImVec2 headerSize(canvas_size.x, (float)ItemHeight); + ImVec2 scrollBarSize(canvas_size.x, 14.f); + ImGui::InvisibleButton("topBar", headerSize); + draw_list->AddRectFilled(canvas_pos, canvas_pos + headerSize, 0xFFFF0000, 0); + ImVec2 childFramePos = ImGui::GetCursorScreenPos(); + ImVec2 childFrameSize(canvas_size.x, canvas_size.y - 8.f - headerSize.y - (hasScrollBar ? scrollBarSize.y : 0)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, 0); + ImGui::BeginChild(889, childFrameSize, ImGuiChildFlags_FrameStyle); + sequence->focused = ImGui::IsWindowFocused(); + ImGui::InvisibleButton("contentBar", ImVec2(canvas_size.x, float(controlHeight))); + const ImVec2 contentMin = ImGui::GetItemRectMin(); + const ImVec2 contentMax = ImGui::GetItemRectMax(); + const ImRect contentRect(contentMin, contentMax); + const float contentHeight = contentMax.y - contentMin.y; + + // full background + draw_list->AddRectFilled(canvas_pos, canvas_pos + canvas_size, 0xFF242424, 0); + + // current frame top + ImRect topRect(ImVec2(canvas_pos.x + legendWidth, canvas_pos.y), ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + ItemHeight)); + + if (!MovingCurrentFrame && !MovingScrollBar && movingEntry == -1 && sequenceOptions & SEQUENCER_CHANGE_FRAME && currentFrame && *currentFrame >= 0 && topRect.Contains(io.MousePos) && io.MouseDown[0]) + { + MovingCurrentFrame = true; + } + if (MovingCurrentFrame) + { + if (frameCount) + { + *currentFrame = (int)((io.MousePos.x - topRect.Min.x) / framePixelWidth) + firstFrameUsed; + if (*currentFrame < sequence->GetFrameMin()) + *currentFrame = sequence->GetFrameMin(); + if (*currentFrame >= sequence->GetFrameMax()) + *currentFrame = sequence->GetFrameMax(); + } + if (!io.MouseDown[0]) + MovingCurrentFrame = false; + } + + //header + draw_list->AddRectFilled(canvas_pos, ImVec2(canvas_size.x + canvas_pos.x, canvas_pos.y + ItemHeight), 0xFF3D3837, 0); + if (sequenceOptions & SEQUENCER_ADD) + { + if (SequencerAddDelButton(draw_list, ImVec2(canvas_pos.x + legendWidth - ItemHeight, canvas_pos.y + 2), true)) + ImGui::OpenPopup("addEntry"); + + if (ImGui::BeginPopup("addEntry")) + { + for (int i = 0; i < sequence->GetItemTypeCount(); i++) + if (ImGui::Selectable(sequence->GetItemTypeName(i))) + { + sequence->Add(i); + *selectedEntry = sequence->GetItemCount() - 1; + } + + ImGui::EndPopup(); + popupOpened = true; + } + } + + //header frame number and lines + int modFrameCount = 10; + int frameStep = 1; + while ((modFrameCount * framePixelWidth) < 150) + { + modFrameCount *= 2; + frameStep *= 2; + }; + int halfModFrameCount = modFrameCount / 2; + + auto drawLine = [&](int i, int regionHeight) { + bool baseIndex = ((i % modFrameCount) == 0) || (i == sequence->GetFrameMax() || i == sequence->GetFrameMin()); + bool halfIndex = (i % halfModFrameCount) == 0; + int px = (int)canvas_pos.x + int(i * framePixelWidth) + legendWidth - int(firstFrameUsed * framePixelWidth); + int tiretStart = baseIndex ? 4 : (halfIndex ? 10 : 14); + int tiretEnd = baseIndex ? regionHeight : ItemHeight; + + if (px <= (canvas_size.x + canvas_pos.x) && px >= (canvas_pos.x + legendWidth)) + { + draw_list->AddLine(ImVec2((float)px, canvas_pos.y + (float)tiretStart), ImVec2((float)px, canvas_pos.y + (float)tiretEnd - 1), 0xFF606060, 1); + + draw_list->AddLine(ImVec2((float)px, canvas_pos.y + (float)ItemHeight), ImVec2((float)px, canvas_pos.y + (float)regionHeight - 1), 0x30606060, 1); + } + + if (baseIndex && px > (canvas_pos.x + legendWidth)) + { + char tmps[512]; + ImFormatString(tmps, IM_ARRAYSIZE(tmps), "%d", i); + draw_list->AddText(ImVec2((float)px + 3.f, canvas_pos.y), 0xFFBBBBBB, tmps); + } + + }; + + auto drawLineContent = [&](int i, int /*regionHeight*/) { + int px = (int)canvas_pos.x + int(i * framePixelWidth) + legendWidth - int(firstFrameUsed * framePixelWidth); + int tiretStart = int(contentMin.y); + int tiretEnd = int(contentMax.y); + + if (px <= (canvas_size.x + canvas_pos.x) && px >= (canvas_pos.x + legendWidth)) + { + //draw_list->AddLine(ImVec2((float)px, canvas_pos.y + (float)tiretStart), ImVec2((float)px, canvas_pos.y + (float)tiretEnd - 1), 0xFF606060, 1); + + draw_list->AddLine(ImVec2(float(px), float(tiretStart)), ImVec2(float(px), float(tiretEnd)), 0x30606060, 1); + } + }; + for (int i = sequence->GetFrameMin(); i <= sequence->GetFrameMax(); i += frameStep) + { + drawLine(i, ItemHeight); + } + drawLine(sequence->GetFrameMin(), ItemHeight); + drawLine(sequence->GetFrameMax(), ItemHeight); + /* + draw_list->AddLine(canvas_pos, ImVec2(canvas_pos.x, canvas_pos.y + controlHeight), 0xFF000000, 1); + draw_list->AddLine(ImVec2(canvas_pos.x, canvas_pos.y + ItemHeight), ImVec2(canvas_size.x, canvas_pos.y + ItemHeight), 0xFF000000, 1); + */ + // clip content + + draw_list->PushClipRect(childFramePos, childFramePos + childFrameSize, true); + + // draw item names in the legend rect on the left + size_t customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + int type; + sequence->Get(i, NULL, NULL, &type, NULL); + ImVec2 tpos(contentMin.x + 3, contentMin.y + i * ItemHeight + 2 + customHeight); + draw_list->AddText(tpos, 0xFFFFFFFF, sequence->GetItemLabel(i)); + + if (sequenceOptions & SEQUENCER_DEL) + { + if (SequencerAddDelButton(draw_list, ImVec2(contentMin.x + legendWidth - ItemHeight + 2 - 10, tpos.y + 2), false)) + delEntry = i; + + if (SequencerAddDelButton(draw_list, ImVec2(contentMin.x + legendWidth - ItemHeight - ItemHeight + 2 - 10, tpos.y + 2), true)) + dupEntry = i; + } + customHeight += sequence->GetCustomHeight(i); + } + + // slots background + customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + unsigned int col = (i & 1) ? 0xFF3A3636 : 0xFF413D3D; + + size_t localCustomHeight = sequence->GetCustomHeight(i); + ImVec2 pos = ImVec2(contentMin.x + legendWidth, contentMin.y + ItemHeight * i + 1 + customHeight); + ImVec2 sz = ImVec2(canvas_size.x + canvas_pos.x, pos.y + ItemHeight - 1 + localCustomHeight); + if (!popupOpened && cy >= pos.y && cy < pos.y + (ItemHeight + localCustomHeight) && movingEntry == -1 && cx>contentMin.x && cx < contentMin.x + canvas_size.x) + { + col += 0x80201008; + pos.x -= legendWidth; + } + draw_list->AddRectFilled(pos, sz, col, 0); + customHeight += localCustomHeight; + } + + draw_list->PushClipRect(childFramePos + ImVec2(float(legendWidth), 0.f), childFramePos + childFrameSize, true); + + // vertical frame lines in content area + for (int i = sequence->GetFrameMin(); i <= sequence->GetFrameMax(); i += frameStep) + { + drawLineContent(i, int(contentHeight)); + } + drawLineContent(sequence->GetFrameMin(), int(contentHeight)); + drawLineContent(sequence->GetFrameMax(), int(contentHeight)); + + // selection + bool selected = selectedEntry && (*selectedEntry >= 0); + if (selected) + { + customHeight = 0; + for (int i = 0; i < *selectedEntry; i++) + customHeight += sequence->GetCustomHeight(i); + draw_list->AddRectFilled(ImVec2(contentMin.x, contentMin.y + ItemHeight * *selectedEntry + customHeight), ImVec2(contentMin.x + canvas_size.x, contentMin.y + ItemHeight * (*selectedEntry + 1) + customHeight), 0x801080FF, 1.f); + } + + // slots + customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + int* start, * end; + unsigned int color; + sequence->Get(i, &start, &end, NULL, &color); + size_t localCustomHeight = sequence->GetCustomHeight(i); + + ImVec2 pos = ImVec2(contentMin.x + legendWidth - firstFrameUsed * framePixelWidth, contentMin.y + ItemHeight * i + 1 + customHeight); + ImVec2 slotP1(pos.x + *start * framePixelWidth, pos.y + 2); + ImVec2 slotP2(pos.x + *end * framePixelWidth + framePixelWidth, pos.y + ItemHeight - 2); + ImVec2 slotP3(pos.x + *end * framePixelWidth + framePixelWidth, pos.y + ItemHeight - 2 + localCustomHeight); + unsigned int slotColor = color | 0xFF000000; + unsigned int slotColorHalf = (color & 0xFFFFFF) | 0x40000000; + + if (slotP1.x <= (canvas_size.x + contentMin.x) && slotP2.x >= (contentMin.x + legendWidth)) + { + draw_list->AddRectFilled(slotP1, slotP3, slotColorHalf, 2); + draw_list->AddRectFilled(slotP1, slotP2, slotColor, 2); + } + if (ImRect(slotP1, slotP2).Contains(io.MousePos) && io.MouseDoubleClicked[0]) + { + sequence->DoubleClick(i); + } + // Ensure grabbable handles + const float max_handle_width = slotP2.x - slotP1.x / 3.0f; + const float min_handle_width = ImMin(10.0f, max_handle_width); + const float handle_width = ImClamp(framePixelWidth / 2.0f, min_handle_width, max_handle_width); + ImRect rects[3] = { ImRect(slotP1, ImVec2(slotP1.x + handle_width, slotP2.y)) + , ImRect(ImVec2(slotP2.x - handle_width, slotP1.y), slotP2) + , ImRect(slotP1, slotP2) }; + + const unsigned int quadColor[] = { 0xFFFFFFFF, 0xFFFFFFFF, slotColor + (selected ? 0 : 0x202020) }; + if (movingEntry == -1 && (sequenceOptions & SEQUENCER_EDIT_STARTEND))// TODOFOCUS && backgroundRect.Contains(io.MousePos)) + { + for (int j = 2; j >= 0; j--) + { + ImRect& rc = rects[j]; + if (!rc.Contains(io.MousePos)) + continue; + draw_list->AddRectFilled(rc.Min, rc.Max, quadColor[j], 2); + } + + for (int j = 0; j < 3; j++) + { + ImRect& rc = rects[j]; + if (!rc.Contains(io.MousePos)) + continue; + if (!ImRect(childFramePos, childFramePos + childFrameSize).Contains(io.MousePos)) + continue; + if (ImGui::IsMouseClicked(0) && !MovingScrollBar && !MovingCurrentFrame) + { + movingEntry = i; + movingPos = cx; + movingPart = j + 1; + sequence->BeginEdit(movingEntry); + break; + } + } + } + + // custom draw + if (localCustomHeight > 0) + { + ImVec2 rp(canvas_pos.x, contentMin.y + ItemHeight * i + 1 + customHeight); + ImRect customRect(rp + ImVec2(legendWidth - (firstFrameUsed - sequence->GetFrameMin() - 0.5f) * framePixelWidth, float(ItemHeight)), + rp + ImVec2(legendWidth + (sequence->GetFrameMax() - firstFrameUsed - 0.5f + 2.f) * framePixelWidth, float(localCustomHeight + ItemHeight))); + ImRect clippingRect(rp + ImVec2(float(legendWidth), float(ItemHeight)), rp + ImVec2(canvas_size.x, float(localCustomHeight + ItemHeight))); + + ImRect legendRect(rp + ImVec2(0.f, float(ItemHeight)), rp + ImVec2(float(legendWidth), float(localCustomHeight))); + ImRect legendClippingRect(canvas_pos + ImVec2(0.f, float(ItemHeight)), canvas_pos + ImVec2(float(legendWidth), float(localCustomHeight + ItemHeight))); + customDraws.push_back({ i, customRect, legendRect, clippingRect, legendClippingRect }); + } + else + { + ImVec2 rp(canvas_pos.x, contentMin.y + ItemHeight * i + customHeight); + ImRect customRect(rp + ImVec2(legendWidth - (firstFrameUsed - sequence->GetFrameMin() - 0.5f) * framePixelWidth, float(0.f)), + rp + ImVec2(legendWidth + (sequence->GetFrameMax() - firstFrameUsed - 0.5f + 2.f) * framePixelWidth, float(ItemHeight))); + ImRect clippingRect(rp + ImVec2(float(legendWidth), float(0.f)), rp + ImVec2(canvas_size.x, float(ItemHeight))); + + compactCustomDraws.push_back({ i, customRect, ImRect(), clippingRect, ImRect() }); + } + customHeight += localCustomHeight; + } + + + // moving + if (/*backgroundRect.Contains(io.MousePos) && */movingEntry >= 0) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + int diffFrame = int((cx - movingPos) / framePixelWidth); + if (std::abs(diffFrame) > 0) + { + int* start, * end; + sequence->Get(movingEntry, &start, &end, NULL, NULL); + if (selectedEntry) + *selectedEntry = movingEntry; + int& l = *start; + int& r = *end; + if (movingPart & 1) + l += diffFrame; + if (movingPart & 2) + r += diffFrame; + if (l < 0) + { + if (movingPart & 2) + r -= l; + l = 0; + } + if (movingPart & 1 && l > r) + l = r; + if (movingPart & 2 && r < l) + r = l; + movingPos += int(diffFrame * framePixelWidth); + } + if (!io.MouseDown[0]) + { + // single select + if (!diffFrame && movingPart && selectedEntry) + { + *selectedEntry = movingEntry; + ret = true; + } + + movingEntry = -1; + sequence->EndEdit(); + } + } + + // cursor + if (currentFrame && firstFrame && *currentFrame >= *firstFrame && *currentFrame <= sequence->GetFrameMax()) + { + static const float cursorWidth = 8.f; + float cursorOffset = contentMin.x + legendWidth + (*currentFrame - firstFrameUsed) * framePixelWidth + framePixelWidth / 2 - cursorWidth * 0.5f; + draw_list->AddLine(ImVec2(cursorOffset, canvas_pos.y), ImVec2(cursorOffset, contentMax.y), 0xA02A2AFF, cursorWidth); + char tmps[512]; + ImFormatString(tmps, IM_ARRAYSIZE(tmps), "%d", *currentFrame); + draw_list->AddText(ImVec2(cursorOffset + 10, canvas_pos.y + 2), 0xFF2A2AFF, tmps); + } + + draw_list->PopClipRect(); + draw_list->PopClipRect(); + + for (auto& customDraw : customDraws) + sequence->CustomDraw(customDraw.index, draw_list, customDraw.customRect, customDraw.legendRect, customDraw.clippingRect, customDraw.legendClippingRect); + for (auto& customDraw : compactCustomDraws) + sequence->CustomDrawCompact(customDraw.index, draw_list, customDraw.customRect, customDraw.clippingRect); + + // copy paste + if (sequenceOptions & SEQUENCER_COPYPASTE) + { + ImRect rectCopy(ImVec2(contentMin.x + 100, canvas_pos.y + 2) + , ImVec2(contentMin.x + 100 + 30, canvas_pos.y + ItemHeight - 2)); + bool inRectCopy = rectCopy.Contains(io.MousePos); + unsigned int copyColor = inRectCopy ? 0xFF1080FF : 0xFF000000; + draw_list->AddText(rectCopy.Min, copyColor, "Copy"); + + ImRect rectPaste(ImVec2(contentMin.x + 140, canvas_pos.y + 2) + , ImVec2(contentMin.x + 140 + 30, canvas_pos.y + ItemHeight - 2)); + bool inRectPaste = rectPaste.Contains(io.MousePos); + unsigned int pasteColor = inRectPaste ? 0xFF1080FF : 0xFF000000; + draw_list->AddText(rectPaste.Min, pasteColor, "Paste"); + + if (inRectCopy && io.MouseReleased[0]) + { + sequence->Copy(); + } + if (inRectPaste && io.MouseReleased[0]) + { + sequence->Paste(); + } + } + // + + ImGui::EndChild(); + ImGui::PopStyleColor(); + if (hasScrollBar) + { + ImGui::InvisibleButton("scrollBar", scrollBarSize); + ImVec2 scrollBarMin = ImGui::GetItemRectMin(); + ImVec2 scrollBarMax = ImGui::GetItemRectMax(); + + // ratio = number of frames visible in control / number to total frames + + float startFrameOffset = ((float)(firstFrameUsed - sequence->GetFrameMin()) / (float)frameCount) * (canvas_size.x - legendWidth); + ImVec2 scrollBarA(scrollBarMin.x + legendWidth, scrollBarMin.y - 2); + ImVec2 scrollBarB(scrollBarMin.x + canvas_size.x, scrollBarMax.y - 1); + draw_list->AddRectFilled(scrollBarA, scrollBarB, 0xFF222222, 0); + + ImRect scrollBarRect(scrollBarA, scrollBarB); + bool inScrollBar = scrollBarRect.Contains(io.MousePos); + + draw_list->AddRectFilled(scrollBarA, scrollBarB, 0xFF101010, 8); + + + ImVec2 scrollBarC(scrollBarMin.x + legendWidth + startFrameOffset, scrollBarMin.y); + ImVec2 scrollBarD(scrollBarMin.x + legendWidth + barWidthInPixels + startFrameOffset, scrollBarMax.y - 2); + draw_list->AddRectFilled(scrollBarC, scrollBarD, (inScrollBar || MovingScrollBar) ? 0xFF606060 : 0xFF505050, 6); + + ImRect barHandleLeft(scrollBarC, ImVec2(scrollBarC.x + 14, scrollBarD.y)); + ImRect barHandleRight(ImVec2(scrollBarD.x - 14, scrollBarC.y), scrollBarD); + + bool onLeft = barHandleLeft.Contains(io.MousePos); + bool onRight = barHandleRight.Contains(io.MousePos); + + static bool sizingRBar = false; + static bool sizingLBar = false; + + draw_list->AddRectFilled(barHandleLeft.Min, barHandleLeft.Max, (onLeft || sizingLBar) ? 0xFFAAAAAA : 0xFF666666, 6); + draw_list->AddRectFilled(barHandleRight.Min, barHandleRight.Max, (onRight || sizingRBar) ? 0xFFAAAAAA : 0xFF666666, 6); + + ImRect scrollBarThumb(scrollBarC, scrollBarD); + static const float MinBarWidth = 44.f; + if (sizingRBar) + { + if (!io.MouseDown[0]) + { + sizingRBar = false; + } + else + { + float barNewWidth = ImMax(barWidthInPixels + io.MouseDelta.x, MinBarWidth); + float barRatio = barNewWidth / barWidthInPixels; + framePixelWidthTarget = framePixelWidth = framePixelWidth / barRatio; + int newVisibleFrameCount = int((canvas_size.x - legendWidth) / framePixelWidthTarget); + int lastFrame = *firstFrame + newVisibleFrameCount; + if (lastFrame > sequence->GetFrameMax()) + { + framePixelWidthTarget = framePixelWidth = (canvas_size.x - legendWidth) / float(sequence->GetFrameMax() - *firstFrame); + } + } + } + else if (sizingLBar) + { + if (!io.MouseDown[0]) + { + sizingLBar = false; + } + else + { + if (fabsf(io.MouseDelta.x) > FLT_EPSILON) + { + float barNewWidth = ImMax(barWidthInPixels - io.MouseDelta.x, MinBarWidth); + float barRatio = barNewWidth / barWidthInPixels; + float previousFramePixelWidthTarget = framePixelWidthTarget; + framePixelWidthTarget = framePixelWidth = framePixelWidth / barRatio; + int newVisibleFrameCount = int(visibleFrameCount / barRatio); + int newFirstFrame = *firstFrame + newVisibleFrameCount - visibleFrameCount; + newFirstFrame = ImClamp(newFirstFrame, sequence->GetFrameMin(), ImMax(sequence->GetFrameMax() - visibleFrameCount, sequence->GetFrameMin())); + if (newFirstFrame == *firstFrame) + { + framePixelWidth = framePixelWidthTarget = previousFramePixelWidthTarget; + } + else + { + *firstFrame = newFirstFrame; + } + } + } + } + else + { + if (MovingScrollBar) + { + if (!io.MouseDown[0]) + { + MovingScrollBar = false; + } + else + { + float framesPerPixelInBar = barWidthInPixels / (float)visibleFrameCount; + *firstFrame = int((io.MousePos.x - panningViewSource.x) / framesPerPixelInBar) - panningViewFrame; + *firstFrame = ImClamp(*firstFrame, sequence->GetFrameMin(), ImMax(sequence->GetFrameMax() - visibleFrameCount, sequence->GetFrameMin())); + } + } + else + { + if (scrollBarThumb.Contains(io.MousePos) && ImGui::IsMouseClicked(0) && firstFrame && !MovingCurrentFrame && movingEntry == -1) + { + MovingScrollBar = true; + panningViewSource = io.MousePos; + panningViewFrame = -*firstFrame; + } + if (!sizingRBar && onRight && ImGui::IsMouseClicked(0)) + sizingRBar = true; + if (!sizingLBar && onLeft && ImGui::IsMouseClicked(0)) + sizingLBar = true; + + } + } + } + } + + ImGui::EndGroup(); + + if (regionRect.Contains(io.MousePos)) + { + bool overCustomDraw = false; + for (auto& custom : customDraws) + { + if (custom.customRect.Contains(io.MousePos)) + { + overCustomDraw = true; + } + } + if (overCustomDraw) + { + } + else + { +#if 0 + frameOverCursor = *firstFrame + (int)(visibleFrameCount * ((io.MousePos.x - (float)legendWidth - canvas_pos.x) / (canvas_size.x - legendWidth))); + //frameOverCursor = max(min(*firstFrame - visibleFrameCount / 2, frameCount - visibleFrameCount), 0); + + /**firstFrame -= frameOverCursor; + *firstFrame *= framePixelWidthTarget / framePixelWidth; + *firstFrame += frameOverCursor;*/ + if (io.MouseWheel < -FLT_EPSILON) + { + *firstFrame -= frameOverCursor; + *firstFrame = int(*firstFrame * 1.1f); + framePixelWidthTarget *= 0.9f; + *firstFrame += frameOverCursor; + } + + if (io.MouseWheel > FLT_EPSILON) + { + *firstFrame -= frameOverCursor; + *firstFrame = int(*firstFrame * 0.9f); + framePixelWidthTarget *= 1.1f; + *firstFrame += frameOverCursor; + } +#endif + } + } + + if (expanded) + { + if (SequencerAddDelButton(draw_list, ImVec2(canvas_pos.x + 2, canvas_pos.y + 2), !*expanded)) + *expanded = !*expanded; + } + + if (delEntry != -1) + { + sequence->Del(delEntry); + if (selectedEntry && (*selectedEntry == delEntry || *selectedEntry >= sequence->GetItemCount())) + *selectedEntry = -1; + } + + if (dupEntry != -1) + { + sequence->Duplicate(dupEntry); + } + return ret; + } +} diff --git a/third_party/ImGuizmo/ImSequencer.h b/third_party/ImGuizmo/ImSequencer.h new file mode 100644 index 0000000..c0a6a37 --- /dev/null +++ b/third_party/ImGuizmo/ImSequencer.h @@ -0,0 +1,79 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#pragma once + +#include + +struct ImDrawList; +struct ImRect; +namespace ImSequencer +{ + enum SEQUENCER_OPTIONS + { + SEQUENCER_EDIT_NONE = 0, + SEQUENCER_EDIT_STARTEND = 1 << 1, + SEQUENCER_CHANGE_FRAME = 1 << 3, + SEQUENCER_ADD = 1 << 4, + SEQUENCER_DEL = 1 << 5, + SEQUENCER_COPYPASTE = 1 << 6, + SEQUENCER_EDIT_ALL = SEQUENCER_EDIT_STARTEND | SEQUENCER_CHANGE_FRAME + }; + + struct SequenceInterface + { + bool focused = false; + virtual int GetFrameMin() const = 0; + virtual int GetFrameMax() const = 0; + virtual int GetItemCount() const = 0; + + virtual void BeginEdit(int /*index*/) {} + virtual void EndEdit() {} + virtual int GetItemTypeCount() const { return 0; } + virtual const char* GetItemTypeName(int /*typeIndex*/) const { return ""; } + virtual const char* GetItemLabel(int /*index*/) const { return ""; } + virtual const char* GetCollapseFmt() const { return "%d Frames / %d entries"; } + + virtual void Get(int index, int** start, int** end, int* type, unsigned int* color) = 0; + virtual void Add(int /*type*/) {} + virtual void Del(int /*index*/) {} + virtual void Duplicate(int /*index*/) {} + + virtual void Copy() {} + virtual void Paste() {} + + virtual size_t GetCustomHeight(int /*index*/) { return 0; } + virtual void DoubleClick(int /*index*/) {} + virtual void CustomDraw(int /*index*/, ImDrawList* /*draw_list*/, const ImRect& /*rc*/, const ImRect& /*legendRect*/, const ImRect& /*clippingRect*/, const ImRect& /*legendClippingRect*/) {} + virtual void CustomDrawCompact(int /*index*/, ImDrawList* /*draw_list*/, const ImRect& /*rc*/, const ImRect& /*clippingRect*/) {} + + virtual ~SequenceInterface() = default; + }; + + + // return true if selection is made + bool Sequencer(SequenceInterface* sequence, int* currentFrame, bool* expanded, int* selectedEntry, int* firstFrame, int sequenceOptions); + +} diff --git a/third_party/ImGuizmo/ImZoomSlider.h b/third_party/ImGuizmo/ImZoomSlider.h new file mode 100644 index 0000000..87853fb --- /dev/null +++ b/third_party/ImGuizmo/ImZoomSlider.h @@ -0,0 +1,245 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v1.92.5 WIP +// +// The MIT License(MIT) +// +// Copyright(c) 2016-2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#pragma once + +namespace ImZoomSlider +{ + typedef int ImGuiZoomSliderFlags; + enum ImGuiPopupFlags_ + { + ImGuiZoomSliderFlags_None = 0, + ImGuiZoomSliderFlags_Vertical = 1, + ImGuiZoomSliderFlags_NoAnchors = 2, + ImGuiZoomSliderFlags_NoMiddleCarets = 4, + ImGuiZoomSliderFlags_NoWheel = 8, + }; + + template bool ImZoomSlider(const T lower, const T higher, T& viewLower, T& viewHigher, float wheelRatio = 0.01f, ImGuiZoomSliderFlags flags = ImGuiZoomSliderFlags_None) + { + bool interacted = false; + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + static const float handleSize = 12; + static const float roundRadius = 3.f; + static const char* controlName = "ImZoomSlider"; + + static bool movingScrollBarSvg = false; + static bool sizingRBarSvg = false; + static bool sizingLBarSvg = false; + static ImGuiID editingId = (ImGuiID)-1; + static float scrollingSource = 0.f; + static float saveViewLower; + static float saveViewHigher; + + const bool isVertical = flags & ImGuiZoomSliderFlags_Vertical; + const ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + const ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + const float canvasSizeLength = isVertical ? ImGui::GetItemRectSize().y : canvasSize.x; + const ImVec2 scrollBarSize = isVertical ? ImVec2(14.f, canvasSizeLength) : ImVec2(canvasSizeLength, 14.f); + + ImGui::InvisibleButton(controlName, scrollBarSize); + const ImGuiID currentId = ImGui::GetID(controlName); + + const bool usingEditingId = currentId == editingId; + const bool canUseControl = usingEditingId || editingId == -1; + const bool movingScrollBar = usingEditingId ? movingScrollBarSvg : false; + const bool sizingRBar = usingEditingId ? sizingRBarSvg : false; + const bool sizingLBar = usingEditingId ? sizingLBarSvg : false; + const int componentIndex = isVertical ? 1 : 0; + const ImVec2 scrollBarMin = ImGui::GetItemRectMin(); + const ImVec2 scrollBarMax = ImGui::GetItemRectMax(); + const ImVec2 scrollBarA = ImVec2(scrollBarMin.x, scrollBarMin.y) - (isVertical ? ImVec2(2,0) : ImVec2(0,2)); + const ImVec2 scrollBarB = isVertical ? ImVec2(scrollBarMax.x - 1.f, scrollBarMin.y + canvasSizeLength) : ImVec2(scrollBarMin.x + canvasSizeLength, scrollBarMax.y - 1.f); + const float scrollStart = ((viewLower - lower) / (higher - lower)) * canvasSizeLength + scrollBarMin[componentIndex]; + const float scrollEnd = ((viewHigher - lower) / (higher - lower)) * canvasSizeLength + scrollBarMin[componentIndex]; + const float screenSize = scrollEnd - scrollStart; + const ImVec2 scrollTopLeft = isVertical ? ImVec2(scrollBarMin.x, scrollStart) : ImVec2(scrollStart, scrollBarMin.y); + const ImVec2 scrollBottomRight = isVertical ? ImVec2(scrollBarMax.x - 2.f, scrollEnd) : ImVec2(scrollEnd, scrollBarMax.y - 2.f); + const bool inScrollBar = canUseControl && ImRect(scrollTopLeft, scrollBottomRight).Contains(io.MousePos); + const ImRect scrollBarRect(scrollBarA, scrollBarB); + const float deltaScreen = io.MousePos[componentIndex] - scrollingSource; + const float deltaView = ((higher - lower) / canvasSizeLength) * deltaScreen; + const uint32_t barColor = ImGui::GetColorU32((inScrollBar || movingScrollBar) ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + const float middleCoord = (scrollStart + scrollEnd) * 0.5f; + const bool insideControl = canUseControl && ImRect(scrollBarMin, scrollBarMax).Contains(io.MousePos); + const bool hasAnchors = !(flags & ImGuiZoomSliderFlags_NoAnchors); + const float viewMinSize = ((3.f * handleSize) / canvasSizeLength) * (higher - lower); + const auto ClipView = [lower, higher, &viewLower, &viewHigher]() { + if (viewLower < lower) + { + const float deltaClip = lower - viewLower; + viewLower += deltaClip; + viewHigher += deltaClip; + } + if (viewHigher > higher) + { + const float deltaClip = viewHigher - higher; + viewLower -= deltaClip; + viewHigher -= deltaClip; + } + }; + + bool onLeft = false; + bool onRight = false; + + draw_list->AddRectFilled(scrollBarA, scrollBarB, 0xFF101010, roundRadius); + draw_list->AddRectFilled(scrollBarA, scrollBarB, 0xFF222222, 0); + draw_list->AddRectFilled(scrollTopLeft, scrollBottomRight, barColor, roundRadius); + + if (!(flags & ImGuiZoomSliderFlags_NoMiddleCarets)) + { + for (float i = 0.5f; i < 3.f; i += 1.f) + { + const float coordA = middleCoord - handleSize * 0.5f; + const float coordB = middleCoord + handleSize * 0.5f; + ImVec2 base = scrollBarMin; + base.x += scrollBarSize.x * 0.25f * i; + base.y += scrollBarSize.y * 0.25f * i; + + if (isVertical) + { + draw_list->AddLine(ImVec2(base.x, coordA), ImVec2(base.x, coordB), ImGui::GetColorU32(ImGuiCol_SliderGrab)); + } + else + { + draw_list->AddLine(ImVec2(coordA, base.y), ImVec2(coordB, base.y), ImGui::GetColorU32(ImGuiCol_SliderGrab)); + } + } + } + + // Mouse wheel + if (io.MouseClicked[0] && insideControl && !inScrollBar) + { + const float ratio = (io.MousePos[componentIndex] - scrollBarMin[componentIndex]) / (scrollBarMax[componentIndex] - scrollBarMin[componentIndex]); + const float size = (higher - lower); + const float halfViewSize = (viewHigher - viewLower) * 0.5f; + const float middle = ratio * size + lower; + viewLower = middle - halfViewSize; + viewHigher = middle + halfViewSize; + ClipView(); + interacted = true; + } + + if (!(flags & ImGuiZoomSliderFlags_NoWheel) && inScrollBar && fabsf(io.MouseWheel) > 0.f) + { + const float ratio = (io.MousePos[componentIndex] - scrollStart) / (scrollEnd - scrollStart); + const float amount = io.MouseWheel * wheelRatio * (viewHigher - viewLower); + + viewLower -= ratio * amount; + viewHigher += (1.f - ratio) * amount; + ClipView(); + interacted = true; + } + + if (screenSize > handleSize * 2.f && hasAnchors) + { + const ImRect barHandleLeft(scrollTopLeft, isVertical ? ImVec2(scrollBottomRight.x, scrollTopLeft.y + handleSize) : ImVec2(scrollTopLeft.x + handleSize, scrollBottomRight.y)); + const ImRect barHandleRight(isVertical ? ImVec2(scrollTopLeft.x, scrollBottomRight.y - handleSize) : ImVec2(scrollBottomRight.x - handleSize, scrollTopLeft.y), scrollBottomRight); + + onLeft = barHandleLeft.Contains(io.MousePos); + onRight = barHandleRight.Contains(io.MousePos); + + draw_list->AddRectFilled(barHandleLeft.Min, barHandleLeft.Max, ImGui::GetColorU32((onLeft || sizingLBar) ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), roundRadius); + draw_list->AddRectFilled(barHandleRight.Min, barHandleRight.Max, ImGui::GetColorU32((onRight || sizingRBar) ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), roundRadius); + } + + if (sizingRBar) + { + if (!io.MouseDown[0]) + { + sizingRBarSvg = false; + editingId = (ImGuiID)-1; + } + else + { + viewHigher = ImMin(saveViewHigher + deltaView, higher); + } + } + else if (sizingLBar) + { + if (!io.MouseDown[0]) + { + sizingLBarSvg = false; + editingId = (ImGuiID)-1; + } + else + { + viewLower = ImMax(saveViewLower + deltaView, lower); + } + } + else + { + if (movingScrollBar) + { + if (!io.MouseDown[0]) + { + movingScrollBarSvg = false; + editingId = (ImGuiID)-1; + } + else + { + viewLower = saveViewLower + deltaView; + viewHigher = saveViewHigher + deltaView; + ClipView(); + } + } + else + { + if (inScrollBar && ImGui::IsMouseClicked(0)) + { + movingScrollBarSvg = true; + scrollingSource = io.MousePos[componentIndex]; + saveViewLower = viewLower; + saveViewHigher = viewHigher; + editingId = currentId; + } + if (!sizingRBar && onRight && ImGui::IsMouseClicked(0) && hasAnchors) + { + sizingRBarSvg = true; + editingId = currentId; + } + if (!sizingLBar && onLeft && ImGui::IsMouseClicked(0) && hasAnchors) + { + sizingLBarSvg = true; + editingId = currentId; + } + } + } + + // minimal size check + if ((viewHigher - viewLower) < viewMinSize) + { + const float middle = (viewLower + viewHigher) * 0.5f; + viewLower = middle - viewMinSize * 0.5f; + viewHigher = middle + viewMinSize * 0.5f; + ClipView(); + } + + return movingScrollBar || sizingRBar || sizingLBar || interacted; + } + +} // namespace diff --git a/third_party/ImGuizmo/Images/nodeeditor.jpg b/third_party/ImGuizmo/Images/nodeeditor.jpg new file mode 100644 index 0000000..cc35b57 Binary files /dev/null and b/third_party/ImGuizmo/Images/nodeeditor.jpg differ diff --git a/third_party/ImGuizmo/LICENSE b/third_party/ImGuizmo/LICENSE new file mode 100644 index 0000000..dcbee45 --- /dev/null +++ b/third_party/ImGuizmo/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Cedric Guillemet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/ImGuizmo/Makefile b/third_party/ImGuizmo/Makefile new file mode 100644 index 0000000..bc86a5d --- /dev/null +++ b/third_party/ImGuizmo/Makefile @@ -0,0 +1,19 @@ +CXXFLAGS=-std=c++11 +CPPFLAGS=-I. -Iexample + +LIB_OBJS = ImGuizmo.o GraphEditor.o ImCurveEdit.o ImGradient.o ImSequencer.o +EXAMPLE_OBJS = example/imgui.o example/imgui_draw.o example/imgui_tables.o example/imgui_widgets.o example/main.o + +EXAMPLE_NAME = example.exe +LDFLAGS=-mwindows -static-libgcc -static-libstdc++ +LIBS=-limm32 -lopengl32 -lgdi32 + +$(EXAMPLE_NAME): $(LIB_OBJS) $(EXAMPLE_OBJS) + $(CXX) $(LDFLAGS) -o $@ $^ $(LIBS) + +example/main.o: CXXFLAGS := -std=c++17 + +clean: + $(RM) $(LIB_OBJS) + $(RM) $(EXAMPLE_OBJS) + $(RM) $(EXAMPLE_NAME) diff --git a/third_party/ImGuizmo/README.md b/third_party/ImGuizmo/README.md new file mode 100644 index 0000000..1cbdc27 --- /dev/null +++ b/third_party/ImGuizmo/README.md @@ -0,0 +1,192 @@ +# ImGuizmo + +Latest stable tagged version is 1.83. Current master version is 1.84 WIP. + +What started with the gizmo is now a collection of dear imgui widgets and more advanced controls. + +## Guizmos + +### ImViewGizmo + +Manipulate view orientation with 1 single line of code + +![Image of ImViewGizmo](http://i.imgur.com/7UVcyDd.gif) + +### ImGuizmo + +ImGuizmo is a small (.h and .cpp) library built ontop of Dear ImGui that allow you to manipulate(Rotate & translate at the moment) 4x4 float matrices. No other dependancies. Coded with Immediate Mode (IM) philosophy in mind. + +Built against DearImgui 1.53WIP + +![Image of Rotation](http://i.imgur.com/y4mcVoT.gif) +![Image of Translation](http://i.imgur.com/o8q8iHq.gif) +![Image of Bounds](http://i.imgur.com/3Ez5LBr.gif) + +There is now a sample for Win32/OpenGL ! With a binary in bin directory. +![Image of Sample](https://i.imgur.com/nXlzyqD.png) + +### ImSequencer + +A WIP little sequencer used to edit frame start/end for different events in a timeline. +![Image of Rotation](http://i.imgur.com/BeyNwCn.png) +Check the sample for the documentation. More to come... + +### Graph Editor + +Nodes + connections. Custom draw inside nodes is possible with the delegate system in place. +![Image of GraphEditor](Images/nodeeditor.jpg) + +### API doc + +Call BeginFrame right after ImGui_XXXX_NewFrame(); + +```C++ +void BeginFrame(); +``` + +return true if mouse cursor is over any gizmo control (axis, plan or screen component) + +```C++ +bool IsOver();** +``` + +return true if mouse IsOver or if the gizmo is in moving state + +```C++ +bool IsUsing();** +``` + +enable/disable the gizmo. Stay in the state until next call to Enable. gizmo is rendered with gray half transparent color when disabled + +```C++ +void Enable(bool enable);** +``` + +helper functions for manualy editing translation/rotation/scale with an input float +translation, rotation and scale float points to 3 floats each +Angles are in degrees (more suitable for human editing) +example: + +```C++ + float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); + ImGui::InputFloat3("Tr", matrixTranslation, 3); + ImGui::InputFloat3("Rt", matrixRotation, 3); + ImGui::InputFloat3("Sc", matrixScale, 3); + ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); +``` + +These functions have some numerical stability issues for now. Use with caution. + +```C++ +void DecomposeMatrixToComponents(const float *matrix, float *translation, float *rotation, float *scale); +void RecomposeMatrixFromComponents(const float *translation, const float *rotation, const float *scale, float *matrix);** +``` + +Render a cube with face color corresponding to face normal. Usefull for debug/test + +```C++ +void DrawCube(const float *view, const float *projection, float *matrix);** +``` + +Call it when you want a gizmo +Needs view and projection matrices. +Matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional. snap points to a float[3] for translation and to a single float for scale or rotation. Snap angle is in Euler Degrees. + +```C++ + enum OPERATION + { + TRANSLATE, + ROTATE, + SCALE + }; + + enum MODE + { + LOCAL, + WORLD + }; + +void Manipulate(const float *view, const float *projection, OPERATION operation, MODE mode, float *matrix, float *deltaMatrix = 0, float *snap = 0);** +``` + +### ImGui Example + +Code for : + +![Image of dialog](http://i.imgur.com/GL5flN1.png) + +```C++ +void EditTransform(const Camera& camera, matrix_t& matrix) +{ + static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::ROTATE); + static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::WORLD); + if (ImGui::IsKeyPressed(90)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + if (ImGui::IsKeyPressed(69)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + if (ImGui::IsKeyPressed(82)) // r Key + mCurrentGizmoOperation = ImGuizmo::SCALE; + if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) + mCurrentGizmoOperation = ImGuizmo::SCALE; + float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + ImGuizmo::DecomposeMatrixToComponents(matrix.m16, matrixTranslation, matrixRotation, matrixScale); + ImGui::InputFloat3("Tr", matrixTranslation, 3); + ImGui::InputFloat3("Rt", matrixRotation, 3); + ImGui::InputFloat3("Sc", matrixScale, 3); + ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix.m16); + + if (mCurrentGizmoOperation != ImGuizmo::SCALE) + { + if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) + mCurrentGizmoMode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) + mCurrentGizmoMode = ImGuizmo::WORLD; + } + static bool useSnap(false); + if (ImGui::IsKeyPressed(83)) + useSnap = !useSnap; + ImGui::Checkbox("", &useSnap); + ImGui::SameLine(); + vec_t snap; + switch (mCurrentGizmoOperation) + { + case ImGuizmo::TRANSLATE: + snap = config.mSnapTranslation; + ImGui::InputFloat3("Snap", &snap.x); + break; + case ImGuizmo::ROTATE: + snap = config.mSnapRotation; + ImGui::InputFloat("Angle Snap", &snap.x); + break; + case ImGuizmo::SCALE: + snap = config.mSnapScale; + ImGui::InputFloat("Scale Snap", &snap.x); + break; + } + ImGuiIO& io = ImGui::GetIO(); + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::Manipulate(camera.mView.m16, camera.mProjection.m16, mCurrentGizmoOperation, mCurrentGizmoMode, matrix.m16, NULL, useSnap ? &snap.x : NULL); +} +``` + +## Install + +ImGuizmo can be installed via [vcpkg](https://github.com/microsoft/vcpkg) and used cmake + +```bash +vcpkg install imguizmo +``` + +See the [vcpkg example](/vcpkg-example) for more details + +## License + +ImGuizmo is licensed under the MIT License, see [LICENSE](/LICENSE) for more information.