Add brush texture list boundary

This commit is contained in:
2026-06-03 17:21:49 +02:00
parent cee5f141a3
commit 9adfad9609
8 changed files with 727 additions and 64 deletions

View File

@@ -2,6 +2,7 @@
#include "foundation/result.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <string_view>
@@ -21,6 +22,12 @@ enum class BrushUiOperation {
stroke_settings_changed,
};
enum class BrushTextureListOperation {
add_texture,
remove_texture,
move_texture,
};
struct BrushUiPlan {
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
@@ -37,6 +44,24 @@ struct BrushUiPlan {
bool update_brush_ui = false;
};
struct BrushTextureListPlan {
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
int item_count = 0;
int current_index = -1;
int target_index = -1;
int move_offset = 0;
std::string source_path;
std::string high_path;
std::string thumbnail_path;
std::string brush_name;
bool user_texture = false;
bool deletes_texture_files = false;
bool saves_list = false;
bool notifies_selection = false;
bool converts_brush_alpha = false;
bool no_op = false;
};
class BrushUiServices {
public:
virtual ~BrushUiServices() = default;
@@ -47,6 +72,41 @@ public:
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};
class BrushTextureListServices {
public:
virtual ~BrushTextureListServices() = default;
virtual pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) = 0;
virtual void remove_texture(int index, bool delete_texture_files) = 0;
virtual void move_texture(int from_index, int to_index) = 0;
virtual void select_texture(int index) = 0;
virtual void save_texture_list() = 0;
};
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
std::string_view source_path) noexcept
{
const auto slash = source_path.find_last_of("/\\");
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
if (name_begin >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
}
const auto dot = source_path.find_last_of('.');
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
}
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
}
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
@@ -129,6 +189,93 @@ public:
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
std::string_view directory_name,
std::string_view data_path,
std::string_view source_path)
{
if (directory_name.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
}
if (data_path.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
}
const auto stem = brush_texture_source_stem(source_path);
if (!stem) {
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::add_texture;
plan.source_path = std::string(source_path);
plan.brush_name = std::string(stem.value());
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
+ plan.brush_name + ".png";
plan.user_texture = true;
plan.saves_list = true;
plan.converts_brush_alpha = directory_name == "brushes";
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
int item_count,
int current_index,
bool current_is_user_texture)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::remove_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
plan.user_texture = current_is_user_texture;
plan.deletes_texture_files = current_is_user_texture;
plan.saves_list = true;
plan.notifies_selection = plan.target_index >= 0;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
int item_count,
int current_index,
int offset)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
if (offset == 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::move_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
plan.move_offset = offset;
plan.saves_list = true;
plan.no_op = plan.target_index == current_index;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan,
BrushUiServices& services)
@@ -168,4 +315,59 @@ public:
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
}
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
const BrushTextureListPlan& plan,
BrushTextureListServices& services)
{
switch (plan.operation) {
case BrushTextureListOperation::add_texture:
{
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|| plan.brush_name.empty()) {
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
}
const auto add_status = services.add_texture_from_source(
plan.source_path,
plan.high_path,
plan.thumbnail_path,
plan.brush_name,
plan.converts_brush_alpha);
if (!add_status.ok()) {
return add_status;
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
case BrushTextureListOperation::remove_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
}
services.remove_texture(plan.current_index, plan.deletes_texture_files);
if (plan.notifies_selection && plan.target_index >= 0) {
services.select_texture(plan.target_index);
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
case BrushTextureListOperation::move_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
}
services.move_texture(plan.current_index, plan.target_index);
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
}
} // namespace pp::app

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_panel_brush.h"
#include "app_core/brush_ui.h"
#include "asset.h"
#include "texture.h"
@@ -75,6 +76,116 @@ Node* NodePanelBrush::clone_instantiate() const
return new NodePanelBrush();
}
void NodePanelBrush::execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan)
{
class LegacyBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
explicit LegacyBrushTextureListServices(NodePanelBrush& panel) noexcept
: panel_(panel)
{
}
pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) override
{
Image img;
if (!img.load_file(std::string(source_path))) {
return pp::foundation::Status::invalid_argument("brush texture source could not be loaded");
}
if (converts_brush_alpha) {
img.gayscale_alpha();
}
auto thumbnail_image = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumbnail_image.save_png(std::string(thumbnail_path));
img.save_png(std::string(high_path));
NodeButtonBrush* brush = new NodeButtonBrush;
panel_.m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
const auto thumbnail_path_string = std::string(thumbnail_path);
brush->set_icon(thumbnail_path_string.c_str());
brush->thumb_path = std::string(thumbnail_path);
brush->high_path = std::string(high_path);
brush->brush_name = std::string(brush_name);
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, &panel_, std::placeholders::_1);
return pp::foundation::Status::success();
}
void remove_texture(int index, bool delete_texture_files) override
{
auto* brush = brush_at(index);
if (!brush) {
return;
}
if (delete_texture_files) {
Asset::delete_file(brush->thumb_path);
Asset::delete_file(brush->high_path);
}
if (panel_.m_current == brush) {
panel_.m_current = nullptr;
}
panel_.m_container->remove_child(brush);
}
void move_texture(int from_index, int to_index) override
{
if (auto* brush = brush_at(from_index)) {
panel_.m_container->move_child(brush, to_index);
}
}
void select_texture(int index) override
{
if (panel_.m_current) {
panel_.m_current->m_selected = false;
}
panel_.m_current = brush_at(index);
if (!panel_.m_current) {
return;
}
panel_.m_current->m_selected = true;
if (panel_.on_brush_changed) {
panel_.on_brush_changed(&panel_, index);
}
}
void save_texture_list() override
{
panel_.save();
}
private:
NodeButtonBrush* brush_at(int index) const
{
if (index < 0 || index >= static_cast<int>(panel_.m_container->m_children.size())) {
return nullptr;
}
return static_cast<NodeButtonBrush*>(panel_.m_container->m_children[index].get());
}
NodePanelBrush& panel_;
};
LegacyBrushTextureListServices services(*this);
const auto status = pp::app::execute_brush_texture_list_plan(plan, services);
if (!status.ok()) {
LOG("Brush texture list action failed: %s", status.message);
}
}
void NodePanelBrush::init()
{
init_template_file("data/dialogs/panel-brushes.xml", "tpl-panel-brushes");
@@ -82,41 +193,9 @@ void NodePanelBrush::init()
m_btn_add = find<NodeButtonCustom>("btn-add");
m_btn_add->on_click = [this](Node*) {
App::I->pick_file({ "JPG", "PNG" }, [this](std::string path) {
std::string name, base, ext;
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
std::smatch m;
if (!std::regex_search(path, m, r))
return;
base = m[1].str();
name = m[2].str();
ext = m[3].str();
Image img;
if (!m_dir_name.empty() && img.load_file(path))
{
std::string path_high = App::I->data_path + "/" + m_dir_name + "/" + name + ".png";
std::string path_thumb = App::I->data_path + "/" + m_dir_name + "/thumbs/" + name + ".png";
//img = img.resize_squared(glm::u8vec4(255));
if (m_dir_name == "brushes")
img.gayscale_alpha();
auto thumb = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumb.save_png(path_thumb);
//auto po2 = img.resize_power2();
img.save_png(path_high);
NodeButtonBrush* brush = new NodeButtonBrush;
m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
brush->set_icon(path_thumb.c_str());
brush->thumb_path = path_thumb;
brush->high_path = path_high;
brush->brush_name = name;
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, this, std::placeholders::_1);
save();
const auto plan = pp::app::plan_brush_texture_list_add(m_dir_name, App::I->data_path, path);
if (plan) {
execute_texture_list_plan(plan.value());
}
});
};
@@ -126,26 +205,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
if (m_current->m_user_brush)
{
// only delete user brushes
Asset::delete_file(m_current->thumb_path);
Asset::delete_file(m_current->high_path);
const auto plan = pp::app::plan_brush_texture_list_remove(
static_cast<int>(m_container->m_children.size()),
idx,
m_current->m_user_brush);
if (plan) {
execute_texture_list_plan(plan.value());
}
m_container->remove_child(m_current);
if (m_container->m_children.size() > 0)
{
idx = std::max(0, std::min(idx, (int)m_container->m_children.size() - 1));
m_current = (NodeButtonBrush*)m_container->m_children[idx].get();
m_current->m_selected = true;
if (on_brush_changed)
on_brush_changed(this, idx);
}
else
{
m_current = nullptr;
}
save();
}
};
@@ -154,9 +220,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx - 1, (int)m_container->m_children.size() - 1));
m_container->move_child(m_current, idx);
save();
const auto plan = pp::app::plan_brush_texture_list_move(
static_cast<int>(m_container->m_children.size()),
idx,
-1);
if (plan) {
execute_texture_list_plan(plan.value());
}
}
};
@@ -165,9 +235,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx + 1, (int)m_container->m_children.size() - 1));
m_container->move_child(m_current, idx);
save();
const auto plan = pp::app::plan_brush_texture_list_move(
static_cast<int>(m_container->m_children.size()),
idx,
1);
if (plan) {
execute_texture_list_plan(plan.value());
}
}
};

View File

@@ -9,6 +9,10 @@
#include "serializer.h"
#include "node_button.h"
namespace pp::app {
struct BrushTextureListPlan;
}
class NodeButtonBrush : public NodeButtonCustom, public Serializer::Type
{
public:
@@ -38,6 +42,7 @@ class NodePanelBrush : public Node
NodeButtonCustom* m_btn_down;
NodeButtonCustom* m_btn_remove;
bool m_interacted = false;
void execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan);
public:
NodeScroll* m_container;
std::string m_dir_name;