feat(editor): TilePicker amb scroll vertical i selecció per clic
This commit is contained in:
1 parent
05cdcfa660
commit
f30bb1fb0f
3 files changed
+183
-29
No files matched your search
+164
-28
@@ -4,14 +4,64 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/notification_presets.hpp"
|
#include "core/rendering/notification_presets.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
|
||||||
|
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Com que
|
||||||
|
// el tileset real (p.ex. 32×16 tiles de 16 px = 512×256) no cabria a
|
||||||
|
// 1× dins del canvas 336×240, linealitzem els índexs i els repartim
|
||||||
|
// en una graella que SÍ cap, amb scroll vertical per veure la resta.
|
||||||
|
struct PickerLayout {
|
||||||
|
int x{0};
|
||||||
|
int y{0};
|
||||||
|
int w{0};
|
||||||
|
int h{0};
|
||||||
|
int cols{0};
|
||||||
|
int rows_visible{0};
|
||||||
|
int tile_size{0};
|
||||||
|
int total_tiles{0};
|
||||||
|
int max_scroll{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
|
||||||
|
PickerLayout lay{};
|
||||||
|
lay.tile_size = tm.getTileSize();
|
||||||
|
const int M = Defaults::Editor::PICKER_MARGIN;
|
||||||
|
const int INNER_W = GameCanvas::WIDTH - (2 * M);
|
||||||
|
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
|
||||||
|
if (lay.tile_size <= 0) { return lay; }
|
||||||
|
lay.cols = INNER_W / lay.tile_size;
|
||||||
|
lay.rows_visible = INNER_H / lay.tile_size;
|
||||||
|
lay.x = M;
|
||||||
|
lay.y = M;
|
||||||
|
lay.w = lay.cols * lay.tile_size;
|
||||||
|
lay.h = lay.rows_visible * lay.tile_size;
|
||||||
|
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
|
||||||
|
if (TILESET != nullptr) {
|
||||||
|
const int TCOLS = TILESET->getWidth() / lay.tile_size;
|
||||||
|
const int TROWS = TILESET->getHeight() / lay.tile_size;
|
||||||
|
lay.total_tiles = TCOLS * TROWS;
|
||||||
|
}
|
||||||
|
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
|
||||||
|
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
|
||||||
|
return lay;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void Editor::enter() {
|
void Editor::enter() {
|
||||||
if (active_) { return; }
|
if (active_) { return; }
|
||||||
active_ = true;
|
active_ = true;
|
||||||
@@ -24,6 +74,7 @@ void Editor::enter() {
|
|||||||
void Editor::exit(Tilemap& tilemap) {
|
void Editor::exit(Tilemap& tilemap) {
|
||||||
if (!active_) { return; }
|
if (!active_) { return; }
|
||||||
active_ = false;
|
active_ = false;
|
||||||
|
picker_open_ = false;
|
||||||
const bool SAVED = tilemap.save(Defaults::Editor::SAVE_PATH);
|
const bool SAVED = tilemap.save(Defaults::Editor::SAVE_PATH);
|
||||||
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
||||||
}
|
}
|
||||||
@@ -39,33 +90,63 @@ void Editor::toggle(Tilemap& tilemap) {
|
|||||||
void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
|
void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
|
||||||
if (!active_) { return; }
|
if (!active_) { return; }
|
||||||
|
|
||||||
// 1. Cursor → cel·la. Convertim la posició del ratolí (espai finestra)
|
// Posició del ratolí en coordenades del canvas (independent del
|
||||||
// a coordenades del canvas amb Screen::windowToCanvas, i d'allí al
|
// zoom de finestra i del mode d'escalat).
|
||||||
// sistema de cel·les del tilemap restant l'origin i dividint pel
|
|
||||||
// tile_size. Fora del mapa → cursor_col/row = -1.
|
|
||||||
float mouse_x = 0.0F;
|
float mouse_x = 0.0F;
|
||||||
float mouse_y = 0.0F;
|
float mouse_y = 0.0F;
|
||||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||||
const auto CANVAS_PT = Screen::get().windowToCanvas(mouse_x, mouse_y);
|
const auto CP = Screen::get().windowToCanvas(mouse_x, mouse_y);
|
||||||
const int CX = static_cast<int>(CANVAS_PT.x) - tilemap.getOriginX();
|
const int CX = static_cast<int>(CP.x);
|
||||||
const int CY = static_cast<int>(CANVAS_PT.y) - tilemap.getOriginY();
|
const int CY = static_cast<int>(CP.y);
|
||||||
|
|
||||||
|
// Consumim tecles pròpies del picker abans que cap altre handler
|
||||||
|
// (GlobalInputs) les vegi. ESC només es consumeix si el picker està
|
||||||
|
// obert — fora d'ell segueix arribant a GlobalInputs per la seva
|
||||||
|
// acció de confirmar-eixida.
|
||||||
|
const SDL_Scancode PICKER_SC = KeyConfig::get().scancode("editor_picker");
|
||||||
|
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||||
|
bool consumed = false;
|
||||||
|
if (it->type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
if (PICKER_SC != SDL_SCANCODE_UNKNOWN && it->key.scancode == PICKER_SC) {
|
||||||
|
picker_open_ = !picker_open_;
|
||||||
|
if (picker_open_) { picker_scroll_ = 0; }
|
||||||
|
consumed = true;
|
||||||
|
} else if (picker_open_ && it->key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
picker_open_ = false;
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (consumed) {
|
||||||
|
it = EventBuffer::events.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picker_open_) {
|
||||||
|
updatePicker(tilemap, CX, CY);
|
||||||
|
} else {
|
||||||
|
updateMap(tilemap, CX, CY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||||
|
// Cursor → cel·la del tilemap. Fora del mapa deixem -1/-1 perquè
|
||||||
|
// `render()` sàpiga no dibuixar preview i `setTileAt` ignori el clic.
|
||||||
const int TS = tilemap.getTileSize();
|
const int TS = tilemap.getTileSize();
|
||||||
int col = -1;
|
const int MX = canvas_x - tilemap.getOriginX();
|
||||||
int row = -1;
|
const int MY = canvas_y - tilemap.getOriginY();
|
||||||
if (TS > 0 && CX >= 0 && CY >= 0) {
|
cursor_col_ = -1;
|
||||||
const int C = CX / TS;
|
cursor_row_ = -1;
|
||||||
const int R = CY / TS;
|
if (TS > 0 && MX >= 0 && MY >= 0) {
|
||||||
|
const int C = MX / TS;
|
||||||
|
const int R = MY / TS;
|
||||||
if (C < tilemap.getCols() && R < tilemap.getRows()) {
|
if (C < tilemap.getCols() && R < tilemap.getRows()) {
|
||||||
col = C;
|
cursor_col_ = C;
|
||||||
row = R;
|
cursor_row_ = R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor_col_ = col;
|
|
||||||
cursor_row_ = row;
|
|
||||||
|
|
||||||
// 2. Clicks: només per-esdeveniment (no per estat). Un clic = un
|
|
||||||
// tile col·locat o brush netejat. El picker encara no existeix, així
|
|
||||||
// que el right-click no obre res: només neteja el brush.
|
|
||||||
for (const auto& ev : EventBuffer::events) {
|
for (const auto& ev : EventBuffer::events) {
|
||||||
if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; }
|
if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; }
|
||||||
if (ev.button.button == SDL_BUTTON_LEFT) {
|
if (ev.button.button == SDL_BUTTON_LEFT) {
|
||||||
@@ -78,26 +159,81 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||||
|
const auto LAY = layoutPicker(tilemap);
|
||||||
|
picker_hover_ = -1;
|
||||||
|
if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) {
|
||||||
|
const int VC = (canvas_x - LAY.x) / LAY.tile_size;
|
||||||
|
const int VR = (canvas_y - LAY.y) / LAY.tile_size;
|
||||||
|
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
|
||||||
|
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& ev : EventBuffer::events) {
|
||||||
|
if (ev.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||||
|
picker_scroll_ -= static_cast<int>(ev.wheel.y);
|
||||||
|
picker_scroll_ = std::clamp(picker_scroll_, 0, LAY.max_scroll);
|
||||||
|
} else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
if (picker_hover_ >= 0) {
|
||||||
|
brush_ = picker_hover_;
|
||||||
|
picker_open_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Editor::render(const Tilemap& tilemap) const {
|
void Editor::render(const Tilemap& tilemap) const {
|
||||||
if (!active_) { return; }
|
if (!active_) { return; }
|
||||||
auto& screen = Screen::get();
|
auto& screen = Screen::get();
|
||||||
constexpr int W = GameCanvas::WIDTH;
|
constexpr int W = GameCanvas::WIDTH;
|
||||||
constexpr int H = GameCanvas::HEIGHT;
|
constexpr int H = GameCanvas::HEIGHT;
|
||||||
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
|
|
||||||
|
|
||||||
// Preview del brush: blitejem el tile seleccionat a la cel·la sota
|
if (picker_open_) {
|
||||||
// el cursor, abans del border, així el border no el talla si caiguera
|
renderPicker(tilemap);
|
||||||
// al perímetre. Cap efecte si no hi ha brush o el cursor està fora.
|
} else if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
|
||||||
if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
|
// Preview del brush sota el cursor (només quan el picker és tancat).
|
||||||
const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize());
|
const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize());
|
||||||
const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize());
|
const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize());
|
||||||
tilemap.renderTilePreview(PX, PY, brush_);
|
tilemap.renderTilePreview(PX, PY, brush_);
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.fillRect(0, 0, W, 1, BORDER); // top
|
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
|
||||||
screen.fillRect(0, H - 1, W, 1, BORDER); // bottom
|
screen.fillRect(0, 0, W, 1, BORDER);
|
||||||
screen.fillRect(0, 0, 1, H, BORDER); // left
|
screen.fillRect(0, H - 1, W, 1, BORDER);
|
||||||
screen.fillRect(W - 1, 0, 1, H, BORDER); // right
|
screen.fillRect(0, 0, 1, H, BORDER);
|
||||||
|
screen.fillRect(W - 1, 0, 1, H, BORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::renderPicker(const Tilemap& tilemap) const {
|
||||||
|
const auto LAY = layoutPicker(tilemap);
|
||||||
|
auto& screen = Screen::get();
|
||||||
|
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
|
||||||
|
|
||||||
|
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
|
||||||
|
const int LOGICAL_ROW = picker_scroll_ + vr;
|
||||||
|
for (int vc = 0; vc < LAY.cols; ++vc) {
|
||||||
|
const int IDX = (LOGICAL_ROW * LAY.cols) + vc;
|
||||||
|
if (IDX >= LAY.total_tiles) { break; }
|
||||||
|
const int PX = LAY.x + (vc * LAY.tile_size);
|
||||||
|
const int PY = LAY.y + (vr * LAY.tile_size);
|
||||||
|
tilemap.renderTilePreview(PX, PY, IDX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picker_hover_ >= 0 && LAY.cols > 0) {
|
||||||
|
const int VR = (picker_hover_ / LAY.cols) - picker_scroll_;
|
||||||
|
const int VC = picker_hover_ % LAY.cols;
|
||||||
|
if (VR >= 0 && VR < LAY.rows_visible) {
|
||||||
|
const int PX = LAY.x + (VC * LAY.tile_size);
|
||||||
|
const int PY = LAY.y + (VR * LAY.tile_size);
|
||||||
|
const int TS = LAY.tile_size;
|
||||||
|
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
|
||||||
|
screen.fillRect(PX, PY, TS, 1, C);
|
||||||
|
screen.fillRect(PX, PY + TS - 1, TS, 1, C);
|
||||||
|
screen.fillRect(PX, PY, 1, TS, C);
|
||||||
|
screen.fillRect(PX + TS - 1, PY, 1, TS, C);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // _DEBUG
|
#endif // _DEBUG
|
||||||
@@ -42,15 +42,24 @@ class Editor {
|
|||||||
void render(const Tilemap& tilemap) const;
|
void render(const Tilemap& tilemap) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Les branques d'update / render segons el mode (picker obert o
|
||||||
|
// pintura al mapa) queden separades per claredat; no formen part
|
||||||
|
// de l'API pública perquè només té sentit cridar-les des d'`update`.
|
||||||
|
void updateMap(Tilemap& tilemap, int canvas_x, int canvas_y);
|
||||||
|
void updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y);
|
||||||
|
void renderPicker(const Tilemap& tilemap) const;
|
||||||
|
|
||||||
bool active_{false};
|
bool active_{false};
|
||||||
|
|
||||||
// Estat de l'edició (brush + cursor + picker). Es poblen al pas
|
// Estat de l'edició (brush + cursor + picker). Es poblen al pas
|
||||||
// següent; els declarem ara perquè `update()` pugui mutar-los i les
|
// següent; els declarem ara perquè `update()` pugui mutar-los i les
|
||||||
// signatures queden estables entre hitos.
|
// signatures queden estables entre hitos.
|
||||||
int brush_{-1}; // Índex de tile al tileset; -1 = sense brush.
|
int brush_{-1}; // Índex de tile al tileset; -1 = sense brush.
|
||||||
int cursor_col_{-1}; // Cel·la sota el cursor (-1 = fora).
|
int cursor_col_{-1}; // Cel·la del mapa sota el cursor (-1 = fora).
|
||||||
int cursor_row_{-1};
|
int cursor_row_{-1};
|
||||||
bool picker_open_{false};
|
bool picker_open_{false};
|
||||||
|
int picker_scroll_{0}; // Fila superior visible del picker (linealitzat).
|
||||||
|
int picker_hover_{-1}; // Índex del tile sota el cursor dins del picker.
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // _DEBUG
|
#endif // _DEBUG
|
||||||
@@ -164,6 +164,15 @@ namespace Defaults {
|
|||||||
constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU;
|
constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU;
|
||||||
// Ruta de persistència: writer surgical sobre la font del repo.
|
// Ruta de persistència: writer surgical sobre la font del repo.
|
||||||
constexpr const char* SAVE_PATH = "data/maps/pyramid.yaml";
|
constexpr const char* SAVE_PATH = "data/maps/pyramid.yaml";
|
||||||
|
// Píxels entre el canvas i el rectangle del TilePicker per totes
|
||||||
|
// les vores. Prou perquè el border blau de l'editor no es
|
||||||
|
// superpose amb la graella del picker.
|
||||||
|
constexpr int PICKER_MARGIN = 8;
|
||||||
|
// Fons del TilePicker (blau fosc, prou contrastat amb els tiles
|
||||||
|
// de color clar i clarament diferent del canvas).
|
||||||
|
constexpr std::uint32_t PICKER_BG_ARGB = 0xFF101824U;
|
||||||
|
// Vora 1 px del tile sota el cursor al picker.
|
||||||
|
constexpr std::uint32_t PICKER_HOVER_ARGB = 0xFFFFFFFFU;
|
||||||
} // namespace Editor
|
} // namespace Editor
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user