...Anm2Ed 2.0

This commit is contained in:
2025-11-13 22:06:09 -05:00
parent 51bf4c2012
commit c57c32aca8
36 changed files with 1003 additions and 333 deletions

View File

@@ -5,7 +5,7 @@ namespace anm2ed::imgui
void Dockspace::tick(Manager& manager, Settings& settings)
{
if (auto document = manager.get(); document)
if (settings.windowIsAnimationPreview) animationPreview.tick(manager, *document, settings);
if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings);
}
void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings,

View File

@@ -22,9 +22,11 @@ namespace anm2ed::imgui
for (auto& document : manager.documents)
{
auto isDirty = document.is_dirty() && document.is_autosave_dirty();
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
if (isDirty && document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document);
if (isDirty)
{
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
if (document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document);
}
}
if (ImGui::Begin("##Documents", nullptr,
@@ -157,5 +159,60 @@ namespace anm2ed::imgui
}
ImGui::End();
if (manager.isAnm2DragDrop)
{
auto drag_drop_reset = [&]()
{
manager.isAnm2DragDrop = false;
manager.anm2DragDropPaths.clear();
manager.anm2DragDropPopup.close();
};
if (manager.anm2DragDropPaths.empty())
drag_drop_reset();
else
{
if (!manager.anm2DragDropPopup.is_open()) manager.anm2DragDropPopup.open();
bool wasOpen = manager.anm2DragDropPopup.is_open();
manager.anm2DragDropPopup.trigger();
if (ImGui::BeginPopupContextWindow(manager.anm2DragDropPopup.label, ImGuiPopupFlags_None))
{
auto document = manager.get();
if (ImGui::MenuItem(manager.anm2DragDropPaths.size() > 1 ? "Open Many Documents" : "Open New Document"))
{
for (auto& path : manager.anm2DragDropPaths)
manager.open(path);
drag_drop_reset();
}
if (ImGui::MenuItem("Merge into Current Document", nullptr, false,
document && !manager.anm2DragDropPaths.empty()))
{
if (document)
{
DOCUMENT_EDIT_PTR(document, "Merge Anm2", Document::ALL, {
for (auto& path : manager.anm2DragDropPaths)
{
anm2::Anm2 source(path);
document->anm2.merge(source, document->directory_get(), path.parent_path());
}
});
drag_drop_reset();
}
}
if (ImGui::MenuItem("Cancel")) drag_drop_reset();
manager.anm2DragDropPopup.end();
ImGui::EndPopup();
}
else if (wasOpen && !manager.anm2DragDropPopup.is_open())
drag_drop_reset();
}
}
}
}

View File

