some bug fixes, added spritesheet filepath setting, etc.
This commit is contained in:
@@ -226,6 +226,8 @@ namespace anm2ed::anm2
|
|||||||
{
|
{
|
||||||
WorkingDirectory workingDirectory(directory);
|
WorkingDirectory workingDirectory(directory);
|
||||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||||
|
if (this->path.empty()) return false;
|
||||||
|
path::ensure_directory(this->path.parent_path());
|
||||||
return texture.write_png(this->path);
|
return texture.write_png(this->path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +252,7 @@ namespace anm2ed::anm2
|
|||||||
hash_combine(seed, std::hash<int>{}(texture.size.y));
|
hash_combine(seed, std::hash<int>{}(texture.size.y));
|
||||||
hash_combine(seed, std::hash<int>{}(texture.channels));
|
hash_combine(seed, std::hash<int>{}(texture.channels));
|
||||||
hash_combine(seed, std::hash<int>{}(texture.filter));
|
hash_combine(seed, std::hash<int>{}(texture.filter));
|
||||||
|
hash_combine(seed, std::hash<std::string>{}(path::to_utf8(path)));
|
||||||
|
|
||||||
if (!texture.pixels.empty())
|
if (!texture.pixels.empty())
|
||||||
{
|
{
|
||||||
@@ -261,19 +264,6 @@ namespace anm2ed::anm2
|
|||||||
hash_combine(seed, 0);
|
hash_combine(seed, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [id, region] : regions)
|
|
||||||
{
|
|
||||||
hash_combine(seed, std::hash<int>{}(id));
|
|
||||||
hash_combine(seed, std::hash<std::string>{}(region.name));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.crop.x));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.crop.y));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.size.x));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.size.y));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.pivot.x));
|
|
||||||
hash_combine(seed, std::hash<float>{}(region.pivot.y));
|
|
||||||
hash_combine(seed, std::hash<int>{}(static_cast<int>(region.origin)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<uint64_t>(seed);
|
return static_cast<uint64_t>(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,7 @@ namespace anm2ed
|
|||||||
#define FILTER_LIST \
|
#define FILTER_LIST \
|
||||||
X(NO_FILTER, {}) \
|
X(NO_FILTER, {}) \
|
||||||
X(ANM2, {"Anm2 file", "anm2;xml"}) \
|
X(ANM2, {"Anm2 file", "anm2;xml"}) \
|
||||||
X(PNG, {"PNG image", "png"}) \
|
X(PNG, {"PNG image", "png;PNG"}) \
|
||||||
X(SOUND, {"WAV file;OGG file", "wav;ogg"}) \
|
X(SOUND, {"WAV file;OGG file", "wav;ogg"}) \
|
||||||
X(GIF, {"GIF image", "gif"}) \
|
X(GIF, {"GIF image", "gif"}) \
|
||||||
X(WEBM, {"WebM video", "webm"}) \
|
X(WEBM, {"WebM video", "webm"}) \
|
||||||
@@ -49,6 +49,7 @@ namespace anm2ed
|
|||||||
X(SOUND_REPLACE, SOUND) \
|
X(SOUND_REPLACE, SOUND) \
|
||||||
X(SPRITESHEET_OPEN, PNG) \
|
X(SPRITESHEET_OPEN, PNG) \
|
||||||
X(SPRITESHEET_REPLACE, PNG) \
|
X(SPRITESHEET_REPLACE, PNG) \
|
||||||
|
X(SPRITESHEET_PATH_SET, PNG) \
|
||||||
X(FFMPEG_PATH_SET, EXECUTABLE) \
|
X(FFMPEG_PATH_SET, EXECUTABLE) \
|
||||||
X(PNG_DIRECTORY_SET, NO_FILTER) \
|
X(PNG_DIRECTORY_SET, NO_FILTER) \
|
||||||
X(PNG_PATH_SET, PNG) \
|
X(PNG_PATH_SET, PNG) \
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#include "animation_preview.h"
|
#include "animation_preview.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
@@ -30,6 +32,57 @@ namespace anm2ed::imgui
|
|||||||
constexpr auto POINT_SIZE = vec2(4, 4);
|
constexpr auto POINT_SIZE = vec2(4, 4);
|
||||||
constexpr auto TRIGGER_TEXT_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
|
constexpr auto TRIGGER_TEXT_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
|
||||||
constexpr auto TRIGGER_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.5f);
|
constexpr auto TRIGGER_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.5f);
|
||||||
|
constexpr auto PLAYBACK_TICK_RATE = 30.0f;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path render_destination_directory(const std::filesystem::path& path, int type)
|
||||||
|
{
|
||||||
|
if (type == render::PNGS) return path;
|
||||||
|
auto directory = path.parent_path();
|
||||||
|
if (directory.empty()) directory = std::filesystem::current_path();
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path render_frame_filename(const std::filesystem::path& format, int index)
|
||||||
|
{
|
||||||
|
auto formatString = path::to_utf8(format);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto name = std::vformat(formatString, std::make_format_args(index));
|
||||||
|
auto filename = path::from_utf8(name).filename();
|
||||||
|
if (filename.empty()) return path::from_utf8(std::format("frame_{:06}.png", index));
|
||||||
|
if (filename.extension().empty()) filename.replace_extension(render::EXTENSIONS[render::SPRITESHEET]);
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return path::from_utf8(std::format("frame_{:06}.png", index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path render_temp_directory_create(const std::filesystem::path& directory)
|
||||||
|
{
|
||||||
|
auto timestamp = (uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
for (int suffix = 0; suffix < 1000; ++suffix)
|
||||||
|
{
|
||||||
|
auto tempDirectory = directory / path::from_utf8(std::format(".anm2ed_render_tmp_{}_{}", timestamp, suffix));
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::create_directories(tempDirectory, ec)) return tempDirectory;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_temp_cleanup(std::filesystem::path& directory, std::vector<std::filesystem::path>& frames)
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
if (!directory.empty()) std::filesystem::remove_all(directory, ec);
|
||||||
|
directory.clear();
|
||||||
|
frames.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||||
|
|
||||||
@@ -55,22 +108,7 @@ namespace anm2ed::imgui
|
|||||||
{
|
{
|
||||||
if (type == render::PNGS)
|
if (type == render::PNGS)
|
||||||
{
|
{
|
||||||
auto& format = settings.renderFormat;
|
if (!renderTempFrames.empty())
|
||||||
auto formatString = path::to_utf8(format);
|
|
||||||
bool isSuccess{true};
|
|
||||||
for (auto [i, frame] : std::views::enumerate(renderFrames))
|
|
||||||
{
|
|
||||||
auto outputPath = path / std::vformat(formatString, std::make_format_args(i));
|
|
||||||
|
|
||||||
if (!frame.write_png(outputPath))
|
|
||||||
{
|
|
||||||
isSuccess = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
logger.info(std::format("Saved frame to: {}", outputPath.string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess)
|
|
||||||
{
|
{
|
||||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(pathString)));
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(pathString)));
|
||||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH),
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH),
|
||||||
@@ -89,14 +127,14 @@ namespace anm2ed::imgui
|
|||||||
auto& rows = settings.renderRows;
|
auto& rows = settings.renderRows;
|
||||||
auto& columns = settings.renderColumns;
|
auto& columns = settings.renderColumns;
|
||||||
|
|
||||||
if (renderFrames.empty())
|
if (renderTempFrames.empty())
|
||||||
{
|
{
|
||||||
toasts.push(localize.get(TOAST_SPRITESHEET_NO_FRAMES));
|
toasts.push(localize.get(TOAST_SPRITESHEET_NO_FRAMES));
|
||||||
logger.warning(localize.get(TOAST_SPRITESHEET_NO_FRAMES, anm2ed::ENGLISH));
|
logger.warning(localize.get(TOAST_SPRITESHEET_NO_FRAMES, anm2ed::ENGLISH));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auto& firstFrame = renderFrames.front();
|
auto firstFrame = Texture(renderTempFrames.front());
|
||||||
if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty())
|
if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty())
|
||||||
{
|
{
|
||||||
toasts.push(localize.get(TOAST_SPRITESHEET_EMPTY));
|
toasts.push(localize.get(TOAST_SPRITESHEET_EMPTY));
|
||||||
@@ -110,9 +148,9 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
|
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
|
||||||
|
|
||||||
for (std::size_t index = 0; index < renderFrames.size(); ++index)
|
for (std::size_t index = 0; index < renderTempFrames.size(); ++index)
|
||||||
{
|
{
|
||||||
const auto& frame = renderFrames[index];
|
auto frame = Texture(renderTempFrames[index]);
|
||||||
auto row = (int)(index / columns);
|
auto row = (int)(index / columns);
|
||||||
auto column = (int)(index % columns);
|
auto column = (int)(index % columns);
|
||||||
if (row >= rows || column >= columns) break;
|
if (row >= rows || column >= columns) break;
|
||||||
@@ -147,7 +185,7 @@ namespace anm2ed::imgui
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size))
|
if (animation_render(ffmpegPath, path, renderTempFrames, audioStream, (render::Type)type, anm2.info.fps))
|
||||||
{
|
{
|
||||||
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString)));
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString)));
|
||||||
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH),
|
||||||
@@ -162,7 +200,13 @@ namespace anm2ed::imgui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFrames.clear();
|
if (type == render::PNGS)
|
||||||
|
{
|
||||||
|
renderTempDirectory.clear();
|
||||||
|
renderTempFrames.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
|
||||||
if (settings.renderIsRawAnimation)
|
if (settings.renderIsRawAnimation)
|
||||||
{
|
{
|
||||||
@@ -188,7 +232,23 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
bind();
|
bind();
|
||||||
auto pixels = pixels_get();
|
auto pixels = pixels_get();
|
||||||
renderFrames.push_back(Texture(pixels.data(), size));
|
auto frameIndex = (int)renderTempFrames.size();
|
||||||
|
auto framePath = renderTempDirectory / render_frame_filename(settings.renderFormat, frameIndex);
|
||||||
|
if (Texture::write_pixels_png(framePath, size, pixels.data()))
|
||||||
|
{
|
||||||
|
renderTempFrames.push_back(framePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(pathString)));
|
||||||
|
if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
playback.isPlaying = false;
|
||||||
|
playback.isFinished = false;
|
||||||
|
manager.isRecording = false;
|
||||||
|
manager.progressPopup.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +274,10 @@ namespace anm2ed::imgui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playback.tick(anm2.info.fps, animation->frameNum,
|
auto fps = std::max(anm2.info.fps, 1);
|
||||||
(animation->isLoop || settings.playbackIsLoop) && !manager.isRecording);
|
auto deltaSeconds = manager.isRecording ? (1.0f / (float)fps) : (1.0f / PLAYBACK_TICK_RATE);
|
||||||
|
playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording,
|
||||||
|
deltaSeconds);
|
||||||
|
|
||||||
frameTime = playback.time;
|
frameTime = playback.time;
|
||||||
}
|
}
|
||||||
@@ -366,7 +428,7 @@ namespace anm2ed::imgui
|
|||||||
combo_negative_one_indexed(localize.get(LABEL_OVERLAY), &overlayIndex, document.animation.labels);
|
combo_negative_one_indexed(localize.get(LABEL_OVERLAY), &overlayIndex, document.animation.labels);
|
||||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY));
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY));
|
||||||
|
|
||||||
ImGui::InputFloat(localize.get(BASIC_ALPHA), &overlayTransparency, 0, 0, "%.0f");
|
ImGui::DragFloat(localize.get(BASIC_ALPHA), &overlayTransparency, DRAG_SPEED, 0, 255, "%.0f");
|
||||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY_ALPHA));
|
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY_ALPHA));
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
@@ -434,7 +496,34 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
manager.isRecordingStart = false;
|
manager.isRecordingStart = false;
|
||||||
manager.isRecording = true;
|
manager.isRecording = true;
|
||||||
|
renderTempFrames.clear();
|
||||||
|
if (settings.renderType == render::PNGS)
|
||||||
|
{
|
||||||
|
renderTempDirectory = settings.renderPath;
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(renderTempDirectory, ec);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto destinationDirectory = render_destination_directory(settings.renderPath, settings.renderType);
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(destinationDirectory, ec);
|
||||||
|
renderTempDirectory = render_temp_directory_create(destinationDirectory);
|
||||||
|
}
|
||||||
|
if (renderTempDirectory.empty())
|
||||||
|
{
|
||||||
|
auto pathString = path::to_utf8(settings.renderPath);
|
||||||
|
toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString)));
|
||||||
|
logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH),
|
||||||
|
std::make_format_args(pathString)));
|
||||||
|
manager.isRecording = false;
|
||||||
|
manager.progressPopup.close();
|
||||||
|
playback.isPlaying = false;
|
||||||
|
playback.isFinished = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
playback.isPlaying = true;
|
playback.isPlaying = true;
|
||||||
|
playback.timing_reset();
|
||||||
playback.time = manager.recordingStart;
|
playback.time = manager.recordingStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,7 +1024,13 @@ namespace anm2ed::imgui
|
|||||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||||
if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||||
{
|
{
|
||||||
renderFrames.clear();
|
if (settings.renderType == render::PNGS)
|
||||||
|
{
|
||||||
|
renderTempDirectory.clear();
|
||||||
|
renderTempFrames.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
render_temp_cleanup(renderTempDirectory, renderTempFrames);
|
||||||
|
|
||||||
pan = savedPan;
|
pan = savedPan;
|
||||||
zoom = savedZoom;
|
zoom = savedZoom;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
#include "audio_stream.h"
|
#include "audio_stream.h"
|
||||||
#include "canvas.h"
|
#include "canvas.h"
|
||||||
#include "manager.h"
|
#include "manager.h"
|
||||||
@@ -26,7 +28,8 @@ namespace anm2ed::imgui
|
|||||||
bool hasPendingZoomPanAdjust{};
|
bool hasPendingZoomPanAdjust{};
|
||||||
bool isMoveDragging{};
|
bool isMoveDragging{};
|
||||||
glm::vec2 moveOffset{};
|
glm::vec2 moveOffset{};
|
||||||
std::vector<resource::Texture> renderFrames{};
|
std::filesystem::path renderTempDirectory{};
|
||||||
|
std::vector<std::filesystem::path> renderTempFrames{};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AnimationPreview();
|
AnimationPreview();
|
||||||
|
|||||||
@@ -445,17 +445,22 @@ namespace anm2ed::imgui
|
|||||||
{
|
{
|
||||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES,
|
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES,
|
||||||
{
|
{
|
||||||
frame->regionID = hoveredRegionId;
|
anm2::FrameChange change{};
|
||||||
|
change.regionID = hoveredRegionId;
|
||||||
if (spritesheet)
|
if (spritesheet)
|
||||||
{
|
{
|
||||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||||
if (regionIt != spritesheet->regions.end())
|
if (regionIt != spritesheet->regions.end())
|
||||||
{
|
{
|
||||||
frame->crop = regionIt->second.crop;
|
change.cropX = regionIt->second.crop.x;
|
||||||
frame->size = regionIt->second.size;
|
change.cropY = regionIt->second.crop.y;
|
||||||
frame->pivot = regionIt->second.pivot;
|
change.sizeX = regionIt->second.size.x;
|
||||||
|
change.sizeY = regionIt->second.size.y;
|
||||||
|
change.pivotX = regionIt->second.pivot.x;
|
||||||
|
change.pivotY = regionIt->second.pivot.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
frame_change_apply(change);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "path_.h"
|
#include "path_.h"
|
||||||
#include "strings.h"
|
#include "strings.h"
|
||||||
#include "toast.h"
|
#include "toast.h"
|
||||||
|
#include "working_directory.h"
|
||||||
|
|
||||||
using namespace anm2ed::types;
|
using namespace anm2ed::types;
|
||||||
using namespace anm2ed::resource;
|
using namespace anm2ed::resource;
|
||||||
@@ -36,6 +37,7 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); };
|
auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); };
|
||||||
auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); };
|
auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); };
|
||||||
|
auto set_file_path_open = [&]() { dialog.file_save(Dialog::SPRITESHEET_PATH_SET); };
|
||||||
auto merge_open = [&]()
|
auto merge_open = [&]()
|
||||||
{
|
{
|
||||||
if (selection.size() <= 1) return;
|
if (selection.size() <= 1) return;
|
||||||
@@ -120,6 +122,21 @@ namespace anm2ed::imgui
|
|||||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto set_file_path = [&](const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
if (selection.size() != 1 || path.empty()) return;
|
||||||
|
|
||||||
|
auto behavior = [&]()
|
||||||
|
{
|
||||||
|
auto id = *selection.begin();
|
||||||
|
if (!anm2.content.spritesheets.contains(id)) return;
|
||||||
|
WorkingDirectory workingDirectory(document.directory_get());
|
||||||
|
anm2.content.spritesheets[id].path = path::make_relative(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
DOCUMENT_EDIT(document, localize.get(EDIT_SET_SPRITESHEET_FILE_PATH), Document::SPRITESHEETS, behavior());
|
||||||
|
};
|
||||||
|
|
||||||
auto save = [&](const std::set<int>& ids)
|
auto save = [&](const std::set<int>& ids)
|
||||||
{
|
{
|
||||||
if (ids.empty()) return;
|
if (ids.empty()) return;
|
||||||
@@ -294,6 +311,8 @@ namespace anm2ed::imgui
|
|||||||
|
|
||||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
||||||
open_directory(anm2.content.spritesheets[*selection.begin()]);
|
open_directory(anm2.content.spritesheets[*selection.begin()]);
|
||||||
|
if (ImGui::MenuItem(localize.get(BASIC_SET_FILE_PATH), nullptr, false, selection.size() == 1))
|
||||||
|
set_file_path_open();
|
||||||
|
|
||||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||||
@@ -536,6 +555,12 @@ namespace anm2ed::imgui
|
|||||||
dialog.reset();
|
dialog.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dialog.is_selected(Dialog::SPRITESHEET_PATH_SET))
|
||||||
|
{
|
||||||
|
set_file_path(dialog.path);
|
||||||
|
dialog.reset();
|
||||||
|
}
|
||||||
|
|
||||||
auto rowTwoWidgetSize = widget_size_with_row_get(2);
|
auto rowTwoWidgetSize = widget_size_with_row_get(2);
|
||||||
|
|
||||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ namespace anm2ed::imgui::wizard
|
|||||||
"##Color Offset B", "##Color Offset G", localize.get(BASIC_COLOR_OFFSET), isColorOffsetR,
|
"##Color Offset B", "##Color Offset G", localize.get(BASIC_COLOR_OFFSET), isColorOffsetR,
|
||||||
isColorOffsetG, isColorOffsetB, colorOffset);
|
isColorOffsetG, isColorOffsetB, colorOffset);
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(itemType != anm2::LAYER);
|
||||||
std::vector<int> fallbackIds{-1};
|
std::vector<int> fallbackIds{-1};
|
||||||
std::vector<std::string> fallbackLabelsString{localize.get(BASIC_NONE)};
|
std::vector<std::string> fallbackLabelsString{localize.get(BASIC_NONE)};
|
||||||
std::vector<const char*> fallbackLabels{fallbackLabelsString[0].c_str()};
|
std::vector<const char*> fallbackLabels{fallbackLabelsString[0].c_str()};
|
||||||
@@ -221,12 +222,11 @@ namespace anm2ed::imgui::wizard
|
|||||||
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
||||||
if (regionIt != document.regionBySpritesheet.end()) regionStorage = ®ionIt->second;
|
if (regionIt != document.regionBySpritesheet.end()) regionStorage = ®ionIt->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto regionIds = regionStorage && !regionStorage->ids.empty() ? regionStorage->ids : fallbackIds;
|
auto regionIds = regionStorage && !regionStorage->ids.empty() ? regionStorage->ids : fallbackIds;
|
||||||
auto regionLabels = regionStorage && !regionStorage->labels.empty() ? regionStorage->labels : fallbackLabels;
|
auto regionLabels = regionStorage && !regionStorage->labels.empty() ? regionStorage->labels : fallbackLabels;
|
||||||
|
|
||||||
PROPERTIES_WIDGET(combo_id_mapped(localize.get(BASIC_REGION), ®ionId, regionIds, regionLabels), "##Is Region",
|
PROPERTIES_WIDGET(combo_id_mapped(localize.get(BASIC_REGION), ®ionId, regionIds, regionLabels), "##Is Region",
|
||||||
isRegion);
|
isRegion);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible);
|
bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible);
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ namespace anm2ed::imgui::wizard
|
|||||||
{
|
{
|
||||||
toasts.push(localize.get(TOAST_PNG_FORMAT_INVALID));
|
toasts.push(localize.get(TOAST_PNG_FORMAT_INVALID));
|
||||||
logger.error(localize.get(TOAST_PNG_FORMAT_INVALID, anm2ed::ENGLISH));
|
logger.error(localize.get(TOAST_PNG_FORMAT_INVALID, anm2ed::ENGLISH));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -269,12 +270,12 @@ namespace anm2ed::imgui::wizard
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!path_valid_check()) return false;
|
if (!path_valid_check()) return false;
|
||||||
|
if (!png_format_valid_check()) return false;
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case render::PNGS:
|
case render::PNGS:
|
||||||
if (!png_directory_valid_check()) return false;
|
if (!png_directory_valid_check()) return false;
|
||||||
if (!png_format_valid_check()) return false;
|
|
||||||
format.replace_extension(render::EXTENSIONS[render::SPRITESHEET]);
|
format.replace_extension(render::EXTENSIONS[render::SPRITESHEET]);
|
||||||
break;
|
break;
|
||||||
case render::SPRITESHEET:
|
case render::SPRITESHEET:
|
||||||
|
|||||||
+23
-4
@@ -1,5 +1,8 @@
|
|||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include <glm/common.hpp>
|
#include <glm/common.hpp>
|
||||||
|
|
||||||
namespace anm2ed
|
namespace anm2ed
|
||||||
@@ -9,24 +12,38 @@ namespace anm2ed
|
|||||||
if (isFinished) time = 0.0f;
|
if (isFinished) time = 0.0f;
|
||||||
isFinished = false;
|
isFinished = false;
|
||||||
isPlaying = !isPlaying;
|
isPlaying = !isPlaying;
|
||||||
|
timing_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Playback::timing_reset() { tickAccumulator = 0.0f; }
|
||||||
|
|
||||||
void Playback::clamp(int length) { time = glm::clamp(time, 0.0f, (float)length - 1.0f); }
|
void Playback::clamp(int length) { time = glm::clamp(time, 0.0f, (float)length - 1.0f); }
|
||||||
|
|
||||||
void Playback::tick(int fps, int length, bool isLoop)
|
void Playback::tick(int fps, int length, bool isLoop, float deltaSeconds)
|
||||||
{
|
{
|
||||||
if (isFinished) return;
|
if (isFinished || !isPlaying || fps <= 0 || length <= 0) return;
|
||||||
|
if (deltaSeconds <= 0.0f) return;
|
||||||
|
|
||||||
time += (float)fps / 30.0f;
|
auto frameDuration = 1.0f / (float)fps;
|
||||||
|
tickAccumulator += deltaSeconds;
|
||||||
|
auto steps = (int)std::floor(tickAccumulator / frameDuration);
|
||||||
|
if (steps <= 0) return;
|
||||||
|
tickAccumulator -= frameDuration * (float)steps;
|
||||||
|
|
||||||
|
time += (float)steps;
|
||||||
|
|
||||||
if (time > (float)length - 1.0f)
|
if (time > (float)length - 1.0f)
|
||||||
{
|
{
|
||||||
if (isLoop)
|
if (isLoop)
|
||||||
time = 0.0f;
|
{
|
||||||
|
time = std::fmod(time, (float)length);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
time = (float)length - 1.0f;
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
|
timing_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,11 +52,13 @@ namespace anm2ed
|
|||||||
{
|
{
|
||||||
--time;
|
--time;
|
||||||
clamp(length);
|
clamp(length);
|
||||||
|
timing_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playback::increment(int length)
|
void Playback::increment(int length)
|
||||||
{
|
{
|
||||||
++time;
|
++time;
|
||||||
clamp(length);
|
clamp(length);
|
||||||
|
timing_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-1
@@ -10,9 +10,13 @@ namespace anm2ed
|
|||||||
bool isFinished{};
|
bool isFinished{};
|
||||||
|
|
||||||
void toggle();
|
void toggle();
|
||||||
|
void timing_reset();
|
||||||
void clamp(int);
|
void clamp(int);
|
||||||
void tick(int, int, bool);
|
void tick(int, int, bool, float);
|
||||||
void decrement(int);
|
void decrement(int);
|
||||||
void increment(int);
|
void increment(int);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float tickAccumulator{};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-18
@@ -1,8 +1,10 @@
|
|||||||
#include "render.h"
|
#include "render.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <functional>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -12,16 +14,32 @@
|
|||||||
#include "sdl.h"
|
#include "sdl.h"
|
||||||
#include "string_.h"
|
#include "string_.h"
|
||||||
|
|
||||||
using namespace anm2ed::resource;
|
|
||||||
using namespace anm2ed::util;
|
using namespace anm2ed::util;
|
||||||
using namespace glm;
|
|
||||||
|
|
||||||
namespace anm2ed
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
|
namespace
|
||||||
std::vector<Texture>& frames, AudioStream& audioStream, render::Type type, ivec2 size)
|
|
||||||
{
|
{
|
||||||
if (frames.empty() || size.x <= 0 || size.y <= 0 || ffmpegPath.empty() || path.empty()) return false;
|
std::string ffmpeg_concat_escape(const std::string& value)
|
||||||
|
{
|
||||||
|
std::string escaped{};
|
||||||
|
escaped.reserve(value.size());
|
||||||
|
for (auto character : value)
|
||||||
|
{
|
||||||
|
if (character == '\'') escaped += "'\\''";
|
||||||
|
else
|
||||||
|
escaped += character;
|
||||||
|
}
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
|
||||||
|
const std::vector<std::filesystem::path>& framePaths, AudioStream& audioStream,
|
||||||
|
render::Type type, int fps)
|
||||||
|
{
|
||||||
|
if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
|
||||||
|
fps = std::max(fps, 1);
|
||||||
|
|
||||||
auto pathString = path::to_utf8(path);
|
auto pathString = path::to_utf8(path);
|
||||||
auto ffmpegPathString = path::to_utf8(ffmpegPath);
|
auto ffmpegPathString = path::to_utf8(ffmpegPath);
|
||||||
@@ -85,8 +103,29 @@ namespace anm2ed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command = std::format("\"{0}\" -y -f rawvideo -pix_fmt rgba -s {1}x{2} -r 30 -i pipe:0", ffmpegPathString, size.x,
|
auto framesListPath = std::filesystem::temp_directory_path() / path::from_utf8(std::format(
|
||||||
size.y);
|
"anm2ed_frames_{}_{}.txt", std::hash<std::string>{}(pathString), SDL_GetTicks())) ;
|
||||||
|
std::ofstream framesListFile(framesListPath);
|
||||||
|
if (!framesListFile)
|
||||||
|
{
|
||||||
|
audio_remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto frameDuration = 1.0 / (double)fps;
|
||||||
|
for (const auto& framePath : framePaths)
|
||||||
|
{
|
||||||
|
auto framePathString = path::to_utf8(framePath);
|
||||||
|
framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
|
||||||
|
framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
|
||||||
|
}
|
||||||
|
auto lastFramePathString = path::to_utf8(framePaths.back());
|
||||||
|
framesListFile << "file '" << ffmpeg_concat_escape(lastFramePathString) << "'\n";
|
||||||
|
framesListFile.close();
|
||||||
|
|
||||||
|
auto framesListPathString = path::to_utf8(framesListPath);
|
||||||
|
command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);
|
||||||
|
command += std::format(" -fps_mode cfr -r {}", fps);
|
||||||
|
|
||||||
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
|
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
|
||||||
|
|
||||||
@@ -110,7 +149,11 @@ namespace anm2ed
|
|||||||
command += std::format(" \"{}\"", pathString);
|
command += std::format(" \"{}\"", pathString);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(framesListPath, ec);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
@@ -127,23 +170,16 @@ namespace anm2ed
|
|||||||
|
|
||||||
if (!process.get())
|
if (!process.get())
|
||||||
{
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(framesListPath, ec);
|
||||||
audio_remove();
|
audio_remove();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& frame : frames)
|
|
||||||
{
|
|
||||||
auto frameSize = frame.pixel_size_get();
|
|
||||||
|
|
||||||
if (fwrite(frame.pixels.data(), 1, frameSize, process.get()) != frameSize)
|
|
||||||
{
|
|
||||||
audio_remove();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.close();
|
process.close();
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(framesListPath, ec);
|
||||||
audio_remove();
|
audio_remove();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -36,6 +36,6 @@ namespace anm2ed::render
|
|||||||
namespace anm2ed
|
namespace anm2ed
|
||||||
{
|
{
|
||||||
std::filesystem::path ffmpeg_log_path();
|
std::filesystem::path ffmpeg_log_path();
|
||||||
bool animation_render(const std::filesystem::path&, const std::filesystem::path&, std::vector<resource::Texture>&,
|
bool animation_render(const std::filesystem::path&, const std::filesystem::path&,
|
||||||
AudioStream&, render::Type, glm::ivec2);
|
const std::vector<std::filesystem::path>&, AudioStream&, render::Type, int);
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,7 @@ namespace anm2ed
|
|||||||
X(BASIC_ROTATION, "Rotation", "Rotacion", "Поворот", "旋转", "회전") \
|
X(BASIC_ROTATION, "Rotation", "Rotacion", "Поворот", "旋转", "회전") \
|
||||||
X(BASIC_SAVE, "Save", "Guardar", "Сохранить", "保存", "저장") \
|
X(BASIC_SAVE, "Save", "Guardar", "Сохранить", "保存", "저장") \
|
||||||
X(BASIC_SCALE, "Scale", "Escalar", "Масштаб", "缩放", "크기") \
|
X(BASIC_SCALE, "Scale", "Escalar", "Масштаб", "缩放", "크기") \
|
||||||
|
X(BASIC_SET_FILE_PATH, "Set File Path", "Set File Path", "Set File Path", "Set File Path", "Set File Path") \
|
||||||
X(BASIC_SIZE, "Size", "Tamaño", "Размер", "大小", "비율") \
|
X(BASIC_SIZE, "Size", "Tamaño", "Размер", "大小", "비율") \
|
||||||
X(BASIC_SOUND, "Sound", "Sonido", "Звук", "声音", "사운드") \
|
X(BASIC_SOUND, "Sound", "Sonido", "Звук", "声音", "사운드") \
|
||||||
X(BASIC_TRIM, "Trim", "Recortar contenido", "Обрезать по содержимому", "修剪", "내용으로 자르기") \
|
X(BASIC_TRIM, "Trim", "Recortar contenido", "Обрезать по содержимому", "修剪", "내용으로 자르기") \
|
||||||
@@ -162,6 +163,7 @@ namespace anm2ed
|
|||||||
X(EDIT_RENAME_EVENT, "Rename Event", "Renombrar Evento", "Переименовать событие", "重命名事件", "이벤트 이름 바꾸기") \
|
X(EDIT_RENAME_EVENT, "Rename Event", "Renombrar Evento", "Переименовать событие", "重命名事件", "이벤트 이름 바꾸기") \
|
||||||
X(EDIT_REPLACE_SPRITESHEET, "Replace Spritesheet", "Reemplazar Spritesheet", "Заменить спрайт-лист", "替换图集", "스프라이트 시트 교체") \
|
X(EDIT_REPLACE_SPRITESHEET, "Replace Spritesheet", "Reemplazar Spritesheet", "Заменить спрайт-лист", "替换图集", "스프라이트 시트 교체") \
|
||||||
X(EDIT_REPLACE_SOUND, "Replace Sound", "Reemplazar Sonido", "Заменить звук", "替换声音", "사운드 교체") \
|
X(EDIT_REPLACE_SOUND, "Replace Sound", "Reemplazar Sonido", "Заменить звук", "替换声音", "사운드 교체") \
|
||||||
|
X(EDIT_SET_SPRITESHEET_FILE_PATH, "Set Spritesheet File Path", "Set Spritesheet File Path", "Set Spritesheet File Path", "Set Spritesheet File Path", "Set Spritesheet File Path") \
|
||||||
X(EDIT_SET_LAYER_PROPERTIES, "Set Layer Properties", "Establecer Propiedades de Capa", "Установить свойства слоя", "更改动画层属性", "레이어 속성 설정") \
|
X(EDIT_SET_LAYER_PROPERTIES, "Set Layer Properties", "Establecer Propiedades de Capa", "Установить свойства слоя", "更改动画层属性", "레이어 속성 설정") \
|
||||||
X(EDIT_SET_REGION_PROPERTIES, "Set Region Properties", "Establecer propiedades de región", "Установить свойства региона", "更改区域属性", "영역 속성 설정") \
|
X(EDIT_SET_REGION_PROPERTIES, "Set Region Properties", "Establecer propiedades de región", "Установить свойства региона", "更改区域属性", "영역 속성 설정") \
|
||||||
X(EDIT_TRIM_REGIONS, "Trim Regions", "Recortar regiones", "Обрезать регионы", "修剪区域", "영역 트리밍") \
|
X(EDIT_TRIM_REGIONS, "Trim Regions", "Recortar regiones", "Обрезать регионы", "修剪区域", "영역 트리밍") \
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ namespace anm2ed::resource
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Texture::write_pixels_png(const std::filesystem::path& path, ivec2 size, const uint8_t* data)
|
||||||
|
{
|
||||||
|
if (!data || size.x <= 0 || size.y <= 0) return false;
|
||||||
|
|
||||||
|
File file(path, "wb");
|
||||||
|
if (auto handle = file.get())
|
||||||
|
{
|
||||||
|
auto write_func = [](void* context, void* bytes, int count)
|
||||||
|
{ fwrite(bytes, 1, count, static_cast<FILE*>(context)); };
|
||||||
|
return stbi_write_png_to_func(write_func, handle, size.x, size.y, CHANNELS, data, size.x * CHANNELS) != 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Texture Texture::merge_append(const Texture& base, const Texture& append, bool isAppendRight)
|
Texture Texture::merge_append(const Texture& base, const Texture& append, bool isAppendRight)
|
||||||
{
|
{
|
||||||
if (base.size.x <= 0 || base.size.y <= 0) return append;
|
if (base.size.x <= 0 || base.size.y <= 0) return append;
|
||||||
|
|||||||
Reference in New Issue
Block a user