diff --git a/.gitignore b/.gitignore index 94fd8af..9e4730e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ workshop/resources cmake-build-debug/ .vs/ .idea/ +.codex \ No newline at end of file diff --git a/compile_commands.json b/compile_commands.json index bc0dce8..e2be980 120000 --- a/compile_commands.json +++ b/compile_commands.json @@ -1 +1 @@ -/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-debug/compile_commands.json \ No newline at end of file +/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-release/compile_commands.json \ No newline at end of file diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index eed8797..dc22786 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -81,7 +81,6 @@ namespace anm2ed::imgui {0.6353f, 0.2235f, 0.3647f, 1.0f}}; constexpr auto FRAME_MULTIPLE = 5; - constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop"; constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; // Extra delay for frame info tooltip. #define ITEM_CHILD_WIDTH ImGui::GetTextLineHeightWithSpacing() * 12.5 @@ -1100,6 +1099,96 @@ namespace anm2ed::imgui ImGui::EndChild(); }; + anm2::Type frameMoveDropType = anm2::NONE; + int frameMoveDropItemID = -1; + int frameMoveDropIndex = -1; + bool isFrameMoveDropTarget = false; + + auto frame_move_drag_clear = [&]() + { + frameMoveDrag = {}; + frameSelectionLocked.clear(); + }; + + auto time_from_index = [](anm2::Item* target, int index) + { + if (!target || target->frames.empty()) return 0.0f; + index = std::clamp(index, 0, (int)target->frames.size()); + float timeAccum = 0.0f; + for (int n = 0; n < index && n < (int)target->frames.size(); ++n) + timeAccum += target->frames[n].duration; + return timeAccum; + }; + + auto frames_move_to = [&](anm2::Type targetType, int targetID, int insertIndex) + { + if (!frameMoveDrag.isActive || !animation || frameMoveDrag.animationIndex != reference.animationIndex) return; + if (frameMoveDrag.type == anm2::TRIGGER || targetType == anm2::TRIGGER) return; + + auto sourceItem = animation->item_get(frameMoveDrag.type, frameMoveDrag.itemID); + auto targetItem = animation->item_get(targetType, targetID); + if (!sourceItem || !targetItem) return; + + std::vector indices = frameMoveDrag.indices; + if (indices.empty() && frameMoveDrag.frameIndex >= 0) indices.push_back(frameMoveDrag.frameIndex); + std::sort(indices.begin(), indices.end()); + indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); + indices.erase(std::remove_if(indices.begin(), indices.end(), + [&](int i) { return i < 0 || i >= (int)sourceItem->frames.size(); }), + indices.end()); + if (indices.empty()) return; + + int insertPosResult = -1; + int insertedCount = 0; + DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, { + std::vector movedFrames; + movedFrames.reserve(indices.size()); + + for (int i : indices) + movedFrames.push_back(std::move(sourceItem->frames[i])); + + for (auto it = indices.rbegin(); it != indices.rend(); ++it) + sourceItem->frames.erase(sourceItem->frames.begin() + *it); + + int desired = std::clamp(insertIndex, 0, (int)targetItem->frames.size()); + if (sourceItem == targetItem) + { + int removedBefore = 0; + for (int i : indices) + if (i < desired) ++removedBefore; + desired -= removedBefore; + } + desired = std::clamp(desired, 0, (int)targetItem->frames.size()); + + insertPosResult = desired; + insertedCount = (int)movedFrames.size(); + targetItem->frames.insert(targetItem->frames.begin() + insertPosResult, + std::make_move_iterator(movedFrames.begin()), + std::make_move_iterator(movedFrames.end())); + }); + + if (insertedCount > 0) + { + frames.selection.clear(); + for (int offset = 0; offset < insertedCount; ++offset) + frames.selection.insert(insertPosResult + offset); + + reference = {reference.animationIndex, targetType, targetID, insertPosResult}; + document.frameTime = time_from_index(targetItem, reference.frameIndex); + frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); + frameSelectionSnapshotReference = reference; + frameSelectionLocked.clear(); + isFrameSelectionLocked = false; + frameFocusIndex = reference.frameIndex; + frameFocusRequested = true; + if (targetType == anm2::LAYER) + { + if (auto it = anm2.content.layers.find(targetID); it != anm2.content.layers.end()) + document.spritesheet.reference = it->second.spritesheetID; + } + } + }; + auto frame_child = [&](anm2::Type type, int id, int& index, float width) { auto item = animation ? animation->item_get(type, id) : nullptr; @@ -1237,7 +1326,7 @@ namespace anm2ed::imgui { float frameTime{}; - if (ImGui::IsWindowHovered() && + if (!frameMoveDrag.isActive && ImGui::IsWindowHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right)) && !ImGui::IsAnyItemHovered()) reference_set_item(type, id); @@ -1253,7 +1342,65 @@ namespace anm2ed::imgui drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor)); } - if (type != anm2::TRIGGER) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); + bool isFrameSelectionStarted = type != anm2::TRIGGER && !frameMoveDrag.isActive; + if (isFrameSelectionStarted) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); + + bool isFrameMovePreview = false; + ImVec2 frameMovePreviewMin{}; + ImVec2 frameMovePreviewMax{}; + bool isFrameMoveHoveredFrame = false; + ImVec2 frameMoveHoveredFrameMin{}; + ImVec2 frameMoveHoveredFrameMax{}; + + if (frameMoveDrag.isActive && type != anm2::TRIGGER) + { + auto mousePos = ImGui::GetIO().MousePos; + auto rowMin = cursorScreenPos; + auto rowMax = ImVec2(cursorScreenPos.x + width, cursorScreenPos.y + frameSize.y); + if (mousePos.x >= rowMin.x && mousePos.x < rowMax.x && mousePos.y >= rowMin.y && mousePos.y < rowMax.y) + { + auto mouseX = mousePos.x - cursorScreenPos.x; + auto targetTime = glm::max(0.0f, mouseX / frameSize.x); + int dropIndex = (int)item->frames.size(); + float dropFrameTime{}; + float frameTime{}; + + for (auto [i, frame] : std::views::enumerate(item->frames)) + { + auto frameStart = frameTime; + auto frameEnd = frameStart + frame.duration; + if (!isFrameMoveHoveredFrame && targetTime >= frameStart && targetTime < frameEnd) + { + isFrameMoveHoveredFrame = true; + frameMoveHoveredFrameMin = ImVec2(cursorScreenPos.x + frameStart * frameSize.x, cursorScreenPos.y); + frameMoveHoveredFrameMax = ImVec2(cursorScreenPos.x + frameEnd * frameSize.x, + cursorScreenPos.y + frameSize.y); + } + + auto midpoint = frameStart + ((float)frame.duration * 0.5f); + if (targetTime < midpoint) + { + dropIndex = (int)i; + dropFrameTime = frameStart; + break; + } + + frameTime = frameEnd; + dropFrameTime = frameTime; + } + + frameMoveDropType = type; + frameMoveDropItemID = id; + frameMoveDropIndex = dropIndex; + isFrameMoveDropTarget = true; + + auto dropX = cursorScreenPos.x + dropFrameTime * frameSize.x; + auto previewWidth = glm::max(frameSize.x, (float)frameMoveDrag.duration * frameSize.x); + frameMovePreviewMin = ImVec2(dropX, cursorScreenPos.y); + frameMovePreviewMax = ImVec2(dropX + previewWidth, cursorScreenPos.y + frameSize.y); + isFrameMovePreview = true; + } + } for (auto [i, frame] : std::views::enumerate(item->frames)) { @@ -1344,12 +1491,9 @@ namespace anm2ed::imgui if (type != anm2::TRIGGER) { - if (!draggedFrame && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) + if (!draggedFrame && !frameMoveDrag.isActive && ImGui::IsItemActive() && + ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - frameDragDrop = {}; - frameDragDrop.type = type; - frameDragDrop.itemID = id; - frameDragDrop.animationIndex = reference.animationIndex; frameSelectionLocked.clear(); auto append_valid_indices = [&](const auto& container) @@ -1358,7 +1502,13 @@ namespace anm2ed::imgui if (idx >= 0 && idx < (int)item->frames.size()) frameSelectionLocked.push_back(idx); }; - if (isReferenced) append_valid_indices(frames.selection); + if (frameSelectionSnapshotReference.animationIndex == reference.animationIndex && + frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id && + std::find(frameSelectionSnapshot.begin(), frameSelectionSnapshot.end(), (int)i) != + frameSelectionSnapshot.end()) + append_valid_indices(frameSelectionSnapshot); + else if (isReferenced) + append_valid_indices(frames.selection); auto contains_index = [&](const std::vector& container, int index) { return std::find(container.begin(), container.end(), index) != container.end(); }; @@ -1381,95 +1531,20 @@ namespace anm2ed::imgui frameSelectionLocked.erase(std::unique(frameSelectionLocked.begin(), frameSelectionLocked.end()), frameSelectionLocked.end()); - frameDragDropPayload = {type, id, reference.animationIndex}; - ImGui::SetDragDropPayload(FRAME_DRAG_PAYLOAD_ID, &frameDragDropPayload, sizeof(frameDragDropPayload)); - ImGui::EndDragDropSource(); - } + int dragDuration = 0; + for (int idx : frameSelectionLocked) + if (idx >= 0 && idx < (int)item->frames.size()) dragDuration += item->frames[idx].duration; + dragDuration = glm::max(1, dragDuration); - if (!draggedFrame && ImGui::BeginDragDropTarget()) - { - if (auto payload = ImGui::AcceptDragDropPayload(FRAME_DRAG_PAYLOAD_ID)) - { - auto source = static_cast(payload->Data); - auto sameAnimation = source && source->animationIndex == reference.animationIndex; - auto sourceItem = - sameAnimation && animation ? animation->item_get(source->type, source->itemID) : nullptr; - auto targetItem = animation ? animation->item_get(type, id) : nullptr; - - auto time_from_index = [&](anm2::Item* target, int index) - { - if (!target || target->frames.empty()) return 0.0f; - index = std::clamp(index, 0, (int)target->frames.size()); - float timeAccum = 0.0f; - for (int n = 0; n < index && n < (int)target->frames.size(); ++n) - timeAccum += target->frames[n].duration; - return timeAccum; - }; - - if (source && sourceItem && targetItem && source->type != anm2::TRIGGER && type != anm2::TRIGGER) - { - std::vector indices = frameSelectionLocked; - if (indices.empty()) indices.push_back((int)i); - std::sort(indices.begin(), indices.end()); - indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); - - int insertPosResult = -1; - int insertedCount = 0; - DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, { - std::vector movedFrames; - movedFrames.reserve(indices.size()); - - for (int i : indices) - if (i >= 0 && i < (int)sourceItem->frames.size()) - movedFrames.push_back(std::move(sourceItem->frames[i])); - - for (auto it = indices.rbegin(); it != indices.rend(); ++it) - if (*it >= 0 && *it < (int)sourceItem->frames.size()) - sourceItem->frames.erase(sourceItem->frames.begin() + *it); - - const int dropIndex = (int)i; - int desired = std::clamp(dropIndex + 1, 0, (int)targetItem->frames.size()); - if (sourceItem == targetItem) - { - if (dropIndex < indices.front()) - desired = dropIndex; - else if (dropIndex > indices.back()) - desired = dropIndex + 1; - else - desired = indices.front(); - - int removedBefore = 0; - for (int i : indices) - if (i < desired) ++removedBefore; - desired -= removedBefore; - } - desired = std::clamp(desired, 0, (int)targetItem->frames.size()); - - insertPosResult = desired; - insertedCount = (int)movedFrames.size(); - targetItem->frames.insert(targetItem->frames.begin() + insertPosResult, - std::make_move_iterator(movedFrames.begin()), - std::make_move_iterator(movedFrames.end())); - }); - - if (insertedCount > 0) - { - frames.selection.clear(); - for (int offset = 0; offset < insertedCount; ++offset) - frames.selection.insert(insertPosResult + offset); - - reference = {reference.animationIndex, type, id, insertPosResult}; - document.frameTime = time_from_index(targetItem, reference.frameIndex); - if (type == anm2::LAYER) - { - if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end()) - document.spritesheet.reference = it->second.spritesheetID; - } - } - } - } - - ImGui::EndDragDropTarget(); + frameMoveDrag = { + .type = type, + .itemID = id, + .animationIndex = reference.animationIndex, + .frameIndex = (int)i, + .duration = dragDuration, + .indices = frameSelectionLocked, + .isActive = true, + }; } } @@ -1517,7 +1592,20 @@ namespace anm2ed::imgui ImGui::PopID(); } - if (type != anm2::TRIGGER) frames.selection.finish(); + if (isFrameMovePreview) + { + drawList->AddRectFilled(frameMovePreviewMin, frameMovePreviewMax, + ImGui::GetColorU32(ImGuiCol_DragDropTargetBg), FRAME_ROUNDING); + drawList->AddRect(frameMovePreviewMin, frameMovePreviewMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget), + FRAME_ROUNDING, 0, ImGui::GetStyle().DragDropTargetBorderSize); + } + + if (isFrameMoveHoveredFrame) + drawList->AddRect(frameMoveHoveredFrameMin, frameMoveHoveredFrameMax, + ImGui::GetColorU32(ImGuiCol_DragDropTarget), FRAME_ROUNDING, 0, + ImGui::GetStyle().DragDropTargetBorderSize * 1.5f); + + if (isFrameSelectionStarted) frames.selection.finish(); if (isFrameSelectionLocked) { @@ -1813,6 +1901,11 @@ namespace anm2ed::imgui isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); frames_child(); + if (frameMoveDrag.isActive && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) + { + if (isFrameMoveDropTarget) frames_move_to(frameMoveDropType, frameMoveDropItemID, frameMoveDropIndex); + frame_move_drag_clear(); + } items_child(); } ImGui::PopStyleVar(); diff --git a/src/imgui/window/timeline.hpp b/src/imgui/window/timeline.hpp index 761f27c..b97c57a 100644 --- a/src/imgui/window/timeline.hpp +++ b/src/imgui/window/timeline.hpp @@ -10,18 +10,15 @@ namespace anm2ed::imgui { - struct FrameDragDrop - { - anm2::Type type{anm2::NONE}; - int itemID{-1}; - int animationIndex{-1}; - }; - - struct FrameDragDropPayload + struct FrameMoveDrag { anm2::Type type{anm2::NONE}; int itemID{-1}; int animationIndex{-1}; + int frameIndex{-1}; + int duration{1}; + std::vector indices{}; + bool isActive{}; }; class Timeline @@ -44,8 +41,7 @@ namespace anm2ed::imgui bool isDraggedFrameSnapshot{}; bool frameFocusRequested{}; int frameFocusIndex{-1}; - FrameDragDrop frameDragDrop{}; - FrameDragDropPayload frameDragDropPayload{}; + FrameMoveDrag frameMoveDrag{}; std::vector frameSelectionSnapshot{}; std::vector frameSelectionLocked{}; bool isFrameSelectionLocked{}; diff --git a/workshop/metadata.xml b/workshop/metadata.xml index 2b36552..e35f337 100644 --- a/workshop/metadata.xml +++ b/workshop/metadata.xml @@ -53,6 +53,6 @@ Alternatively, if you have subscribed to the mod, you can find the latest releas [h3]Happy animating![/h3] [img]https://files.catbox.moe/4auc1c.gif[/img] - 2.17 + 2.18 Public