@@ -2,6 +2,7 @@
#include <imgui/imgui_internal.h>
#include <cmath>
#include <set>
#include <sstream>
#include <unordered_map>
@@ -105,6 +106,50 @@ namespace anm2ed::imgui
ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str());
}
namespace
{
struct CheckerStart
{
float position{};
long long index{};
};
CheckerStart checker_start(float minCoord, float offset, float step)
{
float world = minCoord + offset;
long long idx = static_cast<long long>(std::floor(world / step));
float first = minCoord - (world - static_cast<float>(idx) * step);
return {first, idx};
}
}
void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, vec2 offset, float step)
{
if (!drawList || step <= 0.0f) return;
const ImU32 colorLight = IM_COL32(204, 204, 204, 255);
const ImU32 colorDark = IM_COL32(128, 128, 128, 255);
auto [startY, rowIndex] = checker_start(min.y, offset.y, step);
for (float y = startY; y < max.y; y += step, ++rowIndex)
{
float y1 = glm::max(y, min.y);
float y2 = glm::min(y + step, max.y);
if (y2 <= y1) continue;
auto [startX, columnIndex] = checker_start(min.x, offset.x, step);
for (float x = startX; x < max.x; x += step, ++columnIndex)
{
float x1 = glm::max(x, min.x);
float x2 = glm::min(x + step, max.x);
if (x2 <= x1) continue;
bool isDark = ((rowIndex + columnIndex) & 1LL) != 0;
drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight);
}
}
}
void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected)
{
auto* set = (std::set<int>*)self->UserData;
@@ -248,9 +293,12 @@ namespace anm2ed::imgui
return false;
}
bool shortcut(ImGuiKeyChord chord, shortcut::Type type)
bool shortcut(ImGuiKeyChord chord, shortcut::Type type, bool isRepeat)
{
if (ImGui::GetTopMostPopupModal() != nullptr) return false;
if (isRepeat && (type == shortcut::GLOBAL || type == shortcut::FOCUSED)) return chord_repeating(chord);
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
: ImGuiInputFlags_RouteFocused;
if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET)
@@ -301,15 +349,21 @@ namespace anm2ed::imgui
auto viewport = ImGui::GetMainViewport();
if (position == POPUP_CENTER)
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f)));
else
ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None);
if (POPUP_IS_HEIGHT_SET[type])
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type]));
else
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
switch (position)
{
case POPUP_CENTER:
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f)));
if (POPUP_IS_HEIGHT_SET[type])
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type]));
else
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
break;
case POPUP_BY_ITEM:
ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None);
case POPUP_BY_CURSOR:
default:
break;
}
}
void PopupHelper::end() { isJustOpened = false; }

View File

