feat(editor): spacing, border i centrat vertical del tilepicker via editor.yaml

This commit is contained in:
JailDesigner committed 2026-04-30 09:51:30 +02:00
1 parent bcbf990a5f
commit a53cf4b797
7 files changed
+235 -29

No files matched your search

+83 -28
View File
@@ -14,6 +14,7 @@
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp"
#include "game/editor/editor_config.hpp"
#include "game/levels/level_manager.hpp"
#include "game/map/tilemap.hpp"
#include "utils/defaults.hpp"
@@ -21,12 +22,20 @@
namespace {
// Gruix del border 1 px del picker: reservat dins del rectangle perquè
// no envaeixi els tiles ni se surti fora de la caixa. La cella efectiva
// de cada tile té `pitch = tile_size + spacing` i la graella s'inicia
// un píxel cap dins de cada vora.
constexpr int PICKER_BORDER = 1;
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Si el
// tileset cap en amplada (TCOLS·tile_size ≤ INNER_W), en respectem la
// graella natural — els tiles surten ordenats igual que al GIF, només
// amb scroll vertical si cal. Si no, linealitzem els índexs en una
// graella més ampla que sí cap, amb scroll vertical per veure la resta.
// encapsulats dins d'un rectangle amb `picker.margin` per vora i un
// border 1 px que el delimita. Si el tileset cap en amplada (la suma
// de tiles + spacing + border ≤ INNER_W), en respectem la graella
// natural — els tiles surten ordenats igual que al GIF; si no,
// linealitzem els índexs en una graella més ampla que sí cap. Quan el
// contingut cap sense scroll, la caixa s'arrossega a alçada mínima i
// es centra verticalment dins de l'àrea interna.
struct PickerLayout {
int x{0};
int y{0};
@@ -35,6 +44,8 @@ namespace {
int cols{0};
int rows_visible{0};
int tile_size{0};
int spacing{0};
int pitch{0}; // tile_size + spacing — pas entre orígens de tiles veïns.
int total_tiles{0};
int max_scroll{0};
};
@@ -42,12 +53,20 @@ namespace {
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
PickerLayout lay{};
lay.tile_size = tm.getTileSize();
const int M = Defaults::Editor::PICKER_MARGIN;
lay.spacing = std::max(0, EditorConfig::get().pickerSpacing());
lay.pitch = lay.tile_size + lay.spacing;
const int M = EditorConfig::get().pickerMargin();
const int INNER_W = GameCanvas::WIDTH - (2 * M);
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
if (lay.tile_size <= 0) { return lay; }
const int MAX_COLS = INNER_W / lay.tile_size;
lay.rows_visible = INNER_H / lay.tile_size;
if (lay.tile_size <= 0 || lay.pitch <= 0) { return lay; }
// Espai útil per a tiles, descomptat el border 1 px de cada vora.
// Inversió de N·ts + (N-1)·spacing ≤ AVAIL → N ≤ (AVAIL+spacing)/pitch.
const int AVAIL_W = INNER_W - (2 * PICKER_BORDER);
const int AVAIL_H = INNER_H - (2 * PICKER_BORDER);
const int MAX_COLS = std::max(0, (AVAIL_W + lay.spacing) / lay.pitch);
const int MAX_ROWS = std::max(0, (AVAIL_H + lay.spacing) / lay.pitch);
int tcols = 0;
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
if (TILESET != nullptr) {
@@ -59,17 +78,34 @@ namespace {
// fer servir cols = TCOLS reprodueix exactament la disposició del GIF.
const bool FITS_W = (tcols > 0 && tcols <= MAX_COLS);
lay.cols = FITS_W ? tcols : MAX_COLS;
lay.w = lay.cols * lay.tile_size;
lay.h = lay.rows_visible * lay.tile_size;
// Centrat horitzontalment quan la graella natural és més estreta;
// verticalment mantenim el marge superior i deixem scroll si cal.
lay.x = M + ((INNER_W - lay.w) / 2);
lay.y = M;
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
// Si tot el contingut cap sense scroll, encongim la caixa a les
// files reals; així podem centrar-la verticalment.
lay.rows_visible = std::min(MAX_ROWS, TOTAL_ROWS);
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
// Mida real de la caixa: tiles + spacing intern + 1 px de border per cara.
const int TILES_W = (lay.cols > 0) ? ((lay.cols * lay.tile_size) + ((lay.cols - 1) * lay.spacing)) : 0;
const int TILES_H = (lay.rows_visible > 0) ? ((lay.rows_visible * lay.tile_size) + ((lay.rows_visible - 1) * lay.spacing)) : 0;
lay.w = TILES_W + (2 * PICKER_BORDER);
lay.h = TILES_H + (2 * PICKER_BORDER);
// Centrat horitzontal i vertical dins de l'àrea interna; quan el
// picker arriba a l'alçada màxima, el centrat vertical degenera a
// origen=M (no hi ha espai extra que repartir).
lay.x = M + ((INNER_W - lay.w) / 2);
lay.y = M + ((INNER_H - lay.h) / 2);
return lay;
}
// Origen (cantonada superior-esquerra) del tile a la cel·la (vc, vr)
// dins del picker. Centralitzat perquè render i hit-test no divergeixin.
auto pickerTileOrigin(const PickerLayout& lay, int vc, int vr) -> std::pair<int, int> {
const int PX = lay.x + PICKER_BORDER + (vc * lay.pitch);
const int PY = lay.y + PICKER_BORDER + (vr * lay.pitch);
return {PX, PY};
}
} // namespace
void Editor::enter() {
@@ -179,11 +215,21 @@ void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
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; }
// Espai útil per als tiles dins de la caixa (descomptat el border 1 px).
const int INNER_X = LAY.x + PICKER_BORDER;
const int INNER_Y = LAY.y + PICKER_BORDER;
const int RX = canvas_x - INNER_X;
const int RY = canvas_y - INNER_Y;
if (RX >= 0 && RY >= 0 && LAY.tile_size > 0 && LAY.pitch > 0 && LAY.cols > 0) {
const int VC = RX / LAY.pitch;
const int VR = RY / LAY.pitch;
// Píxels de spacing entre cel·les: el clic cau a la "vora morta".
const bool IN_SPACING_X = (RX % LAY.pitch) >= LAY.tile_size;
const bool IN_SPACING_Y = (RY % LAY.pitch) >= LAY.tile_size;
if (!IN_SPACING_X && !IN_SPACING_Y && VC < LAY.cols && VR < LAY.rows_visible) {
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
}
}
for (const auto& ev : EventBuffer::events) {
@@ -214,7 +260,7 @@ void Editor::render(const Tilemap& tilemap) const {
tilemap.renderTilePreview(PX, PY, brush_);
}
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
const auto BORDER = EditorConfig::get().borderArgb();
screen.fillRect(0, 0, W, 1, BORDER);
screen.fillRect(0, H - 1, W, 1, BORDER);
screen.fillRect(0, 0, 1, H, BORDER);
@@ -223,34 +269,43 @@ void Editor::render(const Tilemap& tilemap) const {
void Editor::renderPicker(const Tilemap& tilemap) const {
const auto LAY = layoutPicker(tilemap);
if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; }
auto& screen = Screen::get();
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
const auto& cfg = EditorConfig::get();
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, cfg.pickerBgArgb());
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);
const auto [PX, PY] = pickerTileOrigin(LAY, vc, vr);
tilemap.renderTilePreview(PX, PY, IDX);
}
}
if (picker_hover_ >= 0 && LAY.cols > 0) {
if (picker_hover_ >= 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 auto [PX, PY] = pickerTileOrigin(LAY, VC, VR);
const int TS = LAY.tile_size;
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
const auto C = cfg.pickerHoverArgb();
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);
}
}
// Border 1 px del picker, dibuixat al damunt del fons i dels tiles
// perquè delimite clarament la caixa també quan hi ha tiles que
// arriben a la vora.
const auto BORDER = cfg.borderArgb();
screen.fillRect(LAY.x, LAY.y, LAY.w, 1, BORDER);
screen.fillRect(LAY.x, LAY.y + LAY.h - 1, LAY.w, 1, BORDER);
screen.fillRect(LAY.x, LAY.y, 1, LAY.h, BORDER);
screen.fillRect(LAY.x + LAY.w - 1, LAY.y, 1, LAY.h, BORDER);
}
#endif // _DEBUG