Compare commits

..

4 Commits

Author SHA1 Message Date
burkkyy ace69f613e Merge pull request 'Made console command registry a better usage pattern' (#4) from dev into main
Reviewed-on: #4
2026-09-19 00:57:44 -07:00
burkkyy e72ada5b40 branch-from command 2026-09-19 00:56:40 -07:00
burkkyy dffb7b7972 Changes:
- editor project
- build system fixes
- dummy data editor panels mock
2026-09-16 02:28:42 -07:00
burkkyy 169ff4a277 Made console command registry a better usage pattern 2026-09-16 01:59:34 -07:00
20 changed files with 430 additions and 42 deletions
+2 -1
View File
@@ -39,5 +39,6 @@ endif()
add_subdirectory(third_party) add_subdirectory(third_party)
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(editor)
add_subdirectory(testbed) add_subdirectory(testbed)
add_subdirectory(examples) add_subdirectory(examples)
+58
View File
@@ -1,13 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json
import os import os
import re import re
import shlex
import shutil import shutil
import struct import struct
import subprocess import subprocess
import sys import sys
COMMAND_TO_METHOD = { COMMAND_TO_METHOD = {
"branch-from": "branch_from",
"bash-completions": "bash_completions", "bash-completions": "bash_completions",
} }
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()} METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
@@ -36,6 +39,28 @@ class DevHelper:
command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug"
self._exec_in_root(command, replace=True) self._exec_in_root(command, replace=True)
##
# Checks out a branch named after a Gitea issue URL.
# If the issue is from this project's repo, the branch name will be:
# issue-<issue_number>/<issue_title>
# If the issue is from another repo (presumably upstream), the branch name will be:
# <issue_owner>-issue-<issue_number>/<issue_title>
def branch_from(self, *args, **kwargs):
issue_url = args[0] if args else None
if issue_url is None:
raise ValueError("Must provide a Gitea issue URL.")
base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)
exists = subprocess.run(
["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
stdout=subprocess.DEVNULL,
).returncode == 0
flag = "" if exists else "-b "
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
def renderdoc(self, *args, **kwargs): def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc() qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True) self._exec_in_root(qrenderdoc, replace=True)
@@ -84,6 +109,39 @@ class DevHelper:
print(" ".join(completions)) print(" ".join(completions))
# Private helpers # Private helpers
def _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None:
raise ValueError(f"Not a Gitea issue URL: {url}")
return match[1], f"{match[2]}/{match[3]}", match[4]
def _fetch_issue_title(self, base_url, repo, issue_number):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
print(f"Running: tea api {endpoint}")
result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
if result.returncode != 0:
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
return json.loads(result.stdout)["title"]
def _format_branch_name(self, issue_repo, issue_number, issue_title):
formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip())))
if issue_repo == self._origin_repo():
return f"issue-{issue_number}/{formatted_title}"
issue_owner = issue_repo.split("/")[0]
return f"{issue_owner}-issue-{issue_number}/{formatted_title}"
def _origin_repo(self):
result = subprocess.run(
["git", "-C", self._project_root(), "remote", "get-url", "origin"],
stdout=subprocess.PIPE,
text=True,
)
match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
return match[1] if match else None
def _find_glslc(self): def _find_glslc(self):
glslc = shutil.which("glslc") glslc = shutil.which("glslc")
if glslc is None: if glslc is None:
+26
View File
@@ -0,0 +1,26 @@
set(NAME editor)
set(ENGINE_NAME ${PROJECT_NAME})
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS *.cpp src/*.cpp)
add_library(${NAME}_deps INTERFACE)
target_link_libraries(${NAME}_deps INTERFACE
${ENGINE_NAME}
)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/build")
add_executable(editor ${SOURCES})
target_link_libraries(editor PRIVATE ${NAME}_deps)
target_include_directories(editor PRIVATE
"${CMAKE_SOURCE_DIR}/engine/include"
"${CMAKE_SOURCE_DIR}/external/imgui"
)
target_compile_definitions(editor PRIVATE
EDITOR_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/shaders"
)
+25
View File
@@ -0,0 +1,25 @@
#include <iostream>
#include <sophia/application.hpp>
#include "panels/asset_browser_panel.hpp"
#include "panels/inspector_panel.hpp"
#include "panels/scene_hierarchy_panel.hpp"
class Editor : public sophia::Application {
public:
Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {}
void onCreate() override {
std::cout << "Editor created" << std::endl;
debugUI().show();
panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>();
panels().emplace<AssetBrowserPanel>();
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
};
SOPHIA_MAIN(Editor)
+27
View File
@@ -0,0 +1,27 @@
#include "asset_browser_panel.hpp"
#include <imgui.h>
void AssetBrowserPanel::onUpdate(const sophia::FrameInfo&) {
ImGui::Begin("Asset Browser");
ImGui::BeginChild("Folders", ImVec2(150, 0), true);
for (int i = 0; i < static_cast<int>(folders.size()); i++) {
if (ImGui::Selectable(folders[static_cast<size_t>(i)].name.c_str(),
selectedFolder == i)) {
selectedFolder = i;
}
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("Files", ImVec2(0, 0), true);
const Folder& folder = folders[static_cast<size_t>(selectedFolder)];
for (const std::string& file : folder.files) {
ImGui::Selectable(file.c_str());
}
ImGui::EndChild();
ImGui::End();
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
class AssetBrowserPanel : public sophia::Panel {
public:
AssetBrowserPanel() : Panel{"Asset Browser"} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
struct Folder {
std::string name;
std::vector<std::string> files;
};
std::vector<Folder> folders = {
{"Models", {"player_capsule.fbx", "ground_plane.fbx", "enemy_grunt.fbx"}},
{"Textures",
{"brick_diffuse.png", "grass_albedo.png", "player_diffuse.png"}},
{"Materials", {"player_mat.mat", "ground_mat.mat"}},
{"Scripts", {"PlayerController.lua", "EnemyAI.lua"}},
{"Scenes", {"MainMenu.scene", "Level_01.scene"}},
};
int selectedFolder = 0;
};
+36
View File
@@ -0,0 +1,36 @@
#include "inspector_panel.hpp"
#include <imgui.h>
#include <cstdio>
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
ImGui::Begin("Inspector");
char nameBuf[128];
std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
name = nameBuf;
}
ImGui::Text("Tag: %s", tag.c_str());
ImGui::Separator();
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat3("Position", &position.x, 0.1f);
ImGui::DragFloat3("Rotation", &rotation.x, 0.1f);
ImGui::DragFloat3("Scale", &scale.x, 0.1f);
}
if (ImGui::CollapsingHeader("Mesh Renderer",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("Mesh: %s", meshName.c_str());
ImGui::Text("Material: %s", materialName.c_str());
}
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%s", scriptName.c_str());
}
ImGui::End();
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <glm/glm.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
class InspectorPanel : public sophia::Panel {
public:
InspectorPanel() : Panel{"Inspector"} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
std::string name = "Player";
std::string tag = "Player";
glm::vec3 position{0.0f, 1.0f, 0.0f};
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
std::string meshName = "player_capsule.fbx";
std::string materialName = "player_mat";
std::string scriptName = "PlayerController.lua";
};
+34
View File
@@ -0,0 +1,34 @@
#include "scene_hierarchy_panel.hpp"
#include <imgui.h>
void SceneHierarchyPanel::drawNode(const Node& node) {
if (node.children.empty()) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen |
ImGuiTreeNodeFlags_SpanAvailWidth;
if (node.name == selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::TreeNodeEx(node.name.c_str(), flags);
if (ImGui::IsItemClicked()) {
selected = node.name;
}
return;
}
if (ImGui::TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) {
for (const Node& child : node.children) {
drawNode(child);
}
ImGui::TreePop();
}
}
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
ImGui::Begin("Scene Hierarchy");
for (const Node& node : nodes) {
drawNode(node);
}
ImGui::End();
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
class SceneHierarchyPanel : public sophia::Panel {
public:
SceneHierarchyPanel() : Panel{"Scene Hierarchy"} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
struct Node {
std::string name;
std::vector<Node> children;
};
void drawNode(const Node& node);
std::vector<Node> nodes = {
{"Main Camera", {}},
{"Directional Light", {}},
{"Ground", {}},
{"Player", {}},
{"Enemies", {{"Enemy_01", {}}, {"Enemy_02", {}}, {"Enemy_03", {}}}},
{"Props", {{"Crate_01", {}}, {"Crate_02", {}}}},
};
std::string selected;
};
+1
View File
@@ -19,6 +19,7 @@ target_include_directories(${ENGINE_NAME} PUBLIC
target_link_libraries(${ENGINE_NAME} PUBLIC target_link_libraries(${ENGINE_NAME} PUBLIC
vulkan vulkan
shaderc
glfw glfw
glm glm
imgui imgui
+3
View File
@@ -6,6 +6,7 @@
#include <sophia/asset_registry.hpp> #include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp> #include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp> #include <sophia/ui/debug_ui.hpp>
@@ -52,6 +53,8 @@ class Application {
Camera2D camera; Camera2D camera;
std::vector<std::string> args; std::vector<std::string> args;
ConsoleCommandRegistry consoleCommandRegistry;
void drawMesh(MeshHandle handle, const glm::mat4& transform); void drawMesh(MeshHandle handle, const glm::mat4& transform);
private: private:
@@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <map>
#include <string>
#include <vector>
namespace sophia {
class ConsoleCommandRegistry {
public:
using Handler = std::function<std::string(const std::vector<std::string>&)>;
struct CommandInfo {
std::string name;
std::string help;
};
void registerCommand(std::string name, std::string help, Handler handler);
bool dispatch(const std::string& name, const std::vector<std::string>& args,
std::string& outResult) const;
std::vector<CommandInfo> list() const;
private:
struct Command {
std::string help;
Handler handler;
};
std::map<std::string, Command> commands;
};
} // namespace sophia
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <sophia/console/console_command_registry.hpp>
#include <sophia/handles.hpp> #include <sophia/handles.hpp>
#include <sophia/ui/panel_registry.hpp> #include <sophia/ui/panel_registry.hpp>
@@ -10,7 +11,7 @@ class DebugUI {
DebugUI(const DebugUI&) = delete; DebugUI(const DebugUI&) = delete;
DebugUI& operator=(const DebugUI&) = delete; DebugUI& operator=(const DebugUI&) = delete;
explicit DebugUI(PanelRegistry& registry); DebugUI(PanelRegistry& registry, ConsoleCommandRegistry& commands);
~DebugUI() = default; ~DebugUI() = default;
void show(); void show();
+1 -1
View File
@@ -206,7 +206,7 @@ Application::Application(const Config& config)
canvas{this}, canvas{this},
args{std::move(commandLineArgs)}, args{std::move(commandLineArgs)},
assetRegistry{impl->getDevice()}, assetRegistry{impl->getDevice()},
_debugUI{this->panelRegistry} {} _debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
Application::~Application() = default; Application::~Application() = default;
@@ -0,0 +1,31 @@
#include <sophia/console/console_command_registry.hpp>
namespace sophia {
void ConsoleCommandRegistry::registerCommand(std::string name, std::string help,
Handler handler) {
commands[std::move(name)] = Command{std::move(help), std::move(handler)};
}
bool ConsoleCommandRegistry::dispatch(const std::string& name,
const std::vector<std::string>& args,
std::string& outResult) const {
auto it = commands.find(name);
if (it == commands.end()) {
return false;
}
outResult = it->second.handler(args);
return true;
}
std::vector<ConsoleCommandRegistry::CommandInfo> ConsoleCommandRegistry::list()
const {
std::vector<CommandInfo> result;
result.reserve(commands.size());
for (const auto& [name, command] : commands) {
result.push_back(CommandInfo{name, command.help});
}
return result;
}
} // namespace sophia
+3 -2
View File
@@ -6,11 +6,12 @@
namespace sophia { namespace sophia {
DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} { DebugUI::DebugUI(PanelRegistry& registry, ConsoleCommandRegistry& commands)
: registry{registry} {
// #ifndef NDEBUG // #ifndef NDEBUG
this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry); this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry);
this->frameStatsHandle = registry.emplace<FrameStatsPanel>(); this->frameStatsHandle = registry.emplace<FrameStatsPanel>();
this->consoleHandle = registry.emplace<ConsolePanel>(); this->consoleHandle = registry.emplace<ConsolePanel>(commands);
// #endif // !NDEBUG // #endif // !NDEBUG
} }
+49 -17
View File
@@ -1,6 +1,7 @@
#include "console_panel.hpp" #include "console_panel.hpp"
#include <algorithm> #include <algorithm>
#include <sstream>
namespace sophia { namespace sophia {
@@ -11,10 +12,6 @@ constexpr ImVec4 COLOR_ERROR = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
constexpr ImVec4 COLOR_INFO = ImVec4(0.4f, 0.7f, 1.0f, 1.0f); constexpr ImVec4 COLOR_INFO = ImVec4(0.4f, 0.7f, 1.0f, 1.0f);
} // namespace } // namespace
void ConsolePanel::registerDefaultCommands() {
log("Console ready. Type 'help' for a list of commands.");
}
void ConsolePanel::log(const std::string& message) { void ConsolePanel::log(const std::string& message) {
entries.push_back(Entry{message, COLOR_DEFAULT}); entries.push_back(Entry{message, COLOR_DEFAULT});
scrollToBottom = true; scrollToBottom = true;
@@ -22,6 +19,16 @@ void ConsolePanel::log(const std::string& message) {
void ConsolePanel::clear() { entries.clear(); } void ConsolePanel::clear() { entries.clear(); }
std::vector<std::string> ConsolePanel::tokenize(const std::string& command) {
std::istringstream stream(command);
std::vector<std::string> tokens;
std::string token;
while (stream >> token) {
tokens.push_back(std::move(token));
}
return tokens;
}
void ConsolePanel::execute(const std::string& command) { void ConsolePanel::execute(const std::string& command) {
entries.push_back(Entry{"> " + command, COLOR_COMMAND}); entries.push_back(Entry{"> " + command, COLOR_COMMAND});
scrollToBottom = true; scrollToBottom = true;
@@ -31,19 +38,47 @@ void ConsolePanel::execute(const std::string& command) {
history.end()); history.end());
history.push_back(command); history.push_back(command);
if (command == "help") { std::vector<std::string> tokens = tokenize(command);
entries.push_back( if (tokens.empty()) {
Entry{"Available commands: help, clear, history", COLOR_INFO}); return;
} else if (command == "clear") { }
const std::string& name = tokens.front();
const std::vector<std::string> args(tokens.begin() + 1, tokens.end());
if (name == "help") {
entries.push_back(Entry{"help - List available commands", COLOR_INFO});
entries.push_back(Entry{"clear - Clear the console output", COLOR_INFO});
entries.push_back(Entry{"history - Show command history", COLOR_INFO});
for (const auto& info : consoleCommandRegistry.list()) {
entries.push_back(Entry{info.name + " - " + info.help, COLOR_INFO});
}
return;
}
if (name == "clear") {
clear(); clear();
} else if (command == "history") { return;
}
if (name == "history") {
for (const std::string& past : history) { for (const std::string& past : history) {
entries.push_back(Entry{past, COLOR_DEFAULT}); entries.push_back(Entry{past, COLOR_DEFAULT});
} }
} else if (commandCallback) { return;
commandCallback(*this, command); }
} else {
entries.push_back(Entry{"Unknown command: '" + command + "'", COLOR_ERROR}); std::string result;
try {
if (consoleCommandRegistry.dispatch(name, args, result)) {
if (!result.empty()) {
entries.push_back(Entry{result, COLOR_INFO});
}
} else {
entries.push_back(Entry{"Unknown command: '" + name + "'", COLOR_ERROR});
}
} catch (const std::exception& e) {
entries.push_back(Entry{e.what(), COLOR_ERROR});
} }
} }
@@ -81,8 +116,6 @@ void ConsolePanel::onUpdate(const FrameInfo&) {
if (ImGui::SmallButton("Clear")) { if (ImGui::SmallButton("Clear")) {
clear(); clear();
} }
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &autoScroll);
ImGui::Separator(); ImGui::Separator();
@@ -97,8 +130,7 @@ void ConsolePanel::onUpdate(const FrameInfo&) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
if (scrollToBottom || if (scrollToBottom) {
(autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
ImGui::SetScrollHereY(1.0f); ImGui::SetScrollHereY(1.0f);
} }
scrollToBottom = false; scrollToBottom = false;
+7 -12
View File
@@ -2,7 +2,7 @@
#include <imgui.h> #include <imgui.h>
#include <functional> #include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <string> #include <string>
@@ -12,39 +12,34 @@ namespace sophia {
class ConsolePanel : public Panel { class ConsolePanel : public Panel {
public: public:
using CommandCallback = explicit ConsolePanel(ConsoleCommandRegistry& consoleCommandRegistry)
std::function<void(ConsolePanel&, const std::string&)>; : Panel{"Console"}, consoleCommandRegistry{consoleCommandRegistry} {
log("Console ready. Type 'help' for a list of commands.");
ConsolePanel() : Panel{"Console"} { registerDefaultCommands(); } }
void onUpdate(const FrameInfo& frameInfo) override; void onUpdate(const FrameInfo& frameInfo) override;
void log(const std::string& message); void log(const std::string& message);
void clear(); void clear();
void setCommandCallback(CommandCallback callback) {
this->commandCallback = std::move(callback);
}
private: private:
struct Entry { struct Entry {
std::string text; std::string text;
ImVec4 color; ImVec4 color;
}; };
void registerDefaultCommands();
void execute(const std::string& command); void execute(const std::string& command);
int textEditCallback(struct ImGuiInputTextCallbackData* data); int textEditCallback(struct ImGuiInputTextCallbackData* data);
static std::vector<std::string> tokenize(const std::string& command);
std::vector<Entry> entries; std::vector<Entry> entries;
std::vector<std::string> history; std::vector<std::string> history;
int historyPos = -1; int historyPos = -1;
char inputBuffer[256] = {}; char inputBuffer[256] = {};
bool autoScroll = true;
bool scrollToBottom = false; bool scrollToBottom = false;
CommandCallback commandCallback; ConsoleCommandRegistry& consoleCommandRegistry;
}; };
} // namespace sophia } // namespace sophia
+3 -8
View File
@@ -3,15 +3,10 @@ set(ENGINE_NAME ${PROJECT_NAME})
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS *.cpp src/*.cpp) file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS *.cpp src/*.cpp)
add_library(deps INTERFACE) add_library(${NAME}_deps INTERFACE)
target_link_libraries(deps INTERFACE target_link_libraries(${NAME}_deps INTERFACE
${ENGINE_NAME} ${ENGINE_NAME}
vulkan
shaderc
glfw
glm
imgui
) )
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
@@ -19,7 +14,7 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CM
add_executable(testbed main.cpp) add_executable(testbed main.cpp)
target_link_libraries(testbed PRIVATE deps) target_link_libraries(testbed PRIVATE ${NAME}_deps)
target_include_directories(testbed PRIVATE target_include_directories(testbed PRIVATE
"${CMAKE_SOURCE_DIR}/engine/include" "${CMAKE_SOURCE_DIR}/engine/include"