@@ -1,6 +1,7 @@
#pragma once
#include <imgui/imgui.h>
#include <glm/glm.hpp>
#include <set>
#include <string>
#include <unordered_map>
@@ -31,7 +32,8 @@ namespace anm2ed::imgui
enum PopupPosition
{
POPUP_CENTER,
POPUP_BY_ITEM
POPUP_BY_ITEM,
POPUP_BY_CURSOR
};
constexpr float POPUP_MULTIPLIERS[] = {
@@ -171,10 +173,11 @@ namespace anm2ed::imgui
ImGuiSelectableFlags = 0, bool* = nullptr);
void set_item_tooltip_shortcut(const char*, const std::string& = {});
void external_storage_set(ImGuiSelectionExternalStorage*, int, bool);
void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float);
ImVec2 icon_size_get();
bool chord_held(ImGuiKeyChord);
bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate);
bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET);
bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET, bool = false);
class MultiSelectStorage : public std::set<int>
{

View File

@@ -3,10 +3,11 @@
#include <algorithm>
#include <array>
#include <cfloat>
#include <cstddef>
#include <cmath>
#include <filesystem>
#include <format>
#include <ranges>
#include <system_error>
#include <tuple>
#include <vector>
@@ -18,6 +19,7 @@
#include "types.h"
#include "icon.h"
#include "toast.h"
using namespace anm2ed::resource;
using namespace anm2ed::types;
@@ -148,11 +150,12 @@ namespace anm2ed::imgui
auto recentFiles = manager.recent_files_ordered();
if (ImGui::BeginMenu("Open Recent", !recentFiles.empty()))
{
for (auto [i, file] : std::views::enumerate(recentFiles))
for (std::size_t index = 0; index < recentFiles.size(); ++index)
{
const auto& file = recentFiles[index];
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
ImGui::PushID(i);
ImGui::PushID((int)index);
if (ImGui::MenuItem(label.c_str())) manager.open(file.string());
ImGui::PopID();
}
@@ -218,8 +221,11 @@ namespace anm2ed::imgui
if (ImGui::BeginMenu("Window"))
{
for (auto [i, member] : std::views::enumerate(WINDOW_MEMBERS))
ImGui::MenuItem(WINDOW_STRINGS[i], nullptr, &(settings.*member));
for (std::size_t index = 0; index < WINDOW_COUNT; ++index)
{
auto member = WINDOW_MEMBERS[index];
ImGui::MenuItem(WINDOW_STRINGS[index], nullptr, &(settings.*member));
}
ImGui::EndMenu();
}
@@ -288,7 +294,7 @@ namespace anm2ed::imgui
generate.size_set(to_vec2(previewSize));
generate.bind();
generate.viewport_set();
generate.clear(backgroundColor);
generate.clear(vec4(backgroundColor, 1.0f));
if (document && document->reference.itemType == anm2::LAYER)
{
@@ -455,8 +461,9 @@ namespace anm2ed::imgui
if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt;
if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super;
for (auto& key : KEY_MAP | std::views::values)
for (const auto& entry : KEY_MAP)
{
auto key = entry.second;
if (ImGui::IsKeyPressed(key))
{
chord |= key;
@@ -521,34 +528,95 @@ namespace anm2ed::imgui
auto& frames = document->frames.selection;
int length = std::max(1, end - start + 1);
auto ffmpeg_is_executable = [](const std::string& pathString)
{
if (pathString.empty()) return false;
std::error_code ec{};
auto status = std::filesystem::status(pathString, ec);
if (ec || !std::filesystem::is_regular_file(status)) return false;
#ifndef _WIN32
constexpr auto EXEC_PERMS = std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec |
std::filesystem::perms::others_exec;
if ((status.permissions() & EXEC_PERMS) == std::filesystem::perms::none) return false;
#endif
return true;
};
auto png_directory_ensure = [](const std::string& directory)
{
if (directory.empty())
{
toasts.error("PNG output directory must be set.");
return false;
}
std::error_code ec{};
auto pathValue = std::filesystem::path(directory);
auto exists = std::filesystem::exists(pathValue, ec);
if (ec)
{
toasts.error(std::format("Could not access directory: {} ({})", directory, ec.message()));
return false;
}
if (exists)
{
if (!std::filesystem::is_directory(pathValue, ec) || ec)
{
toasts.error(std::format("PNG output path must be a directory: {}", directory));
return false;
}
return true;
}
if (!std::filesystem::create_directories(pathValue, ec) || ec)
{
toasts.error(std::format("Could not create directory: {} ({})", directory, ec.message()));
return false;
}
return true;
};
auto range_to_frames_set = [&]()
{
if (auto item = document->item_get())
{
int duration{};
for (std::size_t index = 0; index < item->frames.size(); ++index)
{
const auto& frame = item->frames[index];
if ((int)index == *frames.begin())
start = duration;
else if ((int)index == *frames.rbegin())
{
end = duration;
break;
}
duration += frame.duration;
}
}
};
auto range_to_animation_set = [&]()
{
start = 0;
end = animation->frameNum - 1;
};
auto range_set = [&]()
{
if (!frames.empty())
{
if (auto item = document->item_get())
{
int duration{};
for (auto [i, frame] : std::views::enumerate(item->frames))
{
if (i == *frames.begin())
start = duration;
else if (i == *frames.rbegin())
{
end = duration;
break;
}
duration += frame.duration;
}
}
}
range_to_frames_set();
else if (!isRange)
{
start = 0;
end = animation->frameNum - 1;
}
range_to_animation_set();
length = std::max(1, end - start + 1);
length = std::max(1, end - (start + 1));
};
auto rows_columns_set = [&]()
@@ -652,15 +720,20 @@ namespace anm2ed::imgui
ImGui::SameLine();
ImGui::BeginDisabled(frames.empty());
if (ImGui::Button("To Selected Frames")) range_set();
if (ImGui::Button("To Selected Frames")) range_to_frames_set();
ImGui::SetItemTooltip("If frames are selected, use that range for the rendered animation.");
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("To Animation Range")) range_to_animation_set();
ImGui::SetItemTooltip("Set the range to the normal range of the animation.");
ImGui::BeginDisabled(!isRange);
{
input_int_range("Start", start, 0, animation->frameNum - 1);
input_int_range("Start", start, 0, animation->frameNum);
ImGui::SetItemTooltip("Set the starting time of the animation.");
input_int_range("End", end, start + 1, animation->frameNum);
input_int_range("End", end, start, animation->frameNum);
ImGui::SetItemTooltip("Set the ending time of the animation.");
}
ImGui::EndDisabled();
@@ -687,9 +760,22 @@ namespace anm2ed::imgui
if (ImGui::Button("Render", widgetSize))
{
manager.isRecordingStart = true;
bool isRender = true;
if (!ffmpeg_is_executable(ffmpegPath))
{
toasts.error("FFmpeg path must point to a valid executable file.");
isRender = false;
}
if (isRender && type == render::PNGS) isRender = png_directory_ensure(path);
if (isRender)
{
manager.isRecordingStart = true;
manager.progressPopup.open();
}
renderPopup.close();
manager.progressPopup.open();
}
ImGui::SetItemTooltip("Render the animation using the current settings.");

View File

@@ -1,12 +1,12 @@
#include "animation_preview.h"
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <ranges>
#include <glm/gtc/type_ptr.hpp>
#include "imgui_internal.h"
#include "imgui_.h"
#include "log.h"
#include "math_.h"
#include "toast.h"
@@ -38,6 +38,7 @@ namespace anm2ed::imgui
auto& frameTime = document.frameTime;
auto& end = manager.recordingEnd;
auto& zoom = document.previewZoom;
auto& overlayIndex = document.overlayIndex;
auto& pan = document.previewPan;
if (manager.isRecording)
@@ -46,19 +47,17 @@ namespace anm2ed::imgui
auto& path = settings.renderPath;
auto& type = settings.renderType;
auto pixels = pixels_get();
renderFrames.push_back(Texture(pixels.data(), size));
if (playback.time > end || playback.isFinished)
{
if (type == render::PNGS)
{
auto& format = settings.renderFormat;
bool isSuccess{true};
for (auto [i, frame] : std::views::enumerate(renderFrames))
for (std::size_t index = 0; index < renderFrames.size(); ++index)
{
auto& frame = renderFrames[index];
std::filesystem::path outputPath =
std::filesystem::path(path) / std::vformat(format, std::make_format_args(i));
std::filesystem::path(path) / std::vformat(format, std::make_format_args(index));
if (!frame.write_png(outputPath))
{
@@ -93,10 +92,11 @@ namespace anm2ed::imgui
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
for (auto [i, frame] : std::views::enumerate(renderFrames))
for (std::size_t index = 0; index < renderFrames.size(); ++index)
{
auto row = (int)(i / columns);
auto column = (int)(i % columns);
const auto& frame = renderFrames[index];
auto row = (int)(index / columns);
auto column = (int)(index % columns);
if (row >= rows || column >= columns) break;
if ((int)frame.pixels.size() < frameWidth * frameHeight * CHANNELS) continue;
@@ -131,6 +131,7 @@ namespace anm2ed::imgui
pan = savedPan;
zoom = savedZoom;
settings = savedSettings;
overlayIndex = savedOverlayIndex;
isSizeTrySet = true;
if (settings.timelineIsSound) audioStream.capture_end(mixer);
@@ -140,6 +141,13 @@ namespace anm2ed::imgui
manager.isRecording = false;
manager.progressPopup.close();
}
else
{
bind();
auto pixels = pixels_get();
renderFrames.push_back(Texture(pixels.data(), size));
}
}
if (playback.isPlaying)
@@ -247,7 +255,7 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit3("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Change the background color.");
ImGui::SameLine();
ImGui::Checkbox("Axes", &isAxes);
@@ -311,6 +319,7 @@ namespace anm2ed::imgui
settings.timelineIsOnlyShowLayers = true;
settings.onionskinIsEnabled = false;
savedOverlayIndex = overlayIndex;
savedZoom = zoom;
savedPan = pan;
@@ -329,10 +338,12 @@ namespace anm2ed::imgui
playback.time = manager.recordingStart;
}
if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail()));
viewport_set();
size_set(to_vec2(ImGui::GetContentRegionAvail()));
bind();
clear();
viewport_set();
clear(manager.isRecording && settings.renderIsRawAnimation ? vec4() : vec4(backgroundColor, 1.0f));
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
@@ -468,9 +479,7 @@ namespace anm2ed::imgui
unbind();
ImGui::RenderColorRectWithAlphaCheckerboard(ImGui::GetWindowDrawList(), min, max, 0, CHECKER_SIZE,
to_imvec2(-size + pan));
ImGui::GetCurrentWindow()->DrawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(backgroundColor)));
render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - pan, CHECKER_SIZE);
ImGui::Image(texture, to_imvec2(size));
isPreviewHovered = ImGui::IsItemHovered();

View File

@@ -1,7 +1,5 @@
#pragma once
#include <future>
#include "audio_stream.h"
#include "canvas.h"
#include "manager.h"
@@ -19,15 +17,13 @@ namespace anm2ed::imgui
Settings savedSettings{};
float savedZoom{};
glm::vec2 savedPan{};
int savedOverlayIndex{};
glm::ivec2 mousePos{};
std::vector<resource::Texture> renderFrames{};
std::future<bool> renderFuture{};
bool isRenderFutureValid{};
std::string renderOutputPath{};
public:
AnimationPreview();
void tick(Manager&, Document&, Settings&);
void tick(Manager&, Settings&);
void update(Manager&, Settings&, Resources&);
};
}

