Fleshing out code generally.
This commit is contained in:
@@ -20,12 +20,12 @@ file(GLOB_RECURSE EDITOR_SRC "src/*.cpp")
|
||||
|
||||
include_directories("include")
|
||||
|
||||
CPMAddPackage(NAME mcolor
|
||||
URL https://git.redacted.cc/maxine/mcolor/archive/Prerelease-7.3.zip)
|
||||
|
||||
CPMAddPackage(NAME jlog
|
||||
URL https://git.redacted.cc/josh/jlog/archive/Prerelease-18.zip)
|
||||
|
||||
CPMAddPackage(NAME mcolor
|
||||
URL https://git.redacted.cc/maxine/mcolor/archive/Prerelease-7.2.zip)
|
||||
|
||||
CPMAddPackage(NAME Event
|
||||
URL https://git.redacted.cc/josh/Event/archive/Release-12.zip)
|
||||
@@ -51,7 +51,8 @@ if (UNIX)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
ADD_LIBRARY(Editor2D STATIC ${EDITOR_SRC})
|
||||
ADD_LIBRARY(Editor2D STATIC ${EDITOR_SRC}
|
||||
include/Preferences.hpp)
|
||||
endif()
|
||||
|
||||
set_target_properties(Editor2D PROPERTIES LINKER_LANGUAGE CXX)
|
||||
|
@@ -6,4 +6,4 @@ protected:
|
||||
private:
|
||||
|
||||
|
||||
}
|
||||
};
|
@@ -16,10 +16,44 @@
|
||||
using namespace ReWindow;
|
||||
using namespace JUI::UDimLiterals;
|
||||
|
||||
struct EditorCamera {
|
||||
Vector2 translation;
|
||||
float rotation;
|
||||
float scale;
|
||||
#include <EditorCamera.hpp>
|
||||
|
||||
class Action {
|
||||
public:
|
||||
std::string label;
|
||||
};
|
||||
|
||||
class SetTileAction: public Action {
|
||||
public:
|
||||
int tile_id;
|
||||
int prior_tile_id;
|
||||
Vector2i cell;
|
||||
|
||||
SetTileAction(const Vector2i& cell, int tileID, int priorTileID);
|
||||
};
|
||||
|
||||
class BucketFillAction: public Action {
|
||||
public:
|
||||
int tile_id;
|
||||
Vector2i cell;
|
||||
std::vector<SetTileAction> effected_tiles;
|
||||
|
||||
BucketFillAction();
|
||||
};
|
||||
|
||||
class UndoAction : public Action {};
|
||||
class RedoAction : public Action {};
|
||||
|
||||
class EditHistory {
|
||||
public:
|
||||
void Undo(int index = 0);
|
||||
void Redo(int index = 0);
|
||||
|
||||
std::vector<Action*> actions;
|
||||
};
|
||||
|
||||
class EditHistoryLogWindow : public JUI::Window {
|
||||
public:
|
||||
};
|
||||
|
||||
class EditorApp : public OpenGLWindow {
|
||||
@@ -39,6 +73,11 @@ public:
|
||||
Color4 bg_color = Colors::Black;
|
||||
|
||||
|
||||
bool show_cell_pointer_outline = true;
|
||||
Color4 cell_pointer_outline_color = Colors::Blues::LightSteelBlue;
|
||||
float cell_pointer_outline_width = 1.f;
|
||||
|
||||
|
||||
int grid_pixel_width = 16;
|
||||
int grid_pixel_height = 16;
|
||||
|
||||
@@ -61,336 +100,75 @@ public:
|
||||
|
||||
std::vector<Quad> quads;
|
||||
|
||||
void PopulateQuads() {
|
||||
quads.reserve(tileset_width*tileset_height);
|
||||
for (int i = 0; i < tileset_width*tileset_height; i++) {
|
||||
Vector2i cell = IndexToCell(i, tileset_width);
|
||||
quads[i] = Quad{cell.x*grid_pixel_width, cell.y*grid_pixel_height, grid_pixel_width, grid_pixel_height};
|
||||
}
|
||||
}
|
||||
/// Fill the tile-id lookup table with their respective partial-sprite bounding boxes.
|
||||
void PopulateQuads();
|
||||
|
||||
/// This provides a mapping from a 1D flat array to a 2D grid.
|
||||
/// @return The {x,y} coordinate of the grid-cell that maps to the given index.
|
||||
Vector2i IndexToCell(int index, int width);
|
||||
|
||||
Vector2i IndexToCell(int index, int width) {
|
||||
int x = index % width;
|
||||
int y = index / width;
|
||||
return {x, y};
|
||||
}
|
||||
/// This provides a mapping from a 2D grid to a 1D flat array.
|
||||
/// @return The {x,y} coordinate of the grid-cell that maps to the given index.
|
||||
int CellToIndex(Vector2i cell, int width);
|
||||
|
||||
int CellToIndex(Vector2i cell, int width) {
|
||||
return cell.y*width + cell.x;
|
||||
}
|
||||
EditorApp();
|
||||
|
||||
EditorApp() : OpenGLWindow("Editor App", 1776, 1000, GL_VER_MAJOR, GL_VER_MINOR) {
|
||||
camera.rotation = 0.f;
|
||||
camera.translation = {0.f, 0.f};
|
||||
camera.scale = 1.f;
|
||||
}
|
||||
/// Loads the test.lvl file into the editor.
|
||||
/// @note Placeholder until FileDialogs are added.
|
||||
void LoadTestFile();
|
||||
|
||||
void LoadTestFile() {
|
||||
std::ifstream input;
|
||||
input.open("test.lvl", std::ios::binary | std::ios::in);
|
||||
input.seekg(0, std::ios::end);
|
||||
int data_length = input.tellg();
|
||||
input.seekg(0, std::ios::beg);
|
||||
/// Saves the current editor's data to test.lvl.
|
||||
/// @note Placeholder until FileDialogs are added.
|
||||
void SaveTestFile();
|
||||
|
||||
char* buffer = new char[data_length];
|
||||
input.read(buffer, data_length);
|
||||
input.close();
|
||||
/// Loads test-data, including a tilesheet, and a binary level-data file.
|
||||
/// @note Placeholder until FileDialogs are added.
|
||||
/// @note Placeholder until Tileset json is added.
|
||||
void LoadMisc();
|
||||
|
||||
auto* data = reinterpret_cast<int*>(buffer);
|
||||
memcpy(grid, data, grid_rows * grid_cols * sizeof(int));
|
||||
}
|
||||
|
||||
void SaveTestFile() {
|
||||
std::ofstream output;
|
||||
output.open("test.lvl", std::ios::out | std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(&grid[0][0]), grid_rows * grid_cols * sizeof(int));
|
||||
output.close();
|
||||
}
|
||||
/// Creates a JUI widget that displays the Tileset, and lets you select a tile from it.
|
||||
JUI::Window* CreateTilesetViewerWindow(JUI::Widget* parent);
|
||||
|
||||
void LoadMisc() {
|
||||
test_tilesheet = new JGL::Texture("../megacommando.png");
|
||||
/// Create all JUI elements required for this program.
|
||||
void CreateWidgets();
|
||||
|
||||
auto texture_size = test_tilesheet->GetDimensions();
|
||||
tileset_width = texture_size.x / grid_pixel_width;
|
||||
tileset_height = texture_size.y / grid_pixel_height;
|
||||
/// Toggles the level grid.
|
||||
void ToggleGrid();
|
||||
|
||||
PopulateQuads();
|
||||
bool Open() override;
|
||||
|
||||
for (int x = 0; x < grid_rows; x++) {
|
||||
for (int y = 0; y < grid_cols; y++) {
|
||||
grid[x][y] = -1;
|
||||
}
|
||||
}
|
||||
Vector2i GetTilesetCellFromMouse();
|
||||
|
||||
if (std::filesystem::exists("test.lvl"))
|
||||
LoadTestFile();
|
||||
}
|
||||
Vector2i GetGridCellFromMouse();
|
||||
|
||||
void CameraUpdate(float elapsed);
|
||||
|
||||
JUI::Window* CreateTilesetViewerWindow(JUI::Widget* parent) {
|
||||
using namespace JUI;
|
||||
void SetTile(const Vector2i& cell, int tileID);
|
||||
|
||||
auto* wind = new Window(parent);
|
||||
wind->SetTitle("Tileset Viewer");
|
||||
wind->Size(JUI::UDim2::FromPixels(test_tilesheet->GetDimensions().x, test_tilesheet->GetDimensions().y+20));
|
||||
wind->SetResizable(false);
|
||||
void PlaceTileBrush(const Vector2i& cell);
|
||||
|
||||
auto* img = new Image(wind->Content());
|
||||
img->Content(test_tilesheet);
|
||||
img->FitImageToParent(false);
|
||||
void EraseTile(const Vector2i& cell);
|
||||
|
||||
auto* grid_overlay = new JGL::RenderTarget(test_tilesheet->GetDimensions());
|
||||
// TODO: supplying a translucent grid_overlay_color seems to not work?
|
||||
J2D::Begin(grid_overlay);
|
||||
DrawGrid(AABB2D({0,0}, Vector2(test_tilesheet->GetDimensions())), grid_overlay_color);
|
||||
J2D::End();
|
||||
void Update(float elapsed);
|
||||
|
||||
auto* grid_overlay_tex = new JGL::Texture(*grid_overlay->GetTexture());
|
||||
/// Draws a uniform grid of dashes lines at the boundaries of each tile.
|
||||
/// @param bounds The viewport in which to draw. Grid lines will not be drawn beyond this box.
|
||||
/// @param color The color with which to draw the grid lines.
|
||||
void DrawGrid(const AABB2D& bounds, const Color4& color);
|
||||
|
||||
void DrawGrid(const Color4& color = {255,255,255,128});
|
||||
|
||||
auto* overlay = new JUI::Image(wind->Content());
|
||||
overlay->Content(grid_overlay_tex);
|
||||
overlay->ZIndex(2);
|
||||
/// Draws a colored outline on the cell that the mouse-pointer is over.
|
||||
void DrawCellPointerOutline();
|
||||
|
||||
cell_indicator = new JUI::Rect(wind->Content());
|
||||
cell_indicator->Size(UDim2::FromPixels(grid_pixel_width, grid_pixel_height));
|
||||
cell_indicator->BorderMode(JUI::BorderMode::Outline);
|
||||
cell_indicator->BGColor(Colors::Transparent);
|
||||
cell_indicator->BorderColor(Colors::Blues::CornflowerBlue);
|
||||
cell_indicator->BorderWidth(2);
|
||||
|
||||
void DrawTiles();
|
||||
|
||||
return wind;
|
||||
}
|
||||
void Draw();
|
||||
|
||||
void CreateWidgets() {
|
||||
scene = new JUI::Scene();
|
||||
|
||||
auto* topbar = new JUI::UtilityBar(scene);
|
||||
|
||||
auto* file = topbar->AddSubmenu("File");
|
||||
file->SetFont(JGL::Fonts::Jupiteroid);
|
||||
file->AddButton("New");
|
||||
file->AddButton("Open");
|
||||
file->AddButton("Save", [this]{SaveTestFile();});
|
||||
file->AddButton("Save As");
|
||||
file->AddSeparator(2_px);
|
||||
file->AddButton("About");
|
||||
file->AddButton("Preferences");
|
||||
|
||||
auto* edit = topbar->AddSubmenu("Edit");
|
||||
edit->AddButton("Undo");
|
||||
edit->AddButton("Redo");
|
||||
edit->AddButton("Copy");
|
||||
edit->AddSeparator(2_px);
|
||||
edit->AddButton("Paste");
|
||||
edit->AddButton("Cut Selection");
|
||||
|
||||
auto* view = topbar->AddSubmenu("View");
|
||||
view->AddButton("Zoom In");
|
||||
view->AddButton("Zoom Out");
|
||||
view->AddSeparator(2_px);
|
||||
view->AddButton("Toggle Grid", [this]{ToggleGrid();});
|
||||
view->AddButton("Set Background Color", [this]{bg_color_tool_window->Toggle();});
|
||||
|
||||
auto* level = topbar->AddSubmenu("Level");
|
||||
auto* layer = topbar->AddSubmenu("Layer");
|
||||
layer->AddButton("New");
|
||||
layer->AddButton("Open from File");
|
||||
layer->AddButton("Duplicate Selected");
|
||||
layer->AddButton("Delete Selected");
|
||||
layer->AddButton("Edit Selected");
|
||||
layer->AddButton("Export Layer");
|
||||
|
||||
|
||||
tileset_viewer = CreateTilesetViewerWindow(scene);
|
||||
|
||||
bg_color_tool_window = new JUI::Window(scene);
|
||||
bg_color_tool_window->Close();
|
||||
|
||||
bg_color_tool = new JUI::ColorPicker(bg_color_tool_window->Content());
|
||||
bg_color_tool->OnColorValueChanged += [this] (Color4 new_color) mutable { bg_color = new_color; };
|
||||
|
||||
}
|
||||
|
||||
void ToggleGrid() {
|
||||
grid_overlay_enabled = !grid_overlay_enabled;
|
||||
}
|
||||
|
||||
bool Open() override {
|
||||
if (!OpenGLWindow::Open()) return false;
|
||||
|
||||
auto size = GetSize();
|
||||
auto vec_size = Vector2i(size.x, size.y);
|
||||
if (!JGL::Init(vec_size, 0.f, 1.f))
|
||||
return false;
|
||||
JGL::Update(vec_size);
|
||||
|
||||
glClearColor(0.f, 0.f, 0.f, 0.f);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
// TODO: Delete when we update to the next release of JGL
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // NOTE: This MUST be called for text rendering to work properly!!!
|
||||
|
||||
LoadMisc();
|
||||
CreateWidgets();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2i GetTilesetCellFromMouse() {
|
||||
auto maus = GetMouseCoordinates();
|
||||
|
||||
Vector2 mouse_v2(maus.x, maus.y);
|
||||
|
||||
|
||||
Vector2 rel = tileset_viewer->Content()->GetAbsolutePosition();
|
||||
|
||||
Vector2 rel_mouse = mouse_v2 - rel;
|
||||
|
||||
return Vector2i(
|
||||
Math::Floor(rel_mouse.x/grid_pixel_width),
|
||||
Math::Floor(rel_mouse.y/grid_pixel_height));
|
||||
}
|
||||
|
||||
Vector2i GetGridCellFromMouse() {
|
||||
auto maus = GetMouseCoordinates();
|
||||
|
||||
Vector2 mouse_v2(maus.x, maus.y);
|
||||
|
||||
return Vector2i(
|
||||
Math::Floor(mouse_v2.x/grid_pixel_width),
|
||||
Math::Floor(mouse_v2.y/grid_pixel_height));
|
||||
}
|
||||
|
||||
void CameraUpdate(float elapsed) {
|
||||
float move_rate = 10;
|
||||
float zoom_rate = 0.05f;
|
||||
|
||||
if (IsKeyDown(Keys::LeftArrow))
|
||||
camera.translation.x -= move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::RightArrow))
|
||||
camera.translation.x += move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::UpArrow))
|
||||
camera.translation.y -= move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::DownArrow))
|
||||
camera.translation.y += move_rate*elapsed;
|
||||
|
||||
if (IsKeyDown(Keys::Minus))
|
||||
camera.scale -= zoom_rate*elapsed;
|
||||
if (IsKeyDown(Keys::Equals))
|
||||
camera.scale += zoom_rate*elapsed;
|
||||
|
||||
}
|
||||
|
||||
void Update(float elapsed) {
|
||||
CameraUpdate(elapsed);
|
||||
auto size = GetSize();
|
||||
Vector2i vSize = Vector2i(size.x, size.y);
|
||||
|
||||
|
||||
JGL::Update(vSize);
|
||||
scene->SetViewportSize(Vector2(vSize));
|
||||
scene->Update(elapsed);
|
||||
|
||||
if (tileset_viewer->IsMouseInside()) {
|
||||
if (tileset_viewer->Content()->IsMouseInside()) {
|
||||
cell_indicator->Visible(true);
|
||||
|
||||
Vector2 rel_mouse = Vector2(GetTilesetCellFromMouse());
|
||||
|
||||
rel_mouse.x *= grid_pixel_width;
|
||||
rel_mouse.y *= grid_pixel_height;
|
||||
|
||||
cell_indicator->Position(JUI::UDim2::FromPixels(rel_mouse.x, rel_mouse.y));
|
||||
}
|
||||
} else {
|
||||
cell_indicator->Visible(false);
|
||||
|
||||
if (IsMouseButtonDown(MouseButtons::Left)) {
|
||||
Vector2i grid_cell = GetGridCellFromMouse();
|
||||
grid[grid_cell.x][grid_cell.y] = selected_quad;
|
||||
}
|
||||
|
||||
if (IsMouseButtonDown(MouseButtons::Right)) {
|
||||
Vector2i grid_cell = GetGridCellFromMouse();
|
||||
grid[grid_cell.x][grid_cell.y] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawGrid(const AABB2D& bounds, const Color4& color) {
|
||||
Vector2 viewport_topleft = bounds.minPoint;
|
||||
Vector2 viewport_bottomright = bounds.maxPoint;
|
||||
|
||||
int nearest_grid_left = Math::Floor(viewport_topleft.x / grid_pixel_width);
|
||||
int nearest_grid_right = Math::Floor(viewport_bottomright.x / grid_pixel_width);
|
||||
for (int x = nearest_grid_left; x <= nearest_grid_right; x++) {
|
||||
auto top = Vector2(x * grid_pixel_width, viewport_topleft.y);
|
||||
auto bottom = Vector2(x * grid_pixel_width, viewport_bottomright.y);
|
||||
JGL::J2D::DrawDashedLine(color, top, bottom, 4, 4, 1 / bounds.Width());
|
||||
}
|
||||
int nearest_grid_top = Math::Floor(viewport_topleft.y / grid_pixel_height);
|
||||
int nearest_grid_bottom = Math::Floor(viewport_bottomright.y / grid_pixel_height);
|
||||
for (int y = nearest_grid_top; y <= nearest_grid_bottom; y++) {
|
||||
auto left = Vector2(viewport_topleft.x, y * grid_pixel_height);
|
||||
auto right = Vector2(viewport_bottomright.x, y * grid_pixel_height);
|
||||
JGL::J2D::DrawDashedLine(color, left, right, 4, 4, 1 / bounds.Height());
|
||||
}
|
||||
}
|
||||
void DrawGrid(const Color4& color = {255,255,255,128}) {
|
||||
|
||||
float sw = GetWidth();
|
||||
float sh = GetHeight();
|
||||
|
||||
DrawGrid({{0,0}, {sw, sh}}, color);
|
||||
}
|
||||
|
||||
void Draw() {
|
||||
glClearColor(bg_color.RN(), bg_color.GN(), bg_color.BN(), 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
|
||||
|
||||
J2D::Begin();
|
||||
glPushMatrix();
|
||||
glTranslatef(-camera.translation.x, -camera.translation.y, 0);
|
||||
glRotatef(camera.rotation, 0, 0, 1);
|
||||
glScalef(camera.scale, camera.scale, 1);
|
||||
|
||||
if (grid_overlay_enabled)
|
||||
DrawGrid(grid_overlay_color);
|
||||
//J2D::DrawSprite(test_tilesheet, {32, 32}, 0, Vector2::Zero, Vector2::One, Colors::White);
|
||||
for (int gx = 0; gx < grid_rows; gx++) {
|
||||
for (int gy = 0; gy < grid_cols; gy++) {
|
||||
|
||||
auto quad_idx = grid[gx][gy];
|
||||
|
||||
if (quad_idx < 0)
|
||||
continue;
|
||||
|
||||
auto quad = quads[quad_idx];
|
||||
Vector2 pos(gx*grid_pixel_width, gy*grid_pixel_height);
|
||||
J2D::DrawPartialSprite(test_tilesheet, pos,
|
||||
Vector2(quad.x, quad.y), Vector2(quad.w, quad.h));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
glPopMatrix();
|
||||
J2D::End();
|
||||
|
||||
|
||||
|
||||
scene->Draw();
|
||||
}
|
||||
|
||||
void OnClosing() override {
|
||||
SaveTestFile();
|
||||
}
|
||||
// TODO: Closing the app nominally doesn't work on Linux.
|
||||
void OnClosing() override;
|
||||
|
||||
void OnRefresh(float elapsed) override {
|
||||
Update(elapsed);
|
||||
@@ -430,7 +208,6 @@ public:
|
||||
Vector2 mposv2(e.Position.x, e.Position.y);
|
||||
if (scene->ObserveMouseMovement(mposv2)) return;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void OnKeyDown(const KeyDownEvent &e) override {
|
||||
|
72
include/EditorCamera.hpp
Normal file
72
include/EditorCamera.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <J3ML/LinearAlgebra.hpp>
|
||||
#include <Utils.hpp>
|
||||
|
||||
struct EditorCamera {
|
||||
Lerped<Vector2> translation;
|
||||
Lerped<float> rotation;
|
||||
Lerped<float> scale;
|
||||
|
||||
Vector2 origin{0,0};
|
||||
|
||||
Vector2 screenSize{1024,768};
|
||||
|
||||
[[nodiscard]] Matrix4x4 CurrentTranslationMatrix() const;
|
||||
|
||||
[[nodiscard]] Matrix4x4 GoalTranslationMatrix() const;
|
||||
|
||||
Matrix4x4 CurrentRotationMatrix() const;
|
||||
|
||||
Matrix4x4 GoalRotationMatrix() const;
|
||||
|
||||
Matrix4x4 CurrentScaleMatrix() const;
|
||||
|
||||
Matrix4x4 GoalScaleMatrix() const
|
||||
{
|
||||
return Matrix4x4::Scale({scale.goal, scale.goal, 1});
|
||||
}
|
||||
|
||||
Matrix4x4 CurrentViewMatrix() const
|
||||
{
|
||||
return CurrentTranslationMatrix() * CurrentScaleMatrix();
|
||||
}
|
||||
Matrix4x4 GoalViewMatrix() const
|
||||
{
|
||||
return GoalTranslationMatrix() * GoalScaleMatrix();
|
||||
}
|
||||
|
||||
void DefaultState()
|
||||
{
|
||||
this->rotation.goal = 0.f;
|
||||
this->rotation.current = 0.f;
|
||||
this->rotation.rate = 5.f;
|
||||
this->translation.goal = {0.f, 0.f};
|
||||
this->translation.current = {0.f, 0.f};
|
||||
this->translation.rate = 5.f;
|
||||
this->scale.goal = 1.f;
|
||||
this->scale.current = 1.f;
|
||||
this->scale.rate = 5.f;
|
||||
}
|
||||
|
||||
Vector2 ScreenToCameraSpace(const Vector2& ssv) const {
|
||||
return {
|
||||
(ssv.x + translation.current.x) / scale.current,
|
||||
(ssv.y + translation.current.y) / scale.current
|
||||
};
|
||||
}
|
||||
Vector2 CameraToScreenSpace(const Vector2& csv) const{
|
||||
|
||||
return {
|
||||
(csv.x * scale.current) - translation.current.x,
|
||||
(csv.y * scale.current) - translation.current.y
|
||||
};
|
||||
}
|
||||
|
||||
void Update(float elapsed)
|
||||
{
|
||||
translation.Step(elapsed);
|
||||
rotation.Step(elapsed);
|
||||
scale.Step(elapsed);
|
||||
}
|
||||
};
|
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <TileLayer.hpp>
|
||||
|
||||
class Level {
|
||||
public:
|
||||
@@ -9,6 +11,8 @@ public:
|
||||
std::vector<std::string> tags;
|
||||
|
||||
std::vector<TileLayer> tile_layers;
|
||||
|
||||
|
||||
protected:
|
||||
private:
|
||||
};
|
||||
|
29
include/Preferences.hpp
Normal file
29
include/Preferences.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <JJX/JSON.hpp>
|
||||
#include <Color4.hpp>
|
||||
|
||||
using namespace JJX;
|
||||
|
||||
class Preferences {
|
||||
public:
|
||||
explicit Preferences(const json::value& json)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
json::value Serialize() const
|
||||
{
|
||||
json::value json;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public:
|
||||
Color4 GridColor() const;
|
||||
|
||||
bool ShowBackgroundGrid() const;
|
||||
bool ShowForegroundGrid() const;
|
||||
|
||||
bool AutoSave() const;
|
||||
};
|
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct TileCell {
|
||||
|
||||
int ID;
|
||||
bool flipH;
|
||||
bool flipV;
|
||||
};
|
||||
|
||||
class TileLayer {
|
||||
@@ -11,6 +13,8 @@ public:
|
||||
int cell_width;
|
||||
int cell_height;
|
||||
TileCell cells[][];
|
||||
|
||||
|
||||
protected:
|
||||
private:
|
||||
};
|
17
include/Utils.hpp
Normal file
17
include/Utils.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <J3ML/LinearAlgebra.hpp>
|
||||
|
||||
template <typename T>
|
||||
struct Lerped
|
||||
{
|
||||
T goal;
|
||||
T current;
|
||||
float rate;
|
||||
|
||||
void Step(float elapsed);
|
||||
|
||||
};
|
||||
|
||||
template <> inline void Lerped<float>::Step(float elapsed) { current = Math::Lerp(current, goal, rate*elapsed);}
|
||||
template <> inline void Lerped<Vector2>::Step(float elapsed) { current = Vector2::Lerp(current, goal, rate*elapsed);}
|
@@ -1 +1,410 @@
|
||||
#include <EditorApp.hpp>
|
||||
#include <EditorApp.hpp>
|
||||
|
||||
void EditorApp::PopulateQuads()
|
||||
{
|
||||
quads.reserve(tileset_width*tileset_height);
|
||||
for (int i = 0; i < tileset_width*tileset_height; i++) {
|
||||
Vector2i cell = IndexToCell(i, tileset_width);
|
||||
quads.push_back(Quad{cell.x*grid_pixel_width, cell.y*grid_pixel_height, grid_pixel_width, grid_pixel_height});
|
||||
}
|
||||
}
|
||||
|
||||
Vector2i EditorApp::IndexToCell(int index, int width)
|
||||
{
|
||||
int x = index % width;
|
||||
int y = index / width;
|
||||
return {x, y};
|
||||
}
|
||||
|
||||
int EditorApp::CellToIndex(Vector2i cell, int width)
|
||||
{
|
||||
return cell.y*width + cell.x;
|
||||
}
|
||||
|
||||
EditorApp::EditorApp(): OpenGLWindow("Editor App", 1776, 1000, GL_VER_MAJOR, GL_VER_MINOR)
|
||||
{
|
||||
camera.DefaultState();
|
||||
}
|
||||
|
||||
void EditorApp::LoadTestFile()
|
||||
{
|
||||
std::ifstream input;
|
||||
input.open("test.lvl", std::ios::binary | std::ios::in);
|
||||
input.seekg(0, std::ios::end);
|
||||
int data_length = input.tellg();
|
||||
input.seekg(0, std::ios::beg);
|
||||
|
||||
char* buffer = new char[data_length];
|
||||
input.read(buffer, data_length);
|
||||
input.close();
|
||||
|
||||
auto* data = reinterpret_cast<int*>(buffer);
|
||||
memcpy(grid, data, grid_rows * grid_cols * sizeof(int));
|
||||
}
|
||||
|
||||
void EditorApp::SaveTestFile()
|
||||
{
|
||||
std::ofstream output;
|
||||
output.open("test.lvl", std::ios::out | std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(&grid[0][0]), grid_rows * grid_cols * sizeof(int));
|
||||
output.close();
|
||||
}
|
||||
|
||||
void EditorApp::LoadMisc()
|
||||
{
|
||||
test_tilesheet = new JGL::Texture("../megacommando.png");
|
||||
|
||||
auto texture_size = test_tilesheet->GetDimensions();
|
||||
tileset_width = texture_size.x / grid_pixel_width;
|
||||
tileset_height = texture_size.y / grid_pixel_height;
|
||||
|
||||
PopulateQuads();
|
||||
|
||||
for (int x = 0; x < grid_rows; x++) {
|
||||
for (int y = 0; y < grid_cols; y++) {
|
||||
grid[x][y] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (std::filesystem::exists("test.lvl"))
|
||||
LoadTestFile();
|
||||
}
|
||||
|
||||
JUI::Window* EditorApp::CreateTilesetViewerWindow(JUI::Widget* parent)
|
||||
{
|
||||
using namespace JUI;
|
||||
|
||||
auto* wind = new Window(parent);
|
||||
wind->SetTitle("Tileset Viewer");
|
||||
wind->Size(JUI::UDim2::FromPixels(test_tilesheet->GetDimensions().x, test_tilesheet->GetDimensions().y+20));
|
||||
wind->SetResizable(false);
|
||||
|
||||
auto* img = new Image(wind->Content());
|
||||
img->Content(test_tilesheet);
|
||||
img->FitImageToParent(false);
|
||||
|
||||
auto* grid_overlay = new JGL::RenderTarget(test_tilesheet->GetDimensions());
|
||||
// TODO: supplying a translucent grid_overlay_color seems to not work?
|
||||
J2D::Begin(grid_overlay);
|
||||
DrawGrid(AABB2D({0,0}, Vector2(test_tilesheet->GetDimensions())), grid_overlay_color);
|
||||
J2D::End();
|
||||
|
||||
auto* grid_overlay_tex = new JGL::Texture(*grid_overlay->GetTexture());
|
||||
|
||||
|
||||
auto* overlay = new JUI::Image(wind->Content());
|
||||
overlay->Content(grid_overlay_tex);
|
||||
overlay->ZIndex(2);
|
||||
|
||||
cell_indicator = new JUI::Rect(wind->Content());
|
||||
cell_indicator->Size(UDim2::FromPixels(grid_pixel_width, grid_pixel_height));
|
||||
cell_indicator->BorderMode(JUI::BorderMode::Outline);
|
||||
cell_indicator->BGColor(Colors::Transparent);
|
||||
cell_indicator->BorderColor(Colors::Blues::CornflowerBlue);
|
||||
cell_indicator->BorderWidth(2);
|
||||
|
||||
|
||||
return wind;
|
||||
}
|
||||
|
||||
void EditorApp::CreateWidgets()
|
||||
{
|
||||
scene = new JUI::Scene();
|
||||
|
||||
auto* topbar = new JUI::UtilityBar(scene);
|
||||
|
||||
auto* file = topbar->AddSubmenu("File");
|
||||
file->SetFont(JGL::Fonts::Jupiteroid);
|
||||
file->AddButton("New");
|
||||
file->AddButton("Open");
|
||||
file->AddButton("Save", [this]{SaveTestFile();});
|
||||
file->AddButton("Save As");
|
||||
file->AddSeparator(2_px);
|
||||
file->AddButton("About");
|
||||
file->AddButton("Preferences");
|
||||
|
||||
auto* edit = topbar->AddSubmenu("Edit");
|
||||
edit->AddButton("Undo");
|
||||
edit->AddButton("Redo");
|
||||
edit->AddButton("Copy");
|
||||
edit->AddSeparator(2_px);
|
||||
edit->AddButton("Paste");
|
||||
edit->AddButton("Cut Selection");
|
||||
|
||||
auto* view = topbar->AddSubmenu("View");
|
||||
view->AddButton("Zoom In");
|
||||
view->AddButton("Zoom Out");
|
||||
view->AddSeparator(2_px);
|
||||
view->AddButton("Toggle Grid", [this]{ToggleGrid();});
|
||||
view->AddButton("Set Background Color", [this]{bg_color_tool_window->Toggle();});
|
||||
|
||||
auto* level = topbar->AddSubmenu("Level");
|
||||
auto* layer = topbar->AddSubmenu("Layer");
|
||||
layer->AddButton("New");
|
||||
layer->AddButton("Open from File");
|
||||
layer->AddButton("Duplicate Selected");
|
||||
layer->AddButton("Delete Selected");
|
||||
layer->AddButton("Edit Selected");
|
||||
layer->AddButton("Export Layer");
|
||||
|
||||
|
||||
tileset_viewer = CreateTilesetViewerWindow(scene);
|
||||
|
||||
bg_color_tool_window = new JUI::Window(scene);
|
||||
bg_color_tool_window->Close();
|
||||
|
||||
bg_color_tool = new JUI::ColorPicker(bg_color_tool_window->Content());
|
||||
bg_color_tool->OnColorValueChanged += [this] (Color4 new_color) mutable { bg_color = new_color; };
|
||||
|
||||
}
|
||||
|
||||
void EditorApp::ToggleGrid()
|
||||
{
|
||||
grid_overlay_enabled = !grid_overlay_enabled;
|
||||
}
|
||||
|
||||
bool EditorApp::Open()
|
||||
{
|
||||
if (!OpenGLWindow::Open()) return false;
|
||||
|
||||
auto size = GetSize();
|
||||
auto vec_size = Vector2i(size.x, size.y);
|
||||
if (!JGL::Init(vec_size, 0.f, 1.f))
|
||||
return false;
|
||||
JGL::Update(vec_size);
|
||||
|
||||
glClearColor(0.f, 0.f, 0.f, 0.f);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
// TODO: Delete when we update to the next release of JGL
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // NOTE: This MUST be called for text rendering to work properly!!!
|
||||
|
||||
LoadMisc();
|
||||
CreateWidgets();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2i EditorApp::GetTilesetCellFromMouse()
|
||||
{
|
||||
auto maus = GetMouseCoordinates();
|
||||
|
||||
Vector2 mouse_v2(maus.x, maus.y);
|
||||
|
||||
|
||||
Vector2 rel = tileset_viewer->Content()->GetAbsolutePosition();
|
||||
|
||||
Vector2 rel_mouse = mouse_v2 - rel;
|
||||
|
||||
return Vector2i(
|
||||
Math::Floor(rel_mouse.x/grid_pixel_width),
|
||||
Math::Floor(rel_mouse.y/grid_pixel_height));
|
||||
}
|
||||
|
||||
Vector2i EditorApp::GetGridCellFromMouse()
|
||||
{
|
||||
auto maus = GetMouseCoordinates();
|
||||
|
||||
Vector2 mouse_v2(maus.x, maus.y);
|
||||
|
||||
|
||||
mouse_v2 = camera.ScreenToCameraSpace(mouse_v2);
|
||||
|
||||
return Vector2i(
|
||||
Math::Floor(mouse_v2.x/grid_pixel_width),
|
||||
Math::Floor(mouse_v2.y/grid_pixel_height));
|
||||
}
|
||||
|
||||
void EditorApp::CameraUpdate(float elapsed)
|
||||
{
|
||||
float move_rate = 40;
|
||||
float zoom_rate = 0.1f;
|
||||
|
||||
if (IsKeyDown(Keys::LeftArrow))
|
||||
camera.translation.goal.x -= move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::RightArrow))
|
||||
camera.translation.goal.x += move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::UpArrow))
|
||||
camera.translation.goal.y -= move_rate*elapsed;
|
||||
if (IsKeyDown(Keys::DownArrow))
|
||||
camera.translation.goal.y += move_rate*elapsed;
|
||||
|
||||
if (IsKeyDown(Keys::Minus))
|
||||
camera.scale.goal -= zoom_rate*elapsed;
|
||||
if (IsKeyDown(Keys::Equals))
|
||||
camera.scale.goal += zoom_rate*elapsed;
|
||||
|
||||
camera.Update(elapsed);
|
||||
|
||||
}
|
||||
|
||||
void EditorApp::SetTile(const Vector2i& cell, int tileID)
|
||||
{
|
||||
// out of bounds horizontally
|
||||
if (cell.x > grid_rows) return;
|
||||
|
||||
// out of bounds vertically
|
||||
if (cell.y > grid_cols) return;
|
||||
|
||||
grid[cell.x][cell.y] = tileID;
|
||||
}
|
||||
|
||||
void EditorApp::PlaceTileBrush(const Vector2i& cell)
|
||||
{
|
||||
|
||||
// out of bounds horizontally
|
||||
if (cell.x > grid_rows) return;
|
||||
|
||||
// out of bounds vertically
|
||||
if (cell.y > grid_cols) return;
|
||||
|
||||
SetTile(cell, selected_quad);
|
||||
}
|
||||
|
||||
void EditorApp::EraseTile(const Vector2i& cell)
|
||||
{
|
||||
// out of bounds horizontally
|
||||
if (cell.x > grid_rows) return;
|
||||
|
||||
// out of bounds vertically
|
||||
if (cell.y > grid_cols) return;
|
||||
|
||||
SetTile(cell, -1);
|
||||
}
|
||||
|
||||
void EditorApp::Update(float elapsed)
|
||||
{
|
||||
CameraUpdate(elapsed);
|
||||
auto size = GetSize();
|
||||
Vector2i vSize = Vector2i(size.x, size.y);
|
||||
|
||||
camera.screenSize.x = vSize.x;
|
||||
camera.screenSize.y = vSize.y;
|
||||
|
||||
JGL::Update(vSize);
|
||||
scene->SetViewportSize(Vector2(vSize));
|
||||
scene->Update(elapsed);
|
||||
|
||||
if (tileset_viewer->IsMouseInside()) {
|
||||
if (tileset_viewer->Content()->IsMouseInside()) {
|
||||
cell_indicator->Visible(true);
|
||||
|
||||
Vector2 rel_mouse = Vector2(GetTilesetCellFromMouse());
|
||||
|
||||
rel_mouse.x *= grid_pixel_width;
|
||||
rel_mouse.y *= grid_pixel_height;
|
||||
|
||||
cell_indicator->Position(JUI::UDim2::FromPixels(rel_mouse.x, rel_mouse.y));
|
||||
}
|
||||
} else {
|
||||
cell_indicator->Visible(false);
|
||||
|
||||
if (IsMouseButtonDown(MouseButtons::Left))
|
||||
PlaceTileBrush(GetGridCellFromMouse());
|
||||
|
||||
if (IsMouseButtonDown(MouseButtons::Right))
|
||||
EraseTile(GetGridCellFromMouse());
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::DrawGrid(const AABB2D& bounds, const Color4& color)
|
||||
{
|
||||
Vector2 viewport_topleft = bounds.minPoint;
|
||||
Vector2 viewport_bottomright = bounds.maxPoint;
|
||||
|
||||
int nearest_grid_left = Math::Floor(viewport_topleft.x / grid_pixel_width);
|
||||
int nearest_grid_right = Math::Floor(viewport_bottomright.x / grid_pixel_width);
|
||||
for (int x = nearest_grid_left; x <= nearest_grid_right; x++) {
|
||||
auto top = Vector2(x * grid_pixel_width, viewport_topleft.y);
|
||||
auto bottom = Vector2(x * grid_pixel_width, viewport_bottomright.y);
|
||||
JGL::J2D::DrawDashedLine(color, top, bottom, 4, 4, 1 / bounds.Width());
|
||||
}
|
||||
int nearest_grid_top = Math::Floor(viewport_topleft.y / grid_pixel_height);
|
||||
int nearest_grid_bottom = Math::Floor(viewport_bottomright.y / grid_pixel_height);
|
||||
for (int y = nearest_grid_top; y <= nearest_grid_bottom; y++) {
|
||||
auto left = Vector2(viewport_topleft.x, y * grid_pixel_height);
|
||||
auto right = Vector2(viewport_bottomright.x, y * grid_pixel_height);
|
||||
JGL::J2D::DrawDashedLine(color, left, right, 4, 4, 1 / bounds.Height());
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::DrawGrid(const Color4& color)
|
||||
{
|
||||
|
||||
float sw = GetWidth();
|
||||
float sh = GetHeight();
|
||||
|
||||
DrawGrid({{0,0}, {sw, sh}}, color);
|
||||
}
|
||||
|
||||
void EditorApp::DrawCellPointerOutline()
|
||||
{
|
||||
Vector2 rel_mouse = Vector2(GetGridCellFromMouse());
|
||||
|
||||
rel_mouse.x *= grid_pixel_width;
|
||||
rel_mouse.y *= grid_pixel_height;
|
||||
|
||||
J2D::OutlineRect(cell_pointer_outline_color, rel_mouse, Vector2(grid_pixel_width, grid_pixel_height), cell_pointer_outline_width);
|
||||
}
|
||||
|
||||
void EditorApp::DrawTiles()
|
||||
{
|
||||
for (int gx = 0; gx < grid_rows; gx++) {
|
||||
for (int gy = 0; gy < grid_cols; gy++) {
|
||||
|
||||
auto quad_idx = grid[gx][gy];
|
||||
|
||||
if (quad_idx < 0)
|
||||
continue;
|
||||
|
||||
auto quad = quads[quad_idx];
|
||||
Vector2 pos(gx*grid_pixel_width, gy*grid_pixel_height);
|
||||
J2D::DrawPartialSprite(test_tilesheet, pos,
|
||||
Vector2(quad.x, quad.y), Vector2(quad.w, quad.h));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::Draw()
|
||||
{
|
||||
glClearColor(bg_color.RN(), bg_color.GN(), bg_color.BN(), 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
J2D::Begin();
|
||||
glPushMatrix();
|
||||
glTranslatef(-camera.translation.current.x, -camera.translation.current.y, 0);
|
||||
glRotatef(camera.rotation.current, 0, 0, 1);
|
||||
glScalef(camera.scale.current, camera.scale.current, 1);
|
||||
|
||||
if (grid_overlay_enabled)
|
||||
DrawGrid(grid_overlay_color);
|
||||
|
||||
DrawTiles();
|
||||
|
||||
if (grid_overlay_enabled)
|
||||
DrawGrid(grid_overlay_color);
|
||||
|
||||
DrawCellPointerOutline();
|
||||
|
||||
glPopMatrix();
|
||||
|
||||
auto maus = GetMouseCoordinates();
|
||||
|
||||
Vector2 mouse_v2(maus.x, maus.y);
|
||||
|
||||
auto bruhbruh = camera.CameraToScreenSpace(camera.ScreenToCameraSpace(mouse_v2));
|
||||
|
||||
J2D::DrawPoint(Colors::White, bruhbruh, 4);
|
||||
J2D::End();
|
||||
|
||||
|
||||
|
||||
scene->Draw();
|
||||
}
|
||||
|
||||
void EditorApp::OnClosing()
|
||||
{
|
||||
SaveTestFile();
|
||||
}
|
||||
|
28
src/EditorCamera.cpp
Normal file
28
src/EditorCamera.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <EditorCamera.hpp>
|
||||
|
||||
Matrix4x4 EditorCamera::CurrentTranslationMatrix() const
|
||||
{
|
||||
return Matrix4x4::FromTranslation({translation.current.x, translation.current.y, 0});
|
||||
}
|
||||
|
||||
Matrix4x4 EditorCamera::GoalTranslationMatrix() const
|
||||
{
|
||||
return Matrix4x4::FromTranslation({translation.goal.x, translation.goal.y, 0});
|
||||
}
|
||||
|
||||
Matrix4x4 EditorCamera::CurrentRotationMatrix() const
|
||||
{
|
||||
// TODO: Which axis?
|
||||
return Matrix4x4::RotateX(rotation.current);
|
||||
}
|
||||
|
||||
Matrix4x4 EditorCamera::GoalRotationMatrix() const
|
||||
{
|
||||
// TODO: Which axis?
|
||||
return Matrix4x4::RotateX(rotation.goal);
|
||||
}
|
||||
|
||||
Matrix4x4 EditorCamera::CurrentScaleMatrix() const
|
||||
{
|
||||
return Matrix4x4::Scale({scale.current, scale.current, 1});
|
||||
}
|
Reference in New Issue
Block a user