View File

@@ -1,6 +1,6 @@
#include "animations.h"
#include <ranges>
#include <cstddef>
#include "toast.h"
#include "vector_.h"
@@ -24,6 +24,21 @@ namespace anm2ed::imgui
hovered = -1;
auto animations_remove = [&]()
{
if (!selection.empty())
{
for (auto it = selection.rbegin(); it != selection.rend(); ++it)
{
auto i = *it;
if (overlayIndex == i) overlayIndex = -1;
if (reference.animationIndex == i) reference.animationIndex = -1;
anm2.animations.items.erase(anm2.animations.items.begin() + i);
}
selection.clear();
}
};
if (ImGui::Begin("Animations", &settings.windowIsAnimations))
{
auto childSize = size_without_footer_get();
@@ -32,12 +47,13 @@ namespace anm2ed::imgui
{
selection.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
for (std::size_t index = 0; index < anm2.animations.items.size(); ++index)
{
ImGui::PushID(i);
auto& animation = anm2.animations.items[index];
ImGui::PushID((int)index);
auto isDefault = anm2.animations.defaultAnimation == animation.name;
auto isReferenced = reference.animationIndex == i;
auto isReferenced = reference.animationIndex == (int)index;
auto font = isDefault && isReferenced ? font::BOLD_ITALICS
: isDefault ? font::BOLD
@@ -45,14 +61,14 @@ namespace anm2ed::imgui
: font::REGULAR;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
ImGui::SetNextItemSelectionUserData((int)i);
if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i),
animation.name, selection.contains((int)i)))
ImGui::SetNextItemSelectionUserData((int)index);
if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, index),
animation.name, selection.contains((int)index)))
{
reference = {(int)i};
reference = {(int)index};
document.frames.clear();
}
if (ImGui::IsItemHovered()) hovered = (int)i;
if (ImGui::IsItemHovered()) hovered = (int)index;
ImGui::PopFont();
if (ImGui::BeginItemTooltip())
@@ -121,23 +137,7 @@ namespace anm2ed::imgui
auto cut = [&]()
{
copy();
auto remove = [&]()
{
if (!selection.empty())
{
for (auto& i : selection | std::views::reverse)
anm2.animations.items.erase(anm2.animations.items.begin() + i);
selection.clear();
}
else if (hovered > -1)
{
anm2.animations.items.erase(anm2.animations.items.begin() + hovered);
hovered = -1;
}
};
DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, remove());
DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, animations_remove());
};
auto paste = [&]()
@@ -275,19 +275,7 @@ namespace anm2ed::imgui
shortcut(manager.chords[SHORTCUT_REMOVE]);
if (ImGui::Button("Remove", widgetSize))
{
auto remove = [&]()
{
for (auto& i : selection | std::views::reverse)
{
if (i == overlayIndex) overlayIndex = -1;
anm2.animations.items.erase(anm2.animations.items.begin() + i);
}
selection.clear();
};
DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove());
}
DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, animations_remove());
set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutRemove);
ImGui::SameLine();
@@ -328,14 +316,16 @@ namespace anm2ed::imgui
{
mergeSelection.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
for (std::size_t index = 0; index < anm2.animations.items.size(); ++index)
{
if (i == mergeReference) continue;
if ((int)index == mergeReference) continue;
ImGui::PushID(i);
auto& animation = anm2.animations.items[index];
ImGui::SetNextItemSelectionUserData(i);
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i));
ImGui::PushID((int)index);
ImGui::SetNextItemSelectionUserData((int)index);
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains((int)index));
ImGui::PopID();
}
@@ -399,4 +389,4 @@ namespace anm2ed::imgui
}
ImGui::End();
}
}
}

View File

@@ -1,8 +1,10 @@
#include "spritesheet_editor.h"
#include <cmath>
#include <format>
#include <utility>
#include "imgui_.h"
#include "imgui_internal.h"
#include "math_.h"
#include "tool.h"
@@ -41,6 +43,7 @@ namespace anm2ed::imgui
auto& isGridSnap = settings.editorIsGridSnap;
auto& zoomStep = settings.viewZoomStep;
auto& isBorder = settings.editorIsBorder;
auto& isTransparent = settings.editorIsTransparent;
auto spritesheet = document.spritesheet_get();
auto& tool = settings.tool;
auto& shaderGrid = resources.shaders[shader::GRID];
@@ -101,22 +104,40 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Change the background color.");
auto subChildSize = ImVec2(row_widget_width_get(2), ImGui::GetContentRegionAvail().y);
ImGui::Checkbox("Border", &isBorder);
ImGui::SetItemTooltip("Toggle a border appearing around the spritesheet.");
if (ImGui::BeginChild("##Background Child 1", subChildSize))
{
ImGui::ColorEdit3("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::SetItemTooltip("Change the background color.");
ImGui::Checkbox("Border", &isBorder);
ImGui::SetItemTooltip("Toggle a border appearing around the spritesheet.");
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Background Child 2", subChildSize))
{
ImGui::Checkbox("Transparent", &isTransparent);
ImGui::SetItemTooltip("Toggle the spritesheet editor being transparent.");
}
ImGui::EndChild();
}
ImGui::EndChild();
auto drawList = ImGui::GetCurrentWindow()->DrawList;
auto cursorScreenPos = ImGui::GetCursorScreenPos();
auto min = ImGui::GetCursorScreenPos();
auto max = to_imvec2(to_vec2(min) + size);
size_set(to_vec2(ImGui::GetContentRegionAvail()));
bind();
viewport_set();
clear();
clear(isTransparent ? vec4() : vec4(backgroundColor, 1.0f));
auto frame = document.frame_get();
@@ -127,7 +148,11 @@ namespace anm2ed::imgui
auto spritesheetModel = math::quad_model_get(texture.size);
auto spritesheetTransform = transform * spritesheetModel;
texture_render(shaderTexture, texture.id, spritesheetTransform);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
if (isBorder)
rect_render(dashedShader, spritesheetTransform, spritesheetModel, color::WHITE, BORDER_DASH_LENGTH,
BORDER_DASH_GAP, BORDER_DASH_OFFSET);
@@ -145,14 +170,12 @@ namespace anm2ed::imgui
}
}
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
unbind();
ImGui::RenderColorRectWithAlphaCheckerboard(ImGui::GetWindowDrawList(), min, max, 0, CHECKER_SIZE,
to_imvec2(-size * 0.5f + pan));
ImGui::GetCurrentWindow()->DrawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(backgroundColor)));
ImGui::Image(texture, to_imvec2(size));
render_checker_background(drawList, min, max, -size * 0.5f - pan, CHECKER_SIZE);
if (!isTransparent) drawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(vec4(backgroundColor, 1.0f))));
drawList->AddImage(texture, min, max);
ImGui::InvisibleButton("##Spritesheet Editor", to_imvec2(size));
if (ImGui::IsItemHovered())
{
@@ -325,13 +348,18 @@ namespace anm2ed::imgui
}
case tool::COLOR_PICKER:
{
if (isDuring)
if (spritesheet && isDuring)
{
auto position = to_vec2(ImGui::GetMousePos());
toolColor = pixel_read(position, {settings.windowSize.x, settings.windowSize.y});
toolColor = spritesheet->texture.pixel_read(mousePos);
if (ImGui::BeginTooltip())
{
ImGui::ColorButton("##Color Picker Button", to_imvec4(toolColor));
ImGui::SameLine();
auto rgba8 = glm::clamp(ivec4(toolColor * 255.0f + 0.5f), ivec4(0), ivec4(255));
auto hex = std::format("#{:02X}{:02X}{:02X}{:02X}", rgba8.r, rgba8.g, rgba8.b, rgba8.a);
ImGui::TextUnformatted(hex.c_str());
ImGui::SameLine();
ImGui::Text("(%d, %d, %d, %d)", rgba8.r, rgba8.g, rgba8.b, rgba8.a);
ImGui::EndTooltip();
}
}

View File

@@ -1,7 +1,7 @@
#include "timeline.h"
#include <algorithm>
#include <ranges>
#include <cstddef>
#include <imgui_internal.h>
@@ -53,8 +53,11 @@ namespace anm2ed::imgui
{
if (auto item = animation->item_get(reference.itemType, reference.itemID); item)
{
for (auto& i : frames.selection | std::views::reverse)
for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it)
{
auto i = *it;
item->frames.erase(item->frames.begin() + i);
}
reference.frameIndex = -1;
frames.clear();
@@ -93,7 +96,12 @@ namespace anm2ed::imgui
document.snapshot("Paste Frame(s)");
std::set<int> indices{};
std::string errorString{};
auto insertIndex = reference.frameIndex == -1 ? item->frames.size() : reference.frameIndex + 1;
int insertIndex = (int)item->frames.size();
if (!frames.selection.empty())
insertIndex = std::min((int)item->frames.size(), *frames.selection.rbegin() + 1);
else if (reference.frameIndex >= 0 && reference.frameIndex < (int)item->frames.size())
insertIndex = reference.frameIndex + 1;
auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex;
if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString))
{
@@ -249,7 +257,7 @@ namespace anm2ed::imgui
ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE;
int visibleIcon = item->isVisible ? icon::VISIBLE : icon::INVISIBLE;
if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get()))
DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, item->isVisible = !item->isVisible);
ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show.");
@@ -544,15 +552,17 @@ namespace anm2ed::imgui
frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape);
for (auto [i, frame] : std::views::enumerate(item->frames))
for (std::size_t frameIndex = 0; frameIndex < item->frames.size(); ++frameIndex)
{
ImGui::PushID(i);
auto& frame = item->frames[frameIndex];
ImGui::PushID((int)frameIndex);
auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i};
auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)frameIndex};
auto isFrameVisible = isVisible && frame.isVisible;
auto isReferenced = reference == frameReference;
auto isSelected =
(frames.selection.contains(i) && reference.itemType == type && reference.itemID == id) || isReferenced;
(frames.selection.contains((int)frameIndex) && reference.itemType == type && reference.itemID == id) ||
isReferenced;
if (type == anm2::TRIGGER) frameTime = frame.atFrame;
@@ -569,7 +579,7 @@ namespace anm2ed::imgui
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden);
ImGui::SetNextItemAllowOverlap();
ImGui::SetNextItemSelectionUserData((int)i);
ImGui::SetNextItemSelectionUserData((int)frameIndex);
if (ImGui::Selectable("##Frame Button", true, ImGuiSelectableFlags_None, buttonSize))
{
if (type == anm2::LAYER)
@@ -785,9 +795,10 @@ namespace anm2ed::imgui
draggedTrigger->atFrame = glm::clamp(
hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1);
for (auto&& [i, trigger] : std::views::enumerate(animation->triggers.frames))
for (std::size_t triggerIndex = 0; triggerIndex < animation->triggers.frames.size(); ++triggerIndex)
{
if (i == draggedTriggerIndex) continue;
if ((int)triggerIndex == draggedTriggerIndex) continue;
auto& trigger = animation->triggers.frames[triggerIndex];
if (trigger.atFrame == draggedTrigger->atFrame) draggedTrigger->atFrame--;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
@@ -888,8 +899,9 @@ namespace anm2ed::imgui
frames_child_row(anm2::LAYER, id);
}
for (auto& id : animation->nullAnimations | std::views::keys)
for (const auto& entry : animation->nullAnimations)
{
auto id = entry.first;
if (auto item = animation->item_get(anm2::NULL_, id); item)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
frames_child_row(anm2::NULL_, id);
@@ -1259,9 +1271,12 @@ namespace anm2ed::imgui
if (ImGui::Button("Bake", widgetSize))
{
if (auto item = document.item_get())
for (auto i : frames.selection | std::views::reverse)
for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it)
{
auto i = *it;
DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES,
item->frames_bake(i, interval, isRoundScale, isRoundRotation));
}
bakePopup.close();
}
ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected.");
@@ -1274,40 +1289,42 @@ namespace anm2ed::imgui
ImGui::EndPopup();
}
if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle();
if (animation)
{
if (chord_repeating(manager.chords[SHORTCUT_PREVIOUS_FRAME]))
if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle();
if (shortcut(manager.chords[SHORTCUT_PREVIOUS_FRAME], shortcut::GLOBAL, true))
{
playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX);
document.frameTime = playback.time;
}
if (chord_repeating(manager.chords[SHORTCUT_NEXT_FRAME]))
if (shortcut(manager.chords[SHORTCUT_NEXT_FRAME], shortcut::GLOBAL, true))
{
playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX);
document.frameTime = playback.time;
}
}
if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_SHORTEN_FRAME])) document.snapshot("Shorten Frame");
if (chord_repeating(manager.chords[SHORTCUT_SHORTEN_FRAME]))
{
if (auto frame = document.frame_get())
if (shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL)) document.snapshot("Shorten Frame");
if (shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL, true))
{
frame->shorten();
document.change(Document::FRAMES);
if (auto frame = document.frame_get())
{
frame->shorten();
document.change(Document::FRAMES);
}
}
}
if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_EXTEND_FRAME])) document.snapshot("Extend Frame");
if (chord_repeating(manager.chords[SHORTCUT_EXTEND_FRAME]))
{
if (auto frame = document.frame_get())
if (shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL)) document.snapshot("Extend Frame");
if (shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL, true))
{
frame->extend();
document.change(Document::FRAMES);
if (auto frame = document.frame_get())
{
frame->extend();
document.change(Document::FRAMES);
}
}
}
}

View File

@@ -36,7 +36,8 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders))
{
for (auto [i, file] : std::views::enumerate(manager.recentFiles))
auto recentFiles = manager.recent_files_ordered();
for (auto [i, file] : std::views::enumerate(recentFiles))
{
ImGui::PushID(i);