aboutsummaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/10-common/Color.hpp148
-rw-r--r--source/10-common/DtoHelper.hpp10
-rw-r--r--source/10-common/Enum.hpp110
-rw-r--r--source/10-common/Log.cpp116
-rw-r--r--source/10-common/Log.hpp55
-rw-r--r--source/10-common/LookupTable.hpp64
-rw-r--r--source/10-common/Macros.hpp31
-rw-r--r--source/10-common/OpaqueIterator.hpp31
-rw-r--r--source/10-common/PodVector.hpp297
-rw-r--r--source/10-common/RTTI.hpp44
-rw-r--r--source/10-common/RapidJsonHelper.hpp114
-rw-r--r--source/10-common/RcPtr.hpp120
-rw-r--r--source/10-common/Rect.hpp164
-rw-r--r--source/10-common/RingBuffer.hpp191
-rw-r--r--source/10-common/ScopeGuard.hpp60
-rw-r--r--source/10-common/SmallVector.cpp145
-rw-r--r--source/10-common/SmallVector.hpp1332
-rw-r--r--source/10-common/StbImplementations.c14
-rw-r--r--source/10-common/Type2ObjectMap.hpp38
-rw-r--r--source/10-common/TypeTraits.hpp27
-rw-r--r--source/10-common/Uid.cpp70
-rw-r--r--source/10-common/Uid.hpp46
-rw-r--r--source/10-common/Utils.cpp130
-rw-r--r--source/10-common/Utils.hpp77
-rw-r--r--source/10-common/YCombinator.hpp14
-rw-r--r--source/10-editor-common/ImGuiGuizmo.cpp2897
-rw-r--r--source/10-editor-common/ImGuiGuizmo.hpp232
-rw-r--r--source/10-editor-common/ImGuiNotification.cpp277
-rw-r--r--source/10-editor-common/ImGuiNotification.hpp81
-rw-r--r--source/20-codegen-compiler/CodegenConfig.hpp11
-rw-r--r--source/20-codegen-compiler/CodegenDecl.cpp74
-rw-r--r--source/20-codegen-compiler/CodegenDecl.hpp154
-rw-r--r--source/20-codegen-compiler/CodegenLexer.cpp202
-rw-r--r--source/20-codegen-compiler/CodegenLexer.hpp49
-rw-r--r--source/20-codegen-compiler/CodegenModel.cpp732
-rw-r--r--source/20-codegen-compiler/CodegenModel.hpp61
-rw-r--r--source/20-codegen-compiler/CodegenOutput.cpp39
-rw-r--r--source/20-codegen-compiler/CodegenOutput.hpp34
-rw-r--r--source/20-codegen-compiler/CodegenUtils.cpp171
-rw-r--r--source/20-codegen-compiler/CodegenUtils.hpp57
-rw-r--r--source/20-codegen-compiler/SQLiteHelper.hpp220
-rw-r--r--source/20-codegen-compiler/main.cpp1443
-rw-r--r--source/20-codegen-compiler/test/examples/TestClass.hpp.txt38
-rw-r--r--source/20-codegen-compiler/test/examples/TestEnum.hpp.txt44
-rw-r--r--source/20-codegen-runtime/MacrosCodegen.hpp10
-rw-r--r--source/20-codegen-runtime/Metadata.cpp45
-rw-r--r--source/20-codegen-runtime/Metadata.hpp33
-rw-r--r--source/20-codegen-runtime/MetadataBase.cpp5
-rw-r--r--source/20-codegen-runtime/MetadataBase.hpp53
-rw-r--r--source/20-codegen-runtime/MetadataDetails.hpp7
-rw-r--r--source/30-game/App.cpp168
-rw-r--r--source/30-game/App.hpp62
-rw-r--r--source/30-game/AppConfig.hpp26
-rw-r--r--source/30-game/Camera.cpp46
-rw-r--r--source/30-game/Camera.hpp35
-rw-r--r--source/30-game/CommonVertexIndex.cpp152
-rw-r--r--source/30-game/CommonVertexIndex.hpp77
-rw-r--r--source/30-game/EditorAccessories.cpp26
-rw-r--r--source/30-game/EditorAccessories.hpp8
-rw-r--r--source/30-game/EditorAttachment.hpp12
-rw-r--r--source/30-game/EditorAttachmentImpl.cpp23
-rw-r--r--source/30-game/EditorAttachmentImpl.hpp34
-rw-r--r--source/30-game/EditorCommandPalette.cpp406
-rw-r--r--source/30-game/EditorCommandPalette.hpp94
-rw-r--r--source/30-game/EditorCore.hpp39
-rw-r--r--source/30-game/EditorCorePrivate.cpp1171
-rw-r--r--source/30-game/EditorCorePrivate.hpp136
-rw-r--r--source/30-game/EditorUtils.cpp447
-rw-r--r--source/30-game/EditorUtils.hpp84
-rw-r--r--source/30-game/EditorWorldGuides.cpp26
-rw-r--r--source/30-game/EditorWorldGuides.hpp16
-rw-r--r--source/30-game/FuzzyMatch.cpp174
-rw-r--r--source/30-game/FuzzyMatch.hpp10
-rw-r--r--source/30-game/GameObject.cpp230
-rw-r--r--source/30-game/GameObject.hpp107
-rw-r--r--source/30-game/GraphicsTags.cpp273
-rw-r--r--source/30-game/GraphicsTags.hpp111
-rw-r--r--source/30-game/Image.cpp101
-rw-r--r--source/30-game/Image.hpp38
-rw-r--r--source/30-game/Input.cpp23
-rw-r--r--source/30-game/Input.hpp23
-rw-r--r--source/30-game/Ires.cpp436
-rw-r--r--source/30-game/Ires.hpp130
-rw-r--r--source/30-game/Level.cpp228
-rw-r--r--source/30-game/Level.hpp95
-rw-r--r--source/30-game/Material.cpp526
-rw-r--r--source/30-game/Material.hpp125
-rw-r--r--source/30-game/Mesh.cpp54
-rw-r--r--source/30-game/Mesh.hpp45
-rw-r--r--source/30-game/Player.cpp139
-rw-r--r--source/30-game/Player.hpp59
-rw-r--r--source/30-game/Renderer.cpp256
-rw-r--r--source/30-game/Renderer.hpp92
-rw-r--r--source/30-game/SceneThings.cpp142
-rw-r--r--source/30-game/SceneThings.hpp52
-rw-r--r--source/30-game/Shader.cpp711
-rw-r--r--source/30-game/Shader.hpp180
-rw-r--r--source/30-game/Sprite.cpp328
-rw-r--r--source/30-game/Sprite.hpp111
-rw-r--r--source/30-game/Texture.cpp250
-rw-r--r--source/30-game/Texture.hpp99
-rw-r--r--source/30-game/VertexIndex.cpp84
-rw-r--r--source/30-game/VertexIndex.hpp67
-rw-r--r--source/30-game/World.cpp79
-rw-r--r--source/30-game/World.hpp25
-rw-r--r--source/30-game/main.cpp545
106 files changed, 19453 insertions, 0 deletions
diff --git a/source/10-common/Color.hpp b/source/10-common/Color.hpp
new file mode 100644
index 0000000..ef0c5a9
--- /dev/null
+++ b/source/10-common/Color.hpp
@@ -0,0 +1,148 @@
+#pragma once
+
+#include "Utils.hpp"
+
+#include <algorithm>
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <limits>
+
+class HsvColor;
+class RgbaColor {
+public:
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ uint8_t a;
+
+public:
+ constexpr RgbaColor() noexcept
+ : r{ 255 }
+ , g{ 255 }
+ , b{ 255 }
+ , a{ 255 } {
+ }
+
+ constexpr RgbaColor(float r, float g, float b, float a = 1.0f) noexcept
+ : r{ static_cast<uint8_t>(r * 255.0f) }
+ , g{ static_cast<uint8_t>(g * 255.0f) }
+ , b{ static_cast<uint8_t>(b * 255.0f) }
+ , a{ static_cast<uint8_t>(a * 255.0f) } {
+ }
+
+ constexpr RgbaColor(int r, int g, int b, int a = 255) noexcept
+ : r{ static_cast<uint8_t>(r & 0xFF) }
+ , g{ static_cast<uint8_t>(g & 0xFF) }
+ , b{ static_cast<uint8_t>(b & 0xFF) }
+ , a{ static_cast<uint8_t>(a & 0xFF) } {
+ }
+
+ constexpr RgbaColor(uint32_t rgba) noexcept
+ : r{ static_cast<uint8_t>((rgba >> 0) & 0xFF) }
+ , g{ static_cast<uint8_t>((rgba >> 8) & 0xFF) }
+ , b{ static_cast<uint8_t>((rgba >> 16) & 0xFF) }
+ , a{ static_cast<uint8_t>((rgba >> 24) & 0xFF) } {
+ }
+
+ constexpr uint32_t GetScalar() const noexcept {
+ uint32_t res = 0;
+ res |= r << 0;
+ res |= g << 8;
+ res |= b << 16;
+ res |= a << 24;
+ return res;
+ }
+
+ constexpr void SetScalar(uint32_t scalar) noexcept {
+ r = (scalar >> 0) & 0xFF;
+ g = (scalar >> 8) & 0xFF;
+ b = (scalar >> 16) & 0xFF;
+ a = (scalar >> 24) & 0xFF;
+ }
+
+ constexpr float GetNormalizedRed() const noexcept {
+ return r / 255.0f;
+ }
+
+ constexpr float GetNormalizedGreen() const noexcept {
+ return g / 255.0f;
+ }
+
+ constexpr float GetNormalizedBlue() const noexcept {
+ return b / 255.0f;
+ }
+
+ constexpr float GetNormalizedAlpha() const noexcept {
+ return a / 255.0f;
+ }
+
+ constexpr glm::ivec4 ToIVec() const noexcept {
+ return { r, g, b, a };
+ }
+
+ constexpr glm::vec4 ToVec() const noexcept {
+ return { GetNormalizedRed(), GetNormalizedGreen(), GetNormalizedBlue(), GetNormalizedAlpha() };
+ }
+
+ // Forward declaring because cyclic reference between RgbaColor and HsvColor
+ constexpr HsvColor ToHsv() const noexcept;
+
+ friend constexpr bool operator==(const RgbaColor&, const RgbaColor&) noexcept = default;
+};
+
+constexpr RgbaColor kXAxisColor(0xFF, 0x80, 0x80, 0xFF);
+constexpr RgbaColor kYAxisColor(0x80, 0xFF, 0x80, 0xFF);
+constexpr RgbaColor kZAxisColor(0x80, 0x80, 0xFF, 0xFF);
+
+class HsvColor {
+public:
+ float h;
+ float s;
+ float v;
+ float a;
+
+public:
+ constexpr HsvColor() noexcept
+ : h{ 0.0f }
+ , s{ 0.0f }
+ , v{ 1.0f }
+ , a{ 1.0f } {
+ }
+
+ constexpr HsvColor(float h, float s, float v, float a) noexcept
+ : h{ h }
+ , s{ s }
+ , v{ v }
+ , a{ a } {
+ }
+
+ // Forward declaring because cyclic reference between RgbaColor and HsvColor
+ constexpr RgbaColor ToRgba() const noexcept;
+};
+
+constexpr HsvColor RgbaColor::ToHsv() const noexcept {
+ float r = GetNormalizedRed();
+ float g = GetNormalizedBlue();
+ float b = GetNormalizedGreen();
+ float a = GetNormalizedAlpha();
+
+ auto p = g < b ? glm::vec4(b, g, -1, 2.0f / 3.0f) : glm::vec4(g, b, 0, -1.0f / 3.0f);
+ auto q = r < p.x ? glm::vec4(p.x, p.y, p.w, r) : glm::vec4(r, p.y, p.z, p.x);
+ float c = q.x - std::min(q.w, q.y);
+ float h = Utils::Abs((q.w - q.y) / (6 * c + std::numeric_limits<float>::epsilon()) + q.z);
+
+ glm::vec3 hcv{ h, c, q.x };
+ float s = hcv.y / (hcv.z + std::numeric_limits<float>::epsilon());
+ return HsvColor(hcv.x, s, hcv.z, a);
+}
+
+constexpr RgbaColor HsvColor::ToRgba() const noexcept {
+ float r = Utils::Abs(h * 6 - 3) - 1;
+ float g = 2 - Utils::Abs(h * 6 - 2);
+ float b = 2 - Utils::Abs(h * 6 - 4);
+
+ auto rgb = glm::vec3{ std::clamp(r, 0.0f, 1.0f), std::clamp(g, 0.0f, 1.0f), std::clamp(b, 0.0f, 1.0f) };
+ auto vc = (rgb - glm::vec3{ 0, 0, 0 }) * s + glm::vec3{ 1, 1, 1 } * v;
+
+ return RgbaColor(vc.x, vc.y, vc.z, a);
+}
diff --git a/source/10-common/DtoHelper.hpp b/source/10-common/DtoHelper.hpp
new file mode 100644
index 0000000..871f9c6
--- /dev/null
+++ b/source/10-common/DtoHelper.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <rapidjson/document.h>
+#include <json_dto/pub.hpp>
+
+namespace json_dto {
+
+
+
+}
diff --git a/source/10-common/Enum.hpp b/source/10-common/Enum.hpp
new file mode 100644
index 0000000..7afbe8e
--- /dev/null
+++ b/source/10-common/Enum.hpp
@@ -0,0 +1,110 @@
+#pragma once
+
+#include <initializer_list>
+#include <type_traits>
+
+template <typename TEnum>
+class EnumFlags {
+public:
+ using Enum = TEnum;
+ using Underlying = std::underlying_type_t<TEnum>;
+
+private:
+ Underlying mValue;
+
+public:
+ EnumFlags()
+ : mValue{ 0 } {
+ }
+
+ EnumFlags(TEnum e)
+ : mValue{ static_cast<Underlying>(1) << static_cast<Underlying>(e) } {
+ }
+
+ bool IsSet(EnumFlags mask) const {
+ return (mValue & mask.mValue) == mask.mValue;
+ }
+
+ bool IsSet(std::initializer_list<TEnum> enums) {
+ EnumFlags flags;
+ for (auto& e : enums) {
+ flags.mValue |= static_cast<Underlying>(e);
+ }
+ return IsSet(flags);
+ }
+
+ bool IsSetExclusive(EnumFlags mask) const {
+ return mValue == mask.mValue;
+ }
+
+ bool IsSetExclusive(std::initializer_list<TEnum> enums) {
+ EnumFlags flags;
+ for (auto& e : enums) {
+ flags.mValue |= static_cast<Underlying>(e);
+ }
+ return IsSetExclusive(flags);
+ }
+
+ void SetOn(EnumFlags mask) {
+ mValue |= mask.mValue;
+ }
+
+ void SetOff(EnumFlags mask) {
+ mValue &= ~mask.mValue;
+ }
+
+ void Set(EnumFlags mask, bool enabled) {
+ if (enabled) {
+ SetOn(mask);
+ } else {
+ SetOff(mask);
+ }
+ }
+
+ EnumFlags& operator|=(EnumFlags that) const {
+ mValue |= that.mValue;
+ return *this;
+ }
+
+ EnumFlags& operator&=(EnumFlags that) const {
+ mValue &= that.mValue;
+ return *this;
+ }
+
+ EnumFlags& operator^=(EnumFlags that) const {
+ mValue ^= that.mValue;
+ return *this;
+ }
+
+ EnumFlags& operator|=(TEnum e) const {
+ mValue |= 1 << static_cast<Underlying>(e);
+ return *this;
+ }
+
+ EnumFlags& operator&=(TEnum e) const {
+ mValue &= 1 << static_cast<Underlying>(e);
+ return *this;
+ }
+
+ EnumFlags& operator^=(TEnum e) const {
+ mValue ^= 1 << static_cast<Underlying>(e);
+ return *this;
+ }
+
+ EnumFlags operator|(EnumFlags that) const { return EnumFlags(mValue | that.mValue); }
+ EnumFlags operator&(EnumFlags that) const { return EnumFlags(mValue & that.mValue); }
+ EnumFlags operator^(EnumFlags that) const { return EnumFlags(mValue ^ that.mValue); }
+
+ EnumFlags operator|(TEnum e) const { return EnumFlags(mValue | 1 << static_cast<Underlying>(e)); }
+ EnumFlags operator&(TEnum e) const { return EnumFlags(mValue & 1 << static_cast<Underlying>(e)); }
+ EnumFlags operator^(TEnum e) const { return EnumFlags(mValue ^ 1 << static_cast<Underlying>(e)); }
+
+ EnumFlags operator~() const { return EnumFlags(~mValue); }
+};
+
+// Helper class for enumerating enum elements for ImGui::Begin/EndCombo
+template <typename TEnum>
+struct EnumElement {
+ const char* name;
+ TEnum value;
+};
diff --git a/source/10-common/Log.cpp b/source/10-common/Log.cpp
new file mode 100644
index 0000000..83d81e9
--- /dev/null
+++ b/source/10-common/Log.cpp
@@ -0,0 +1,116 @@
+#include "Log.hpp"
+
+#include "Macros.hpp"
+
+#include <robin_hood.h>
+#include <algorithm>
+#include <cstdio>
+
+namespace ProjectBrussel_UNITY_ID {
+using namespace Log;
+
+const char* MapMessageLevelToString(MessageLevel level) {
+ switch (level) {
+ using enum MessageLevel;
+ case Debug: return "DEBUG";
+ case Info: return "INFO";
+ case Warning: return "WARN";
+ case Error: return "ERROR";
+ default: UNREACHABLE;
+ }
+}
+
+void PrintMessage(const Message& msg) {
+ using namespace std::chrono;
+
+ auto t = system_clock::to_time_t(msg.time);
+ char timeStr[128];
+ strftime(timeStr, sizeof(timeStr), "%H:%M:%S", localtime(&t));
+ printf("[%s][%s][%s:%u] %.*s\n",
+ MapMessageLevelToString(msg.level),
+ timeStr,
+ msg.srcLoc.function_name(),
+ msg.srcLoc.line(),
+ PRINTF_STRING_VIEW(msg.text));
+}
+
+MessageBufferId gNextBufferId = 0;
+robin_hood::unordered_map<MessageBufferId, MessageBuffer*> gBuffers;
+} // namespace ProjectBrussel_UNITY_ID
+
+namespace Log {
+bool gPrintToStdOut = true;
+#if BRUSSEL_DEV_ENV
+MessageBuffer gDefaultBuffer;
+MessageBufferId gDefaultBufferId;
+#endif
+} // namespace Log
+
+Log::MessageBufferId Log::RegisterBuffer(MessageBuffer& buffer) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ auto id = gNextBufferId++;
+ gBuffers.try_emplace(id, &buffer);
+ return id;
+}
+
+void Log::UnregisterBuffer(MessageBufferId id) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ gBuffers.erase(id);
+}
+
+Log::MessageBuffer* Log::GetBuffer(MessageBufferId id) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ auto iter = gBuffers.find(id);
+ if (iter != gBuffers.end()) {
+ return iter->second;
+ } else {
+ return nullptr;
+ }
+}
+
+void Log::DumpRegisteredBuffers() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ puts("================ BEGIN LOG BUFFER DUMP ================");
+ for (const auto& [id, buffer] : gBuffers) {
+ printf("Buffer #%d at %p\n", id, buffer);
+ printf("Buffer size: %zu\n", buffer->messages.capacity());
+ bool needsWrapAround = buffer->messages.GetHeadIdx() >= buffer->messages.GetTailIdx();
+ if (needsWrapAround) {
+ printf("Fill size: %zu in [%zu,%zu) and [0,%zu)\n",
+ buffer->messages.size(),
+ // First chunk: [begin,end)
+ buffer->messages.GetHeadIdx(),
+ buffer->messages.capacity(),
+ // Second chunk: [0,end)
+ buffer->messages.GetTailIdx());
+ } else {
+ printf("Fill size: %zu in [%zu,%zu)\n",
+ buffer->messages.size(),
+ // [begin,end)
+ buffer->messages.GetHeadIdx(),
+ buffer->messages.GetTailIdx());
+ }
+ for (const auto& msg : buffer->messages) {
+ // Indent log messages in this buffer
+ printf("\t");
+ PrintMessage(msg);
+ }
+ }
+ puts("================ END LOG BUFFER DUMP ================");
+}
+
+void Log::Add(const Message& msg) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (gPrintToStdOut) {
+ PrintMessage(msg);
+ }
+
+ for (auto& [_, buffer] : gBuffers) {
+ buffer->messages.push_back(msg);
+ }
+}
diff --git a/source/10-common/Log.hpp b/source/10-common/Log.hpp
new file mode 100644
index 0000000..aeba984
--- /dev/null
+++ b/source/10-common/Log.hpp
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "RingBuffer.hpp"
+
+#include <fmt/format.h>
+#include <chrono>
+#include <source_location>
+#include <string_view>
+
+// NOTE: we keep this on one line so std::soruce_location reports the correct information
+#define GENERIC_LOG(lvl, fmtString, ...) Log::Add(Log::Message{ .level = lvl, .time = std::chrono::system_clock::now(), .srcLoc = std::source_location::current(), .text = fmt::format(fmtString __VA_OPT__(, ) __VA_ARGS__) })
+
+#define LOG_DEBUG(...) GENERIC_LOG(Log::MessageLevel::Debug, __VA_ARGS__)
+#define LOG_INFO(...) GENERIC_LOG(Log::MessageLevel::Info, __VA_ARGS__)
+#define LOG_WARNING(...) GENERIC_LOG(Log::MessageLevel::Warning, __VA_ARGS__)
+#define LOG_ERROR(...) GENERIC_LOG(Log::MessageLevel::Error, __VA_ARGS__)
+
+namespace Log {
+enum class MessageLevel {
+ Debug,
+ Info,
+ Warning,
+ Error,
+};
+
+struct Message {
+ MessageLevel level;
+ std::chrono::time_point<std::chrono::system_clock> time;
+ std::source_location srcLoc;
+ std::string text;
+};
+
+/// A mRing buffer of log messages for programmatic inspection at runtime.
+struct MessageBuffer {
+ RingBuffer<Message> messages;
+};
+
+/// Unique ID identifying a currently registered MessageBuffer.
+using MessageBufferId = int;
+
+MessageBufferId RegisterBuffer(MessageBuffer& buffer);
+void UnregisterBuffer(MessageBufferId id);
+MessageBuffer* GetBuffer(MessageBufferId id);
+void DumpRegisteredBuffers();
+
+extern bool gPrintToStdOut;
+#if BRUSSEL_DEV_ENV
+// NOTE: initialized in main.cpp
+extern MessageBuffer gDefaultBuffer;
+extern MessageBufferId gDefaultBufferId;
+#endif
+
+// TODO improve this interface: don't copy std::string when there is in fact no MessageBuffer registered
+void Add(const Message& msg);
+} // namespace Log
diff --git a/source/10-common/LookupTable.hpp b/source/10-common/LookupTable.hpp
new file mode 100644
index 0000000..54548f2
--- /dev/null
+++ b/source/10-common/LookupTable.hpp
@@ -0,0 +1,64 @@
+#pragma once
+
+#include <robin_hood.h>
+#include <string_view>
+
+// BIDI stands for bi-directional
+#define BIDI_LUT_DECL(name, aType, aCount, bType, bCount) \
+ int gLutBidi_##name##_A2B[aCount]; \
+ int gLutBidi_##name##_B2A[bCount]; \
+ using name##AType = aType; \
+ using name##BType = bType; \
+ void InitializeLutBidi##name()
+#define BIDI_LUT_MAP_FOR(name) \
+ int* lutMappingA2B = gLutBidi_##name##_A2B; \
+ int* lutMappingB2A = gLutBidi_##name##_B2A
+#define BIDI_LUT_MAP(from, to) \
+ lutMappingA2B[from] = to; \
+ lutMappingB2A[to] = from
+#define BIDI_LUT_INIT(name) InitializeLutBidi##name()
+#define BIDI_LUT_A2B_LOOKUP(name, from) (name##BType)(gLutBidi_##name##_A2B[from])
+#define BIDI_LUT_B2A_LOOKUP(name, to) (name##AType)(gLutBidi_##name##_B2A[to])
+
+// Forward string lookup
+#define FSTR_LUT_DECL(name, enumMinValue, enumMaxValue) \
+ constexpr int kLutFwMinVal_##name = enumMinValue; \
+ const char* gLutFw_##name[(int)enumMaxValue - (int)enumMinValue]; \
+ void InitializeLutFw##name()
+#define FSTR_LUT_MAP_FOR(name) \
+ const char** lutMapping = gLutFw_##name; \
+ int lutMappingMinValue = kLutFwMinVal_##name
+#define FSTR_LUT_MAP(value, text) lutMapping[value - lutMappingMinValue] = text
+#define FSTR_LUT_MAP_ENUM(enumValue) FSTR_LUT_MAP(enumValue, #enumValue)
+#define FSTR_LUT_LOOKUP(name, enumValue) gLutFw_##name[enumValue - kLutFwMinVal_##name]
+#define FSTR_LUT_INIT(name) InitializeLutFw##name()
+
+// RSTR stands for reverse-string lookup
+#define RSTR_LUT_DECL(name, enumMinValue, enumMaxValue) \
+ robin_hood::unordered_flat_map<std::string_view, decltype(enumMaxValue)> gLutRv_##name; \
+ void InitializeLutRv##name()
+#define RSTR_LUT_MAP_FOR(name) auto& lutMapping = gLutRv_##name;
+#define RSTR_LUT_MAP(value, text) lutMapping.insert_or_assign(std::string_view(text), value);
+#define RSTR_LUT(name) gLutRv_##name
+#define BSTR_LUT_LOOKUP(name, string) gLutRv_##name.find(std::string_view(text))->second
+#define RSTR_LUT_INIT(name) InitializeLutRv##name()
+
+// BSTR stands for bi-directional string lookup
+#define BSTR_LUT_DECL(name, enumMinValue, enumMaxValue) \
+ constexpr int kLutBstrMinVal_##name = enumMinValue; \
+ const char* gLutBstr_##name##_V2S[(int)enumMaxValue - (int)enumMinValue]; \
+ robin_hood::unordered_flat_map<std::string_view, decltype(enumMaxValue)> gLutBstr_##name##_S2V; \
+ void InitializeLutBstr##name()
+#define BSTR_LUT_MAP_FOR(name) \
+ const char** lutMappingV2S = gLutBstr_##name##_V2S; \
+ auto& lutMappingS2V = gLutBstr_##name##_S2V; \
+ int lutMappingMinValue = kLutBstrMinVal_##name
+#define BSTR_LUT_MAP(value, text) \
+ lutMappingV2S[value - lutMappingMinValue] = text; \
+ lutMappingS2V.insert_or_assign(std::string_view(text), value);
+#define BSTR_LUT_MAP_ENUM(enumValue) BSTR_LUT_MAP(enumValue, #enumValue)
+#define BSTR_LUT_V2S(name) gLutBstr_##name##_V2S
+#define BSTR_LUT_S2V(name) gLutBstr_##name##_S2V
+#define BSTR_LUT_V2S_LOOKUP(name, enumValue) gLutBstr_##name##_V2S[enumValue - kLutBstrMinVal_##name]
+#define BSTR_LUT_S2V_LOOKUP(name, string) gLutBstr_##name##_S2V.find(std::string_view(text))->second
+#define BSTR_LUT_INIT(name) InitializeLutBstr##name()
diff --git a/source/10-common/Macros.hpp b/source/10-common/Macros.hpp
new file mode 100644
index 0000000..a255ada
--- /dev/null
+++ b/source/10-common/Macros.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#define STRINGIFY_IMPL(text) #text
+#define STRINGIFY(text) STRINGIFY_IMPL(text)
+
+#define CONCAT_IMPL(a, b) a##b
+#define CONCAT(a, b) CONCAT_IMPL(a, b)
+#define CONCAT_3(a, b, c) CONCAT(a, CONCAT(b, c))
+#define CONCAT_4(a, b, c, d) CONCAT(CONCAT(a, b), CONCAT(c, d))
+
+#define UNIQUE_NAME(prefix) CONCAT(prefix, __COUNTER__)
+#define UNIQUE_NAME_LINE(prefix) CONCAT(prefix, __LINE__)
+#define DISCARD UNIQUE_NAME(_discard)
+
+#define UNUSED(x) (void)x;
+
+#define PRINTF_STRING_VIEW(s) (int)s.size(), s.data()
+
+#if defined(_MSC_VER)
+# define UNREACHABLE __assume(0)
+#elif defined(__GNUC__) || defined(__clang__)
+# define UNREACHABLE __builtin_unreachable()
+#else
+# define UNREACHABLE
+#endif
+
+#if _WIN32
+# define PLATFORM_PATH_STR "%ls"
+#else
+# define PLATFORM_PATH_STR "%s"
+#endif
diff --git a/source/10-common/OpaqueIterator.hpp b/source/10-common/OpaqueIterator.hpp
new file mode 100644
index 0000000..128cbc6
--- /dev/null
+++ b/source/10-common/OpaqueIterator.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+template <typename T>
+class IOpaqueIterator {
+public:
+ virtual ~IOpaqueIterator() = default;
+ virtual bool HasNext() const = 0;
+ virtual T Next() = 0;
+};
+
+template <typename TContainer>
+class ContainerOpaqueIterator : public IOpaqueIterator<typename TContainer::reference_type> {
+private:
+ typename TContainer::iterator mIter;
+ typename TContainer::const_iterator mEnd;
+
+public:
+ ContainerOpaqueIterator(TContainer& container)
+ : mIter{ container.begin() }
+ , mEnd{ container.end() } {}
+
+ virtual bool HasNext() const override {
+ return mIter != mEnd;
+ }
+
+ virtual typename TContainer::reference_type Next() override {
+ auto result = *mIter;
+ ++mIter;
+ return result;
+ }
+};
diff --git a/source/10-common/PodVector.hpp b/source/10-common/PodVector.hpp
new file mode 100644
index 0000000..bd92e7d
--- /dev/null
+++ b/source/10-common/PodVector.hpp
@@ -0,0 +1,297 @@
+// File adapted from dear-imgui's ImVector, implemented in https://github.com/ocornut/imgUI/blob/master/imgui.h
+#pragma once
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <span>
+
+template <typename T>
+class PodVector {
+public:
+ using value_type = T;
+ using iterator = value_type*;
+ using const_iterator = const value_type*;
+
+private:
+ int mSize;
+ int mCapacity;
+ T* mData;
+
+public:
+ PodVector() {
+ mSize = mCapacity = 0;
+ mData = nullptr;
+ }
+
+ PodVector(const PodVector<T>& src) {
+ mSize = mCapacity = 0;
+ mData = nullptr;
+ operator=(src);
+ }
+
+ PodVector<T>& operator=(const PodVector<T>& src) {
+ clear();
+ resize(src.mSize);
+ std::memcpy(mData, src.mData, (size_t)mSize * sizeof(T));
+ return *this;
+ }
+
+ PodVector(PodVector&& src) {
+ mSize = src.mSize;
+ mCapacity = src.mCapacity;
+ mData = src.mData;
+
+ src.mSize = src.mCapacity = 0;
+ src.mData = nullptr;
+ }
+
+ PodVector& operator=(PodVector&& src) {
+ if (this != &src) {
+ std::free(mData);
+
+ mSize = src.mSize;
+ mCapacity = src.mCapacity;
+ mData = src.mData;
+
+ src.mSize = src.mCapacity = 0;
+ src.mData = nullptr;
+ }
+ return *this;
+ }
+
+ ~PodVector() {
+ std::free(mData);
+ }
+
+ bool empty() const { return mSize == 0; }
+ int size() const { return mSize; }
+ int size_in_bytes() const { return mSize * (int)sizeof(T); }
+ int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); }
+ int capacity() const { return mCapacity; }
+
+ T& operator[](int i) {
+ assert(i >= 0 && i < mSize);
+ return mData[i];
+ }
+
+ const T& operator[](int i) const {
+ assert(i >= 0 && i < mSize);
+ return mData[i];
+ }
+
+ void clear() {
+ if (mData) {
+ mSize = mCapacity = 0;
+ std::free(mData);
+ mData = nullptr;
+ }
+ }
+
+ T* begin() { return mData; }
+ const T* begin() const { return mData; }
+ T* end() { return mData + mSize; }
+ const T* end() const { return mData + mSize; }
+
+ T* data() { return mData; }
+
+ T& front() {
+ assert(mSize > 0);
+ return mData[0];
+ }
+
+ const T& front() const {
+ assert(mSize > 0);
+ return mData[0];
+ }
+
+ T& back() {
+ assert(mSize > 0);
+ return mData[mSize - 1];
+ }
+
+ const T& back() const {
+ assert(mSize > 0);
+ return mData[mSize - 1];
+ }
+
+ void swap(PodVector<T>& rhs) {
+ int rhs_size = rhs.mSize;
+ rhs.mSize = mSize;
+ mSize = rhs_size;
+ int rhs_cap = rhs.mCapacity;
+ rhs.mCapacity = mCapacity;
+ mCapacity = rhs_cap;
+ T* rhs_mDataTmp = rhs.mData;
+ rhs.mData = mData;
+ mData = rhs_mDataTmp;
+ }
+
+ int grow_capacity(int sz) const {
+ int newCapacity = mCapacity ? (mCapacity + mCapacity / 2) : 8;
+ return newCapacity > sz ? newCapacity : sz;
+ }
+
+ void resize(int new_size) {
+ if (new_size > mCapacity) reserve(grow_capacity(new_size));
+ mSize = new_size;
+ }
+
+ void resize_more(int size) {
+ resize(mSize + size);
+ }
+
+ void resize(int new_size, const T& v) {
+ if (new_size > mCapacity) reserve(grow_capacity(new_size));
+ if (new_size > mSize) {
+ for (int n = mSize; n < new_size; n++) {
+ std::memcpy(&mData[n], &v, sizeof(v));
+ }
+ }
+ mSize = new_size;
+ }
+
+ void resize_more(int size, const T& v) {
+ resize(mSize + size, v);
+ }
+
+ void shrink(int new_size) {
+ assert(new_size <= mSize);
+ mSize = new_size;
+ }
+
+ /// Resize a vector to a smaller mSize, guaranteed not to cause a reallocation
+ void reserve(int newCapacity) {
+ if (newCapacity <= mCapacity) return;
+ auto tmp = (T*)std::malloc((size_t)newCapacity * sizeof(T));
+ if (mData) {
+ std::memcpy(tmp, mData, (size_t)mSize * sizeof(T));
+ std::free(mData);
+ }
+ mData = tmp;
+ mCapacity = newCapacity;
+ }
+
+ void reserve_more(int size) {
+ reserve(mSize + size);
+ }
+
+ /// NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the PodVector data itself! e.g. v.push_back(v[10]) is forbidden.
+ void push_back(const T& v) {
+ if (mSize == mCapacity) reserve(grow_capacity(mSize + 1));
+ std::memcpy(&mData[mSize], &v, sizeof(v));
+ mSize++;
+ }
+
+ void pop_back() {
+ assert(mSize > 0);
+ mSize--;
+ }
+
+ void push_front(const T& v) {
+ if (mSize == 0) {
+ push_back(v);
+ } else {
+ insert(mData, v);
+ }
+ }
+
+ T* erase(const T* it) {
+ assert(it >= mData && it < mData + mSize);
+ const ptrdiff_t off = it - mData;
+ std::memmove(mData + off, mData + off + 1, ((size_t)mSize - (size_t)off - 1) * sizeof(T));
+ mSize--;
+ return mData + off;
+ }
+
+ T* erase(const T* it, const T* it_last) {
+ assert(it >= mData && it < mData + mSize && it_last > it && it_last <= mData + mSize);
+ const ptrdiff_t count = it_last - it;
+ const ptrdiff_t off = it - mData;
+ std::memmove(mData + off, mData + off + count, ((size_t)mSize - (size_t)off - count) * sizeof(T));
+ mSize -= (int)count;
+ return mData + off;
+ }
+
+ T* erase_unsorted(const T* it) {
+ assert(it >= mData && it < mData + mSize);
+ const ptrdiff_t off = it - mData;
+ if (it < mData + mSize - 1) std::memcpy(mData + off, mData + mSize - 1, sizeof(T));
+ mSize--;
+ return mData + off;
+ }
+
+ T* insert(const T* it, const T& v) {
+ assert(it >= mData && it <= mData + mSize);
+ const ptrdiff_t off = it - mData;
+ if (mSize == mCapacity) reserve(grow_capacity(mSize + 1));
+ if (off < (int)mSize) std::memmove(mData + off + 1, mData + off, ((size_t)mSize - (size_t)off) * sizeof(T));
+ std::memcpy(&mData[off], &v, sizeof(v));
+ mSize++;
+ return mData + off;
+ }
+
+ bool contains(const T& v) const {
+ const T* data = mData;
+ const T* dataEnd = mData + mSize;
+ while (data < dataEnd) {
+ if (*data++ == v) return true;
+ }
+ return false;
+ }
+
+ T* find(const T& v) {
+ T* data = mData;
+ const T* dataEnd = mData + mSize;
+ while (data < dataEnd)
+ if (*data == v)
+ break;
+ else
+ ++data;
+ return data;
+ }
+
+ const T* find(const T& v) const {
+ const T* data = mData;
+ const T* dataEnd = mData + mSize;
+ while (data < dataEnd)
+ if (*data == v)
+ break;
+ else
+ ++data;
+ return data;
+ }
+
+ bool find_erase(const T& v) {
+ const T* it = find(v);
+ if (it < mData + mSize) {
+ erase(it);
+ return true;
+ }
+ return false;
+ }
+
+ bool find_erase_unsorted(const T& v) {
+ const T* it = find(v);
+ if (it < mData + mSize) {
+ erase_unsorted(it);
+ return true;
+ }
+ return false;
+ }
+
+ int index_from_ptr(const T* it) const {
+ assert(it >= mData && it < mData + mSize);
+ const ptrdiff_t off = it - mData;
+ return (int)off;
+ }
+
+ // Custom utility functions
+
+ std::span<T> as_span() { return { mData, (size_t)mSize }; }
+ std::span<uint8_t> as_data_span() { return { (uint8_t*)mData, (size_t)mSize * sizeof(T) }; }
+ std::span<const T> as_span() const { return { mData, (size_t)mSize }; }
+ std::span<const uint8_t> as_data_span() const { return { (uint8_t*)mData, (size_t)mSize * sizeof(T) }; }
+};
diff --git a/source/10-common/RTTI.hpp b/source/10-common/RTTI.hpp
new file mode 100644
index 0000000..bd9475b
--- /dev/null
+++ b/source/10-common/RTTI.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <cassert>
+
+template <typename T, typename TBase>
+bool is_a(TBase* t) {
+ assert(t != nullptr);
+ return T::IsInstance(t);
+}
+
+template <typename T, typename TBase>
+bool is_a_nullable(TBase* t) {
+ if (t) {
+ return is_a<T, TBase>(t);
+ } else {
+ return false;
+ }
+}
+
+template <typename T, typename TBase>
+T* dyn_cast(TBase* t) {
+ assert(t != nullptr);
+ if (T::IsInstance(t)) {
+ return static_cast<T*>(t);
+ } else {
+ return nullptr;
+ }
+}
+
+template <typename T, typename TBase>
+const T* dyn_cast(const TBase* t) {
+ assert(t != nullptr);
+ if (T::IsInstance(t)) {
+ return static_cast<const T*>(t);
+ } else {
+ return nullptr;
+ }
+}
+
+template <typename T, typename TBase>
+T* dyn_cast_nullable(TBase* t) {
+ if (!t) return nullptr;
+ return dyn_cast<T, TBase>(t);
+}
diff --git a/source/10-common/RapidJsonHelper.hpp b/source/10-common/RapidJsonHelper.hpp
new file mode 100644
index 0000000..a992dbc
--- /dev/null
+++ b/source/10-common/RapidJsonHelper.hpp
@@ -0,0 +1,114 @@
+#pragma once
+
+#include <rapidjson/document.h>
+#include <cstring>
+#include <string>
+#include <string_view>
+
+#define BRUSSEL_JSON_GET(object, name, type, out, failAction) \
+ { \
+ auto it = (object).FindMember(name); \
+ if (it == (object).MemberEnd()) failAction; \
+ auto& value = it->value; \
+ if (!value.Is<type>()) failAction; \
+ (out) = value.Get<type>(); \
+ }
+
+#define BRUSSEL_JSON_GET_DEFAULT(object, name, type, out, theDefault) \
+ do { \
+ auto it = (object).FindMember(name); \
+ if (it == (object).MemberEnd()) { \
+ (out) = theDefault; \
+ break; \
+ } \
+ auto& value = it->value; \
+ if (!value.Is<type>()) { \
+ (out) = theDefault; \
+ break; \
+ } \
+ (out) = value.Get<type>(); \
+ } while (0);
+
+namespace rapidjson {
+
+inline const Value* GetProperty(const Value& value, std::string_view name) {
+ for (auto it = value.MemberBegin(); it != value.MemberEnd(); ++it) {
+ if (it->name.GetStringLength() != name.size()) continue;
+ if (std::memcmp(it->name.GetString(), name.data(), name.size())) continue;
+
+ return &it->value;
+ }
+ return nullptr;
+}
+
+inline const Value* GetProperty(const Value& value, Type type, std::string_view name) {
+ for (auto it = value.MemberBegin(); it != value.MemberEnd(); ++it) {
+ if (it->name.GetStringLength() != name.size()) continue;
+ if (it->value.GetType() != type) continue;
+ if (std::memcmp(it->name.GetString(), name.data(), name.size())) continue;
+
+ return &it->value;
+ }
+ return nullptr;
+}
+
+inline std::string_view AsStringView(const Value& value) {
+ return std::string_view(value.GetString(), value.GetStringLength());
+}
+
+inline std::string_view AsStringView(const GenericStringRef<char>& strRef) {
+ return std::string_view(strRef.s, strRef.length);
+}
+
+inline std::string AsString(const Value& value) {
+ return std::string(value.GetString(), value.GetStringLength());
+}
+
+inline std::string AsString(const GenericStringRef<char>& strRef) {
+ return std::string(strRef.s, strRef.length);
+}
+
+// RapidJson itself already provides std::string and const char* overloads
+inline GenericStringRef<char> StringRef(std::string_view str) {
+ return GenericStringRef<char>(
+ str.data() ? str.data() : "",
+ str.size());
+}
+
+template <typename TIter, typename TSentienl>
+rapidjson::Value WriteVectorPrimitives(rapidjson::Document& root, TIter begin, TSentienl end) {
+ using TElement = typename TIter::value_type;
+
+ rapidjson::Value list;
+ while (begin != end) {
+ if constexpr (std::is_same_v<TElement, std::string>) {
+ auto& elm = *begin;
+ list.PushBack(rapidjson::Value(elm.c_str(), elm.size()), root.GetAllocator());
+ } else {
+ list.PushBack(*begin, root.GetAllocator());
+ }
+ ++begin;
+ }
+ return list;
+}
+
+template <typename TContainer>
+bool ReadVectorPrimitives(const rapidjson::Value& value, TContainer& list) {
+ using TElement = typename TContainer::value_type;
+
+ if (!value.IsArray()) return false;
+
+ list.reserve(value.Size());
+ for (auto& elm : value.GetArray()) {
+ if (!elm.Is<TElement>()) return {};
+ list.push_back(elm.Get<TElement>());
+ }
+
+ return true;
+}
+
+} // namespace rapidjson
+
+inline rapidjson::GenericStringRef<char> operator""_rj_sv(const char* str, size_t len) {
+ return rapidjson::StringRef(str, len);
+}
diff --git a/source/10-common/RcPtr.hpp b/source/10-common/RcPtr.hpp
new file mode 100644
index 0000000..e3e420e
--- /dev/null
+++ b/source/10-common/RcPtr.hpp
@@ -0,0 +1,120 @@
+#pragma once
+
+#include "Macros.hpp"
+#include "TypeTraits.hpp"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <type_traits>
+
+class RefCounted {
+public:
+ // DO NOT MODIFY this field, unless explicitly documented the use
+ uint32_t refCount = 0;
+ uint32_t weakCount = 0; // TODO implement
+};
+
+template <typename T, typename TDeleter = DefaultDeleter<T>>
+class RcPtr : TDeleter {
+private:
+ static_assert(std::is_base_of_v<RefCounted, T>);
+ T* mPtr;
+
+public:
+ RcPtr()
+ : mPtr{ nullptr } {
+ }
+
+ explicit RcPtr(T* ptr)
+ : mPtr{ ptr } {
+ if (ptr) {
+ ++ptr->RefCounted::refCount;
+ }
+ }
+
+ ~RcPtr() {
+ CleanUp();
+ }
+
+ void Attach(T* ptr) {
+ CleanUp();
+ mPtr = ptr;
+ if (ptr) {
+ ++ptr->RefCounted::refCount;
+ }
+ }
+
+ void Detatch() {
+ CleanUp();
+ mPtr = nullptr;
+ }
+
+ RcPtr(const RcPtr& that)
+ : mPtr{ that.mPtr } {
+ if (mPtr) {
+ ++mPtr->RefCounted::refCount;
+ }
+ }
+
+ RcPtr& operator=(const RcPtr& that) {
+ CleanUp();
+ mPtr = that.mPtr;
+ if (mPtr) {
+ ++mPtr->RefCounted::refCount;
+ }
+ return *this;
+ }
+
+ RcPtr(RcPtr&& that)
+ : mPtr{ that.mPtr } {
+ that.mPtr = nullptr;
+ }
+
+ RcPtr& operator=(RcPtr&& that) {
+ CleanUp();
+ mPtr = that.mPtr;
+ that.mPtr = nullptr;
+ return *this;
+ }
+
+ template <typename TBase>
+ requires std::is_base_of_v<TBase, T>
+ operator RcPtr<TBase>() const {
+ return RcPtr<TBase>(mPtr);
+ }
+
+ bool operator==(std::nullptr_t ptr) const {
+ return mPtr == nullptr;
+ }
+
+ bool operator==(const T* ptr) const {
+ return mPtr == ptr;
+ }
+
+ bool operator==(T* ptr) const {
+ return mPtr == ptr;
+ }
+
+ template <typename TThat>
+ bool operator==(const RcPtr<TThat>& ptr) const {
+ return mPtr == ptr.Get();
+ }
+
+ T* Get() const {
+ return mPtr;
+ }
+
+ T& operator*() const { return *mPtr; }
+ T* operator->() const { return mPtr; }
+
+private:
+ void CleanUp() {
+ if (mPtr) {
+ --mPtr->RefCounted::refCount;
+ if (mPtr->RefCounted::refCount == 0) {
+ TDeleter::operator()(mPtr);
+ }
+ }
+ }
+};
diff --git a/source/10-common/Rect.hpp b/source/10-common/Rect.hpp
new file mode 100644
index 0000000..86a1268
--- /dev/null
+++ b/source/10-common/Rect.hpp
@@ -0,0 +1,164 @@
+#pragma once
+
+#include <glm/glm.hpp>
+
+/// Rect is a rectangle representation based on a point and a dimensions, in television coordinate space
+/// (x increases from left to right, y increases from top to bottom).
+template <typename T>
+class Rect {
+public:
+ using ScalarType = T;
+ using VectorType = glm::vec<2, T>;
+
+public:
+ T x;
+ T y;
+ T width;
+ T height;
+
+public:
+ Rect()
+ : x{ 0 }, y{ 0 }, width{ 0 }, height{ 0 } {
+ }
+
+ Rect(T x, T y, T width, T height)
+ : x{ x }, y{ y }, width{ width }, height{ height } {
+ }
+
+ Rect(VectorType pos, VectorType size)
+ : x{ pos.x }
+ , y{ pos.y }
+ , width{ size.x }
+ , height{ size.y } {
+ }
+
+ T x0() const { return x; }
+ T y0() const { return y; }
+ T x1() const { return x + width; }
+ T y1() const { return y + height; }
+
+ VectorType TopLeft() const {
+ return VectorType{ x, y };
+ }
+
+ VectorType TopRight() const {
+ return VectorType{ x + width, y };
+ }
+
+ VectorType BottomLeft() const {
+ return VectorType{ x, y + height };
+ }
+
+ VectorType BottomRight() const {
+ return VectorType{ x + width, y + height };
+ }
+
+ VectorType Center() const {
+ return TopLeft() + VectorType{ width / 2, height / 2 };
+ }
+
+ VectorType Dimensions() const {
+ return VectorType{ width, height };
+ }
+
+ VectorType Extents() const {
+ return VectorType{ width / 2, height / 2 };
+ }
+
+ /// Assumes `bySize * 2` is smaller than both `width` and `height` (does not produce a negative-dimension rectangle).
+ Rect Shrink(T bySize) const {
+ T two = bySize * 2;
+ return Rect{ x + bySize, y + bySize, width - two, height - two };
+ }
+
+ Rect Shrink(T left, T top, T right, T bottom) const {
+ return Rect{
+ x + left,
+ y + top,
+ width - left - right,
+ height - top - bottom,
+ };
+ }
+
+ Rect Expand(T bySize) const {
+ T two = bySize * 2;
+ return Rect{ x - bySize, y - bySize, width + two, height + two };
+ }
+
+ Rect Expand(T left, T top, T right, T bottom) const {
+ return Rect{
+ x - left,
+ y - top,
+ width + left + right,
+ height + top + bottom,
+ };
+ }
+
+ bool Contains(VectorType point) const {
+ return point.x >= x &&
+ point.y >= y &&
+ point.x < x + width &&
+ point.y < y + height;
+ }
+
+ bool Intersects(const Rect& that) const {
+ bool xBetween = x > that.x0() && x < that.x1();
+ bool yBetween = y > that.y0() && y < that.y1();
+ return xBetween && yBetween;
+ }
+
+ // Write min()/max() tenary by hand so that we don't have to include <algorithm>
+ // This file is practically going to be included in every file in this project
+
+ static Rect Intersection(const Rect& a, const Rect& b) {
+ auto x0 = a.x0() > b.x0() ? a.x0() : b.x0(); // Max
+ auto y0 = a.y0() > b.y0() ? a.y0() : b.y0(); // Max
+ auto x1 = a.x1() < b.x1() ? a.x1() : b.x1(); // Min
+ auto y1 = a.y1() < b.y1() ? a.y1() : b.y1(); // Min
+ auto width = x1 - x0;
+ auto height = y1 - x0;
+ return Rect{ x0, y0, width, height };
+ }
+
+ static Rect Union(const Rect& a, const Rect& b) {
+ auto x0 = a.x0() < b.x0() ? a.x0() : b.x0(); // Min
+ auto y0 = a.y0() < b.y0() ? a.y0() : b.y0(); // Min
+ auto x1 = a.x1() > b.x1() ? a.x1() : b.x1(); // Max
+ auto y1 = a.y1() > b.y1() ? a.y1() : b.y1(); // Max
+ auto width = x1 - x0;
+ auto height = y1 - x0;
+ return Rect{ x0, y0, width, height };
+ }
+
+ friend bool operator==(const Rect<T>&, const Rect<T>&) = default;
+
+ Rect operator+(glm::vec<2, T> offset) const {
+ return { x + offset.x, y + offset.y, width, height };
+ }
+
+ Rect operator-(glm::vec<2, T> offset) const {
+ return { x - offset.x, y - offset.y, width, height };
+ }
+
+ Rect& operator+=(glm::vec<2, T> offset) {
+ x += offset.x;
+ y += offset.y;
+ return *this;
+ }
+
+ Rect& operator-=(glm::vec<2, T> offset) {
+ x -= offset.x;
+ y -= offset.y;
+ return *this;
+ }
+
+ template <typename TTarget>
+ Rect<TTarget> Cast() const {
+ return {
+ static_cast<TTarget>(x),
+ static_cast<TTarget>(y),
+ static_cast<TTarget>(width),
+ static_cast<TTarget>(height),
+ };
+ }
+};
diff --git a/source/10-common/RingBuffer.hpp b/source/10-common/RingBuffer.hpp
new file mode 100644
index 0000000..4eaa007
--- /dev/null
+++ b/source/10-common/RingBuffer.hpp
@@ -0,0 +1,191 @@
+#pragma once
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <iterator>
+
+class RingBufferSentinel {};
+
+template <typename TContainer>
+class RingBufferIterator {
+public:
+ using difference_type = TContainer::difference_type;
+ using value_type = TContainer::value_type; // C++20 relaxed usage requirements of `typename`, now locations where a type is required (like here in a using statement) it's no longer mandatory
+ using pointer = value_type*;
+ using reference = value_type&;
+ using iterator_category = std::random_access_iterator_tag;
+
+public:
+ TContainer* container;
+ TContainer::size_type curr; // C++20 relaxed usage requirements of `typename`, same here
+ bool needsWrapAround;
+ bool hasWrappedAround = false;
+
+public:
+ reference operator*() const {
+ return container->mRing[curr];
+ }
+
+ RingBufferIterator& operator++() {
+ assert(*this != RingBufferSentinel{});
+ ++curr;
+ if (needsWrapAround && curr == container->mCapacity) {
+ hasWrappedAround = true;
+ curr = 0;
+ }
+ return *this;
+ }
+
+ bool operator==(const RingBufferIterator& that) const {
+ assert(this->container == that.container);
+ return this->curr == that.curr;
+ }
+
+ bool operator==(const RingBufferSentinel&) const {
+ return curr == container->mTailIdx && (!needsWrapAround || hasWrappedAround);
+ }
+};
+
+template <typename T>
+class RingBuffer {
+public:
+ using value_type = T;
+ using reference = T&;
+ using const_reference = const T&;
+ friend class RingBufferIterator<RingBuffer>;
+ using iterator = RingBufferIterator<RingBuffer>;
+ friend class RingBufferIterator<const RingBuffer>;
+ using const_iterator = RingBufferIterator<const RingBuffer>;
+ using sentinel = RingBufferSentinel; // Not a part of C++'s Container named requirements, added here for convenience
+ using difference_type = ptrdiff_t;
+ using size_type = size_t;
+
+private:
+ T* mRing = nullptr;
+ size_type mHeadIdx = 0;
+ size_type mTailIdx = 0;
+ size_type mCapacity = 0;
+ size_type mSize = 0;
+
+public:
+ RingBuffer() noexcept = default;
+
+ ~RingBuffer() noexcept {
+ delete[] mRing;
+ }
+
+ RingBuffer(const RingBuffer&) noexcept = delete;
+ RingBuffer& operator=(const RingBuffer&) noexcept = delete;
+
+ RingBuffer(RingBuffer&& that) noexcept
+ : mRing{ that.mRing }
+ , mHeadIdx{ that.mHeadIdx }
+ , mTailIdx{ that.mTailIdx }
+ , mCapacity{ that.mCapacity }
+ , mSize{ that.mSize } {
+ that.mRing = nullptr;
+ }
+
+ RingBuffer& operator=(RingBuffer&& that) noexcept {
+ if (this != &that) {
+ auto oldThisRing = this->mRing;
+ this->mRing = that.mRing;
+ that.mRing = nullptr;
+ delete oldThisRing;
+
+ this->mHeadIdx = that.mHeadIdx;
+ this->mTailIdx = that.mTailIdx;
+ this->mCapacity = that.mCapacity;
+ this->mSize = that.mSize;
+ }
+
+ return *this;
+ }
+
+ [[nodiscard]] iterator begin() {
+ return {
+ .container = this,
+ .curr = mHeadIdx,
+ .needsWrapAround = mHeadIdx >= mTailIdx,
+ };
+ }
+
+ [[nodiscard]] const_iterator begin() const { return cbegin(); }
+
+ // Same type for both const this and non-const `this`
+ [[nodiscard]] sentinel end() const { return sentinel{}; }
+
+ [[nodiscard]] const_iterator cbegin() const {
+ return {
+ .container = this,
+ .curr = mHeadIdx,
+ .needsWrapAround = mHeadIdx >= mTailIdx,
+ };
+ }
+
+ [[nodiscard]] sentinel cend() const { return sentinel{}; }
+
+ [[nodiscard]] T& operator[](size_type i) { return const_cast<T&>(const_cast<const RingBuffer&>(*this)[i]); }
+ [[nodiscard]] const T& operator[](size_type i) const {
+ assert(mRing != nullptr);
+ size_type idx = mHeadIdx + i;
+ if (idx >= mCapacity) {
+ idx -= mCapacity;
+ }
+ return mRing[idx];
+ }
+
+ void push_back(T t) {
+ assert(mRing != nullptr);
+ if (mTailIdx == mCapacity) {
+ // Ring buffer is filled to the right, warp around to the beginning
+ // mHeadIdx > 0 must be true, since we checked that as condition (1) above
+ mRing[0] = std::move(t);
+ mTailIdx = 1;
+ } else {
+ mRing[mTailIdx] = std::move(t);
+ mTailIdx += 1;
+ }
+
+ // Push mHeadIdx backwards if overwrote element in a filled buffer
+ bool bufferFilled = mSize == mCapacity;
+ if (bufferFilled && mTailIdx > mHeadIdx) {
+ mHeadIdx += 1;
+ if (mHeadIdx == mCapacity) {
+ mHeadIdx = 0;
+ }
+ }
+
+ if (!bufferFilled) {
+ ++mSize;
+ }
+ }
+
+ [[nodiscard]] size_type capacity() const {
+ return mCapacity;
+ }
+
+ [[nodiscard]] size_type size() const {
+ return mSize;
+ }
+
+ [[nodiscard]] T* GetBuffer() const { return mRing; }
+ [[nodiscard]] size_type GetHeadIdx() const { return mHeadIdx; }
+ [[nodiscard]] size_type GetTailIdx() const { return mTailIdx; }
+
+ void resize(size_type newCapacity) {
+ auto size = this->size();
+
+ auto oldRing = mRing;
+ auto newRing = mRing = new T[newCapacity];
+ if (oldRing != nullptr) {
+ std::rotate_copy(oldRing, oldRing + mHeadIdx, oldRing + mCapacity, newRing);
+ delete[] oldRing;
+ }
+
+ mCapacity = newCapacity;
+ mHeadIdx = 0;
+ mTailIdx = size;
+ }
+};
diff --git a/source/10-common/ScopeGuard.hpp b/source/10-common/ScopeGuard.hpp
new file mode 100644
index 0000000..4e1a348
--- /dev/null
+++ b/source/10-common/ScopeGuard.hpp
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "Macros.hpp"
+
+#include <utility>
+
+template <typename TCleanupFunc>
+class ScopeGuard {
+private:
+ TCleanupFunc mFunc;
+ bool mDismissed = false;
+
+public:
+ /// Specifically left this implicit so that constructs like
+ /// \code
+ /// ScopeGuard sg = [&]() { res.Cleanup(); };
+ /// \endcode
+ /// would work. It is highly discourage and unlikely that one would want to use ScopeGuard as a function
+ /// parameter, so the normal argument that implicit conversion are harmful doesn't really apply here.
+ // Deliberately not explicit to allow usages like: ScopeGuard var = lambda;
+ ScopeGuard(TCleanupFunc&& function) noexcept
+ : mFunc{ std::move(function) } {
+ }
+
+ ~ScopeGuard() noexcept {
+ if (!mDismissed) {
+ mFunc();
+ }
+ }
+
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ ScopeGuard(ScopeGuard&& that) noexcept
+ : mFunc{ std::move(that.mFunc) } {
+ that.Cancel();
+ }
+
+ ScopeGuard& operator=(ScopeGuard&& that) noexcept {
+ if (!mDismissed) {
+ mFunc();
+ }
+ this->mFunc = std::move(that.mFunc);
+ this->cancelled = std::exchange(that.cancelled, true);
+ }
+
+ void Dismiss() noexcept {
+ mDismissed = true;
+ }
+};
+
+template <typename T>
+auto GuardDeletion(T* ptr) {
+ return ScopeGuard([ptr]() {
+ delete ptr;
+ });
+}
+
+#define SCOPE_GUARD(name) ScopeGuard name = [&]()
+#define DEFER ScopeGuard UNIQUE_NAME(scopeGuard) = [&]()
diff --git a/source/10-common/SmallVector.cpp b/source/10-common/SmallVector.cpp
new file mode 100644
index 0000000..65953f0
--- /dev/null
+++ b/source/10-common/SmallVector.cpp
@@ -0,0 +1,145 @@
+// Obtained from https://github.com/llvm/llvm-project/blob/main/llvm/lib/Support/SmallVector.cpp
+// commit 4b82bb6d82f65f98f23d0e4c2cd5297dc162864c
+// adapted in code style and utilities to fix this project
+
+//===- llvm/ADT/SmallVector.cpp - 'Normally small' vectors ----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the SmallVector class.
+//
+//===----------------------------------------------------------------------===//
+
+#include "SmallVector.hpp"
+
+#include <cstdlib>
+#include <stdexcept>
+#include <string>
+
+// Check that no bytes are wasted and everything is well-aligned.
+namespace {
+// These structures may cause binary compat warnings on AIX. Suppress the
+// warning since we are only using these types for the static assertions below.
+#if defined(_AIX)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Waix-compat"
+#endif
+struct Struct16B {
+ alignas(16) void* X;
+};
+struct Struct32B {
+ alignas(32) void* X;
+};
+#if defined(_AIX)
+# pragma GCC diagnostic pop
+#endif
+} // namespace
+static_assert(sizeof(SmallVector<void*, 0>) ==
+ sizeof(unsigned) * 2 + sizeof(void*),
+ "wasted space in SmallVector size 0");
+static_assert(alignof(SmallVector<Struct16B, 0>) >= alignof(Struct16B),
+ "wrong alignment for 16-byte aligned T");
+static_assert(alignof(SmallVector<Struct32B, 0>) >= alignof(Struct32B),
+ "wrong alignment for 32-byte aligned T");
+static_assert(sizeof(SmallVector<Struct16B, 0>) >= alignof(Struct16B),
+ "missing padding for 16-byte aligned T");
+static_assert(sizeof(SmallVector<Struct32B, 0>) >= alignof(Struct32B),
+ "missing padding for 32-byte aligned T");
+static_assert(sizeof(SmallVector<void*, 1>) ==
+ sizeof(unsigned) * 2 + sizeof(void*) * 2,
+ "wasted space in SmallVector size 1");
+
+static_assert(sizeof(SmallVector<char, 0>) ==
+ sizeof(void*) * 2 + sizeof(void*),
+ "1 byte elements have word-sized type for size and capacity");
+
+/// Report that MinSize doesn't fit into this vector's size type. Throws
+/// std::length_error or calls report_fatal_error.
+[[noreturn]] static void report_size_overflow(size_t MinSize, size_t MaxSize);
+static void report_size_overflow(size_t MinSize, size_t MaxSize) {
+ std::string Reason = "SmallVector unable to grow. Requested capacity (" +
+ std::to_string(MinSize) +
+ ") is larger than maximum value for size type (" +
+ std::to_string(MaxSize) + ")";
+ throw std::length_error(Reason);
+}
+
+/// Report that this vector is already at maximum capacity. Throws
+/// std::length_error or calls report_fatal_error.
+[[noreturn]] static void report_at_maximum_capacity(size_t MaxSize);
+static void report_at_maximum_capacity(size_t MaxSize) {
+ std::string Reason =
+ "SmallVector capacity unable to grow. Already at maximum size " +
+ std::to_string(MaxSize);
+ throw std::length_error(Reason);
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <typename Size_T>
+static size_t getNewCapacity(size_t MinSize, size_t TSize, size_t OldCapacity) {
+ constexpr size_t MaxSize = std::numeric_limits<Size_T>::max();
+
+ // Ensure we can fit the new capacity.
+ // This is only going to be applicable when the capacity is 32 bit.
+ if (MinSize > MaxSize)
+ report_size_overflow(MinSize, MaxSize);
+
+ // Ensure we can meet the guarantee of space for at least one more element.
+ // The above check alone will not catch the case where grow is called with a
+ // default MinSize of 0, but the current capacity cannot be increased.
+ // This is only going to be applicable when the capacity is 32 bit.
+ if (OldCapacity == MaxSize)
+ report_at_maximum_capacity(MaxSize);
+
+ // In theory 2*capacity can overflow if the capacity is 64 bit, but the
+ // original capacity would never be large enough for this to be a problem.
+ size_t NewCapacity = 2 * OldCapacity + 1; // Always grow.
+ return std::min(std::max(NewCapacity, MinSize), MaxSize);
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <typename Size_T>
+void* SmallVectorBase<Size_T>::mallocForGrow(size_t MinSize, size_t TSize, size_t& NewCapacity) {
+ NewCapacity = getNewCapacity<Size_T>(MinSize, TSize, this->capacity());
+ return malloc(NewCapacity * TSize);
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <typename Size_T>
+void SmallVectorBase<Size_T>::grow_pod(void* FirstEl, size_t MinSize, size_t TSize) {
+ size_t NewCapacity = getNewCapacity<Size_T>(MinSize, TSize, this->capacity());
+ void* NewElts;
+ if (BeginX == FirstEl) {
+ NewElts = malloc(NewCapacity * TSize);
+
+ // Copy the elements over. No need to run dtors on PODs.
+ memcpy(NewElts, this->BeginX, size() * TSize);
+ } else {
+ // If this wasn't grown from the inline copy, grow the allocated space.
+ NewElts = realloc(this->BeginX, NewCapacity * TSize);
+ }
+
+ this->BeginX = NewElts;
+ this->Capacity = NewCapacity;
+}
+
+template class SmallVectorBase<uint32_t>;
+
+// Disable the uint64_t instantiation for 32-bit builds.
+// Both uint32_t and uint64_t instantiations are needed for 64-bit builds.
+// This instantiation will never be used in 32-bit builds, and will cause
+// warnings when sizeof(Size_T) > sizeof(size_t).
+#if SIZE_MAX > UINT32_MAX
+template class SmallVectorBase<uint64_t>;
+
+// Assertions to ensure this #if stays in sync with SmallVectorSizeType.
+static_assert(sizeof(SmallVectorSizeType<char>) == sizeof(uint64_t),
+ "Expected SmallVectorBase<uint64_t> variant to be in use.");
+#else
+static_assert(sizeof(SmallVectorSizeType<char>) == sizeof(uint32_t),
+ "Expected SmallVectorBase<uint32_t> variant to be in use.");
+#endif
diff --git a/source/10-common/SmallVector.hpp b/source/10-common/SmallVector.hpp
new file mode 100644
index 0000000..3fc7519
--- /dev/null
+++ b/source/10-common/SmallVector.hpp
@@ -0,0 +1,1332 @@
+// Obtained from https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/ADT/SmallVector.h
+// commit 4b82bb6d82f65f98f23d0e4c2cd5297dc162864c
+// adapted in code style and utilities to fix this project
+
+//===- llvm/ADT/SmallVector.h - 'Normally small' vectors --------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines the SmallVector class.
+///
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <initializer_list>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# pragma warning(disable : 4267) // The compiler detected a conversion from size_t to a smaller type.
+#endif
+
+#if __has_builtin(__builtin_expect) || defined(__GNUC__)
+# define LLVM_LIKELY(EXPR) __builtin_expect((bool)(EXPR), true)
+# define LLVM_UNLIKELY(EXPR) __builtin_expect((bool)(EXPR), false)
+#else
+# define LLVM_LIKELY(EXPR) (EXPR)
+# define LLVM_UNLIKELY(EXPR) (EXPR)
+#endif
+
+template <typename IteratorT>
+class iterator_range;
+
+/// This is all the stuff common to all SmallVectors.
+///
+/// The template parameter specifies the type which should be used to hold the
+/// Size and Capacity of the SmallVector, so it can be adjusted.
+/// Using 32 bit size is desirable to shrink the size of the SmallVector.
+/// Using 64 bit size is desirable for cases like SmallVector<char>, where a
+/// 32 bit size would limit the vector to ~4GB. SmallVectors are used for
+/// buffering bitcode output - which can exceed 4GB.
+template <typename Size_T>
+class SmallVectorBase {
+protected:
+ void* BeginX;
+ Size_T Size = 0, Capacity;
+
+ /// The maximum value of the Size_T used.
+ static constexpr size_t SizeTypeMax() {
+ return std::numeric_limits<Size_T>::max();
+ }
+
+ SmallVectorBase() = delete;
+ SmallVectorBase(void* FirstEl, size_t TotalCapacity)
+ : BeginX(FirstEl), Capacity(TotalCapacity) {}
+
+ /// This is a helper for \a grow() that's out of line to reduce code
+ /// duplication. This function will report a fatal error if it can't grow at
+ /// least to \p MinSize.
+ void* mallocForGrow(size_t MinSize, size_t TSize, size_t& NewCapacity);
+
+ /// This is an implementation of the grow() method which only works
+ /// on POD-like data types and is out of line to reduce code duplication.
+ /// This function will report a fatal error if it cannot increase capacity.
+ void grow_pod(void* FirstEl, size_t MinSize, size_t TSize);
+
+public:
+ size_t size() const { return Size; }
+ size_t capacity() const { return Capacity; }
+
+ [[nodiscard]] bool empty() const { return !Size; }
+
+protected:
+ /// Set the array size to \p N, which the current array must have enough
+ /// capacity for.
+ ///
+ /// This does not construct or destroy any elements in the vector.
+ void set_size(size_t N) {
+ assert(N <= capacity());
+ Size = N;
+ }
+};
+
+template <typename T>
+using SmallVectorSizeType =
+ typename std::conditional<sizeof(T) < 4 && sizeof(void*) >= 8, uint64_t, uint32_t>::type;
+
+/// Figure out the offset of the first element.
+template <typename T, typename = void>
+struct SmallVectorAlignmentAndSize {
+ alignas(SmallVectorBase<SmallVectorSizeType<T>>) char Base[sizeof(
+ SmallVectorBase<SmallVectorSizeType<T>>)];
+ alignas(T) char FirstEl[sizeof(T)];
+};
+
+/// This is the part of SmallVectorTemplateBase which does not depend on whether
+/// the type T is a POD. The extra dummy template argument is used by ArrayRef
+/// to avoid unnecessarily requiring T to be complete.
+template <typename T, typename = void>
+class SmallVectorTemplateCommon
+ : public SmallVectorBase<SmallVectorSizeType<T>> {
+ using Base = SmallVectorBase<SmallVectorSizeType<T>>;
+
+ /// Find the address of the first element. For this pointer math to be valid
+ /// with small-size of 0 for T with lots of alignment, it's important that
+ /// SmallVectorStorage is properly-aligned even for small-size of 0.
+ void* getFirstEl() const {
+ return const_cast<void*>(reinterpret_cast<const void*>(
+ reinterpret_cast<const char*>(this) +
+ offsetof(SmallVectorAlignmentAndSize<T>, FirstEl)));
+ }
+ // Space after 'FirstEl' is clobbered, do not add any instance vars after it.
+
+protected:
+ SmallVectorTemplateCommon(size_t Size)
+ : Base(getFirstEl(), Size) {}
+
+ void grow_pod(size_t MinSize, size_t TSize) {
+ Base::grow_pod(getFirstEl(), MinSize, TSize);
+ }
+
+ /// Return true if this is a smallvector which has not had dynamic
+ /// memory allocated for it.
+ bool isSmall() const { return this->BeginX == getFirstEl(); }
+
+ /// Put this vector in a state of being small.
+ void resetToSmall() {
+ this->BeginX = getFirstEl();
+ this->Size = this->Capacity = 0; // FIXME: Setting Capacity to 0 is suspect.
+ }
+
+ /// Return true if V is an internal reference to the given range.
+ bool isReferenceToRange(const void* V, const void* First, const void* Last) const {
+ // Use std::less to avoid UB.
+ std::less<> LessThan;
+ return !LessThan(V, First) && LessThan(V, Last);
+ }
+
+ /// Return true if V is an internal reference to this vector.
+ bool isReferenceToStorage(const void* V) const {
+ return isReferenceToRange(V, this->begin(), this->end());
+ }
+
+ /// Return true if First and Last form a valid (possibly empty) range in this
+ /// vector's storage.
+ bool isRangeInStorage(const void* First, const void* Last) const {
+ // Use std::less to avoid UB.
+ std::less<> LessThan;
+ return !LessThan(First, this->begin()) && !LessThan(Last, First) &&
+ !LessThan(this->end(), Last);
+ }
+
+ /// Return true unless Elt will be invalidated by resizing the vector to
+ /// NewSize.
+ bool isSafeToReferenceAfterResize(const void* Elt, size_t NewSize) {
+ // Past the end.
+ if (LLVM_LIKELY(!isReferenceToStorage(Elt)))
+ return true;
+
+ // Return false if Elt will be destroyed by shrinking.
+ if (NewSize <= this->size())
+ return Elt < this->begin() + NewSize;
+
+ // Return false if we need to grow.
+ return NewSize <= this->capacity();
+ }
+
+ /// Check whether Elt will be invalidated by resizing the vector to NewSize.
+ void assertSafeToReferenceAfterResize(const void* Elt, size_t NewSize) {
+ assert(isSafeToReferenceAfterResize(Elt, NewSize) &&
+ "Attempting to reference an element of the vector in an operation "
+ "that invalidates it");
+ }
+
+ /// Check whether Elt will be invalidated by increasing the size of the
+ /// vector by N.
+ void assertSafeToAdd(const void* Elt, size_t N = 1) {
+ this->assertSafeToReferenceAfterResize(Elt, this->size() + N);
+ }
+
+ /// Check whether any part of the range will be invalidated by clearing.
+ void assertSafeToReferenceAfterClear(const T* From, const T* To) {
+ if (From == To)
+ return;
+ this->assertSafeToReferenceAfterResize(From, 0);
+ this->assertSafeToReferenceAfterResize(To - 1, 0);
+ }
+ template <
+ class ItTy,
+ std::enable_if_t<!std::is_same<std::remove_const_t<ItTy>, T*>::value,
+ bool> = false>
+ void assertSafeToReferenceAfterClear(ItTy, ItTy) {}
+
+ /// Check whether any part of the range will be invalidated by growing.
+ void assertSafeToAddRange(const T* From, const T* To) {
+ if (From == To)
+ return;
+ this->assertSafeToAdd(From, To - From);
+ this->assertSafeToAdd(To - 1, To - From);
+ }
+ template <
+ class ItTy,
+ std::enable_if_t<!std::is_same<std::remove_const_t<ItTy>, T*>::value,
+ bool> = false>
+ void assertSafeToAddRange(ItTy, ItTy) {}
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ template <typename U>
+ static const T* reserveForParamAndGetAddressImpl(U* This, const T& Elt, size_t N) {
+ size_t NewSize = This->size() + N;
+ if (LLVM_LIKELY(NewSize <= This->capacity()))
+ return &Elt;
+
+ bool ReferencesStorage = false;
+ int64_t Index = -1;
+ if (!U::TakesParamByValue) {
+ if (LLVM_UNLIKELY(This->isReferenceToStorage(&Elt))) {
+ ReferencesStorage = true;
+ Index = &Elt - This->begin();
+ }
+ }
+ This->grow(NewSize);
+ return ReferencesStorage ? This->begin() + Index : &Elt;
+ }
+
+public:
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+ using value_type = T;
+ using iterator = T*;
+ using const_iterator = const T*;
+
+ using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+
+ using reference = T&;
+ using const_reference = const T&;
+ using pointer = T*;
+ using const_pointer = const T*;
+
+ using Base::capacity;
+ using Base::empty;
+ using Base::size;
+
+ // forward iterator creation methods.
+ iterator begin() { return (iterator)this->BeginX; }
+ const_iterator begin() const { return (const_iterator)this->BeginX; }
+ iterator end() { return begin() + size(); }
+ const_iterator end() const { return begin() + size(); }
+
+ // reverse iterator creation methods.
+ reverse_iterator rbegin() { return reverse_iterator(end()); }
+ const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); }
+ reverse_iterator rend() { return reverse_iterator(begin()); }
+ const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
+
+ size_type size_in_bytes() const { return size() * sizeof(T); }
+ size_type max_size() const {
+ return std::min(this->SizeTypeMax(), size_type(-1) / sizeof(T));
+ }
+
+ size_t capacity_in_bytes() const { return capacity() * sizeof(T); }
+
+ /// Return a pointer to the vector's buffer, even if empty().
+ pointer data() { return pointer(begin()); }
+ /// Return a pointer to the vector's buffer, even if empty().
+ const_pointer data() const { return const_pointer(begin()); }
+
+ reference operator[](size_type idx) {
+ assert(idx < size());
+ return begin()[idx];
+ }
+ const_reference operator[](size_type idx) const {
+ assert(idx < size());
+ return begin()[idx];
+ }
+
+ reference front() {
+ assert(!empty());
+ return begin()[0];
+ }
+ const_reference front() const {
+ assert(!empty());
+ return begin()[0];
+ }
+
+ reference back() {
+ assert(!empty());
+ return end()[-1];
+ }
+ const_reference back() const {
+ assert(!empty());
+ return end()[-1];
+ }
+};
+
+/// SmallVectorTemplateBase<TriviallyCopyable = false> - This is where we put
+/// method implementations that are designed to work with non-trivial T's.
+///
+/// We approximate is_trivially_copyable with trivial move/copy construction and
+/// trivial destruction. While the standard doesn't specify that you're allowed
+/// copy these types with memcpy, there is no way for the type to observe this.
+/// This catches the important case of std::pair<POD, POD>, which is not
+/// trivially assignable.
+template <typename T, bool = (std::is_trivially_copy_constructible<T>::value) && (std::is_trivially_move_constructible<T>::value) && std::is_trivially_destructible<T>::value>
+class SmallVectorTemplateBase : public SmallVectorTemplateCommon<T> {
+ friend class SmallVectorTemplateCommon<T>;
+
+protected:
+ static constexpr bool TakesParamByValue = false;
+ using ValueParamT = const T&;
+
+ SmallVectorTemplateBase(size_t Size)
+ : SmallVectorTemplateCommon<T>(Size) {}
+
+ static void destroy_range(T* S, T* E) {
+ while (S != E) {
+ --E;
+ E->~T();
+ }
+ }
+
+ /// Move the range [I, E) into the uninitialized memory starting with "Dest",
+ /// constructing elements as needed.
+ template <typename It1, typename It2>
+ static void uninitialized_move(It1 I, It1 E, It2 Dest) {
+ std::uninitialized_copy(std::make_move_iterator(I),
+ std::make_move_iterator(E),
+ Dest);
+ }
+
+ /// Copy the range [I, E) onto the uninitialized memory starting with "Dest",
+ /// constructing elements as needed.
+ template <typename It1, typename It2>
+ static void uninitialized_copy(It1 I, It1 E, It2 Dest) {
+ std::uninitialized_copy(I, E, Dest);
+ }
+
+ /// Grow the allocated memory (without initializing new elements), doubling
+ /// the size of the allocated memory. Guarantees space for at least one more
+ /// element, or MinSize more elements if specified.
+ void grow(size_t MinSize = 0);
+
+ /// Create a new allocation big enough for \p MinSize and pass back its size
+ /// in \p NewCapacity. This is the first section of \a grow().
+ T* mallocForGrow(size_t MinSize, size_t& NewCapacity) {
+ return static_cast<T*>(
+ SmallVectorBase<SmallVectorSizeType<T>>::mallocForGrow(
+ MinSize, sizeof(T), NewCapacity));
+ }
+
+ /// Move existing elements over to the new allocation \p NewElts, the middle
+ /// section of \a grow().
+ void moveElementsForGrow(T* NewElts);
+
+ /// Transfer ownership of the allocation, finishing up \a grow().
+ void takeAllocationForGrow(T* NewElts, size_t NewCapacity);
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ const T* reserveForParamAndGetAddress(const T& Elt, size_t N = 1) {
+ return this->reserveForParamAndGetAddressImpl(this, Elt, N);
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ T* reserveForParamAndGetAddress(T& Elt, size_t N = 1) {
+ return const_cast<T*>(
+ this->reserveForParamAndGetAddressImpl(this, Elt, N));
+ }
+
+ static T&& forward_value_param(T&& V) { return std::move(V); }
+ static const T& forward_value_param(const T& V) { return V; }
+
+ void growAndAssign(size_t NumElts, const T& Elt) {
+ // Grow manually in case Elt is an internal reference.
+ size_t NewCapacity;
+ T* NewElts = mallocForGrow(NumElts, NewCapacity);
+ std::uninitialized_fill_n(NewElts, NumElts, Elt);
+ this->destroy_range(this->begin(), this->end());
+ takeAllocationForGrow(NewElts, NewCapacity);
+ this->set_size(NumElts);
+ }
+
+ template <typename... ArgTypes>
+ T& growAndEmplaceBack(ArgTypes&&... Args) {
+ // Grow manually in case one of Args is an internal reference.
+ size_t NewCapacity;
+ T* NewElts = mallocForGrow(0, NewCapacity);
+ ::new ((void*)(NewElts + this->size())) T(std::forward<ArgTypes>(Args)...);
+ moveElementsForGrow(NewElts);
+ takeAllocationForGrow(NewElts, NewCapacity);
+ this->set_size(this->size() + 1);
+ return this->back();
+ }
+
+public:
+ void push_back(const T& Elt) {
+ const T* EltPtr = reserveForParamAndGetAddress(Elt);
+ ::new ((void*)this->end()) T(*EltPtr);
+ this->set_size(this->size() + 1);
+ }
+
+ void push_back(T&& Elt) {
+ T* EltPtr = reserveForParamAndGetAddress(Elt);
+ ::new ((void*)this->end()) T(::std::move(*EltPtr));
+ this->set_size(this->size() + 1);
+ }
+
+ void pop_back() {
+ this->set_size(this->size() - 1);
+ this->end()->~T();
+ }
+};
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void SmallVectorTemplateBase<T, TriviallyCopyable>::grow(size_t MinSize) {
+ size_t NewCapacity;
+ T* NewElts = mallocForGrow(MinSize, NewCapacity);
+ moveElementsForGrow(NewElts);
+ takeAllocationForGrow(NewElts, NewCapacity);
+}
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void SmallVectorTemplateBase<T, TriviallyCopyable>::moveElementsForGrow(
+ T* NewElts) {
+ // Move the elements over.
+ this->uninitialized_move(this->begin(), this->end(), NewElts);
+
+ // Destroy the original elements.
+ destroy_range(this->begin(), this->end());
+}
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void SmallVectorTemplateBase<T, TriviallyCopyable>::takeAllocationForGrow(
+ T* NewElts, size_t NewCapacity) {
+ // If this wasn't grown from the inline copy, deallocate the old space.
+ if (!this->isSmall())
+ free(this->begin());
+
+ this->BeginX = NewElts;
+ this->Capacity = NewCapacity;
+}
+
+/// SmallVectorTemplateBase<TriviallyCopyable = true> - This is where we put
+/// method implementations that are designed to work with trivially copyable
+/// T's. This allows using memcpy in place of copy/move construction and
+/// skipping destruction.
+template <typename T>
+class SmallVectorTemplateBase<T, true> : public SmallVectorTemplateCommon<T> {
+ friend class SmallVectorTemplateCommon<T>;
+
+protected:
+ /// True if it's cheap enough to take parameters by value. Doing so avoids
+ /// overhead related to mitigations for reference invalidation.
+ static constexpr bool TakesParamByValue = sizeof(T) <= 2 * sizeof(void*);
+
+ /// Either const T& or T, depending on whether it's cheap enough to take
+ /// parameters by value.
+ using ValueParamT =
+ typename std::conditional<TakesParamByValue, T, const T&>::type;
+
+ SmallVectorTemplateBase(size_t Size)
+ : SmallVectorTemplateCommon<T>(Size) {}
+
+ // No need to do a destroy loop for POD's.
+ static void destroy_range(T*, T*) {}
+
+ /// Move the range [I, E) onto the uninitialized memory
+ /// starting with "Dest", constructing elements into it as needed.
+ template <typename It1, typename It2>
+ static void uninitialized_move(It1 I, It1 E, It2 Dest) {
+ // Just do a copy.
+ uninitialized_copy(I, E, Dest);
+ }
+
+ /// Copy the range [I, E) onto the uninitialized memory
+ /// starting with "Dest", constructing elements into it as needed.
+ template <typename It1, typename It2>
+ static void uninitialized_copy(It1 I, It1 E, It2 Dest) {
+ // Arbitrary iterator types; just use the basic implementation.
+ std::uninitialized_copy(I, E, Dest);
+ }
+
+ /// Copy the range [I, E) onto the uninitialized memory
+ /// starting with "Dest", constructing elements into it as needed.
+ template <typename T1, typename T2>
+ static void uninitialized_copy(
+ T1* I, T1* E, T2* Dest, std::enable_if_t<std::is_same<typename std::remove_const<T1>::type, T2>::value>* = nullptr) {
+ // Use memcpy for PODs iterated by pointers (which includes SmallVector
+ // iterators): std::uninitialized_copy optimizes to memmove, but we can
+ // use memcpy here. Note that I and E are iterators and thus might be
+ // invalid for memcpy if they are equal.
+ if (I != E)
+ memcpy(reinterpret_cast<void*>(Dest), I, (E - I) * sizeof(T));
+ }
+
+ /// Double the size of the allocated memory, guaranteeing space for at
+ /// least one more element or MinSize if specified.
+ void grow(size_t MinSize = 0) { this->grow_pod(MinSize, sizeof(T)); }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ const T* reserveForParamAndGetAddress(const T& Elt, size_t N = 1) {
+ return this->reserveForParamAndGetAddressImpl(this, Elt, N);
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ T* reserveForParamAndGetAddress(T& Elt, size_t N = 1) {
+ return const_cast<T*>(
+ this->reserveForParamAndGetAddressImpl(this, Elt, N));
+ }
+
+ /// Copy \p V or return a reference, depending on \a ValueParamT.
+ static ValueParamT forward_value_param(ValueParamT V) { return V; }
+
+ void growAndAssign(size_t NumElts, T Elt) {
+ // Elt has been copied in case it's an internal reference, side-stepping
+ // reference invalidation problems without losing the realloc optimization.
+ this->set_size(0);
+ this->grow(NumElts);
+ std::uninitialized_fill_n(this->begin(), NumElts, Elt);
+ this->set_size(NumElts);
+ }
+
+ template <typename... ArgTypes>
+ T& growAndEmplaceBack(ArgTypes&&... Args) {
+ // Use push_back with a copy in case Args has an internal reference,
+ // side-stepping reference invalidation problems without losing the realloc
+ // optimization.
+ push_back(T(std::forward<ArgTypes>(Args)...));
+ return this->back();
+ }
+
+public:
+ void push_back(ValueParamT Elt) {
+ const T* EltPtr = reserveForParamAndGetAddress(Elt);
+ memcpy(reinterpret_cast<void*>(this->end()), EltPtr, sizeof(T));
+ this->set_size(this->size() + 1);
+ }
+
+ void pop_back() { this->set_size(this->size() - 1); }
+};
+
+/// This class consists of common code factored out of the SmallVector class to
+/// reduce code duplication based on the SmallVector 'N' template parameter.
+template <typename T>
+class SmallVectorImpl : public SmallVectorTemplateBase<T> {
+ using SuperClass = SmallVectorTemplateBase<T>;
+
+public:
+ using iterator = typename SuperClass::iterator;
+ using const_iterator = typename SuperClass::const_iterator;
+ using reference = typename SuperClass::reference;
+ using size_type = typename SuperClass::size_type;
+
+protected:
+ using SmallVectorTemplateBase<T>::TakesParamByValue;
+ using ValueParamT = typename SuperClass::ValueParamT;
+
+ // Default ctor - Initialize to empty.
+ explicit SmallVectorImpl(unsigned N)
+ : SmallVectorTemplateBase<T>(N) {}
+
+ void assignRemote(SmallVectorImpl&& RHS) {
+ this->destroy_range(this->begin(), this->end());
+ if (!this->isSmall())
+ free(this->begin());
+ this->BeginX = RHS.BeginX;
+ this->Size = RHS.Size;
+ this->Capacity = RHS.Capacity;
+ RHS.resetToSmall();
+ }
+
+public:
+ SmallVectorImpl(const SmallVectorImpl&) = delete;
+
+ ~SmallVectorImpl() {
+ // Subclass has already destructed this vector's elements.
+ // If this wasn't grown from the inline copy, deallocate the old space.
+ if (!this->isSmall())
+ free(this->begin());
+ }
+
+ void clear() {
+ this->destroy_range(this->begin(), this->end());
+ this->Size = 0;
+ }
+
+private:
+ // Make set_size() private to avoid misuse in subclasses.
+ using SuperClass::set_size;
+
+ template <bool ForOverwrite>
+ void resizeImpl(size_type N) {
+ if (N == this->size())
+ return;
+
+ if (N < this->size()) {
+ this->truncate(N);
+ return;
+ }
+
+ this->reserve(N);
+ for (auto I = this->end(), E = this->begin() + N; I != E; ++I)
+ if (ForOverwrite)
+ new (&*I) T;
+ else
+ new (&*I) T();
+ this->set_size(N);
+ }
+
+public:
+ void resize(size_type N) { resizeImpl<false>(N); }
+
+ /// Like resize, but \ref T is POD, the new values won't be initialized.
+ void resize_for_overwrite(size_type N) { resizeImpl<true>(N); }
+
+ /// Like resize, but requires that \p N is less than \a size().
+ void truncate(size_type N) {
+ assert(this->size() >= N && "Cannot increase size with truncate");
+ this->destroy_range(this->begin() + N, this->end());
+ this->set_size(N);
+ }
+
+ void resize(size_type N, ValueParamT NV) {
+ if (N == this->size())
+ return;
+
+ if (N < this->size()) {
+ this->truncate(N);
+ return;
+ }
+
+ // N > this->size(). Defer to append.
+ this->append(N - this->size(), NV);
+ }
+
+ void reserve(size_type N) {
+ if (this->capacity() < N)
+ this->grow(N);
+ }
+
+ void pop_back_n(size_type NumItems) {
+ assert(this->size() >= NumItems);
+ truncate(this->size() - NumItems);
+ }
+
+ [[nodiscard]] T pop_back_val() {
+ T Result = ::std::move(this->back());
+ this->pop_back();
+ return Result;
+ }
+
+ void swap(SmallVectorImpl& RHS);
+
+ /// Add the specified range to the end of the SmallVector.
+ template <typename in_iter,
+ typename = std::enable_if_t<std::is_convertible<
+ typename std::iterator_traits<in_iter>::iterator_category,
+ std::input_iterator_tag>::value>>
+ void append(in_iter in_start, in_iter in_end) {
+ this->assertSafeToAddRange(in_start, in_end);
+ size_type NumInputs = std::distance(in_start, in_end);
+ this->reserve(this->size() + NumInputs);
+ this->uninitialized_copy(in_start, in_end, this->end());
+ this->set_size(this->size() + NumInputs);
+ }
+
+ /// Append \p NumInputs copies of \p Elt to the end.
+ void append(size_type NumInputs, ValueParamT Elt) {
+ const T* EltPtr = this->reserveForParamAndGetAddress(Elt, NumInputs);
+ std::uninitialized_fill_n(this->end(), NumInputs, *EltPtr);
+ this->set_size(this->size() + NumInputs);
+ }
+
+ void append(std::initializer_list<T> IL) {
+ append(IL.begin(), IL.end());
+ }
+
+ void append(const SmallVectorImpl& RHS) { append(RHS.begin(), RHS.end()); }
+
+ void assign(size_type NumElts, ValueParamT Elt) {
+ // Note that Elt could be an internal reference.
+ if (NumElts > this->capacity()) {
+ this->growAndAssign(NumElts, Elt);
+ return;
+ }
+
+ // Assign over existing elements.
+ std::fill_n(this->begin(), std::min(NumElts, this->size()), Elt);
+ if (NumElts > this->size())
+ std::uninitialized_fill_n(this->end(), NumElts - this->size(), Elt);
+ else if (NumElts < this->size())
+ this->destroy_range(this->begin() + NumElts, this->end());
+ this->set_size(NumElts);
+ }
+
+ // FIXME: Consider assigning over existing elements, rather than clearing &
+ // re-initializing them - for all assign(...) variants.
+
+ template <typename in_iter,
+ typename = std::enable_if_t<std::is_convertible<
+ typename std::iterator_traits<in_iter>::iterator_category,
+ std::input_iterator_tag>::value>>
+ void assign(in_iter in_start, in_iter in_end) {
+ this->assertSafeToReferenceAfterClear(in_start, in_end);
+ clear();
+ append(in_start, in_end);
+ }
+
+ void assign(std::initializer_list<T> IL) {
+ clear();
+ append(IL);
+ }
+
+ void assign(const SmallVectorImpl& RHS) { assign(RHS.begin(), RHS.end()); }
+
+ iterator erase(const_iterator CI) {
+ // Just cast away constness because this is a non-const member function.
+ iterator I = const_cast<iterator>(CI);
+
+ assert(this->isReferenceToStorage(CI) && "Iterator to erase is out of bounds.");
+
+ iterator N = I;
+ // Shift all elts down one.
+ std::move(I + 1, this->end(), I);
+ // Drop the last elt.
+ this->pop_back();
+ return (N);
+ }
+
+ iterator erase(const_iterator CS, const_iterator CE) {
+ // Just cast away constness because this is a non-const member function.
+ iterator S = const_cast<iterator>(CS);
+ iterator E = const_cast<iterator>(CE);
+
+ assert(this->isRangeInStorage(S, E) && "Range to erase is out of bounds.");
+
+ iterator N = S;
+ // Shift all elts down.
+ iterator I = std::move(E, this->end(), S);
+ // Drop the last elts.
+ this->destroy_range(I, this->end());
+ this->set_size(I - this->begin());
+ return (N);
+ }
+
+private:
+ template <typename ArgType>
+ iterator insert_one_impl(iterator I, ArgType&& Elt) {
+ // Callers ensure that ArgType is derived from T.
+ static_assert(
+ std::is_same<std::remove_const_t<std::remove_reference_t<ArgType>>,
+ T>::value,
+ "ArgType must be derived from T!");
+
+ if (I == this->end()) { // Important special case for empty vector.
+ this->push_back(::std::forward<ArgType>(Elt));
+ return this->end() - 1;
+ }
+
+ assert(this->isReferenceToStorage(I) && "Insertion iterator is out of bounds.");
+
+ // Grow if necessary.
+ size_t Index = I - this->begin();
+ std::remove_reference_t<ArgType>* EltPtr =
+ this->reserveForParamAndGetAddress(Elt);
+ I = this->begin() + Index;
+
+ ::new ((void*)this->end()) T(::std::move(this->back()));
+ // Push everything else over.
+ std::move_backward(I, this->end() - 1, this->end());
+ this->set_size(this->size() + 1);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ static_assert(!TakesParamByValue || std::is_same<ArgType, T>::value,
+ "ArgType must be 'T' when taking by value!");
+ if (!TakesParamByValue && this->isReferenceToRange(EltPtr, I, this->end()))
+ ++EltPtr;
+
+ *I = ::std::forward<ArgType>(*EltPtr);
+ return I;
+ }
+
+public:
+ iterator insert(iterator I, T&& Elt) {
+ return insert_one_impl(I, this->forward_value_param(std::move(Elt)));
+ }
+
+ iterator insert(iterator I, const T& Elt) {
+ return insert_one_impl(I, this->forward_value_param(Elt));
+ }
+
+ iterator insert(iterator I, size_type NumToInsert, ValueParamT Elt) {
+ // Convert iterator to elt# to avoid invalidating iterator when we reserve()
+ size_t InsertElt = I - this->begin();
+
+ if (I == this->end()) { // Important special case for empty vector.
+ append(NumToInsert, Elt);
+ return this->begin() + InsertElt;
+ }
+
+ assert(this->isReferenceToStorage(I) && "Insertion iterator is out of bounds.");
+
+ // Ensure there is enough space, and get the (maybe updated) address of
+ // Elt.
+ const T* EltPtr = this->reserveForParamAndGetAddress(Elt, NumToInsert);
+
+ // Uninvalidate the iterator.
+ I = this->begin() + InsertElt;
+
+ // If there are more elements between the insertion point and the end of the
+ // range than there are being inserted, we can use a simple approach to
+ // insertion. Since we already reserved space, we know that this won't
+ // reallocate the vector.
+ if (size_t(this->end() - I) >= NumToInsert) {
+ T* OldEnd = this->end();
+ append(std::move_iterator<iterator>(this->end() - NumToInsert),
+ std::move_iterator<iterator>(this->end()));
+
+ // Copy the existing elements that get replaced.
+ std::move_backward(I, OldEnd - NumToInsert, OldEnd);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ if (!TakesParamByValue && I <= EltPtr && EltPtr < this->end())
+ EltPtr += NumToInsert;
+
+ std::fill_n(I, NumToInsert, *EltPtr);
+ return I;
+ }
+
+ // Otherwise, we're inserting more elements than exist already, and we're
+ // not inserting at the end.
+
+ // Move over the elements that we're about to overwrite.
+ T* OldEnd = this->end();
+ this->set_size(this->size() + NumToInsert);
+ size_t NumOverwritten = OldEnd - I;
+ this->uninitialized_move(I, OldEnd, this->end() - NumOverwritten);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ if (!TakesParamByValue && I <= EltPtr && EltPtr < this->end())
+ EltPtr += NumToInsert;
+
+ // Replace the overwritten part.
+ std::fill_n(I, NumOverwritten, *EltPtr);
+
+ // Insert the non-overwritten middle part.
+ std::uninitialized_fill_n(OldEnd, NumToInsert - NumOverwritten, *EltPtr);
+ return I;
+ }
+
+ template <typename ItTy,
+ typename = std::enable_if_t<std::is_convertible<
+ typename std::iterator_traits<ItTy>::iterator_category,
+ std::input_iterator_tag>::value>>
+ iterator insert(iterator I, ItTy From, ItTy To) {
+ // Convert iterator to elt# to avoid invalidating iterator when we reserve()
+ size_t InsertElt = I - this->begin();
+
+ if (I == this->end()) { // Important special case for empty vector.
+ append(From, To);
+ return this->begin() + InsertElt;
+ }
+
+ assert(this->isReferenceToStorage(I) && "Insertion iterator is out of bounds.");
+
+ // Check that the reserve that follows doesn't invalidate the iterators.
+ this->assertSafeToAddRange(From, To);
+
+ size_t NumToInsert = std::distance(From, To);
+
+ // Ensure there is enough space.
+ reserve(this->size() + NumToInsert);
+
+ // Uninvalidate the iterator.
+ I = this->begin() + InsertElt;
+
+ // If there are more elements between the insertion point and the end of the
+ // range than there are being inserted, we can use a simple approach to
+ // insertion. Since we already reserved space, we know that this won't
+ // reallocate the vector.
+ if (size_t(this->end() - I) >= NumToInsert) {
+ T* OldEnd = this->end();
+ append(std::move_iterator<iterator>(this->end() - NumToInsert),
+ std::move_iterator<iterator>(this->end()));
+
+ // Copy the existing elements that get replaced.
+ std::move_backward(I, OldEnd - NumToInsert, OldEnd);
+
+ std::copy(From, To, I);
+ return I;
+ }
+
+ // Otherwise, we're inserting more elements than exist already, and we're
+ // not inserting at the end.
+
+ // Move over the elements that we're about to overwrite.
+ T* OldEnd = this->end();
+ this->set_size(this->size() + NumToInsert);
+ size_t NumOverwritten = OldEnd - I;
+ this->uninitialized_move(I, OldEnd, this->end() - NumOverwritten);
+
+ // Replace the overwritten part.
+ for (T* J = I; NumOverwritten > 0; --NumOverwritten) {
+ *J = *From;
+ ++J;
+ ++From;
+ }
+
+ // Insert the non-overwritten middle part.
+ this->uninitialized_copy(From, To, OldEnd);
+ return I;
+ }
+
+ void insert(iterator I, std::initializer_list<T> IL) {
+ insert(I, IL.begin(), IL.end());
+ }
+
+ template <typename... ArgTypes>
+ reference emplace_back(ArgTypes&&... Args) {
+ if (LLVM_UNLIKELY(this->size() >= this->capacity()))
+ return this->growAndEmplaceBack(std::forward<ArgTypes>(Args)...);
+
+ ::new ((void*)this->end()) T(std::forward<ArgTypes>(Args)...);
+ this->set_size(this->size() + 1);
+ return this->back();
+ }
+
+ SmallVectorImpl& operator=(const SmallVectorImpl& RHS);
+
+ SmallVectorImpl& operator=(SmallVectorImpl&& RHS);
+
+ bool operator==(const SmallVectorImpl& RHS) const {
+ if (this->size() != RHS.size()) return false;
+ return std::equal(this->begin(), this->end(), RHS.begin());
+ }
+ bool operator!=(const SmallVectorImpl& RHS) const {
+ return !(*this == RHS);
+ }
+
+ bool operator<(const SmallVectorImpl& RHS) const {
+ return std::lexicographical_compare(this->begin(), this->end(), RHS.begin(), RHS.end());
+ }
+};
+
+template <typename T>
+void SmallVectorImpl<T>::swap(SmallVectorImpl<T>& RHS) {
+ if (this == &RHS) return;
+
+ // We can only avoid copying elements if neither vector is small.
+ if (!this->isSmall() && !RHS.isSmall()) {
+ std::swap(this->BeginX, RHS.BeginX);
+ std::swap(this->Size, RHS.Size);
+ std::swap(this->Capacity, RHS.Capacity);
+ return;
+ }
+ this->reserve(RHS.size());
+ RHS.reserve(this->size());
+
+ // Swap the shared elements.
+ size_t NumShared = this->size();
+ if (NumShared > RHS.size()) NumShared = RHS.size();
+ for (size_type i = 0; i != NumShared; ++i)
+ std::swap((*this)[i], RHS[i]);
+
+ // Copy over the extra elts.
+ if (this->size() > RHS.size()) {
+ size_t EltDiff = this->size() - RHS.size();
+ this->uninitialized_copy(this->begin() + NumShared, this->end(), RHS.end());
+ RHS.set_size(RHS.size() + EltDiff);
+ this->destroy_range(this->begin() + NumShared, this->end());
+ this->set_size(NumShared);
+ } else if (RHS.size() > this->size()) {
+ size_t EltDiff = RHS.size() - this->size();
+ this->uninitialized_copy(RHS.begin() + NumShared, RHS.end(), this->end());
+ this->set_size(this->size() + EltDiff);
+ this->destroy_range(RHS.begin() + NumShared, RHS.end());
+ RHS.set_size(NumShared);
+ }
+}
+
+template <typename T>
+SmallVectorImpl<T>& SmallVectorImpl<T>::
+operator=(const SmallVectorImpl<T>& RHS) {
+ // Avoid self-assignment.
+ if (this == &RHS) return *this;
+
+ // If we already have sufficient space, assign the common elements, then
+ // destroy any excess.
+ size_t RHSSize = RHS.size();
+ size_t CurSize = this->size();
+ if (CurSize >= RHSSize) {
+ // Assign common elements.
+ iterator NewEnd;
+ if (RHSSize)
+ NewEnd = std::copy(RHS.begin(), RHS.begin() + RHSSize, this->begin());
+ else
+ NewEnd = this->begin();
+
+ // Destroy excess elements.
+ this->destroy_range(NewEnd, this->end());
+
+ // Trim.
+ this->set_size(RHSSize);
+ return *this;
+ }
+
+ // If we have to grow to have enough elements, destroy the current elements.
+ // This allows us to avoid copying them during the grow.
+ // FIXME: don't do this if they're efficiently moveable.
+ if (this->capacity() < RHSSize) {
+ // Destroy current elements.
+ this->clear();
+ CurSize = 0;
+ this->grow(RHSSize);
+ } else if (CurSize) {
+ // Otherwise, use assignment for the already-constructed elements.
+ std::copy(RHS.begin(), RHS.begin() + CurSize, this->begin());
+ }
+
+ // Copy construct the new elements in place.
+ this->uninitialized_copy(RHS.begin() + CurSize, RHS.end(), this->begin() + CurSize);
+
+ // Set end.
+ this->set_size(RHSSize);
+ return *this;
+}
+
+template <typename T>
+SmallVectorImpl<T>& SmallVectorImpl<T>::operator=(SmallVectorImpl<T>&& RHS) {
+ // Avoid self-assignment.
+ if (this == &RHS) return *this;
+
+ // If the RHS isn't small, clear this vector and then steal its buffer.
+ if (!RHS.isSmall()) {
+ this->assignRemote(std::move(RHS));
+ return *this;
+ }
+
+ // If we already have sufficient space, assign the common elements, then
+ // destroy any excess.
+ size_t RHSSize = RHS.size();
+ size_t CurSize = this->size();
+ if (CurSize >= RHSSize) {
+ // Assign common elements.
+ iterator NewEnd = this->begin();
+ if (RHSSize)
+ NewEnd = std::move(RHS.begin(), RHS.end(), NewEnd);
+
+ // Destroy excess elements and trim the bounds.
+ this->destroy_range(NewEnd, this->end());
+ this->set_size(RHSSize);
+
+ // Clear the RHS.
+ RHS.clear();
+
+ return *this;
+ }
+
+ // If we have to grow to have enough elements, destroy the current elements.
+ // This allows us to avoid copying them during the grow.
+ // FIXME: this may not actually make any sense if we can efficiently move
+ // elements.
+ if (this->capacity() < RHSSize) {
+ // Destroy current elements.
+ this->clear();
+ CurSize = 0;
+ this->grow(RHSSize);
+ } else if (CurSize) {
+ // Otherwise, use assignment for the already-constructed elements.
+ std::move(RHS.begin(), RHS.begin() + CurSize, this->begin());
+ }
+
+ // Move-construct the new elements in place.
+ this->uninitialized_move(RHS.begin() + CurSize, RHS.end(), this->begin() + CurSize);
+
+ // Set end.
+ this->set_size(RHSSize);
+
+ RHS.clear();
+ return *this;
+}
+
+/// Storage for the SmallVector elements. This is specialized for the N=0 case
+/// to avoid allocating unnecessary storage.
+template <typename T, unsigned N>
+struct SmallVectorStorage {
+ alignas(T) char InlineElts[N * sizeof(T)];
+};
+
+/// We need the storage to be properly aligned even for small-size of 0 so that
+/// the pointer math in \a SmallVectorTemplateCommon::getFirstEl() is
+/// well-defined.
+template <typename T>
+struct alignas(T) SmallVectorStorage<T, 0> {};
+
+/// Forward declaration of SmallVector so that
+/// calculateSmallVectorDefaultInlinedElements can reference
+/// `sizeof(SmallVector<T, 0>)`.
+template <typename T, unsigned N>
+class SmallVector;
+
+/// Helper class for calculating the default number of inline elements for
+/// `SmallVector<T>`.
+///
+/// This should be migrated to a constexpr function when our minimum
+/// compiler support is enough for multi-statement constexpr functions.
+template <typename T>
+struct CalculateSmallVectorDefaultInlinedElements {
+ // Parameter controlling the default number of inlined elements
+ // for `SmallVector<T>`.
+ //
+ // The default number of inlined elements ensures that
+ // 1. There is at least one inlined element.
+ // 2. `sizeof(SmallVector<T>) <= kPreferredSmallVectorSizeof` unless
+ // it contradicts 1.
+ static constexpr size_t kPreferredSmallVectorSizeof = 64;
+
+ // static_assert that sizeof(T) is not "too big".
+ //
+ // Because our policy guarantees at least one inlined element, it is possible
+ // for an arbitrarily large inlined element to allocate an arbitrarily large
+ // amount of inline storage. We generally consider it an antipattern for a
+ // SmallVector to allocate an excessive amount of inline storage, so we want
+ // to call attention to these cases and make sure that users are making an
+ // intentional decision if they request a lot of inline storage.
+ //
+ // We want this assertion to trigger in pathological cases, but otherwise
+ // not be too easy to hit. To accomplish that, the cutoff is actually somewhat
+ // larger than kPreferredSmallVectorSizeof (otherwise,
+ // `SmallVector<SmallVector<T>>` would be one easy way to trip it, and that
+ // pattern seems useful in practice).
+ //
+ // One wrinkle is that this assertion is in theory non-portable, since
+ // sizeof(T) is in general platform-dependent. However, we don't expect this
+ // to be much of an issue, because most LLVM development happens on 64-bit
+ // hosts, and therefore sizeof(T) is expected to *decrease* when compiled for
+ // 32-bit hosts, dodging the issue. The reverse situation, where development
+ // happens on a 32-bit host and then fails due to sizeof(T) *increasing* on a
+ // 64-bit host, is expected to be very rare.
+ static_assert(
+ sizeof(T) <= 256,
+ "You are trying to use a default number of inlined elements for "
+ "`SmallVector<T>` but `sizeof(T)` is really big! Please use an "
+ "explicit number of inlined elements with `SmallVector<T, N>` to make "
+ "sure you really want that much inline storage.");
+
+ // Discount the size of the header itself when calculating the maximum inline
+ // bytes.
+ static constexpr size_t PreferredInlineBytes =
+ kPreferredSmallVectorSizeof - sizeof(SmallVector<T, 0>);
+ static constexpr size_t NumElementsThatFit = PreferredInlineBytes / sizeof(T);
+ static constexpr size_t value =
+ NumElementsThatFit == 0 ? 1 : NumElementsThatFit;
+};
+
+/// This is a 'vector' (really, a variable-sized array), optimized
+/// for the case when the array is small. It contains some number of elements
+/// in-place, which allows it to avoid heap allocation when the actual number of
+/// elements is below that threshold. This allows normal "small" cases to be
+/// fast without losing generality for large inputs.
+///
+/// \note
+/// In the absence of a well-motivated choice for the number of inlined
+/// elements \p N, it is recommended to use \c SmallVector<T> (that is,
+/// omitting the \p N). This will choose a default number of inlined elements
+/// reasonable for allocation on the stack (for example, trying to keep \c
+/// sizeof(SmallVector<T>) around 64 bytes).
+///
+/// \warning This does not attempt to be exception safe.
+///
+/// \see https://llvm.org/docs/ProgrammersManual.html#llvm-adt-smallvector-h
+template <typename T,
+ unsigned N = CalculateSmallVectorDefaultInlinedElements<T>::value>
+class SmallVector : public SmallVectorImpl<T>,
+ SmallVectorStorage<T, N> {
+public:
+ SmallVector()
+ : SmallVectorImpl<T>(N) {}
+
+ ~SmallVector() {
+ // Destroy the constructed elements in the vector.
+ this->destroy_range(this->begin(), this->end());
+ }
+
+ explicit SmallVector(size_t Size, const T& Value = T())
+ : SmallVectorImpl<T>(N) {
+ this->assign(Size, Value);
+ }
+
+ template <typename ItTy,
+ typename = std::enable_if_t<std::is_convertible<
+ typename std::iterator_traits<ItTy>::iterator_category,
+ std::input_iterator_tag>::value>>
+ SmallVector(ItTy S, ItTy E)
+ : SmallVectorImpl<T>(N) {
+ this->append(S, E);
+ }
+
+ template <typename RangeTy>
+ explicit SmallVector(const iterator_range<RangeTy>& R)
+ : SmallVectorImpl<T>(N) {
+ this->append(R.begin(), R.end());
+ }
+
+ SmallVector(std::initializer_list<T> IL)
+ : SmallVectorImpl<T>(N) {
+ this->assign(IL);
+ }
+
+ SmallVector(const SmallVector& RHS)
+ : SmallVectorImpl<T>(N) {
+ if (!RHS.empty())
+ SmallVectorImpl<T>::operator=(RHS);
+ }
+
+ SmallVector& operator=(const SmallVector& RHS) {
+ SmallVectorImpl<T>::operator=(RHS);
+ return *this;
+ }
+
+ SmallVector(SmallVector&& RHS)
+ : SmallVectorImpl<T>(N) {
+ if (!RHS.empty())
+ SmallVectorImpl<T>::operator=(::std::move(RHS));
+ }
+
+ SmallVector(SmallVectorImpl<T>&& RHS)
+ : SmallVectorImpl<T>(N) {
+ if (!RHS.empty())
+ SmallVectorImpl<T>::operator=(::std::move(RHS));
+ }
+
+ SmallVector& operator=(SmallVector&& RHS) {
+ if (N) {
+ SmallVectorImpl<T>::operator=(::std::move(RHS));
+ return *this;
+ }
+ // SmallVectorImpl<T>::operator= does not leverage N==0. Optimize the
+ // case.
+ if (this == &RHS)
+ return *this;
+ if (RHS.empty()) {
+ this->destroy_range(this->begin(), this->end());
+ this->Size = 0;
+ } else {
+ this->assignRemote(std::move(RHS));
+ }
+ return *this;
+ }
+
+ SmallVector& operator=(SmallVectorImpl<T>&& RHS) {
+ SmallVectorImpl<T>::operator=(::std::move(RHS));
+ return *this;
+ }
+
+ SmallVector& operator=(std::initializer_list<T> IL) {
+ this->assign(IL);
+ return *this;
+ }
+};
+
+template <typename T, unsigned N>
+inline size_t capacity_in_bytes(const SmallVector<T, N>& X) {
+ return X.capacity_in_bytes();
+}
+
+template <typename RangeType>
+using ValueTypeFromRangeType =
+ typename std::remove_const<typename std::remove_reference<
+ decltype(*std::begin(std::declval<RangeType&>()))>::type>::type;
+
+/// Given a range of type R, iterate the entire range and return a
+/// SmallVector with elements of the vector. This is useful, for example,
+/// when you want to iterate a range and then sort the results.
+template <unsigned Size, typename R>
+SmallVector<ValueTypeFromRangeType<R>, Size> to_vector(R&& Range) {
+ return { std::begin(Range), std::end(Range) };
+}
+template <typename R>
+SmallVector<ValueTypeFromRangeType<R>,
+ CalculateSmallVectorDefaultInlinedElements<
+ ValueTypeFromRangeType<R>>::value>
+to_vector(R&& Range) {
+ return { std::begin(Range), std::end(Range) };
+}
+
+namespace std {
+
+/// Implement std::swap in terms of SmallVector swap.
+template <typename T>
+inline void swap(SmallVectorImpl<T>& LHS, SmallVectorImpl<T>& RHS) {
+ LHS.swap(RHS);
+}
+
+/// Implement std::swap in terms of SmallVector swap.
+template <typename T, unsigned N>
+inline void swap(SmallVector<T, N>& LHS, SmallVector<T, N>& RHS) {
+ LHS.swap(RHS);
+}
+
+} // namespace std
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#endif
diff --git a/source/10-common/StbImplementations.c b/source/10-common/StbImplementations.c
new file mode 100644
index 0000000..73bbc2a
--- /dev/null
+++ b/source/10-common/StbImplementations.c
@@ -0,0 +1,14 @@
+#define STB_RECT_PACK_IMPLEMENTATION
+#include <stb_rect_pack.h>
+
+#define STB_TRUETYPE_IMPLEMENTATION
+#include <stb_truetype.h>
+
+#define STB_IMAGE_IMPLEMENTATION
+#include <stb_image.h>
+
+#define STB_SPRINTF_IMPLEMENTATION
+#include <stb_sprintf.h>
+
+#define STB_C_LEXER_IMPLEMENTATION
+#include <stb_c_lexer.h>
diff --git a/source/10-common/Type2ObjectMap.hpp b/source/10-common/Type2ObjectMap.hpp
new file mode 100644
index 0000000..0976d2e
--- /dev/null
+++ b/source/10-common/Type2ObjectMap.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "TypeTraits.hpp"
+
+#include <cstddef>
+
+template <typename TValue>
+class Type2ObjectMap {
+public:
+ template <typename TType>
+ TType& Insert(TType&& value) {
+ // TODO
+ }
+
+ template <typename TType>
+ TType& InsertOrAssign(TType& value) {
+ // TODO
+ }
+
+ template <typename TType>
+ TType Remove() {
+ // TODO
+ }
+
+ template <typename TType>
+ const TValue* Find() const {
+ // TODO
+ }
+
+ template <typename TType>
+ TValue* Find() {
+ return const_cast<TValue*>(const_cast<const Type2ObjectMap*>(this)->Find<TType>());
+ }
+
+ size_t size() const {
+ // TODO
+ }
+};
diff --git a/source/10-common/TypeTraits.hpp b/source/10-common/TypeTraits.hpp
new file mode 100644
index 0000000..73a56f9
--- /dev/null
+++ b/source/10-common/TypeTraits.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <cstddef>
+
+/// This template will be instanciated for each unique type, and the char variable will be ODR-used which gives it an unique address.
+template <typename T>
+struct TypeIdentifier {
+ static const char obj = 0;
+};
+
+template <typename T>
+struct DefaultDeleter {
+ void operator()(T* ptr) const {
+ delete ptr;
+ }
+};
+
+template <typename>
+struct RemoveMemberPtrImpl {};
+
+template <typename T, typename U>
+struct RemoveMemberPtrImpl<U T::*> {
+ using Type = U;
+};
+
+template <typename T>
+using RemoveMemberPtr = typename RemoveMemberPtrImpl<T>::Type;
diff --git a/source/10-common/Uid.cpp b/source/10-common/Uid.cpp
new file mode 100644
index 0000000..58dfffd
--- /dev/null
+++ b/source/10-common/Uid.cpp
@@ -0,0 +1,70 @@
+#include "Uid.hpp"
+
+#include "RapidJsonHelper.hpp"
+
+#include <rapidjson/document.h>
+#include <cstring>
+#include <random>
+
+Uid Uid::Create() {
+ std::random_device rd;
+ std::mt19937_64 gen(rd());
+ std::uniform_int_distribution<uint64_t> dist(
+ std::numeric_limits<uint64_t>::min(),
+ std::numeric_limits<uint64_t>::max());
+
+ Uid uid;
+ uid.upper = dist(gen);
+ uid.lower = dist(gen);
+ return uid;
+}
+
+bool Uid::IsNull() const {
+ return upper == 0 && lower == 0;
+}
+
+void Uid::ReadString(std::string_view str) {
+ sscanf(str.data(), BRUSSEL_Uid_SCAN_STR, &upper, &lower);
+}
+
+std::string Uid::WriteString() {
+ char buf[256];
+ snprintf(buf, sizeof(buf), BRUSSEL_Uid_FORMAT_STR, upper, lower);
+ return std::string(buf);
+}
+
+void Uid::Read(const rapidjson::Value& value) {
+ if (value.IsString()) {
+ ReadString(rapidjson::AsStringView(value));
+ } else if (value.IsArray()) {
+ // Compatibility support
+ assert(value.Size() == 2);
+ auto& upper = value[0];
+ assert(upper.IsUint64());
+ auto& lower = value[1];
+ assert(lower.IsUint64());
+
+ this->upper = upper.GetUint64();
+ this->lower = lower.GetUint64();
+ } else {
+ assert(false);
+ }
+}
+
+void Uid::WriteInto(rapidjson::Value& value, rapidjson::Document& root) const {
+#if BRUSSEL_Uid_WRITE_USE_ARRAY
+ value.Reserve(2, root.GetAllocator());
+ value.PushBack((uint64_t)upper, root.GetAllocator());
+ value.PushBack((uint64_t)lower, root.GetAllocator());
+#else
+ char buf[256];
+ int len = snprintf(buf, sizeof(buf), BRUSSEL_Uid_FORMAT_STR, upper, lower);
+ value.SetString(buf, len, root.GetAllocator());
+#endif
+}
+
+rapidjson::Value Uid::Write(rapidjson::Document& root) const {
+ rapidjson::Value result(rapidjson::kArrayType);
+ WriteInto(result, root);
+ return result;
+}
diff --git a/source/10-common/Uid.hpp b/source/10-common/Uid.hpp
new file mode 100644
index 0000000..a691911
--- /dev/null
+++ b/source/10-common/Uid.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "Utils.hpp"
+
+#include <rapidjson/fwd.h>
+#include <cinttypes>
+#include <functional>
+#include <string>
+#include <string_view>
+
+#define BRUSSEL_Uid_SCAN_STR "%" PRIx64 "-%" PRIx64
+#define BRUSSEL_Uid_SCAN_EXPAND(uid) &((uid).upper), &((uid).upper)
+#define BRUSSEL_Uid_FORMAT_STR "%016" PRIx64 "-%016" PRIx64
+#define BRUSSEL_Uid_FORMAT_EXPAND(uid) (uid).upper, (uid).lower
+
+// Serialize Uid object as an array with two elements, instead of the simple string format
+#define BRUSSEL_Uid_WRITE_USE_ARRAY 0
+
+struct Uid {
+ uint64_t upper = 0;
+ uint64_t lower = 0;
+
+ // Generate a random Uid
+ static Uid Create();
+
+ bool IsNull() const;
+
+ void ReadString(std::string_view str);
+ std::string WriteString();
+
+ void Read(const rapidjson::Value& value);
+ void WriteInto(rapidjson::Value& value, rapidjson::Document& root) const;
+ rapidjson::Value Write(rapidjson::Document& root) const;
+
+ auto operator<=>(const Uid&) const = default;
+};
+
+template <>
+struct std::hash<Uid> {
+ size_t operator()(const Uid& uid) const {
+ size_t hash = 0;
+ Utils::HashCombine(hash, uid.upper);
+ Utils::HashCombine(hash, uid.lower);
+ return hash;
+ }
+};
diff --git a/source/10-common/Utils.cpp b/source/10-common/Utils.cpp
new file mode 100644
index 0000000..f0ff76d
--- /dev/null
+++ b/source/10-common/Utils.cpp
@@ -0,0 +1,130 @@
+#include "Utils.hpp"
+
+#include "Macros.hpp"
+#include "ScopeGuard.hpp"
+
+#ifdef _WIN32
+# include <Windows.h>
+#endif
+
+namespace fs = std::filesystem;
+
+#ifdef _WIN32
+# define BRUSSEL_MODE_STRING(string) L##string
+#else
+# define BRUSSEL_MODE_STRING(string) string
+#endif
+
+#if _WIN32
+using FopenModeString = const wchar_t*;
+#else
+using FopenModeString = const char*;
+#endif
+
+static FopenModeString GetModeString(Utils::IoMode mode, bool binary) {
+ using namespace Utils;
+ if (binary) {
+ switch (mode) {
+ case Read: return BRUSSEL_MODE_STRING("rb");
+ case WriteTruncate: return BRUSSEL_MODE_STRING("wb");
+ case WriteAppend: return BRUSSEL_MODE_STRING("ab");
+ }
+ } else {
+ switch (mode) {
+ case Read: return BRUSSEL_MODE_STRING("r");
+ case WriteTruncate: return BRUSSEL_MODE_STRING("w");
+ case WriteAppend: return BRUSSEL_MODE_STRING("a");
+ }
+ }
+ return nullptr;
+}
+
+FILE* Utils::OpenCstdioFile(const fs::path& path, IoMode mode, bool binary) {
+#ifdef _WIN32
+ // fs::path::c_str() returns `const wchar_t*` under Windows, because NT uses UTF-16 natively
+ // NOTE: _wfopen() only affects the type of path parameter, otherwise the file stream created is identical to the one by fopen()
+ return _wfopen(path.c_str(), ::GetModeString(mode, binary));
+#else
+ return fopen(path.c_str(), ::GetModeString(mode, binary));
+#endif
+}
+
+FILE* Utils::OpenCstdioFile(const char* path, IoMode mode, bool binary) {
+#ifdef _WIN32
+ // On Windows, fopen() accepts ANSI codepage encoded path, convert our UTF-8 string to UTF-16 to ensure that no matter what the locale is, the path continues to work
+ WCHAR platformPath[MAX_PATH];
+ if (MultiByteToWideChar(CP_UTF8, 0, path, -1, platformPath, MAX_PATH) == 0) {
+ return nullptr;
+ }
+ return _wfopen(platformPath, ::GetModeString(mode, binary));
+#else
+ return fopen(path, ::GetModeString(mode, binary));
+#endif
+}
+
+std::string Utils::ReadFileAsString(const fs::path& path) {
+ auto file = Utils::OpenCstdioFile(path, Utils::Read);
+ if (!file) throw std::runtime_error("Failed to open source file.");
+ DEFER { fclose(file); };
+
+ fseek(file, 0, SEEK_END);
+ auto fileSize = ftell(file);
+ rewind(file);
+
+ std::string result(fileSize, '\0');
+ fread(result.data(), fileSize, 1, file);
+
+ return result;
+}
+
+bool Utils::ReadCstdioLine(FILE* file, std::string& buffer) {
+ buffer.clear();
+ while (true) {
+ int c = fgetc(file);
+ if (c == EOF) {
+ if (buffer.empty() || buffer.back() != '\n') {
+ buffer += '\n';
+ }
+ return false;
+ } else if (c == '\n') {
+ buffer += '\n';
+ return true;
+ } else {
+ buffer += c;
+ }
+ }
+}
+
+bool Utils::ReadCstdioLine(FILE* file, char* buffer, size_t bufferSize, size_t* outLineLength) {
+ // TODO
+ assert(false && "Unimplemented");
+}
+
+bool Utils::InRangeInclusive(int n, int lower, int upper) {
+ if (lower > upper) {
+ std::swap(lower, upper);
+ }
+ return n >= lower && n <= upper;
+}
+
+bool Utils::LineContains(glm::ivec2 p1, glm::ivec2 p2, glm::ivec2 candidate) {
+ bool verticalLine = p1.x == p2.x && InRangeInclusive(candidate.x, p1.x, p2.x);
+ bool horizontalLine = p1.y == p2.y && InRangeInclusive(candidate.y, p1.y, p2.y);
+ return verticalLine && horizontalLine;
+}
+
+bool Utils::IsColinear(glm::ivec2 p1, glm::ivec2 p2) {
+ return p1.x == p2.x || p1.y == p2.y;
+}
+
+std::string Utils::MakeRandomNumberedName(const char* tag) {
+ int n = std::rand();
+#define RNG_NAME_PATTERN "Unnamed %s #%d", tag, n
+ // NOTE: does not include null-terminator
+ int size = snprintf(nullptr, 0, RNG_NAME_PATTERN);
+ std::string result;
+ result.resize(size); // std::string::resize handles storage for null-terminator alreaedy
+ snprintf(result.data(), size, RNG_NAME_PATTERN);
+#undef RNG_NAME_PATTERN
+ return result;
+}
diff --git a/source/10-common/Utils.hpp b/source/10-common/Utils.hpp
new file mode 100644
index 0000000..668261b
--- /dev/null
+++ b/source/10-common/Utils.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <robin_hood.h>
+#include <cstdio>
+#include <cstring>
+#include <filesystem>
+#include <glm/glm.hpp>
+#include <string>
+#include <string_view>
+
+namespace Utils {
+
+enum IoMode {
+ Read,
+ WriteTruncate,
+ WriteAppend,
+};
+
+FILE* OpenCstdioFile(const std::filesystem::path& path, IoMode mode, bool binary = false);
+FILE* OpenCstdioFile(const char* path, IoMode mode, bool binary = false);
+
+/// Retrieve a whole line (marked by `\n` or EOF) into the buffer. If the line ends with EOF, two things happen:
+/// 1. a `\n` character is appended to the line content, emulating as-if the line ended with `\n`.
+/// 2. `false` is returned
+/// Otherwise, `true` is returned.
+///
+/// Empty lines are not skipped at all, including the very last empty line if it exists.
+bool ReadCstdioLine(FILE* file, std::string& buffer);
+/// Same as the other overload, except working with a fixed-size buffer.
+/// NOTE: this also gives the length of the line compared to `std::fgets`.
+/// `std::fgets` requires us to run `std::strlen` on the output again to find the length
+bool ReadCstdioLine(FILE* file, char* buffer, size_t bufferSize, size_t* outLineLength = nullptr);
+
+std::string ReadFileAsString(const std::filesystem::path& path);
+
+constexpr float Abs(float v) noexcept {
+ return v < 0.0f ? -v : v;
+}
+
+bool InRangeInclusive(int n, int lower, int upper);
+bool LineContains(glm::ivec2 p1, glm::ivec2 p2, glm::ivec2 candidate);
+
+bool IsColinear(glm::ivec2 p1, glm::ivec2 p2);
+
+template <typename T>
+void HashCombine(std::size_t& seed, const T& v) {
+ seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+}
+
+std::string MakeRandomNumberedName(const char* tag);
+
+} // namespace Utils
+
+struct StringHash {
+ using is_transparent = void;
+
+ std::size_t operator()(const std::string& key) const { return robin_hood::hash_bytes(key.c_str(), key.size()); }
+ std::size_t operator()(std::string_view key) const { return robin_hood::hash_bytes(key.data(), key.size()); }
+ std::size_t operator()(const char* key) const { return robin_hood::hash_bytes(key, std::strlen(key)); }
+};
+
+struct StringEqual {
+ using is_transparent = int;
+
+ bool operator()(std::string_view lhs, const std::string& rhs) const {
+ const std::string_view view = rhs;
+ return lhs == view;
+ }
+
+ bool operator()(const char* lhs, const std::string& rhs) const {
+ return std::strcmp(lhs, rhs.c_str()) == 0;
+ }
+
+ bool operator()(const std::string& lhs, const std::string& rhs) const {
+ return lhs == rhs;
+ }
+};
diff --git a/source/10-common/YCombinator.hpp b/source/10-common/YCombinator.hpp
new file mode 100644
index 0000000..2da06c8
--- /dev/null
+++ b/source/10-common/YCombinator.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+template <typename Func>
+struct YCombinator {
+ // NOTE: implicit constructor allows initializing this
+ Func func;
+
+ template <typename... Ts>
+ decltype(auto) operator()(Ts&&... args) const {
+ // NOTE: static_cast<Ts>(args)... is equivalent to std::forward<Ts>(args)...
+ // written this way so that we don't have to include <utility>, as well as reducing template instanciations to help compile time
+ return func(*this, static_cast<Ts>(args)...);
+ }
+};
diff --git a/source/10-editor-common/ImGuiGuizmo.cpp b/source/10-editor-common/ImGuiGuizmo.cpp
new file mode 100644
index 0000000..3786076
--- /dev/null
+++ b/source/10-editor-common/ImGuiGuizmo.cpp
@@ -0,0 +1,2897 @@
+// https://github.com/CedricGuillemet/ImGuizmo
+// v 1.84 WIP
+//
+// The MIT License(MIT)
+//
+// Copyright(c) 2021 Cedric Guillemet
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files(the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+#ifndef IMGUI_DEFINE_MATH_OPERATORS
+# define IMGUI_DEFINE_MATH_OPERATORS
+#endif
+#include "ImGuiGuizmo.hpp"
+#include "imgui_internal.h"
+
+#if defined(_MSC_VER) || defined(__MINGW32__)
+# include <malloc.h>
+#endif
+#if !defined(_MSC_VER) && !defined(__MINGW64_VERSION_MAJOR)
+# define _malloca(x) alloca(x)
+# define _freea(x)
+#endif
+
+// includes patches for multiview from
+// https://github.com/CedricGuillemet/ImGuizmo/issues/15
+
+namespace IMGUIZMO_NAMESPACE {
+static const float ZPI = 3.14159265358979323846f;
+static const float RAD2DEG = (180.f / ZPI);
+static const float DEG2RAD = (ZPI / 180.f);
+const float screenRotateSize = 0.06f;
+// scale a bit so translate axis do not touch when in universal
+const float rotationDisplayFactor = 1.2f;
+
+static OPERATION operator&(OPERATION lhs, OPERATION rhs) {
+ return static_cast<OPERATION>(static_cast<int>(lhs) & static_cast<int>(rhs));
+}
+
+static bool operator!=(OPERATION lhs, int rhs) {
+ return static_cast<int>(lhs) != rhs;
+}
+
+static bool Intersects(OPERATION lhs, OPERATION rhs) {
+ return (lhs & rhs) != 0;
+}
+
+// True if lhs contains rhs
+static bool Contains(OPERATION lhs, OPERATION rhs) {
+ return (lhs & rhs) == rhs;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// utility and math
+
+void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) {
+ r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12];
+ r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13];
+ r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14];
+ r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15];
+
+ r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12];
+ r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13];
+ r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14];
+ r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15];
+
+ r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12];
+ r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13];
+ r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14];
+ r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15];
+
+ r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12];
+ r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13];
+ r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14];
+ r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15];
+}
+
+void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) {
+ float temp, temp2, temp3, temp4;
+ temp = 2.0f * znear;
+ temp2 = right - left;
+ temp3 = top - bottom;
+ temp4 = zfar - znear;
+ m16[0] = temp / temp2;
+ m16[1] = 0.0;
+ m16[2] = 0.0;
+ m16[3] = 0.0;
+ m16[4] = 0.0;
+ m16[5] = temp / temp3;
+ m16[6] = 0.0;
+ m16[7] = 0.0;
+ m16[8] = (right + left) / temp2;
+ m16[9] = (top + bottom) / temp3;
+ m16[10] = (-zfar - znear) / temp4;
+ m16[11] = -1.0f;
+ m16[12] = 0.0;
+ m16[13] = 0.0;
+ m16[14] = (-temp * zfar) / temp4;
+ m16[15] = 0.0;
+}
+
+void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) {
+ float ymax, xmax;
+ ymax = znear * tanf(fovyInDegrees * DEG2RAD);
+ xmax = ymax * aspectRatio;
+ Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16);
+}
+
+void Cross(const float* a, const float* b, float* r) {
+ r[0] = a[1] * b[2] - a[2] * b[1];
+ r[1] = a[2] * b[0] - a[0] * b[2];
+ r[2] = a[0] * b[1] - a[1] * b[0];
+}
+
+float Dot(const float* a, const float* b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+}
+
+void Normalize(const float* a, float* r) {
+ float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON);
+ r[0] = a[0] * il;
+ r[1] = a[1] * il;
+ r[2] = a[2] * il;
+}
+
+void LookAt(const float* eye, const float* at, const float* up, float* m16) {
+ float X[3], Y[3], Z[3], tmp[3];
+
+ tmp[0] = eye[0] - at[0];
+ tmp[1] = eye[1] - at[1];
+ tmp[2] = eye[2] - at[2];
+ Normalize(tmp, Z);
+ Normalize(up, Y);
+ Cross(Y, Z, tmp);
+ Normalize(tmp, X);
+ Cross(Z, X, tmp);
+ Normalize(tmp, Y);
+
+ m16[0] = X[0];
+ m16[1] = Y[0];
+ m16[2] = Z[0];
+ m16[3] = 0.0f;
+ m16[4] = X[1];
+ m16[5] = Y[1];
+ m16[6] = Z[1];
+ m16[7] = 0.0f;
+ m16[8] = X[2];
+ m16[9] = Y[2];
+ m16[10] = Z[2];
+ m16[11] = 0.0f;
+ m16[12] = -Dot(X, eye);
+ m16[13] = -Dot(Y, eye);
+ m16[14] = -Dot(Z, eye);
+ m16[15] = 1.0f;
+}
+
+template <typename T>
+T Clamp(T x, T y, T z) {
+ return ((x < y) ? y : ((x > z) ? z : x));
+}
+template <typename T>
+T max(T x, T y) {
+ return (x > y) ? x : y;
+}
+template <typename T>
+T min(T x, T y) {
+ return (x < y) ? x : y;
+}
+template <typename T>
+bool IsWithin(T x, T y, T z) {
+ return (x >= y) && (x <= z);
+}
+
+struct matrix_t;
+struct vec_t {
+public:
+ float x, y, z, w;
+
+ void Lerp(const vec_t& v, float t) {
+ x += (v.x - x) * t;
+ y += (v.y - y) * t;
+ z += (v.z - z) * t;
+ w += (v.w - w) * t;
+ }
+
+ void Set(float v) { x = y = z = w = v; }
+ void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) {
+ x = _x;
+ y = _y;
+ z = _z;
+ w = _w;
+ }
+
+ vec_t& operator-=(const vec_t& v) {
+ x -= v.x;
+ y -= v.y;
+ z -= v.z;
+ w -= v.w;
+ return *this;
+ }
+ vec_t& operator+=(const vec_t& v) {
+ x += v.x;
+ y += v.y;
+ z += v.z;
+ w += v.w;
+ return *this;
+ }
+ vec_t& operator*=(const vec_t& v) {
+ x *= v.x;
+ y *= v.y;
+ z *= v.z;
+ w *= v.w;
+ return *this;
+ }
+ vec_t& operator*=(float v) {
+ x *= v;
+ y *= v;
+ z *= v;
+ w *= v;
+ return *this;
+ }
+
+ vec_t operator*(float f) const;
+ vec_t operator-() const;
+ vec_t operator-(const vec_t& v) const;
+ vec_t operator+(const vec_t& v) const;
+ vec_t operator*(const vec_t& v) const;
+
+ const vec_t& operator+() const { return (*this); }
+ float Length() const { return sqrtf(x * x + y * y + z * z); };
+ float LengthSq() const { return (x * x + y * y + z * z); };
+ vec_t Normalize() {
+ (*this) *= (1.f / (Length() > FLT_EPSILON ? Length() : FLT_EPSILON));
+ return (*this);
+ }
+ vec_t Normalize(const vec_t& v) {
+ this->Set(v.x, v.y, v.z, v.w);
+ this->Normalize();
+ return (*this);
+ }
+ vec_t Abs() const;
+
+ void Cross(const vec_t& v) {
+ vec_t res;
+ res.x = y * v.z - z * v.y;
+ res.y = z * v.x - x * v.z;
+ res.z = x * v.y - y * v.x;
+
+ x = res.x;
+ y = res.y;
+ z = res.z;
+ w = 0.f;
+ }
+
+ void Cross(const vec_t& v1, const vec_t& v2) {
+ x = v1.y * v2.z - v1.z * v2.y;
+ y = v1.z * v2.x - v1.x * v2.z;
+ z = v1.x * v2.y - v1.y * v2.x;
+ w = 0.f;
+ }
+
+ float Dot(const vec_t& v) const {
+ return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w);
+ }
+
+ float Dot3(const vec_t& v) const {
+ return (x * v.x) + (y * v.y) + (z * v.z);
+ }
+
+ void Transform(const matrix_t& matrix);
+ void Transform(const vec_t& s, const matrix_t& matrix);
+
+ void TransformVector(const matrix_t& matrix);
+ void TransformPoint(const matrix_t& matrix);
+ void TransformVector(const vec_t& v, const matrix_t& matrix) {
+ (*this) = v;
+ this->TransformVector(matrix);
+ }
+ void TransformPoint(const vec_t& v, const matrix_t& matrix) {
+ (*this) = v;
+ this->TransformPoint(matrix);
+ }
+
+ float& operator[](size_t index) { return ((float*)&x)[index]; }
+ const float& operator[](size_t index) const { return ((float*)&x)[index]; }
+ bool operator!=(const vec_t& other) const { return memcmp(this, &other, sizeof(vec_t)); }
+};
+
+vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) {
+ vec_t res;
+ res.x = _x;
+ res.y = _y;
+ res.z = _z;
+ res.w = _w;
+ return res;
+}
+vec_t makeVect(ImVec2 v) {
+ vec_t res;
+ res.x = v.x;
+ res.y = v.y;
+ res.z = 0.f;
+ res.w = 0.f;
+ return res;
+}
+vec_t vec_t::operator*(float f) const {
+ return makeVect(x * f, y * f, z * f, w * f);
+}
+vec_t vec_t::operator-() const {
+ return makeVect(-x, -y, -z, -w);
+}
+vec_t vec_t::operator-(const vec_t& v) const {
+ return makeVect(x - v.x, y - v.y, z - v.z, w - v.w);
+}
+vec_t vec_t::operator+(const vec_t& v) const {
+ return makeVect(x + v.x, y + v.y, z + v.z, w + v.w);
+}
+vec_t vec_t::operator*(const vec_t& v) const {
+ return makeVect(x * v.x, y * v.y, z * v.z, w * v.w);
+}
+vec_t vec_t::Abs() const {
+ return makeVect(fabsf(x), fabsf(y), fabsf(z));
+}
+
+vec_t Normalized(const vec_t& v) {
+ vec_t res;
+ res = v;
+ res.Normalize();
+ return res;
+}
+vec_t Cross(const vec_t& v1, const vec_t& v2) {
+ vec_t res;
+ res.x = v1.y * v2.z - v1.z * v2.y;
+ res.y = v1.z * v2.x - v1.x * v2.z;
+ res.z = v1.x * v2.y - v1.y * v2.x;
+ res.w = 0.f;
+ return res;
+}
+
+float Dot(const vec_t& v1, const vec_t& v2) {
+ return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z);
+}
+
+vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) {
+ vec_t normal, res;
+ normal.Normalize(p_normal);
+ res.w = normal.Dot(p_point1);
+ res.x = normal.x;
+ res.y = normal.y;
+ res.z = normal.z;
+ return res;
+}
+
+struct matrix_t {
+public:
+ union {
+ float m[4][4];
+ float m16[16];
+ struct
+ {
+ vec_t right, up, dir, position;
+ } v;
+ vec_t component[4];
+ };
+
+ operator float*() { return m16; }
+ operator const float*() const { return m16; }
+ void Translation(float _x, float _y, float _z) { this->Translation(makeVect(_x, _y, _z)); }
+
+ void Translation(const vec_t& vt) {
+ v.right.Set(1.f, 0.f, 0.f, 0.f);
+ v.up.Set(0.f, 1.f, 0.f, 0.f);
+ v.dir.Set(0.f, 0.f, 1.f, 0.f);
+ v.position.Set(vt.x, vt.y, vt.z, 1.f);
+ }
+
+ void Scale(float _x, float _y, float _z) {
+ v.right.Set(_x, 0.f, 0.f, 0.f);
+ v.up.Set(0.f, _y, 0.f, 0.f);
+ v.dir.Set(0.f, 0.f, _z, 0.f);
+ v.position.Set(0.f, 0.f, 0.f, 1.f);
+ }
+ void Scale(const vec_t& s) { Scale(s.x, s.y, s.z); }
+
+ matrix_t& operator*=(const matrix_t& mat) {
+ matrix_t tmpMat;
+ tmpMat = *this;
+ tmpMat.Multiply(mat);
+ *this = tmpMat;
+ return *this;
+ }
+ matrix_t operator*(const matrix_t& mat) const {
+ matrix_t matT;
+ matT.Multiply(*this, mat);
+ return matT;
+ }
+
+ void Multiply(const matrix_t& matrix) {
+ matrix_t tmp;
+ tmp = *this;
+
+ FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this);
+ }
+
+ void Multiply(const matrix_t& m1, const matrix_t& m2) {
+ FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this);
+ }
+
+ float GetDeterminant() const {
+ return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] -
+ m[0][2] * m[1][1] * m[2][0] - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1];
+ }
+
+ float Inverse(const matrix_t& srcMatrix, bool affine = false);
+ void SetToIdentity() {
+ v.right.Set(1.f, 0.f, 0.f, 0.f);
+ v.up.Set(0.f, 1.f, 0.f, 0.f);
+ v.dir.Set(0.f, 0.f, 1.f, 0.f);
+ v.position.Set(0.f, 0.f, 0.f, 1.f);
+ }
+ void Transpose() {
+ matrix_t tmpm;
+ for (int l = 0; l < 4; l++)
+ {
+ for (int c = 0; c < 4; c++)
+ {
+ tmpm.m[l][c] = m[c][l];
+ }
+ }
+ (*this) = tmpm;
+ }
+
+ void RotationAxis(const vec_t& axis, float angle);
+
+ void OrthoNormalize() {
+ v.right.Normalize();
+ v.up.Normalize();
+ v.dir.Normalize();
+ }
+};
+
+void vec_t::Transform(const matrix_t& matrix) {
+ vec_t out;
+
+ out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0];
+ out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1];
+ out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2];
+ out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3];
+
+ x = out.x;
+ y = out.y;
+ z = out.z;
+ w = out.w;
+}
+
+void vec_t::Transform(const vec_t& s, const matrix_t& matrix) {
+ *this = s;
+ Transform(matrix);
+}
+
+void vec_t::TransformPoint(const matrix_t& matrix) {
+ vec_t out;
+
+ out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0];
+ out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1];
+ out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2];
+ out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3];
+
+ x = out.x;
+ y = out.y;
+ z = out.z;
+ w = out.w;
+}
+
+void vec_t::TransformVector(const matrix_t& matrix) {
+ vec_t out;
+
+ out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0];
+ out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1];
+ out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2];
+ out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3];
+
+ x = out.x;
+ y = out.y;
+ z = out.z;
+ w = out.w;
+}
+
+float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) {
+ float det = 0;
+
+ if (affine)
+ {
+ det = GetDeterminant();
+ float s = 1 / det;
+ m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s;
+ m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s;
+ m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s;
+ m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s;
+ m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s;
+ m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s;
+ m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s;
+ m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s;
+ m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s;
+ m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]);
+ m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]);
+ m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]);
+ } else
+ {
+ // transpose matrix
+ float src[16];
+ for (int i = 0; i < 4; ++i)
+ {
+ src[i] = srcMatrix.m16[i * 4];
+ src[i + 4] = srcMatrix.m16[i * 4 + 1];
+ src[i + 8] = srcMatrix.m16[i * 4 + 2];
+ src[i + 12] = srcMatrix.m16[i * 4 + 3];
+ }
+
+ // calculate pairs for first 8 elements (cofactors)
+ float tmp[12]; // temp array for pairs
+ tmp[0] = src[10] * src[15];
+ tmp[1] = src[11] * src[14];
+ tmp[2] = src[9] * src[15];
+ tmp[3] = src[11] * src[13];
+ tmp[4] = src[9] * src[14];
+ tmp[5] = src[10] * src[13];
+ tmp[6] = src[8] * src[15];
+ tmp[7] = src[11] * src[12];
+ tmp[8] = src[8] * src[14];
+ tmp[9] = src[10] * src[12];
+ tmp[10] = src[8] * src[13];
+ tmp[11] = src[9] * src[12];
+
+ // calculate first 8 elements (cofactors)
+ m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]);
+ m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]);
+ m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]);
+ m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]);
+ m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]);
+ m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]);
+ m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]);
+ m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]);
+
+ // calculate pairs for second 8 elements (cofactors)
+ tmp[0] = src[2] * src[7];
+ tmp[1] = src[3] * src[6];
+ tmp[2] = src[1] * src[7];
+ tmp[3] = src[3] * src[5];
+ tmp[4] = src[1] * src[6];
+ tmp[5] = src[2] * src[5];
+ tmp[6] = src[0] * src[7];
+ tmp[7] = src[3] * src[4];
+ tmp[8] = src[0] * src[6];
+ tmp[9] = src[2] * src[4];
+ tmp[10] = src[0] * src[5];
+ tmp[11] = src[1] * src[4];
+
+ // calculate second 8 elements (cofactors)
+ m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]);
+ m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]);
+ m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]);
+ m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]);
+ m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]);
+ m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]);
+ m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]);
+ m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]);
+
+ // calculate determinant
+ det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3];
+
+ // calculate matrix inverse
+ float invdet = 1 / det;
+ for (int j = 0; j < 16; ++j)
+ {
+ m16[j] *= invdet;
+ }
+ }
+
+ return det;
+}
+
+void matrix_t::RotationAxis(const vec_t& axis, float angle) {
+ float length2 = axis.LengthSq();
+ if (length2 < FLT_EPSILON)
+ {
+ SetToIdentity();
+ return;
+ }
+
+ vec_t n = axis * (1.f / sqrtf(length2));
+ float s = sinf(angle);
+ float c = cosf(angle);
+ float k = 1.f - c;
+
+ float xx = n.x * n.x * k + c;
+ float yy = n.y * n.y * k + c;
+ float zz = n.z * n.z * k + c;
+ float xy = n.x * n.y * k;
+ float yz = n.y * n.z * k;
+ float zx = n.z * n.x * k;
+ float xs = n.x * s;
+ float ys = n.y * s;
+ float zs = n.z * s;
+
+ m[0][0] = xx;
+ m[0][1] = xy + zs;
+ m[0][2] = zx - ys;
+ m[0][3] = 0.f;
+ m[1][0] = xy - zs;
+ m[1][1] = yy;
+ m[1][2] = yz + xs;
+ m[1][3] = 0.f;
+ m[2][0] = zx + ys;
+ m[2][1] = yz - xs;
+ m[2][2] = zz;
+ m[2][3] = 0.f;
+ m[3][0] = 0.f;
+ m[3][1] = 0.f;
+ m[3][2] = 0.f;
+ m[3][3] = 1.f;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+
+enum MOVETYPE {
+ MT_NONE,
+ MT_MOVE_X,
+ MT_MOVE_Y,
+ MT_MOVE_Z,
+ MT_MOVE_YZ,
+ MT_MOVE_ZX,
+ MT_MOVE_XY,
+ MT_MOVE_SCREEN,
+ MT_ROTATE_X,
+ MT_ROTATE_Y,
+ MT_ROTATE_Z,
+ MT_ROTATE_SCREEN,
+ MT_SCALE_X,
+ MT_SCALE_Y,
+ MT_SCALE_Z,
+ MT_SCALE_XYZ
+};
+
+static bool IsTranslateType(int type) {
+ return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN;
+}
+
+static bool IsRotateType(int type) {
+ return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN;
+}
+
+static bool IsScaleType(int type) {
+ return type >= MT_SCALE_X && type <= MT_SCALE_XYZ;
+}
+
+// Matches MT_MOVE_AB order
+static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y };
+
+struct Context {
+ Context()
+ : mbUsing(false), mbEnable(true), mbUsingBounds(false) {
+ }
+
+ ImDrawList* mDrawList;
+
+ MODE mMode;
+ matrix_t mViewMat;
+ matrix_t mProjectionMat;
+ matrix_t mModel;
+ matrix_t mModelLocal; // orthonormalized model
+ matrix_t mModelInverse;
+ matrix_t mModelSource;
+ matrix_t mModelSourceInverse;
+ matrix_t mMVP;
+ matrix_t mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition
+ matrix_t mViewProjection;
+
+ vec_t mModelScaleOrigin;
+ vec_t mCameraEye;
+ vec_t mCameraRight;
+ vec_t mCameraDir;
+ vec_t mCameraUp;
+ vec_t mRayOrigin;
+ vec_t mRayVector;
+
+ float mRadiusSquareCenter;
+ ImVec2 mScreenSquareCenter;
+ ImVec2 mScreenSquareMin;
+ ImVec2 mScreenSquareMax;
+
+ float mScreenFactor;
+ vec_t mRelativeOrigin;
+
+ bool mbUsing;
+ bool mbEnable;
+ bool mbMouseOver;
+ bool mReversed; // reversed projection matrix
+
+ // translation
+ vec_t mTranslationPlan;
+ vec_t mTranslationPlanOrigin;
+ vec_t mMatrixOrigin;
+ vec_t mTranslationLastDelta;
+
+ // rotation
+ vec_t mRotationVectorSource;
+ float mRotationAngle;
+ float mRotationAngleOrigin;
+ // vec_t mWorldToLocalAxis;
+
+ // scale
+ vec_t mScale;
+ vec_t mScaleValueOrigin;
+ vec_t mScaleLast;
+ float mSaveMousePosx;
+
+ // save axis factor when using gizmo
+ bool mBelowAxisLimit[3];
+ bool mBelowPlaneLimit[3];
+ float mAxisFactor[3];
+
+ // bounds stretching
+ // NOTE: these variable only lives during the duration of a drag
+ /// Position in world space, of the knob on the opposite side of the knob being dragged.
+ /// This is the point that needs to space regardless of where anchor is placed.
+ vec_t mBoundsPivot;
+ /// Position in world space, of the knob begin dragged.
+ /// This is the point that's being moved.
+ vec_t mBoundsAnchor;
+ vec_t mBoundsPlan;
+ /// Position in local space, of the knob on the opposite side of the knob being dragged
+ vec_t mBoundsLocalPivot;
+ int mBoundsBestAxis;
+ /// The axes that are being modified by the current operation. May contain 1 or 2 elements.
+ /// Unused elements are filled with -1 during the operation.
+ int mBoundsAxis[2];
+ /// The index of the corner that pivot data is fetched from (opposite side from anchor).
+ int mBoundsPivotCornerIndex;
+ bool mbUsingBounds;
+ bool mbIsUsingBigAnchor;
+ /// Model matrix passed into ImGuizmo::Manipulate()
+ matrix_t mBoundsMatrix;
+
+ //
+ int mCurrentOperation;
+
+ float mX = 0.f;
+ float mY = 0.f;
+ float mWidth = 0.f;
+ float mHeight = 0.f;
+ float mXMax = 0.f;
+ float mYMax = 0.f;
+ float mDisplayRatio = 1.f;
+
+ bool mIsOrthographic = false;
+
+ int mActualID = -1;
+ int mEditingID = -1;
+ OPERATION mOperation = OPERATION(-1);
+
+ bool mAllowAxisFlip = true;
+ float mGizmoSizeClipSpace = 0.1f;
+};
+
+static Context gContext;
+
+static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) };
+static const ImU32 directionColor[3] = { IM_COL32(0xAA, 0, 0, 0xFF), IM_COL32(0, 0xAA, 0, 0xFF), IM_COL32(0, 0, 0xAA, 0XFF) };
+
+// Alpha: 100%: FF, 87%: DE, 70%: B3, 54%: 8A, 50%: 80, 38%: 61, 12%: 1F
+static const ImU32 planeColor[3] = { IM_COL32(0xAA, 0, 0, 0x61), IM_COL32(0, 0xAA, 0, 0x61), IM_COL32(0, 0, 0xAA, 0x61) };
+static const ImU32 selectionColor = IM_COL32(0xFF, 0x80, 0x10, 0x8A);
+static const ImU32 inactiveColor = IM_COL32(0x99, 0x99, 0x99, 0x99);
+static const ImU32 translationLineColor = IM_COL32(0xAA, 0xAA, 0xAA, 0xAA);
+static const char* translationInfoMask[] = { "X : %5.3f", "Y : %5.3f", "Z : %5.3f", "Y : %5.3f Z : %5.3f", "X : %5.3f Z : %5.3f", "X : %5.3f Y : %5.3f", "X : %5.3f Y : %5.3f Z : %5.3f" };
+static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" };
+static const char* rotationInfoMask[] = { "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" };
+static const int translationInfoIndex[] = { 0, 0, 0, 1, 0, 0, 2, 0, 0, 1, 2, 0, 0, 2, 0, 0, 1, 0, 0, 1, 2 };
+static const float quadMin = 0.5f;
+static const float quadMax = 0.8f;
+static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin };
+static const int halfCircleSegmentCount = 64;
+static const float snapTension = 0.5f;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion);
+static int GetRotateType(OPERATION op);
+static int GetScaleType(OPERATION op);
+
+static ImVec2 worldToPos(const vec_t& worldPos, const matrix_t& mat, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) {
+ vec_t trans;
+ trans.TransformPoint(worldPos, mat);
+ trans *= 0.5f / trans.w;
+ trans += makeVect(0.5f, 0.5f);
+ trans.y = 1.f - trans.y;
+ trans.x *= size.x;
+ trans.y *= size.y;
+ trans.x += position.x;
+ trans.y += position.y;
+ return ImVec2(trans.x, trans.y);
+}
+
+static void ComputeCameraRay(vec_t& rayOrigin, vec_t& rayDir, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) {
+ ImGuiIO& io = ImGui::GetIO();
+
+ matrix_t mViewProjInverse;
+ mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat);
+
+ const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f;
+ const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f;
+
+ const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f;
+ const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON);
+
+ rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse);
+ rayOrigin *= 1.f / rayOrigin.w;
+ vec_t rayEnd;
+ rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse);
+ rayEnd *= 1.f / rayEnd.w;
+ rayDir = Normalized(rayEnd - rayOrigin);
+}
+
+static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) {
+ vec_t startOfSegment = start;
+ const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP;
+ startOfSegment.TransformPoint(mvp);
+ if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction
+ {
+ startOfSegment *= 1.f / startOfSegment.w;
+ }
+
+ vec_t endOfSegment = end;
+ endOfSegment.TransformPoint(mvp);
+ if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction
+ {
+ endOfSegment *= 1.f / endOfSegment.w;
+ }
+
+ vec_t clipSpaceAxis = endOfSegment - startOfSegment;
+ clipSpaceAxis.y /= gContext.mDisplayRatio;
+ float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y);
+ return segmentLengthInClipSpace;
+}
+
+static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) {
+ vec_t pts[] = { ptO, ptA, ptB };
+ for (unsigned int i = 0; i < 3; i++)
+ {
+ pts[i].TransformPoint(gContext.mMVP);
+ if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction
+ {
+ pts[i] *= 1.f / pts[i].w;
+ }
+ }
+ vec_t segA = pts[1] - pts[0];
+ vec_t segB = pts[2] - pts[0];
+ segA.y /= gContext.mDisplayRatio;
+ segB.y /= gContext.mDisplayRatio;
+ vec_t segAOrtho = makeVect(-segA.y, segA.x);
+ segAOrtho.Normalize();
+ float dt = segAOrtho.Dot3(segB);
+ float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt);
+ return surface;
+}
+
+inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) {
+ vec_t c = point - vertPos1;
+ vec_t V;
+
+ V.Normalize(vertPos2 - vertPos1);
+ float d = (vertPos2 - vertPos1).Length();
+ float t = V.Dot3(c);
+
+ if (t < 0.f)
+ {
+ return vertPos1;
+ }
+
+ if (t > d)
+ {
+ return vertPos2;
+ }
+
+ return vertPos1 + V * t;
+}
+
+static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) {
+ const float numer = plan.Dot3(rOrigin) - plan.w;
+ const float denom = plan.Dot3(rVector);
+
+ if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect
+ {
+ return -1.0f;
+ }
+
+ return -(numer / denom);
+}
+
+static float DistanceToPlane(const vec_t& point, const vec_t& plan) {
+ return plan.Dot3(point) + plan.w;
+}
+
+static bool IsInContextRect(ImVec2 p) {
+ return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax);
+}
+
+static bool IsHoveringWindow() {
+ ImGuiContext& g = *ImGui::GetCurrentContext();
+ ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName);
+ if (g.HoveredWindow == window) // Mouse hovering drawlist window
+ return true;
+ if (g.HoveredWindow != NULL) // Any other window is hovered
+ return false;
+ if (ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max, false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows)
+ return true;
+ return false;
+}
+
+void SetRect(float x, float y, float width, float height) {
+ gContext.mX = x;
+ gContext.mY = y;
+ gContext.mWidth = width;
+ gContext.mHeight = height;
+ gContext.mXMax = gContext.mX + gContext.mWidth;
+ gContext.mYMax = gContext.mY + gContext.mXMax;
+ gContext.mDisplayRatio = width / height;
+}
+
+void SetOrthographic(bool isOrthographic) {
+ gContext.mIsOrthographic = isOrthographic;
+}
+
+void SetDrawlist(ImDrawList* drawlist) {
+ gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList();
+}
+
+void SetImGuiContext(ImGuiContext* ctx) {
+ ImGui::SetCurrentContext(ctx);
+}
+
+void BeginFrame() {
+ const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus;
+
+#ifdef IMGUI_HAS_VIEWPORT
+ ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size);
+ ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos);
+#else
+ ImGuiIO& io = ImGui::GetIO();
+ ImGui::SetNextWindowSize(io.DisplaySize);
+ ImGui::SetNextWindowPos(ImVec2(0, 0));
+#endif
+
+ ImGui::PushStyleColor(ImGuiCol_WindowBg, 0);
+ ImGui::PushStyleColor(ImGuiCol_Border, 0);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+
+ ImGui::Begin("gizmo", NULL, flags);
+ gContext.mDrawList = ImGui::GetWindowDrawList();
+ ImGui::End();
+ ImGui::PopStyleVar();
+ ImGui::PopStyleColor(2);
+}
+
+bool IsUsing() {
+ return gContext.mbUsing || gContext.mbUsingBounds;
+}
+
+bool IsOver() {
+ return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) ||
+ (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) ||
+ (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing();
+}
+
+bool IsOver(OPERATION op) {
+ if (IsUsing())
+ {
+ return true;
+ }
+ if (Intersects(op, SCALE) && GetScaleType(op) != MT_NONE)
+ {
+ return true;
+ }
+ if (Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE)
+ {
+ return true;
+ }
+ if (Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE)
+ {
+ return true;
+ }
+ return false;
+}
+
+void Enable(bool enable) {
+ gContext.mbEnable = enable;
+ if (!enable)
+ {
+ gContext.mbUsing = false;
+ gContext.mbUsingBounds = false;
+ }
+}
+
+static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) {
+ gContext.mMode = mode;
+ gContext.mViewMat = *(matrix_t*)view;
+ gContext.mProjectionMat = *(matrix_t*)projection;
+ gContext.mbMouseOver = IsHoveringWindow();
+
+ gContext.mModelLocal = *(matrix_t*)matrix;
+ gContext.mModelLocal.OrthoNormalize();
+
+ if (mode == LOCAL)
+ {
+ gContext.mModel = gContext.mModelLocal;
+ } else
+ {
+ gContext.mModel.Translation(((matrix_t*)matrix)->v.position);
+ }
+ gContext.mModelSource = *(matrix_t*)matrix;
+ gContext.mModelScaleOrigin.Set(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length());
+
+ gContext.mModelInverse.Inverse(gContext.mModel);
+ gContext.mModelSourceInverse.Inverse(gContext.mModelSource);
+ gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat;
+ gContext.mMVP = gContext.mModel * gContext.mViewProjection;
+ gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection;
+
+ matrix_t viewInverse;
+ viewInverse.Inverse(gContext.mViewMat);
+ gContext.mCameraDir = viewInverse.v.dir;
+ gContext.mCameraEye = viewInverse.v.position;
+ gContext.mCameraRight = viewInverse.v.right;
+ gContext.mCameraUp = viewInverse.v.up;
+
+ // projection reverse
+ vec_t nearPos, farPos;
+ nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat);
+ farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat);
+
+ gContext.mReversed = (nearPos.z / nearPos.w) > (farPos.z / farPos.w);
+
+ // compute scale from the size of camera right vector projected on screen at the matrix position
+ vec_t pointRight = viewInverse.v.right;
+ pointRight.TransformPoint(gContext.mViewProjection);
+ gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / (pointRight.x / pointRight.w - gContext.mMVP.v.position.x / gContext.mMVP.v.position.w);
+
+ vec_t rightViewInverse = viewInverse.v.right;
+ rightViewInverse.TransformVector(gContext.mModelInverse);
+ float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse);
+ gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength;
+
+ ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP);
+ gContext.mScreenSquareCenter = centerSSpace;
+ gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f);
+ gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f);
+
+ ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector);
+}
+
+static void ComputeColors(ImU32* colors, int type, OPERATION operation) {
+ if (gContext.mbEnable)
+ {
+ switch (operation)
+ {
+ case TRANSLATE:
+ colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE;
+ for (int i = 0; i < 3; i++)
+ {
+ colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : directionColor[i];
+ colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : planeColor[i];
+ colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4];
+ }
+ break;
+ case ROTATE:
+ colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE;
+ for (int i = 0; i < 3; i++)
+ {
+ colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : directionColor[i];
+ }
+ break;
+ case SCALEU:
+ case SCALE:
+ colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE;
+ for (int i = 0; i < 3; i++)
+ {
+ colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : directionColor[i];
+ }
+ break;
+ // note: this internal function is only called with three possible values for operation
+ default:
+ break;
+ }
+ } else
+ {
+ for (int i = 0; i < 7; i++)
+ {
+ colors[i] = inactiveColor;
+ }
+ }
+}
+
+static void ComputeTripodAxisAndVisibility(const int axisIndex, vec_t& dirAxis, vec_t& dirPlaneX, vec_t& dirPlaneY, bool& belowAxisLimit, bool& belowPlaneLimit, const bool localCoordinates = false) {
+ dirAxis = directionUnary[axisIndex];
+ dirPlaneX = directionUnary[(axisIndex + 1) % 3];
+ dirPlaneY = directionUnary[(axisIndex + 2) % 3];
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ // when using, use stored factors so the gizmo doesn't flip when we translate
+ belowAxisLimit = gContext.mBelowAxisLimit[axisIndex];
+ belowPlaneLimit = gContext.mBelowPlaneLimit[axisIndex];
+
+ dirAxis *= gContext.mAxisFactor[axisIndex];
+ dirPlaneX *= gContext.mAxisFactor[(axisIndex + 1) % 3];
+ dirPlaneY *= gContext.mAxisFactor[(axisIndex + 2) % 3];
+ } else
+ {
+ // new method
+ float lenDir = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis, localCoordinates);
+ float lenDirMinus = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirAxis, localCoordinates);
+
+ float lenDirPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneX, localCoordinates);
+ float lenDirMinusPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneX, localCoordinates);
+
+ float lenDirPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneY, localCoordinates);
+ float lenDirMinusPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneY, localCoordinates);
+
+ // For readability
+ bool& allowFlip = gContext.mAllowAxisFlip;
+ float mulAxis = (allowFlip && lenDir < lenDirMinus && fabsf(lenDir - lenDirMinus) > FLT_EPSILON) ? -1.f : 1.f;
+ float mulAxisX = (allowFlip && lenDirPlaneX < lenDirMinusPlaneX && fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f;
+ float mulAxisY = (allowFlip && lenDirPlaneY < lenDirMinusPlaneY && fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f;
+ dirAxis *= mulAxis;
+ dirPlaneX *= mulAxisX;
+ dirPlaneY *= mulAxisY;
+
+ // for axis
+ float axisLengthInClipSpace = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates);
+
+ float paraSurf = GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor);
+ belowPlaneLimit = (paraSurf > 0.0025f);
+ belowAxisLimit = (axisLengthInClipSpace > 0.02f);
+
+ // and store values
+ gContext.mAxisFactor[axisIndex] = mulAxis;
+ gContext.mAxisFactor[(axisIndex + 1) % 3] = mulAxisX;
+ gContext.mAxisFactor[(axisIndex + 2) % 3] = mulAxisY;
+ gContext.mBelowAxisLimit[axisIndex] = belowAxisLimit;
+ gContext.mBelowPlaneLimit[axisIndex] = belowPlaneLimit;
+ }
+}
+
+static void ComputeSnap(float* value, float snap) {
+ if (snap <= FLT_EPSILON)
+ {
+ return;
+ }
+
+ float modulo = fmodf(*value, snap);
+ float moduloRatio = fabsf(modulo) / snap;
+ if (moduloRatio < snapTension)
+ {
+ *value -= modulo;
+ } else if (moduloRatio > (1.f - snapTension))
+ {
+ *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f);
+ }
+}
+static void ComputeSnap(vec_t& value, const float* snap) {
+ for (int i = 0; i < 3; i++)
+ {
+ ComputeSnap(&value[i], snap[i]);
+ }
+}
+
+static float ComputeAngleOnPlan() {
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position);
+
+ vec_t perpendicularVector;
+ perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan);
+ perpendicularVector.Normalize();
+ float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f);
+ float angle = acosf(acosAngle);
+ angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f;
+ return angle;
+}
+
+static void DrawRotationGizmo(OPERATION op, int type) {
+ if (!Intersects(op, ROTATE))
+ {
+ return;
+ }
+ ImDrawList* drawList = gContext.mDrawList;
+
+ // colors
+ ImU32 colors[7];
+ ComputeColors(colors, type, ROTATE);
+
+ vec_t cameraToModelNormalized;
+ if (gContext.mIsOrthographic)
+ {
+ matrix_t viewInverse;
+ viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat);
+ cameraToModelNormalized = viewInverse.v.dir;
+ } else
+ {
+ cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye);
+ }
+
+ cameraToModelNormalized.TransformVector(gContext.mModelInverse);
+
+ gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight;
+
+ bool hasRSC = Intersects(op, ROTATE_SCREEN);
+ for (int axis = 0; axis < 3; axis++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(ROTATE_Z >> axis)))
+ {
+ continue;
+ }
+ const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis);
+ const int circleMul = (hasRSC && !usingAxis) ? 1 : 2;
+
+ ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1));
+
+ float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f;
+
+ for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++)
+ {
+ float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)halfCircleSegmentCount);
+ vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f);
+ vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * rotationDisplayFactor;
+ circlePos[i] = worldToPos(pos, gContext.mMVP);
+ }
+ if (!gContext.mbUsing || usingAxis)
+ {
+ drawList->AddPolyline(circlePos, circleMul * halfCircleSegmentCount + 1, colors[3 - axis], false, 2);
+ }
+
+ float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0])));
+ if (radiusAxis > gContext.mRadiusSquareCenter)
+ {
+ gContext.mRadiusSquareCenter = radiusAxis;
+ }
+ }
+ if (hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN))
+ {
+ drawList->AddCircle(worldToPos(gContext.mModel.v.position, gContext.mViewProjection), gContext.mRadiusSquareCenter, colors[0], 64, 3.f);
+ }
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsRotateType(type))
+ {
+ ImVec2 circlePos[halfCircleSegmentCount + 1];
+
+ circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+ for (unsigned int i = 1; i < halfCircleSegmentCount; i++)
+ {
+ float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1));
+ matrix_t rotateVectorMatrix;
+ rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng);
+ vec_t pos;
+ pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix);
+ pos *= gContext.mScreenFactor * rotationDisplayFactor;
+ circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection);
+ }
+ drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount, IM_COL32(0xFF, 0x80, 0x10, 0x80));
+ drawList->AddPolyline(circlePos, halfCircleSegmentCount, IM_COL32(0xFF, 0x80, 0x10, 0xFF), true, 2);
+
+ ImVec2 destinationPosOnScreen = circlePos[1];
+ char tmps[512];
+ ImFormatString(tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps);
+ }
+}
+
+static void DrawHatchedAxis(const vec_t& axis) {
+ for (int j = 1; j < 10; j++)
+ {
+ ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP);
+ gContext.mDrawList->AddLine(baseSSpace2, worldDirSSpace2, IM_COL32(0, 0, 0, 0x80), 6.f);
+ }
+}
+
+static void DrawScaleGizmo(OPERATION op, int type) {
+ ImDrawList* drawList = gContext.mDrawList;
+
+ if (!Intersects(op, SCALE))
+ {
+ return;
+ }
+
+ // colors
+ ImU32 colors[7];
+ ComputeColors(colors, type, SCALE);
+
+ // draw
+ vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f };
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ scaleDisplay = gContext.mScale;
+ }
+
+ for (int i = 0; i < 3; i++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(SCALE_X << i)))
+ {
+ continue;
+ }
+ const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i);
+ if (!gContext.mbUsing || usingAxis)
+ {
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ bool belowAxisLimit, belowPlaneLimit;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true);
+
+ // draw axis
+ if (belowAxisLimit)
+ {
+ bool hasTranslateOnAxis = Contains(op, static_cast<OPERATION>(TRANSLATE_X << i));
+ float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f;
+ ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP);
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, IM_COL32(0x40, 0x40, 0x40, 0xFF), 3.f);
+ drawList->AddCircleFilled(worldDirSSpaceNoScale, 6.f, IM_COL32(0x40, 0x40, 0x40, 0xFF));
+ }
+
+ if (!hasTranslateOnAxis || gContext.mbUsing)
+ {
+ drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], 3.f);
+ }
+ drawList->AddCircleFilled(worldDirSSpace, 6.f, colors[i + 1]);
+
+ if (gContext.mAxisFactor[i] < 0.f)
+ {
+ DrawHatchedAxis(dirAxis * scaleDisplay[i]);
+ }
+ }
+ }
+ }
+
+ // draw screen cirle
+ drawList->AddCircleFilled(gContext.mScreenSquareCenter, 6.f, colors[0], 32);
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(type))
+ {
+ // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection);
+ ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+ /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y);
+ dif.Normalize();
+ dif *= 5.f;
+ drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor);
+ drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor);
+ drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f);
+ */
+ char tmps[512];
+ // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin;
+ int componentInfoIndex = (type - MT_SCALE_X) * 3;
+ ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps);
+ }
+}
+
+static void DrawScaleUniveralGizmo(OPERATION op, int type) {
+ ImDrawList* drawList = gContext.mDrawList;
+
+ if (!Intersects(op, SCALEU))
+ {
+ return;
+ }
+
+ // colors
+ ImU32 colors[7];
+ ComputeColors(colors, type, SCALEU);
+
+ // draw
+ vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f };
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ scaleDisplay = gContext.mScale;
+ }
+
+ for (int i = 0; i < 3; i++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(SCALE_XU << i)))
+ {
+ continue;
+ }
+ const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i);
+ if (!gContext.mbUsing || usingAxis)
+ {
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ bool belowAxisLimit, belowPlaneLimit;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true);
+
+ // draw axis
+ if (belowAxisLimit)
+ {
+ bool hasTranslateOnAxis = Contains(op, static_cast<OPERATION>(TRANSLATE_X << i));
+ float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f;
+ // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal);
+ // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal);
+
+#if 0
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, IM_COL32(0x40, 0x40, 0x40, 0xFF), 3.f);
+ drawList->AddCircleFilled(worldDirSSpaceNoScale, 6.f, IM_COL32(0x40, 0x40, 0x40, 0xFF));
+ }
+ /*
+ if (!hasTranslateOnAxis || gContext.mbUsing)
+ {
+ drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], 3.f);
+ }
+ */
+#endif
+ drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]);
+ }
+ }
+ }
+
+ // draw screen cirle
+ drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, 3.f);
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(type))
+ {
+ // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection);
+ ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+ /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y);
+ dif.Normalize();
+ dif *= 5.f;
+ drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor);
+ drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor);
+ drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f);
+ */
+ char tmps[512];
+ // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin;
+ int componentInfoIndex = (type - MT_SCALE_X) * 3;
+ ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps);
+ }
+}
+
+static void DrawTranslationGizmo(OPERATION op, int type) {
+ ImDrawList* drawList = gContext.mDrawList;
+ if (!drawList)
+ {
+ return;
+ }
+
+ if (!Intersects(op, TRANSLATE))
+ {
+ return;
+ }
+
+ // colors
+ ImU32 colors[7];
+ ComputeColors(colors, type, TRANSLATE);
+
+ const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+
+ // draw
+ bool belowAxisLimit = false;
+ bool belowPlaneLimit = false;
+ for (int i = 0; i < 3; ++i)
+ {
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit);
+
+ if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i))
+ {
+ // draw axis
+ if (belowAxisLimit && Intersects(op, static_cast<OPERATION>(TRANSLATE_X << i)))
+ {
+ ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP);
+
+ drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], 3.f);
+
+ // Arrow head begin
+ ImVec2 dir(origin - worldDirSSpace);
+
+ float d = sqrtf(ImLengthSqr(dir));
+ dir /= d; // Normalize
+ dir *= 6.0f;
+
+ ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector
+ ImVec2 a(worldDirSSpace + dir);
+ drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]);
+ // Arrow head end
+
+ if (gContext.mAxisFactor[i] < 0.f)
+ {
+ DrawHatchedAxis(dirAxis);
+ }
+ }
+ }
+ // draw plane
+ if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i))
+ {
+ if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i]))
+ {
+ ImVec2 screenQuadPts[4];
+ for (int j = 0; j < 4; ++j)
+ {
+ vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor;
+ screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP);
+ }
+ drawList->AddPolyline(screenQuadPts, 4, directionColor[i], true, 1.0f);
+ drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]);
+ }
+ }
+ }
+
+ drawList->AddCircleFilled(gContext.mScreenSquareCenter, 6.f, colors[0], 32);
+
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsTranslateType(type))
+ {
+ ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection);
+ ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+ vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f };
+ dif.Normalize();
+ dif *= 5.f;
+ drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor);
+ drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor);
+ drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f);
+
+ char tmps[512];
+ vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin;
+ int componentInfoIndex = (type - MT_MOVE_X) * 3;
+ ImFormatString(tmps, sizeof(tmps), translationInfoMask[type - MT_MOVE_X], deltaInfo[translationInfoIndex[componentInfoIndex]], deltaInfo[translationInfoIndex[componentInfoIndex + 1]], deltaInfo[translationInfoIndex[componentInfoIndex + 2]]);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps);
+ }
+}
+
+static bool CanActivate() {
+ if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive())
+ {
+ return true;
+ }
+ return false;
+}
+
+static bool HandleAndDrawLocalBounds(float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) {
+ ImGuiIO& io = ImGui::GetIO();
+ ImDrawList* drawList = gContext.mDrawList;
+
+ // compute best projection axis
+ vec_t axesWorldDirections[3];
+ vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f };
+ int axes[3];
+ unsigned int numAxes = 1;
+ axes[0] = gContext.mBoundsBestAxis;
+ int bestAxis = axes[0];
+ if (!gContext.mbUsingBounds)
+ {
+ numAxes = 0;
+ float bestDot = 0.f;
+ for (int i = 0; i < 3; i++)
+ {
+ vec_t dirPlaneNormalWorld;
+ dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource);
+ dirPlaneNormalWorld.Normalize();
+
+ float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld));
+ if (dt >= bestDot)
+ {
+ bestDot = dt;
+ bestAxis = i;
+ bestAxisWorldDirection = dirPlaneNormalWorld;
+ }
+
+ if (dt >= 0.1f)
+ {
+ axes[numAxes] = i;
+ axesWorldDirections[numAxes] = dirPlaneNormalWorld;
+ ++numAxes;
+ }
+ }
+ }
+
+ if (numAxes == 0)
+ {
+ axes[0] = bestAxis;
+ axesWorldDirections[0] = bestAxisWorldDirection;
+ numAxes = 1;
+ }
+
+ else if (bestAxis != axes[0])
+ {
+ unsigned int bestIndex = 0;
+ for (unsigned int i = 0; i < numAxes; i++)
+ {
+ if (axes[i] == bestAxis)
+ {
+ bestIndex = i;
+ break;
+ }
+ }
+ int tempAxis = axes[0];
+ axes[0] = axes[bestIndex];
+ axes[bestIndex] = tempAxis;
+ vec_t tempDirection = axesWorldDirections[0];
+ axesWorldDirections[0] = axesWorldDirections[bestIndex];
+ axesWorldDirections[bestIndex] = tempDirection;
+ }
+
+ matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection;
+ for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex)
+ {
+ bestAxis = axes[axisIndex];
+ bestAxisWorldDirection = axesWorldDirections[axisIndex];
+
+ // Corners of the plane (rectangle) containing bestAxis
+ vec_t corners[4];
+
+ int secondAxis = (bestAxis + 1) % 3;
+ int thirdAxis = (bestAxis + 2) % 3;
+ // ImU32 col[] = { IM_COL32(255, 0, 0, 255), IM_COL32(0, 255, 0, 255), IM_COL32(0, 0, 255, 255) };
+ for (int i = 0; i < 4; i++) {
+ corners[i].w = 0.0f;
+ corners[i][bestAxis] = 0.0f;
+ corners[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)];
+ corners[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))];
+
+ // ImVec2 pos = worldToPos(corners[i], boundsMVP);
+ // drawList->AddCircleFilled(pos, 10.0f, col[axisIndex]);
+ }
+
+ // draw bounds
+ unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80);
+
+ for (int i = 0; i < 4; i++)
+ {
+ ImVec2 worldBound1 = worldToPos(corners[i], boundsMVP);
+ ImVec2 worldBound2 = worldToPos(corners[(i + 1) % 4], boundsMVP);
+ if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2))
+ {
+ continue;
+ }
+ float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2));
+ int stepCount = (int)(boundDistance / 10.f);
+ stepCount = min(stepCount, 1000);
+ float stepLength = 1.f / (float)stepCount;
+ for (int j = 0; j < stepCount; j++)
+ {
+ float t1 = (float)j * stepLength;
+ float t2 = (float)j * stepLength + stepLength * 0.5f;
+ ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1));
+ ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2));
+ // drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f);
+ drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f);
+ }
+ vec_t midPoint = (corners[i] + corners[(i + 1) % 4]) * 0.5f;
+ ImVec2 midBound = worldToPos(midPoint, boundsMVP);
+ static const float AnchorBigRadius = 8.f;
+ static const float AnchorSmallRadius = 6.f;
+ bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius);
+ bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius);
+
+ int type = MT_NONE;
+ vec_t gizmoHitProportion;
+
+ if (Intersects(operation, TRANSLATE))
+ {
+ type = GetMoveType(operation, &gizmoHitProportion);
+ }
+ if (Intersects(operation, ROTATE) && type == MT_NONE)
+ {
+ type = GetRotateType(operation);
+ }
+ if (Intersects(operation, SCALE) && type == MT_NONE)
+ {
+ type = GetScaleType(operation);
+ }
+
+ if (type != MT_NONE)
+ {
+ overBigAnchor = false;
+ overSmallAnchor = false;
+ }
+
+ unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha);
+ unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha);
+
+ drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK);
+ drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor);
+
+ drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK);
+ drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor);
+ int oppositeIndex = (i + 2) % 4;
+ // big anchor on corners
+ if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate())
+ {
+ gContext.mBoundsPivot.TransformPoint(corners[(i + 2) % 4], gContext.mModelSource);
+ gContext.mBoundsAnchor.TransformPoint(corners[i], gContext.mModelSource);
+ gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection);
+ gContext.mBoundsBestAxis = bestAxis;
+ gContext.mBoundsAxis[0] = secondAxis;
+ gContext.mBoundsAxis[1] = thirdAxis;
+
+ gContext.mBoundsLocalPivot.Set(0.f);
+ gContext.mBoundsLocalPivot[secondAxis] = corners[oppositeIndex][secondAxis];
+ gContext.mBoundsLocalPivot[thirdAxis] = corners[oppositeIndex][thirdAxis];
+ gContext.mBoundsPivotCornerIndex = oppositeIndex;
+
+ gContext.mbUsingBounds = true;
+ gContext.mEditingID = gContext.mActualID;
+ gContext.mBoundsMatrix = gContext.mModelSource;
+
+ gContext.mbIsUsingBigAnchor = true;
+ }
+ // small anchor on middle of segment
+ if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate())
+ {
+ vec_t midPointOpposite = (corners[(i + 2) % 4] + corners[(i + 3) % 4]) * 0.5f;
+ gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource);
+ gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource);
+ gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection);
+ gContext.mBoundsBestAxis = bestAxis;
+ int indices[] = { secondAxis, thirdAxis };
+ gContext.mBoundsAxis[0] = indices[i % 2];
+ gContext.mBoundsAxis[1] = -1;
+
+ int localPivotComponentIdx = gContext.mBoundsAxis[0];
+ gContext.mBoundsLocalPivot.Set(0.f);
+ gContext.mBoundsLocalPivot[localPivotComponentIdx] = corners[oppositeIndex][localPivotComponentIdx]; // bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f);
+ gContext.mBoundsPivotCornerIndex = oppositeIndex;
+
+ gContext.mbUsingBounds = true;
+ gContext.mEditingID = gContext.mActualID;
+ gContext.mBoundsMatrix = gContext.mModelSource;
+
+ gContext.mbIsUsingBigAnchor = false;
+ }
+ }
+
+ ImGui::Text("bounds pivot: %.2f, %.2f, %.2f", gContext.mBoundsPivot.x, gContext.mBoundsPivot.y, gContext.mBoundsPivot.z);
+ ImGui::Text("bounds anchor: %.2f, %.2f, %.2f", gContext.mBoundsAnchor.x, gContext.mBoundsAnchor.y, gContext.mBoundsAnchor.z);
+ ImGui::Text("bounds plan: %.2f, %.2f, %.2f", gContext.mBoundsPlan.x, gContext.mBoundsPlan.y, gContext.mBoundsPlan.z);
+ ImGui::Text("bounds local pivot: %.2f, %.2f, %.2f", gContext.mBoundsLocalPivot.x, gContext.mBoundsLocalPivot.y, gContext.mBoundsLocalPivot.z);
+ if (gContext.mbUsingBounds && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID))
+ {
+ // compute projected mouse position on plan
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan);
+ vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len;
+
+ // compute a reference and delta vectors base on mouse move
+ vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs();
+ vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs();
+
+ ImGui::Text("Delta: %.2f, %.2f, %.2f", deltaVector.x, deltaVector.y, deltaVector.z);
+ ImGui::Text("Ref: %.2f, %.2f, %.2f", referenceVector.x, referenceVector.y, referenceVector.z);
+ ImGui::Separator();
+
+ // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length
+ for (int axisIndex1 : gContext.mBoundsAxis) {
+ if (axisIndex1 == -1) {
+ continue;
+ }
+
+ vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs();
+ // ImGui::Text("Axisdir: %.2f, %.2f, %.2f", axisDir.x, axisDir.y, axisDir.z);
+
+ float refAxisComp = axisDir.Dot(referenceVector);
+ float deltaAxisComp = axisDir.Dot(deltaVector);
+ // ImGui::Text("refAxisComp: %.2f", refAxisComp);
+
+ float length = deltaAxisComp;
+ if (snapValues) {
+ ComputeSnap(&length, snapValues[axisIndex1]);
+ }
+
+ // ImGui::Text("axis idx %d", axisIndex1);
+ // TODO(hnosm): logic that mapps mouse pos to bound seems to account for translation fixup already?
+ bounds[axisIndex1] = -length / 2;
+ bounds[axisIndex1 + 3] = +length / 2;
+ }
+
+ // Update corner positions, translation fixup code needs them
+ for (int i = 0; i < 4; i++) {
+ corners[i].w = 0.0f;
+ corners[i][bestAxis] = 0.0f;
+ corners[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)];
+ corners[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))];
+ }
+
+ // Translation (object center) fixup - make sure pivot stays in place
+ // TODO(hnosm): is there a better way to write this that doesn't involve transferring a bunch of extra state from begin drag frame?
+ vec_t newLocalPivot;
+ if (gContext.mbIsUsingBigAnchor) {
+ newLocalPivot.Set(0.0f);
+ newLocalPivot[secondAxis] = corners[gContext.mBoundsPivotCornerIndex][secondAxis];
+ newLocalPivot[thirdAxis] = corners[gContext.mBoundsPivotCornerIndex][thirdAxis];
+ } else {
+ newLocalPivot.Set(0.0f);
+ int localPivotComponentIdx = gContext.mBoundsAxis[0];
+ newLocalPivot[localPivotComponentIdx] = corners[gContext.mBoundsPivotCornerIndex][localPivotComponentIdx];
+ }
+
+ vec_t delta = gContext.mBoundsLocalPivot - newLocalPivot;
+ vec_t oldTranslation = gContext.mBoundsMatrix.component[3];
+ matrix->component[3] = oldTranslation + delta;
+
+ // info text
+ char tmps[512];
+ ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection);
+ ImFormatString(tmps, sizeof(tmps),
+ // Size of the bounds in each axis direction
+ "X: %.2f Y: %.2f Z:%.2f",
+ (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length(),
+ (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length(),
+ (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length());
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps);
+ drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps);
+ }
+
+ if (!io.MouseDown[0]) {
+ gContext.mbUsingBounds = false;
+ gContext.mEditingID = -1;
+ }
+ if (gContext.mbUsingBounds)
+ {
+ break;
+ }
+ }
+
+ return gContext.mbUsingBounds;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+
+static int GetScaleType(OPERATION op) {
+ if (gContext.mbUsing)
+ {
+ return MT_NONE;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ int type = MT_NONE;
+
+ // screen
+ if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x &&
+ io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y &&
+ Contains(op, SCALE))
+ {
+ type = MT_SCALE_XYZ;
+ }
+
+ // compute
+ for (int i = 0; i < 3 && type == MT_NONE; i++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(SCALE_X << i)))
+ {
+ continue;
+ }
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ bool belowAxisLimit, belowPlaneLimit;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true);
+ dirAxis.TransformVector(gContext.mModelLocal);
+ dirPlaneX.TransformVector(gContext.mModelLocal);
+ dirPlaneY.TransformVector(gContext.mModelLocal);
+
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis));
+ vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len;
+
+ const float startOffset = Contains(op, static_cast<OPERATION>(TRANSLATE_X << i)) ? 1.0f : 0.1f;
+ const float endOffset = Contains(op, static_cast<OPERATION>(TRANSLATE_X << i)) ? 1.4f : 1.0f;
+ const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection);
+ const ImVec2 axisStartOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection);
+ const ImVec2 axisEndOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection);
+
+ vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen));
+
+ if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size
+ {
+ type = MT_SCALE_X + i;
+ }
+ }
+
+ // universal
+
+ vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f };
+ float dist = deltaScreen.Length();
+ if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f)
+ {
+ type = MT_SCALE_XYZ;
+ }
+
+ for (int i = 0; i < 3 && type == MT_NONE; i++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(SCALE_XU << i)))
+ {
+ continue;
+ }
+
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ bool belowAxisLimit, belowPlaneLimit;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true);
+
+ // draw axis
+ if (belowAxisLimit)
+ {
+ bool hasTranslateOnAxis = Contains(op, static_cast<OPERATION>(TRANSLATE_X << i));
+ float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f;
+ // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal);
+ // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP);
+ ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal);
+
+ float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos));
+ if (distance < 12.f)
+ {
+ type = MT_SCALE_X + i;
+ }
+ }
+ }
+ return type;
+}
+
+static int GetRotateType(OPERATION op) {
+ if (gContext.mbUsing)
+ {
+ return MT_NONE;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ int type = MT_NONE;
+
+ vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f };
+ float dist = deltaScreen.Length();
+ if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f))
+ {
+ type = MT_ROTATE_SCREEN;
+ }
+
+ const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir };
+
+ vec_t modelViewPos;
+ modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat);
+
+ for (int i = 0; i < 3 && type == MT_NONE; i++)
+ {
+ if (!Intersects(op, static_cast<OPERATION>(ROTATE_X << i)))
+ {
+ continue;
+ }
+ // pickup plan
+ vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]);
+
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan);
+ const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len;
+ vec_t intersectViewPos;
+ intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat);
+
+ if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON)
+ {
+ continue;
+ }
+
+ const vec_t localPos = intersectWorldPos - gContext.mModel.v.position;
+ vec_t idealPosOnCircle = Normalized(localPos);
+ idealPosOnCircle.TransformVector(gContext.mModelInverse);
+ const ImVec2 idealPosOnCircleScreen = worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP);
+
+ // gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE);
+ const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos;
+
+ const float distance = makeVect(distanceOnScreen).Length();
+ if (distance < 8.f) // pixel size
+ {
+ type = MT_ROTATE_X + i;
+ }
+ }
+
+ return type;
+}
+
+static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) {
+ if (!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver)
+ {
+ return MT_NONE;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ int type = MT_NONE;
+
+ // screen
+ if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x &&
+ io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y &&
+ Contains(op, TRANSLATE))
+ {
+ type = MT_MOVE_SCREEN;
+ }
+
+ const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY));
+
+ // compute
+ for (int i = 0; i < 3 && type == MT_NONE; i++)
+ {
+ vec_t dirPlaneX, dirPlaneY, dirAxis;
+ bool belowAxisLimit, belowPlaneLimit;
+ ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit);
+ dirAxis.TransformVector(gContext.mModel);
+ dirPlaneX.TransformVector(gContext.mModel);
+ dirPlaneY.TransformVector(gContext.mModel);
+
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis));
+ vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len;
+
+ const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY);
+ const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY);
+
+ vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen));
+ if ((closestPointOnAxis - screenCoord).Length() < 12.f && Intersects(op, static_cast<OPERATION>(TRANSLATE_X << i))) // pixel size
+ {
+ type = MT_MOVE_X + i;
+ }
+
+ const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor));
+ const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor));
+ if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && Contains(op, TRANSLATE_PLANS[i]))
+ {
+ type = MT_MOVE_YZ + i;
+ }
+
+ if (gizmoHitProportion)
+ {
+ *gizmoHitProportion = makeVect(dx, dy, 0.f);
+ }
+ }
+ return type;
+}
+
+static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) {
+ if (!Intersects(op, TRANSLATE) || type != MT_NONE)
+ {
+ return false;
+ }
+ const ImGuiIO& io = ImGui::GetIO();
+ const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN;
+ bool modified = false;
+
+ // move
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation))
+ {
+ ImGui::CaptureMouseFromApp();
+ const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ const float len = fabsf(signedLength); // near plan
+ const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len;
+
+ // compute delta
+ const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor;
+ vec_t delta = newOrigin - gContext.mModel.v.position;
+
+ // 1 axis constraint
+ if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z)
+ {
+ const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X;
+ const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex];
+ const float lengthOnAxis = Dot(axisValue, delta);
+ delta = axisValue * lengthOnAxis;
+ }
+
+ // snap
+ if (snap)
+ {
+ vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin;
+ if (applyRotationLocaly)
+ {
+ matrix_t modelSourceNormalized = gContext.mModelSource;
+ modelSourceNormalized.OrthoNormalize();
+ matrix_t modelSourceNormalizedInverse;
+ modelSourceNormalizedInverse.Inverse(modelSourceNormalized);
+ cumulativeDelta.TransformVector(modelSourceNormalizedInverse);
+ ComputeSnap(cumulativeDelta, snap);
+ cumulativeDelta.TransformVector(modelSourceNormalized);
+ } else
+ {
+ ComputeSnap(cumulativeDelta, snap);
+ }
+ delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position;
+ }
+
+ if (delta != gContext.mTranslationLastDelta)
+ {
+ modified = true;
+ }
+ gContext.mTranslationLastDelta = delta;
+
+ // compute matrix & delta
+ matrix_t deltaMatrixTranslation;
+ deltaMatrixTranslation.Translation(delta);
+ if (deltaMatrix)
+ {
+ memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16);
+ }
+
+ const matrix_t res = gContext.mModelSource * deltaMatrixTranslation;
+ *(matrix_t*)matrix = res;
+
+ if (!io.MouseDown[0])
+ {
+ gContext.mbUsing = false;
+ }
+
+ type = gContext.mCurrentOperation;
+ } else
+ {
+ // find new possible way to move
+ vec_t gizmoHitProportion;
+ type = GetMoveType(op, &gizmoHitProportion);
+ if (type != MT_NONE)
+ {
+ ImGui::CaptureMouseFromApp();
+ }
+ if (CanActivate() && type != MT_NONE)
+ {
+ gContext.mbUsing = true;
+ gContext.mEditingID = gContext.mActualID;
+ gContext.mCurrentOperation = type;
+ vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir };
+
+ vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye);
+ for (unsigned int i = 0; i < 3; i++)
+ {
+ vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized);
+ movePlanNormal[i].Cross(orthoVector);
+ movePlanNormal[i].Normalize();
+ }
+ // pickup plan
+ gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]);
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len;
+ gContext.mMatrixOrigin = gContext.mModel.v.position;
+
+ gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor);
+ }
+ }
+ return modified;
+}
+
+static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) {
+ if ((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver)
+ {
+ return false;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ bool modified = false;
+
+ if (!gContext.mbUsing)
+ {
+ // find new possible way to scale
+ type = GetScaleType(op);
+ if (type != MT_NONE)
+ {
+ ImGui::CaptureMouseFromApp();
+ }
+ if (CanActivate() && type != MT_NONE)
+ {
+ gContext.mbUsing = true;
+ gContext.mEditingID = gContext.mActualID;
+ gContext.mCurrentOperation = type;
+ const vec_t movePlanNormal[] = { gContext.mModel.v.up, gContext.mModel.v.dir, gContext.mModel.v.right, gContext.mModel.v.dir, gContext.mModel.v.up, gContext.mModel.v.right, -gContext.mCameraDir };
+ // pickup plan
+
+ gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_SCALE_X]);
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len;
+ gContext.mMatrixOrigin = gContext.mModel.v.position;
+ gContext.mScale.Set(1.f, 1.f, 1.f);
+ gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor);
+ gContext.mScaleValueOrigin = makeVect(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length());
+ gContext.mSaveMousePosx = io.MousePos.x;
+ }
+ }
+ // scale
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation))
+ {
+ ImGui::CaptureMouseFromApp();
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len;
+ vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor;
+ vec_t delta = newOrigin - gContext.mModelLocal.v.position;
+
+ // 1 axis constraint
+ if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z)
+ {
+ int axisIndex = gContext.mCurrentOperation - MT_SCALE_X;
+ const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex];
+ float lengthOnAxis = Dot(axisValue, delta);
+ delta = axisValue * lengthOnAxis;
+
+ vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position;
+ float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector);
+
+ gContext.mScale[axisIndex] = max(ratio, 0.001f);
+ } else
+ {
+ float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f;
+ gContext.mScale.Set(max(1.f + scaleDelta, 0.001f));
+ }
+
+ // snap
+ if (snap)
+ {
+ float scaleSnap[] = { snap[0], snap[0], snap[0] };
+ ComputeSnap(gContext.mScale, scaleSnap);
+ }
+
+ // no 0 allowed
+ for (int i = 0; i < 3; i++)
+ gContext.mScale[i] = max(gContext.mScale[i], 0.001f);
+
+ if (gContext.mScaleLast != gContext.mScale)
+ {
+ modified = true;
+ }
+ gContext.mScaleLast = gContext.mScale;
+
+ // compute matrix & delta
+ matrix_t deltaMatrixScale;
+ deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin);
+
+ matrix_t res = deltaMatrixScale * gContext.mModelLocal;
+ *(matrix_t*)matrix = res;
+
+ if (deltaMatrix)
+ {
+ vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin;
+
+ vec_t originalScaleDivider;
+ originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x;
+ originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y;
+ originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z;
+
+ deltaScale = deltaScale * originalScaleDivider;
+
+ deltaMatrixScale.Scale(deltaScale);
+ memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16);
+ }
+
+ if (!io.MouseDown[0])
+ {
+ gContext.mbUsing = false;
+ gContext.mScale.Set(1.f, 1.f, 1.f);
+ }
+
+ type = gContext.mCurrentOperation;
+ }
+ return modified;
+}
+
+static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) {
+ if (!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver)
+ {
+ return false;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ bool applyRotationLocaly = gContext.mMode == LOCAL;
+ bool modified = false;
+
+ if (!gContext.mbUsing)
+ {
+ type = GetRotateType(op);
+
+ if (type != MT_NONE)
+ {
+ ImGui::CaptureMouseFromApp();
+ }
+
+ if (type == MT_ROTATE_SCREEN)
+ {
+ applyRotationLocaly = true;
+ }
+
+ if (CanActivate() && type != MT_NONE)
+ {
+ gContext.mbUsing = true;
+ gContext.mEditingID = gContext.mActualID;
+ gContext.mCurrentOperation = type;
+ const vec_t rotatePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir };
+ // pickup plan
+ if (applyRotationLocaly)
+ {
+ gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]);
+ } else
+ {
+ gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]);
+ }
+
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan);
+ vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position;
+ gContext.mRotationVectorSource = Normalized(localPos);
+ gContext.mRotationAngleOrigin = ComputeAngleOnPlan();
+ }
+ }
+
+ // rotation
+ if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation))
+ {
+ ImGui::CaptureMouseFromApp();
+ gContext.mRotationAngle = ComputeAngleOnPlan();
+ if (snap)
+ {
+ float snapInRadian = snap[0] * DEG2RAD;
+ ComputeSnap(&gContext.mRotationAngle, snapInRadian);
+ }
+ vec_t rotationAxisLocalSpace;
+
+ rotationAxisLocalSpace.TransformVector(makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), gContext.mModelInverse);
+ rotationAxisLocalSpace.Normalize();
+
+ matrix_t deltaRotation;
+ deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin);
+ if (gContext.mRotationAngle != gContext.mRotationAngleOrigin)
+ {
+ modified = true;
+ }
+ gContext.mRotationAngleOrigin = gContext.mRotationAngle;
+
+ matrix_t scaleOrigin;
+ scaleOrigin.Scale(gContext.mModelScaleOrigin);
+
+ if (applyRotationLocaly)
+ {
+ *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal;
+ } else
+ {
+ matrix_t res = gContext.mModelSource;
+ res.v.position.Set(0.f);
+
+ *(matrix_t*)matrix = res * deltaRotation;
+ ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position;
+ }
+
+ if (deltaMatrix)
+ {
+ *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel;
+ }
+
+ if (!io.MouseDown[0])
+ {
+ gContext.mbUsing = false;
+ gContext.mEditingID = -1;
+ }
+ type = gContext.mCurrentOperation;
+ }
+ return modified;
+}
+
+void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) {
+ matrix_t mat = *(matrix_t*)matrix;
+
+ scale[0] = mat.v.right.Length();
+ scale[1] = mat.v.up.Length();
+ scale[2] = mat.v.dir.Length();
+
+ mat.OrthoNormalize();
+
+ rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]);
+ rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2]));
+ rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]);
+
+ translation[0] = mat.v.position.x;
+ translation[1] = mat.v.position.y;
+ translation[2] = mat.v.position.z;
+}
+
+void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) {
+ matrix_t& mat = *(matrix_t*)matrix;
+
+ matrix_t rot[3];
+ for (int i = 0; i < 3; i++)
+ {
+ rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD);
+ }
+
+ mat = rot[0] * rot[1] * rot[2];
+
+ float validScale[3];
+ for (int i = 0; i < 3; i++)
+ {
+ if (fabsf(scale[i]) < FLT_EPSILON)
+ {
+ validScale[i] = 0.001f;
+ } else
+ {
+ validScale[i] = scale[i];
+ }
+ }
+ mat.v.right *= validScale[0];
+ mat.v.up *= validScale[1];
+ mat.v.dir *= validScale[2];
+ mat.v.position.Set(translation[0], translation[1], translation[2], 1.f);
+}
+
+void SetID(int id) {
+ gContext.mActualID = id;
+}
+
+void AllowAxisFlip(bool value) {
+ gContext.mAllowAxisFlip = value;
+}
+
+bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix, const float* snap, float* localBounds, const float* boundsSnap) {
+ // Scale is always local or matrix will be skewed when applying world scale or oriented matrix
+ ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode);
+
+ // set delta to identity
+ if (deltaMatrix)
+ {
+ ((matrix_t*)deltaMatrix)->SetToIdentity();
+ }
+
+ // behind camera
+ vec_t camSpacePosition;
+ camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP);
+ if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f)
+ {
+ return false;
+ }
+
+ // --
+ int type = MT_NONE;
+ bool manipulated = false;
+ if (gContext.mbEnable)
+ {
+ if (!gContext.mbUsingBounds)
+ {
+ manipulated |= HandleTranslation(matrix, deltaMatrix, operation, type, snap) ||
+ HandleScale(matrix, deltaMatrix, operation, type, snap) ||
+ HandleRotation(matrix, deltaMatrix, operation, type, snap);
+ }
+ }
+ if (localBounds && !gContext.mbUsing)
+ {
+ manipulated |= HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation);
+ }
+
+ gContext.mOperation = operation;
+ if (!gContext.mbUsingBounds)
+ {
+ DrawRotationGizmo(operation, type);
+ DrawTranslationGizmo(operation, type);
+ DrawScaleGizmo(operation, type);
+ DrawScaleUniveralGizmo(operation, type);
+ }
+ return manipulated;
+}
+
+void SetGizmoSizeClipSpace(float value) {
+ gContext.mGizmoSizeClipSpace = value;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void ComputeFrustumPlanes(vec_t* frustum, const float* clip) {
+ frustum[0].x = clip[3] - clip[0];
+ frustum[0].y = clip[7] - clip[4];
+ frustum[0].z = clip[11] - clip[8];
+ frustum[0].w = clip[15] - clip[12];
+
+ frustum[1].x = clip[3] + clip[0];
+ frustum[1].y = clip[7] + clip[4];
+ frustum[1].z = clip[11] + clip[8];
+ frustum[1].w = clip[15] + clip[12];
+
+ frustum[2].x = clip[3] + clip[1];
+ frustum[2].y = clip[7] + clip[5];
+ frustum[2].z = clip[11] + clip[9];
+ frustum[2].w = clip[15] + clip[13];
+
+ frustum[3].x = clip[3] - clip[1];
+ frustum[3].y = clip[7] - clip[5];
+ frustum[3].z = clip[11] - clip[9];
+ frustum[3].w = clip[15] - clip[13];
+
+ frustum[4].x = clip[3] - clip[2];
+ frustum[4].y = clip[7] - clip[6];
+ frustum[4].z = clip[11] - clip[10];
+ frustum[4].w = clip[15] - clip[14];
+
+ frustum[5].x = clip[3] + clip[2];
+ frustum[5].y = clip[7] + clip[6];
+ frustum[5].z = clip[11] + clip[10];
+ frustum[5].w = clip[15] + clip[14];
+
+ for (int i = 0; i < 6; i++)
+ {
+ frustum[i].Normalize();
+ }
+}
+
+void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) {
+ matrix_t viewInverse;
+ viewInverse.Inverse(*(matrix_t*)view);
+
+ struct CubeFace {
+ float z;
+ ImVec2 faceCoordsScreen[4];
+ ImU32 color;
+ };
+ CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6);
+
+ if (!faces)
+ {
+ return;
+ }
+
+ vec_t frustum[6];
+ matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection;
+ ComputeFrustumPlanes(frustum, viewProjection.m16);
+
+ int cubeFaceCount = 0;
+ for (int cube = 0; cube < matrixCount; cube++)
+ {
+ const float* matrix = &matrices[cube * 16];
+
+ matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection;
+
+ for (int iFace = 0; iFace < 6; iFace++)
+ {
+ const int normalIndex = (iFace % 3);
+ const int perpXIndex = (normalIndex + 1) % 3;
+ const int perpYIndex = (normalIndex + 2) % 3;
+ const float invert = (iFace > 2) ? -1.f : 1.f;
+
+ const vec_t faceCoords[4] = {
+ directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex],
+ directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex],
+ directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex],
+ directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex],
+ };
+
+ // clipping
+ /*
+ bool skipFace = false;
+ for (unsigned int iCoord = 0; iCoord < 4; iCoord++)
+ {
+ vec_t camSpacePosition;
+ camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res);
+ if (camSpacePosition.z < 0.001f)
+ {
+ skipFace = true;
+ break;
+ }
+ }
+ if (skipFace)
+ {
+ continue;
+ }
+ */
+ vec_t centerPosition, centerPositionVP;
+ centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix);
+ centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res);
+
+ bool inFrustum = true;
+ for (int iFrustum = 0; iFrustum < 6; iFrustum++)
+ {
+ float dist = DistanceToPlane(centerPosition, frustum[iFrustum]);
+ if (dist < 0.f)
+ {
+ inFrustum = false;
+ break;
+ }
+ }
+
+ if (!inFrustum)
+ {
+ continue;
+ }
+ CubeFace& cubeFace = faces[cubeFaceCount];
+
+ // 3D->2D
+ // ImVec2 faceCoordsScreen[4];
+ for (unsigned int iCoord = 0; iCoord < 4; iCoord++)
+ {
+ cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res);
+ }
+ cubeFace.color = directionColor[normalIndex] | IM_COL32(0x80, 0x80, 0x80, 0);
+
+ cubeFace.z = centerPositionVP.z / centerPositionVP.w;
+ cubeFaceCount++;
+ }
+ }
+ qsort(faces, cubeFaceCount, sizeof(CubeFace), [](void const* _a, void const* _b) {
+ CubeFace* a = (CubeFace*)_a;
+ CubeFace* b = (CubeFace*)_b;
+ if (a->z < b->z)
+ {
+ return 1;
+ }
+ return -1;
+ });
+ // draw face with lighter color
+ for (int iFace = 0; iFace < cubeFaceCount; iFace++)
+ {
+ const CubeFace& cubeFace = faces[iFace];
+ gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color);
+ }
+
+ _freea(faces);
+}
+
+void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) {
+ matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection;
+ vec_t frustum[6];
+ ComputeFrustumPlanes(frustum, viewProjection.m16);
+ matrix_t res = *(matrix_t*)matrix * viewProjection;
+
+ for (float f = -gridSize; f <= gridSize; f += 1.f)
+ {
+ for (int dir = 0; dir < 2; dir++)
+ {
+ vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize);
+ vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize);
+ bool visible = true;
+ for (int i = 0; i < 6; i++)
+ {
+ float dA = DistanceToPlane(ptA, frustum[i]);
+ float dB = DistanceToPlane(ptB, frustum[i]);
+ if (dA < 0.f && dB < 0.f)
+ {
+ visible = false;
+ break;
+ }
+ if (dA > 0.f && dB > 0.f)
+ {
+ continue;
+ }
+ if (dA < 0.f)
+ {
+ float len = fabsf(dA - dB);
+ float t = fabsf(dA) / len;
+ ptA.Lerp(ptB, t);
+ }
+ if (dB < 0.f)
+ {
+ float len = fabsf(dB - dA);
+ float t = fabsf(dB) / len;
+ ptB.Lerp(ptA, t);
+ }
+ }
+ if (visible)
+ {
+ ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF);
+ col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col;
+ col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF) : col;
+
+ float thickness = 1.f;
+ thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness;
+ thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness;
+
+ gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness);
+ }
+ }
+ }
+}
+
+void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) {
+ static bool isDraging = false;
+ static bool isClicking = false;
+ static bool isInside = false;
+ static vec_t interpolationUp;
+ static vec_t interpolationDir;
+ static int interpolationFrames = 0;
+ const vec_t referenceUp = makeVect(0.f, 1.f, 0.f);
+
+ matrix_t svgView, svgProjection;
+ svgView = gContext.mViewMat;
+ svgProjection = gContext.mProjectionMat;
+
+ ImGuiIO& io = ImGui::GetIO();
+ gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor);
+ matrix_t viewInverse;
+ viewInverse.Inverse(*(matrix_t*)view);
+
+ const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length;
+
+ // view/projection matrices
+ const float distance = 3.f;
+ matrix_t cubeProjection, cubeView;
+ float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG;
+ Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16);
+
+ vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]);
+ vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]);
+ vec_t eye = dir * distance;
+ vec_t zero = makeVect(0.f, 0.f);
+ LookAt(&eye.x, &zero.x, &up.x, cubeView.m16);
+
+ // set context
+ gContext.mViewMat = cubeView;
+ gContext.mProjectionMat = cubeProjection;
+ ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size);
+
+ const matrix_t res = cubeView * cubeProjection;
+
+ // panels
+ static const ImVec2 panelPosition[9] = { ImVec2(0.75f, 0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) };
+
+ static const ImVec2 panelSize[9] = { ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) };
+
+ // tag faces
+ bool boxes[27]{};
+ for (int iPass = 0; iPass < 2; iPass++)
+ {
+ for (int iFace = 0; iFace < 6; iFace++)
+ {
+ const int normalIndex = (iFace % 3);
+ const int perpXIndex = (normalIndex + 1) % 3;
+ const int perpYIndex = (normalIndex + 2) % 3;
+ const float invert = (iFace > 2) ? -1.f : 1.f;
+ const vec_t indexVectorX = directionUnary[perpXIndex] * invert;
+ const vec_t indexVectorY = directionUnary[perpYIndex] * invert;
+ const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY;
+
+ // plan local space
+ const vec_t n = directionUnary[normalIndex] * invert;
+ vec_t viewSpaceNormal = n;
+ vec_t viewSpacePoint = n * 0.5f;
+ viewSpaceNormal.TransformVector(cubeView);
+ viewSpaceNormal.Normalize();
+ viewSpacePoint.TransformPoint(cubeView);
+ const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal);
+
+ // back face culling
+ if (viewSpaceFacePlan.w > 0.f)
+ {
+ continue;
+ }
+
+ const vec_t facePlan = BuildPlan(n * 0.5f, n);
+
+ const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan);
+ vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f);
+
+ float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f;
+ float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f;
+
+ // panels
+ const vec_t dx = directionUnary[perpXIndex];
+ const vec_t dy = directionUnary[perpYIndex];
+ const vec_t origin = directionUnary[normalIndex] - dx - dy;
+ for (int iPanel = 0; iPanel < 9; iPanel++)
+ {
+ vec_t boxCoord = boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f);
+ const ImVec2 p = panelPosition[iPanel] * 2.f;
+ const ImVec2 s = panelSize[iPanel] * 2.f;
+ ImVec2 faceCoordsScreen[4];
+ vec_t panelPos[4] = { dx * p.x + dy * p.y,
+ dx * p.x + dy * (p.y + s.y),
+ dx * (p.x + s.x) + dy * (p.y + s.y),
+ dx * (p.x + s.x) + dy * p.y };
+
+ for (unsigned int iCoord = 0; iCoord < 4; iCoord++)
+ {
+ faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size);
+ }
+
+ const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] };
+ bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && localy < panelCorners[1].y;
+ int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z);
+ IM_ASSERT(boxCoordInt < 27);
+ boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver;
+
+ // draw face with lighter color
+ if (iPass)
+ {
+ gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, (directionColor[normalIndex] | IM_COL32(0x80, 0x80, 0x80, 0x80)) | (isInside ? IM_COL32(0x08, 0x08, 0x08, 0) : 0));
+ if (boxes[boxCoordInt])
+ {
+ gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80));
+
+ if (!io.MouseDown[0] && !isDraging && isClicking)
+ {
+ // apply new view direction
+ int cx = boxCoordInt / 9;
+ int cy = (boxCoordInt - cx * 9) / 3;
+ int cz = boxCoordInt % 3;
+ interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz);
+ interpolationDir.Normalize();
+
+ if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f)
+ {
+ vec_t right = viewInverse.v.right;
+ if (fabsf(right.x) > fabsf(right.z))
+ {
+ right.z = 0.f;
+ } else
+ {
+ right.x = 0.f;
+ }
+ right.Normalize();
+ interpolationUp = Cross(interpolationDir, right);
+ interpolationUp.Normalize();
+ } else
+ {
+ interpolationUp = referenceUp;
+ }
+ interpolationFrames = 40;
+ isClicking = false;
+ }
+ if (io.MouseClicked[0] && !isDraging)
+ {
+ isClicking = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (interpolationFrames)
+ {
+ interpolationFrames--;
+ vec_t newDir = viewInverse.v.dir;
+ newDir.Lerp(interpolationDir, 0.2f);
+ newDir.Normalize();
+
+ vec_t newUp = viewInverse.v.up;
+ newUp.Lerp(interpolationUp, 0.3f);
+ newUp.Normalize();
+ newUp = interpolationUp;
+ vec_t newEye = camTarget + newDir * length;
+ LookAt(&newEye.x, &camTarget.x, &newUp.x, view);
+ }
+ isInside = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos);
+
+ // drag view
+ if (!isDraging && io.MouseClicked[0] && isInside)
+ {
+ isDraging = true;
+ isClicking = false;
+ } else if (isDraging && !io.MouseDown[0])
+ {
+ isDraging = false;
+ }
+
+ if (isDraging)
+ {
+ matrix_t rx, ry, roll;
+
+ rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f);
+ ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f);
+
+ roll = rx * ry;
+
+ vec_t newDir = viewInverse.v.dir;
+ newDir.TransformVector(roll);
+ newDir.Normalize();
+
+ // clamp
+ vec_t planDir = Cross(viewInverse.v.right, referenceUp);
+ planDir.y = 0.f;
+ planDir.Normalize();
+ float dt = Dot(planDir, newDir);
+ if (dt < 0.0f)
+ {
+ newDir += planDir * dt;
+ newDir.Normalize();
+ }
+
+ vec_t newEye = camTarget + newDir * length;
+ LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view);
+ }
+
+ // restore view/projection because it was used to compute ray
+ ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode);
+}
+}; // namespace IMGUIZMO_NAMESPACE
diff --git a/source/10-editor-common/ImGuiGuizmo.hpp b/source/10-editor-common/ImGuiGuizmo.hpp
new file mode 100644
index 0000000..0560050
--- /dev/null
+++ b/source/10-editor-common/ImGuiGuizmo.hpp
@@ -0,0 +1,232 @@
+// https://github.com/CedricGuillemet/ImGuizmo
+// v 1.84 WIP
+//
+// The MIT License(MIT)
+//
+// Copyright(c) 2021 Cedric Guillemet
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files(the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// -------------------------------------------------------------------------------------------
+// History :
+// 2019/11/03 View gizmo
+// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation fixed. Display message is incorrect (X: ... Y:...) in local mode.
+// 2016/09/09 Hatched negative axis. Snapping. Documentation update.
+// 2016/09/04 Axis switch and translation plan autohiding. Scale transform stability improved
+// 2016/09/01 Mogwai changed to Manipulate. Draw debug cube. Fixed inverted scale. Mixing scale and translation/rotation gives bad results.
+// 2016/08/31 First version
+//
+// -------------------------------------------------------------------------------------------
+// Future (no order):
+//
+// - Multi view
+// - display rotation/translation/scale infos in local/world space and not only local
+// - finish local/world matrix application
+// - OPERATION as bitmask
+//
+// -------------------------------------------------------------------------------------------
+// Example
+#if 0
+void EditTransform(const Camera& camera, matrix_t& matrix)
+{
+ static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::ROTATE);
+ static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::WORLD);
+ if (ImGui::IsKeyPressed(90))
+ mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
+ if (ImGui::IsKeyPressed(69))
+ mCurrentGizmoOperation = ImGuizmo::ROTATE;
+ if (ImGui::IsKeyPressed(82)) // r Key
+ mCurrentGizmoOperation = ImGuizmo::SCALE;
+ if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE))
+ mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE))
+ mCurrentGizmoOperation = ImGuizmo::ROTATE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE))
+ mCurrentGizmoOperation = ImGuizmo::SCALE;
+ float matrixTranslation[3], matrixRotation[3], matrixScale[3];
+ ImGuizmo::DecomposeMatrixToComponents(matrix.m16, matrixTranslation, matrixRotation, matrixScale);
+ ImGui::InputFloat3("Tr", matrixTranslation, 3);
+ ImGui::InputFloat3("Rt", matrixRotation, 3);
+ ImGui::InputFloat3("Sc", matrixScale, 3);
+ ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix.m16);
+
+ if (mCurrentGizmoOperation != ImGuizmo::SCALE)
+ {
+ if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL))
+ mCurrentGizmoMode = ImGuizmo::LOCAL;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD))
+ mCurrentGizmoMode = ImGuizmo::WORLD;
+ }
+ static bool useSnap(false);
+ if (ImGui::IsKeyPressed(83))
+ useSnap = !useSnap;
+ ImGui::Checkbox("", &useSnap);
+ ImGui::SameLine();
+ vec_t snap;
+ switch (mCurrentGizmoOperation)
+ {
+ case ImGuizmo::TRANSLATE:
+ snap = config.mSnapTranslation;
+ ImGui::InputFloat3("Snap", &snap.x);
+ break;
+ case ImGuizmo::ROTATE:
+ snap = config.mSnapRotation;
+ ImGui::InputFloat("Angle Snap", &snap.x);
+ break;
+ case ImGuizmo::SCALE:
+ snap = config.mSnapScale;
+ ImGui::InputFloat("Scale Snap", &snap.x);
+ break;
+ }
+ ImGuiIO& io = ImGui::GetIO();
+ ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y);
+ ImGuizmo::Manipulate(camera.mView.m16, camera.mProjection.m16, mCurrentGizmoOperation, mCurrentGizmoMode, matrix.m16, NULL, useSnap ? &snap.x : NULL);
+}
+#endif
+#pragma once
+
+#ifdef USE_IMGUI_API
+# include "imconfig.h"
+#endif
+#ifndef IMGUI_API
+# define IMGUI_API
+#endif
+
+// NOTE(hnosm): added so that we don't have to force #include <ImGuizmo.h> after everything else
+#include <imgui.h>
+
+#ifndef IMGUIZMO_NAMESPACE
+# define IMGUIZMO_NAMESPACE ImGuizmo
+#endif
+
+namespace IMGUIZMO_NAMESPACE {
+// call inside your own window and before Manipulate() in order to draw gizmo to that window.
+// Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()).
+IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr);
+
+// call BeginFrame right after ImGui_XXXX_NewFrame();
+IMGUI_API void BeginFrame();
+
+// this is necessary because when imguizmo is compiled into a dll, and imgui into another
+// globals are not shared between them.
+// More details at https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam
+// expose method to set imgui context
+IMGUI_API void SetImGuiContext(ImGuiContext* ctx);
+
+// return true if mouse cursor is over any gizmo control (axis, plan or screen component)
+IMGUI_API bool IsOver();
+
+// return true if mouse IsOver or if the gizmo is in moving state
+IMGUI_API bool IsUsing();
+
+// enable/disable the gizmo. Stay in the state until next call to Enable.
+// gizmo is rendered with gray half transparent color when disabled
+IMGUI_API void Enable(bool enable);
+
+// helper functions for manualy editing translation/rotation/scale with an input float
+// translation, rotation and scale float points to 3 floats each
+// Angles are in degrees (more suitable for human editing)
+// example:
+// float matrixTranslation[3], matrixRotation[3], matrixScale[3];
+// ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale);
+// ImGui::InputFloat3("Tr", matrixTranslation, 3);
+// ImGui::InputFloat3("Rt", matrixRotation, 3);
+// ImGui::InputFloat3("Sc", matrixScale, 3);
+// ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16);
+//
+// These functions have some numerical stability issues for now. Use with caution.
+IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale);
+IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix);
+
+IMGUI_API void SetRect(float x, float y, float width, float height);
+// default is false
+IMGUI_API void SetOrthographic(bool isOrthographic);
+
+// Render a cube with face color corresponding to face normal. Usefull for debug/tests
+IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount);
+IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize);
+
+// call it when you want a gizmo
+// Needs view and projection matrices.
+// matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional
+// translation is applied in world space
+enum OPERATION {
+ TRANSLATE_X = (1u << 0),
+ TRANSLATE_Y = (1u << 1),
+ TRANSLATE_Z = (1u << 2),
+ ROTATE_X = (1u << 3),
+ ROTATE_Y = (1u << 4),
+ ROTATE_Z = (1u << 5),
+ ROTATE_SCREEN = (1u << 6),
+ SCALE_X = (1u << 7),
+ SCALE_Y = (1u << 8),
+ SCALE_Z = (1u << 9),
+ BOUNDS = (1u << 10),
+ SCALE_XU = (1u << 11),
+ SCALE_YU = (1u << 12),
+ SCALE_ZU = (1u << 13),
+
+ TRANSLATE = TRANSLATE_X | TRANSLATE_Y | TRANSLATE_Z,
+ ROTATE = ROTATE_X | ROTATE_Y | ROTATE_Z | ROTATE_SCREEN,
+ SCALE = SCALE_X | SCALE_Y | SCALE_Z,
+ SCALEU = SCALE_XU | SCALE_YU | SCALE_ZU, // universal
+ UNIVERSAL = TRANSLATE | ROTATE | SCALEU
+};
+
+inline OPERATION operator|(OPERATION lhs, OPERATION rhs) {
+ return static_cast<OPERATION>(static_cast<int>(lhs) | static_cast<int>(rhs));
+}
+
+enum MODE {
+ LOCAL,
+ WORLD
+};
+
+IMGUI_API bool Manipulate(
+ const float* view,
+ const float* projection,
+ OPERATION operation,
+ MODE mode,
+ float* matrix,
+ float* deltaMatrix = NULL,
+ const float* snap = NULL,
+ float* localBounds = NULL,
+ const float* boundsSnap = NULL);
+
+//
+// Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en
+// It seems to be a defensive patent in the US. I don't think it will bring troubles using it as
+// other software are using the same mechanics. But just in case, you are now warned!
+//
+IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor);
+
+IMGUI_API void SetID(int id);
+
+// return true if the cursor is over the operation's gizmo
+IMGUI_API bool IsOver(OPERATION op);
+IMGUI_API void SetGizmoSizeClipSpace(float value);
+
+// Allow axis to flip
+// When true (default), the guizmo axis flip for better visibility
+// When false, they always stay along the positive world/local axis
+IMGUI_API void AllowAxisFlip(bool value);
+} // namespace IMGUIZMO_NAMESPACE
diff --git a/source/10-editor-common/ImGuiNotification.cpp b/source/10-editor-common/ImGuiNotification.cpp
new file mode 100644
index 0000000..5d375b3
--- /dev/null
+++ b/source/10-editor-common/ImGuiNotification.cpp
@@ -0,0 +1,277 @@
+// Adapted from https://github.com/patrickcjk/imgui-notify
+#include "ImGuiNotification.hpp"
+
+#include "Macros.hpp"
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui_internal.h>
+
+#include <chrono>
+#include <cstdarg>
+#include <cstdio>
+#include <utility>
+#include <vector>
+
+ImGuiToast::ImGuiToast(ImGuiToastType type, int dismissTime) {
+ IM_ASSERT(type < ImGuiToastType_COUNT);
+
+ mType = type;
+ mDismissTime = dismissTime;
+
+ using namespace std::chrono;
+ auto timeStamp = system_clock::now().time_since_epoch();
+ mCreationTime = duration_cast<milliseconds>(timeStamp).count();
+
+ memset(mTitle, 0, sizeof(mTitle));
+ memset(mContent, 0, sizeof(mContent));
+}
+
+ImGuiToast::ImGuiToast(ImGuiToastType type, const char* format, ...)
+ : ImGuiToast(type) {
+ if (format) {
+ va_list args;
+ va_start(args, format);
+ SetContent(format, args);
+ va_end(args);
+ }
+}
+
+ImGuiToast::ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...)
+ : ImGuiToast(type, dismissTime) {
+ if (format) {
+ va_list args;
+ va_start(args, format);
+ SetContent(format, args);
+ va_end(args);
+ }
+}
+
+void ImGuiToast::SetTitle(const char* format, ...) {
+ if (format) {
+ va_list args;
+ va_start(args, format);
+ SetTitle(format, args);
+ va_end(args);
+ }
+}
+
+void ImGuiToast::SetContent(const char* format, ...) {
+ if (format) {
+ va_list args;
+ va_start(args, format);
+ SetContent(format, args);
+ va_end(args);
+ }
+}
+
+void ImGuiToast::SetType(const ImGuiToastType& type) {
+ IM_ASSERT(type < ImGuiToastType_COUNT);
+ mType = type;
+}
+
+const char* ImGuiToast::GetTitle() {
+ return mTitle;
+}
+
+const char* ImGuiToast::GetDefaultTitle() {
+ if (!strlen(mTitle)) {
+ switch (mType) {
+ case ImGuiToastType_None: return nullptr;
+ case ImGuiToastType_Success: return "Success";
+ case ImGuiToastType_Warning: return "Warning";
+ case ImGuiToastType_Error: return "Error";
+ case ImGuiToastType_Info: return "Info";
+ case ImGuiToastType_COUNT: UNREACHABLE;
+ }
+ }
+
+ return mTitle;
+}
+
+ImGuiToastType ImGuiToast::GetType() {
+ return mType;
+}
+
+ImVec4 ImGuiToast::GetColor() {
+ switch (mType) {
+ case ImGuiToastType_None: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
+ case ImGuiToastType_Success: return ImVec4(0, 1.0f, 0, 1.0f); // Green
+ case ImGuiToastType_Warning: return ImVec4(1.0f, 1.0f, 0, 1.0f); // Yellow
+ case ImGuiToastType_Error: return ImVec4(1.0f, 0, 0, 1.0f); // Red
+ case ImGuiToastType_Info: return ImVec4(0, 0.616, 1.0f, 1.0f); // Blue
+ case ImGuiToastType_COUNT: UNREACHABLE;
+ }
+ return ImVec4();
+}
+
+const char* ImGuiToast::GetIcon() {
+ switch (mType) {
+ case ImGuiToastType_None: return nullptr;
+#if 1
+ // TODO add IconFontHeaders and replace with proper icons
+ case ImGuiToastType_Success: return nullptr;
+ case ImGuiToastType_Warning: return nullptr;
+ case ImGuiToastType_Error: return nullptr;
+ case ImGuiToastType_Info: return nullptr;
+#else
+ case ImGuiToastType_Success: return ICON_FA_CHECK_CIRCLE;
+ case ImGuiToastType_Warning: return ICON_FA_EXCLAMATION_TRIANGLE;
+ case ImGuiToastType_Error: return ICON_FA_TIMES_CIRCLE;
+ case ImGuiToastType_Info: return ICON_FA_INFO_CIRCLE;
+#endif
+ case ImGuiToastType_COUNT: UNREACHABLE;
+ }
+ return nullptr;
+}
+
+const char* ImGuiToast::GetContent() {
+ return this->mContent;
+}
+
+uint64_t ImGuiToast::GetElapsedTime() {
+ using namespace std::chrono;
+ auto timeStamp = system_clock::now().time_since_epoch();
+ auto timeStampI = duration_cast<milliseconds>(timeStamp).count();
+ return timeStampI - mCreationTime;
+}
+
+ImGuiToastPhase ImGuiToast::GetPhase() {
+ const auto elapsed = GetElapsedTime();
+
+ if (elapsed > kNotifyFadeInOutTime + mDismissTime + kNotifyFadeInOutTime) {
+ return ImGuiToastPhase_Expired;
+ } else if (elapsed > kNotifyFadeInOutTime + mDismissTime) {
+ return ImGuiToastPhase_FadeOut;
+ } else if (elapsed > kNotifyFadeInOutTime) {
+ return ImGuiToastPhase_Wait;
+ } else {
+ return ImGuiToastPhase_FadeIn;
+ }
+}
+
+float ImGuiToast::GetFadePercent() {
+ const auto phase = GetPhase();
+ const auto elapsed = GetElapsedTime();
+
+ if (phase == ImGuiToastPhase_FadeIn)
+ {
+ return ((float)elapsed / (float)kNotifyFadeInOutTime) * kNotifyOpacity;
+ } else if (phase == ImGuiToastPhase_FadeOut)
+ {
+ return (1.0f - (((float)elapsed - (float)kNotifyFadeInOutTime - (float)mDismissTime) / (float)kNotifyFadeInOutTime)) * kNotifyOpacity;
+ }
+
+ return 1.0f * kNotifyOpacity;
+}
+
+void ImGuiToast::SetTitle(const char* format, va_list args) {
+ vsnprintf(mTitle, sizeof(mTitle), format, args);
+}
+
+void ImGuiToast::SetContent(const char* format, va_list args) {
+ vsnprintf(mContent, sizeof(mContent), format, args);
+}
+
+namespace ImGui {
+static std::vector<ImGuiToast> notifications;
+}
+
+static bool IsNullOrEmpty(const char* str) {
+ return !str || !strlen(str);
+}
+
+void ImGui::AddNotification(ImGuiToast toast) {
+ notifications.push_back(std::move(toast));
+}
+
+void ImGui::RemoveNotification(int index) {
+ notifications.erase(notifications.begin() + index);
+}
+
+void ImGui::ShowNotifications() {
+ auto vpSize = GetMainViewport()->Size;
+
+ float height = 0.0f;
+ for (auto i = 0; i < notifications.size(); i++) {
+ auto* currentToast = &notifications[i];
+
+ // Remove toast if expired
+ if (currentToast->GetPhase() == ImGuiToastPhase_Expired) {
+ RemoveNotification(i);
+ continue;
+ }
+
+ // Get icon, title and other data
+ const auto icon = currentToast->GetIcon();
+ const auto title = currentToast->GetTitle();
+ const auto content = currentToast->GetContent();
+ const auto defaultTitle = currentToast->GetDefaultTitle();
+ const auto opacity = currentToast->GetFadePercent(); // Get opacity based of the current phase
+
+ // Window rendering
+ auto textColor = currentToast->GetColor();
+ textColor.w = opacity;
+
+ // Generate new unique name for this toast
+ char windowName[50];
+ snprintf(windowName, std::size(windowName), "##TOAST%d", i);
+
+ SetNextWindowBgAlpha(opacity);
+ SetNextWindowPos(ImVec2(vpSize.x - kNotifyPaddingX, vpSize.y - kNotifyPaddingY - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f));
+ Begin(windowName, nullptr, kNotifyToastFlags);
+ BringWindowToDisplayFront(GetCurrentWindow());
+
+ // Here we render the toast content
+ {
+ PushTextWrapPos(vpSize.x / 3.0f); // We want to support multi-line text, this will wrap the text after 1/3 of the screen width
+
+ bool wasTitleRendered = false;
+
+ // If an icon is set
+ if (!::IsNullOrEmpty(icon)) {
+ // Render icon text
+ PushStyleColor(ImGuiCol_Text, textColor);
+ TextUnformatted(icon);
+ PopStyleColor();
+ wasTitleRendered = true;
+ }
+
+ // If a title is set
+ if (!::IsNullOrEmpty(title)) {
+ // If a title and an icon is set, we want to render on same line
+ if (!::IsNullOrEmpty(icon))
+ SameLine();
+
+ TextUnformatted(title); // Render title text
+ wasTitleRendered = true;
+ } else if (!::IsNullOrEmpty(defaultTitle)) {
+ if (!::IsNullOrEmpty(icon))
+ SameLine();
+
+ TextUnformatted(defaultTitle); // Render default title text (ImGuiToastType_Success -> "Success", etc...)
+ wasTitleRendered = true;
+ }
+
+ // In case ANYTHING was rendered in the top, we want to add a small padding so the text (or icon) looks centered vertically
+ if (wasTitleRendered && !::IsNullOrEmpty(content)) {
+ SetCursorPosY(GetCursorPosY() + 5.0f); // Must be a better way to do this!!!!
+ }
+
+ // If a content is set
+ if (!::IsNullOrEmpty(content)) {
+ if (wasTitleRendered) {
+ Separator();
+ }
+
+ TextUnformatted(content); // Render content text
+ }
+
+ PopTextWrapPos();
+ }
+
+ // Save height for next toasts
+ height += GetWindowHeight() + kNotifyPaddingMessageY;
+
+ End();
+ }
+}
diff --git a/source/10-editor-common/ImGuiNotification.hpp b/source/10-editor-common/ImGuiNotification.hpp
new file mode 100644
index 0000000..3af8c2d
--- /dev/null
+++ b/source/10-editor-common/ImGuiNotification.hpp
@@ -0,0 +1,81 @@
+// Adapted from https://github.com/patrickcjk/imgui-notify
+#pragma once
+
+#include <imgui.h>
+#include <cstdint>
+
+enum ImGuiToastType {
+ ImGuiToastType_None,
+ ImGuiToastType_Success,
+ ImGuiToastType_Warning,
+ ImGuiToastType_Error,
+ ImGuiToastType_Info,
+ ImGuiToastType_COUNT
+};
+
+enum ImGuiToastPhase {
+ ImGuiToastPhase_FadeIn,
+ ImGuiToastPhase_Wait,
+ ImGuiToastPhase_FadeOut,
+ ImGuiToastPhase_Expired,
+ ImGuiToastPhase_COUNT
+};
+
+enum ImGuiToastPos {
+ ImGuiToastPos_TopLeft,
+ ImGuiToastPos_TopCenter,
+ ImGuiToastPos_TopRight,
+ ImGuiToastPos_BottomLeft,
+ ImGuiToastPos_BottomCenter,
+ ImGuiToastPos_BottomRight,
+ ImGuiToastPos_Center,
+ ImGuiToastPos_COUNT
+};
+
+constexpr int kNotifyMaxMsgLength = 4096; // Max message content length
+constexpr float kNotifyPaddingX = 20.0f; // Bottom-left X padding
+constexpr float kNotifyPaddingY = 20.0f; // Bottom-left Y padding
+constexpr float kNotifyPaddingMessageY = 10.0f; // Padding Y between each message
+constexpr uint64_t kNotifyFadeInOutTime = 150; // Fade in and out duration
+constexpr uint64_t kNotifyDefaultDismiss = 3000; // Auto dismiss after X ms (default, applied only of no data provided in constructors)
+constexpr float kNotifyOpacity = 1.0f; // 0-1 Toast opacity
+constexpr ImGuiWindowFlags kNotifyToastFlags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing;
+
+class ImGuiToast {
+private:
+ ImGuiToastType mType = ImGuiToastType_None;
+ char mTitle[kNotifyMaxMsgLength] = {};
+ char mContent[kNotifyMaxMsgLength] = {};
+ int mDismissTime = kNotifyDefaultDismiss;
+ uint64_t mCreationTime = 0;
+
+public:
+ ImGuiToast(ImGuiToastType type, int dismissTime = kNotifyDefaultDismiss);
+ ImGuiToast(ImGuiToastType type, const char* format, ...);
+ ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...);
+
+ void SetTitle(const char* format, ...);
+ void SetContent(const char* format, ...);
+ void SetType(const ImGuiToastType& type);
+
+ const char* GetTitle();
+ const char* GetDefaultTitle();
+ ImGuiToastType GetType();
+ ImVec4 GetColor();
+ const char* GetIcon();
+ const char* GetContent();
+
+ uint64_t GetElapsedTime();
+ ImGuiToastPhase GetPhase();
+ float GetFadePercent();
+
+private:
+ void SetTitle(const char* format, va_list args);
+ void SetContent(const char* format, va_list args);
+};
+
+namespace ImGui {
+void AddNotification(ImGuiToast toast);
+void RemoveNotification(int index);
+void ShowNotifications();
+} // namespace ImGui
diff --git a/source/20-codegen-compiler/CodegenConfig.hpp b/source/20-codegen-compiler/CodegenConfig.hpp
new file mode 100644
index 0000000..b9dc56c
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenConfig.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#ifndef CODEGEN_DEBUG_PRINT
+# define CODEGEN_DEBUG_PRINT 0
+#endif
+
+#if CODEGEN_DEBUG_PRINT
+# define DEBUG_PRINTF(...) printf(__VA_ARGS__)
+#else
+# define DEBUG_PRINTF(...)
+#endif
diff --git a/source/20-codegen-compiler/CodegenDecl.cpp b/source/20-codegen-compiler/CodegenDecl.cpp
new file mode 100644
index 0000000..11e1bb5
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenDecl.cpp
@@ -0,0 +1,74 @@
+#include "CodegenDecl.hpp"
+
+#include "CodegenUtils.hpp"
+
+#include <fmt/format.h>
+#include <Utils.hpp>
+
+const std::string& DeclStruct::GetMangledName() const {
+ if (mangledName.empty()) {
+ mangledName = Utils::MakeMangledName(name, container);
+ }
+ return mangledName;
+}
+
+std::string DeclXGlobalVar::MangleCtorName(std::string_view targetName) {
+ return fmt::format("{}_MANGLED_ctor", targetName);
+}
+
+std::string DeclXGlobalVar::MangleDtorName(std::string_view targetName) {
+ return fmt::format("{}_MANGLED_dtor", targetName);
+}
+
+const std::string& DeclEnum::GetMangledName() const {
+ if (mangledName.empty()) {
+ mangledName = Utils::MakeMangledName(name, container);
+ }
+ return mangledName;
+}
+
+static EnumValuePattern NextPattern(EnumValuePattern val) {
+ return (EnumValuePattern)(val + 1);
+}
+
+EnumValuePattern DeclEnum::CalcPattern() const {
+ if (elements.empty()) return EVP_Continuous;
+
+ auto pattern = EVP_Continuous;
+restart:
+ auto lastVal = elements[0].value;
+ for (size_t i = 1; i < elements.size(); ++i) {
+ auto currVal = elements[i].value;
+ switch (pattern) {
+ case EVP_Continuous: {
+ bool satisfy = lastVal + 1 == currVal;
+ if (!satisfy) {
+ pattern = NextPattern(pattern);
+ goto restart;
+ }
+ } break;
+
+ case EVP_Bits: {
+ bool satisfy = (lastVal << 1) == currVal;
+ if (!satisfy) {
+ pattern = NextPattern(pattern);
+ goto restart;
+ }
+ } break;
+
+ // A random pattern can match anything
+ case EVP_Random:
+ case EVP_COUNT: break;
+ }
+ lastVal = currVal;
+ }
+
+ return pattern;
+}
+
+EnumValuePattern DeclEnum::GetPattern() const {
+ if (pattern == EVP_COUNT) {
+ pattern = CalcPattern();
+ }
+ return pattern;
+}
diff --git a/source/20-codegen-compiler/CodegenDecl.hpp b/source/20-codegen-compiler/CodegenDecl.hpp
new file mode 100644
index 0000000..f1ac5b1
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenDecl.hpp
@@ -0,0 +1,154 @@
+#pragma once
+
+#include "CodegenOutput.hpp"
+
+#include <string>
+#include <vector>
+
+// TODO replace std::string name with std::string_view into the token storage?
+
+struct SourceFile {
+ std::string filename;
+ CodegenOutput preHeaderOutput;
+ CodegenOutput postHeaderOutput;
+ CodegenOutput postSourceOutput;
+ CodegenOutput tuOutput; // "tu" = Translation Unit, produces a separately compiled .cpp file
+ bool header = false;
+ /// Whether this file is being reprocessed in this invocation of codegen.exe or not.
+ bool reprocessing = false;
+};
+
+struct DeclNamespace {
+ // NOTE: namespace doesn't have a source file field, because the same namespace can be "reopened" in multipled files
+
+ DeclNamespace* container = nullptr;
+ std::string name;
+ const std::string* fullname = nullptr; // View into storage map key
+};
+
+struct DeclStruct;
+struct DeclMemberVariable {
+ DeclStruct* containerStruct = nullptr;
+ std::string name;
+ std::string type;
+ std::string getterName;
+ std::string setterName;
+ bool isGetterGenerated = false;
+ bool isSetterGenerated = false;
+};
+struct DeclMemberFunction {
+ DeclStruct* containerStruct = nullptr;
+ // TODO
+};
+
+// Structs or classes
+struct DeclStruct {
+ SourceFile* sourceFile = nullptr;
+ DeclNamespace* container = nullptr;
+ std::vector<const DeclStruct*> baseClasses;
+ std::vector<DeclMemberVariable> memberVariables;
+ std::vector<DeclMemberVariable> generatedVariables;
+ std::vector<DeclMemberFunction> memberFunctions;
+ std::vector<DeclMemberFunction> generatedFunctions;
+ std::string name;
+ mutable std::string mangledName;
+ const std::string* fullname = nullptr; // View into storage map key
+
+ // Scanned generation options
+ bool generating : 1 = false;
+ bool generatingInheritanceHiearchy : 1 = false;
+
+ const std::string& GetName() const { return name; }
+ const std::string& GetFullName() const { return *fullname; }
+ const std::string& GetMangledName() const;
+};
+
+struct DeclXGlobalVar {
+ std::string name;
+ bool hasCtor = false;
+ bool hasDtor = false;
+
+ static std::string MangleCtorName(std::string_view targetName);
+ std::string GetMangledCtorName() const { return MangleCtorName(name); }
+ static std::string MangleDtorName(std::string_view targetName);
+ std::string GetMangledDtorName() const { return MangleDtorName(name); }
+};
+
+enum EnumUnderlyingType {
+ EUT_Int8,
+ EUT_Int16,
+ EUT_Int32,
+ EUT_Int64,
+ EUT_Uint8,
+ EUT_Uint16,
+ EUT_Uint32,
+ EUT_Uint64,
+ EUT_COUNT,
+};
+
+enum EnumValuePattern {
+ // The numbers cover n..m with no gaps
+ EVP_Continuous,
+ // The numbers cover for i in n..m, 1 << i
+ // e.g. [0] = 1 << 0,
+ // [1] = 1 << 1.
+ // [2] = 1 << 2. etc.
+ EVP_Bits,
+ // The numbesr don't have a particular pattern
+ EVP_Random,
+ EVP_COUNT,
+};
+
+struct DeclEnumElement {
+ std::string name;
+ // TODO support int64_t, etc. enum underlying types
+ uint64_t value;
+};
+
+struct DeclEnum {
+ SourceFile* sourceFile = nullptr;
+ DeclNamespace* container = nullptr;
+ std::string name;
+ mutable std::string mangledName;
+ const std::string* fullname = nullptr; // View into storage map key
+ std::vector<DeclEnumElement> elements;
+ EnumUnderlyingType underlyingType;
+ // Start with invalid value, calculate on demand
+ mutable EnumValuePattern pattern = EVP_COUNT;
+
+ // TODO replace this with a regex?
+ std::string generateRemovingPrefix;
+ std::string generatingAddingPrefix;
+ // NOTE: this flag acts as a gate for every specific generating option, must be enabled for them to work
+ bool generating : 1 = false;
+ bool generateToString : 1 = false;
+ bool generateFromString : 1 = false;
+ // NOTE: see GenerateForEnum() for the exact heuristics
+ bool generateExcludeUseHeuristics : 1 = false;
+
+ const std::string& GetName() const { return name; }
+ const std::string& GetFullName() const { return *fullname; }
+ const std::string& GetMangledName() const;
+
+ std::string_view GetUnderlyingTypeName() const;
+
+ EnumValuePattern CalcPattern() const;
+ EnumValuePattern GetPattern() const;
+};
+
+struct DeclFunctionArgument {
+ std::string type;
+ std::string name;
+};
+
+struct DeclFunction {
+ SourceFile* sourceFile = nullptr;
+ DeclNamespace* container = nullptr;
+ // Things like extern, static, etc. that gets written before the function return type
+ std::string prefix;
+ std::string name;
+ const std::string* fullname = nullptr; // View into storage map key
+ std::string returnType;
+ std::vector<DeclFunctionArgument> arguments;
+ std::string body;
+};
diff --git a/source/20-codegen-compiler/CodegenLexer.cpp b/source/20-codegen-compiler/CodegenLexer.cpp
new file mode 100644
index 0000000..ecb2186
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenLexer.cpp
@@ -0,0 +1,202 @@
+#include "CodegenLexer.hpp"
+
+#include <cassert>
+
+int StbLexerToken::Reamalgamate() const {
+ if (type == CLEX_ext_single_char) {
+ return text[0];
+ } else {
+ return type;
+ }
+}
+
+bool StbTokenIsSingleChar(int lexerToken) {
+ return lexerToken >= 0 && lexerToken < 256;
+}
+
+bool StbTokenIsMultiChar(int lexerToken) {
+ return !StbTokenIsMultiChar(lexerToken);
+}
+
+std::string CombineTokens(std::span<const StbLexerToken> tokens, std::string_view separator) {
+ if (tokens.empty()) {
+ return {};
+ }
+
+ size_t length = 0;
+ for (auto& token : tokens) {
+ length += token.text.size();
+ length += separator.size();
+ }
+ // Intentionally counting an extra separator: leave space for the last append below
+
+ std::string result;
+ result.reserve(length);
+ for (auto& token : tokens) {
+ result += token.text;
+ result += separator;
+ }
+ // Remove the trailing separator
+ result.resize(result.size() - separator.size());
+
+ return result;
+}
+
+const StbLexerToken& CodegenLexer::Current() const {
+ assert(idx < tokens.size());
+ return tokens[idx];
+}
+
+void CodegenLexer::InitializeFrom(std::string_view source) {
+ this->tokens = {};
+ this->idx = 0;
+
+ stb_lexer lexer;
+ char stringStorage[65536];
+ const char* srcBegin = source.data();
+ const char* srcEnd = srcBegin + source.length();
+ stb_c_lexer_init(&lexer, srcBegin, srcEnd, stringStorage, sizeof(stringStorage));
+
+ struct TokenCombiningPattern {
+ StbLexerToken result;
+ char matchChars[16];
+ };
+
+ const TokenCombiningPattern kDoubleColon = {
+ .result = {
+ .text = "::",
+ .type = CLEX_ext_double_colon,
+ },
+ .matchChars = { ':', ':', '\0' },
+ };
+ const TokenCombiningPattern kDotDotDot = {
+ .result = {
+ .text = "...",
+ .type = CLEX_ext_dot_dot_dot,
+ },
+ .matchChars = { '.', '.', '.', '\0' },
+ };
+
+ const TokenCombiningPattern* currentState = nullptr;
+ int currentStateCharIdx = 0;
+
+ while (true) {
+ // See stb_c_lexer.h's comments, here are a few additinos that aren't made clear in the file:
+ // - `lexer->token` (noted as "token" below) after calling stb_c_lexer_get_token() contains either:
+ // 1. 0 <= token < 256: an ASCII character (more precisely a single char that the lexer ate; technically can be an incomplete code unit)
+ // 2. token < 0: an unknown token
+ // 3. One of the `CLEX_*` enums: a special, recognized token such as an operator
+
+ int stbToken = stb_c_lexer_get_token(&lexer);
+ if (stbToken == 0) {
+ // EOF
+ break;
+ }
+
+ if (lexer.token == CLEX_parse_error) {
+ printf("[ERROR] stb_c_lexer countered a parse error.\n");
+ // TODO how to handle?
+ continue;
+ }
+
+ StbLexerToken token;
+ if (StbTokenIsSingleChar(lexer.token)) {
+ char c = lexer.token;
+
+ token.type = CLEX_ext_single_char;
+ token.text = std::string(1, c);
+
+ if (!currentState) {
+#define TRY_START_MATCH(states) \
+ if (states.matchChars[0] == c) { \
+ currentState = &states; \
+ currentStateCharIdx = 1; \
+ }
+ TRY_START_MATCH(kDoubleColon);
+ TRY_START_MATCH(kDotDotDot);
+#undef TRY_START_MATCH
+ } else {
+ if (currentState->matchChars[currentStateCharIdx] == c) {
+ // Match success
+ ++currentStateCharIdx;
+
+ // If we matched all of the chars...
+ if (currentState->matchChars[currentStateCharIdx] == '\0') {
+ // We matched (currentStateCharIdx) tokens though this one is pushed into the vector, leaving (currentStateCharIdx - 1) tokens to be removed
+ for (int i = 0, count = currentStateCharIdx - 1; i < count; ++i) {
+ tokens.pop_back();
+ }
+
+ // Set the current token to desired result
+ token = currentState->result;
+
+ currentState = nullptr;
+ currentStateCharIdx = 0;
+ }
+ } else {
+ // Match fail, reset
+
+ currentState = nullptr;
+ currentStateCharIdx = 0;
+ }
+ }
+ } else {
+ token.type = lexer.token;
+ // WORKAROUND: use null terminated string, stb_c_lexer doens't set string_len properly when parsing identifiers
+ token.text = std::string(lexer.string);
+
+ switch (token.type) {
+ case CLEX_intlit:
+ token.lexerIntNumber = lexer.int_number;
+ break;
+
+ case CLEX_floatlit:
+ token.lexerRealNumber = lexer.real_number;
+ break;
+ }
+ }
+ tokens.push_back(std::move(token));
+ token = {};
+ }
+}
+
+const StbLexerToken* CodegenLexer::TryConsumeToken(int type) {
+ auto& token = tokens[idx];
+ if (token.type == type) {
+ ++idx;
+ return &token;
+ }
+ return nullptr;
+}
+
+const StbLexerToken* CodegenLexer::TryConsumeSingleCharToken(char c) {
+ auto& token = tokens[idx];
+ if (token.type == CLEX_ext_single_char &&
+ token.text[0] == c)
+ {
+ ++idx;
+ return &token;
+ }
+ return nullptr;
+}
+
+void CodegenLexer::SkipUntilToken(int type) {
+ while (idx < tokens.size()) {
+ if (Current().type == type) {
+ break;
+ }
+ ++idx;
+ }
+}
+
+void CodegenLexer::SkipUntilTokenSingleChar(char c) {
+ while (idx < tokens.size()) {
+ auto& curr = Current();
+ if (curr.type == CLEX_ext_single_char &&
+ curr.text[0] == c)
+ {
+ break;
+ }
+ ++idx;
+ }
+}
diff --git a/source/20-codegen-compiler/CodegenLexer.hpp b/source/20-codegen-compiler/CodegenLexer.hpp
new file mode 100644
index 0000000..ec8c8b7
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenLexer.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <LookupTable.hpp>
+
+#include <stb_c_lexer.h>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+enum {
+ CLEX_ext_single_char = CLEX_first_unused_token,
+ CLEX_ext_double_colon,
+ CLEX_ext_dot_dot_dot,
+ CLEX_ext_COUNT,
+};
+
+struct StbLexerToken {
+ std::string text;
+
+ union {
+ double lexerRealNumber;
+ long lexerIntNumber;
+ };
+
+ // Can either be CLEX_* or CLEX_ext_* values
+ int type;
+
+ int Reamalgamate() const;
+};
+
+bool StbTokenIsSingleChar(int lexerToken);
+bool StbTokenIsMultiChar(int lexerToken);
+std::string CombineTokens(std::span<const StbLexerToken> tokens, std::string_view separator = {});
+
+struct CodegenLexer {
+ std::vector<StbLexerToken> tokens;
+ size_t idx = 0;
+
+ void InitializeFrom(std::string_view source);
+
+ const StbLexerToken& Current() const;
+
+ const StbLexerToken* TryConsumeToken(int type);
+ const StbLexerToken* TryConsumeSingleCharToken(char c);
+
+ void SkipUntilToken(int type);
+ void SkipUntilTokenSingleChar(char c);
+};
diff --git a/source/20-codegen-compiler/CodegenModel.cpp b/source/20-codegen-compiler/CodegenModel.cpp
new file mode 100644
index 0000000..303ad4e
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenModel.cpp
@@ -0,0 +1,732 @@
+#include "CodegenModel.hpp"
+
+#include "CodegenUtils.hpp"
+#include "SQLiteHelper.hpp"
+
+#include <Macros.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <robin_hood.h>
+#include <cassert>
+#include <cstdint>
+#include <stdexcept>
+#include <string>
+#include <variant>
+
+using namespace std::literals;
+
+// TODO only delete unused records from model instead of regenerating all records every time
+
+struct SomeDecl {
+ std::variant<DeclStruct, DeclFunction, DeclEnum> v;
+};
+
+class CodegenRuntimeModel::Private {
+ friend class CodegenArchiveModel;
+
+public:
+ // We want address stability for everything
+ robin_hood::unordered_node_map<std::string, SomeDecl, StringHash, StringEqual> decls;
+ robin_hood::unordered_node_map<std::string, DeclNamespace, StringHash, StringEqual> namespaces;
+};
+
+// A number for `PRAGMA user_vesrion`, representing the current database version. Increment when the table format changes.
+#define CURRENT_DATABASE_VERSION 1
+constexpr int64_t kGlobalNamespaceId = 1;
+
+namespace {
+void PrintErrMsgIfPresent(char*& errMsg) {
+ if (errMsg) {
+ printf("SQLite error: %s\n", errMsg);
+ sqlite3_free(errMsg);
+ }
+}
+} // namespace
+
+class CodegenArchiveModel::Private {
+ friend class CodegenRuntimeModel;
+
+public:
+ // NOTE: this must be the first field, because we want it to destruct after all other statement fields
+ SQLiteDatabase database;
+ /* Core Statements */
+ SQLiteStatement beginTransactionStmt;
+ SQLiteStatement commitTransactionStmt;
+ SQLiteStatement rollbackTransactionStmt;
+ SQLiteStatement findFileStmt;
+ SQLiteStatement storeFileStmt;
+ SQLiteStatement findNamespaceStmt;
+ SQLiteStatement getNamespaceStmt;
+ SQLiteStatement storeNamespaceStmt;
+ /* Component Statements, initalized on demand */
+ SQLiteStatement storeStructStmt;
+ SQLiteStatement storeStructBaseClassStmt;
+ SQLiteStatement storeStructPropertyStmt;
+ // TODO store method
+ SQLiteStatement storeEnumStmt;
+ SQLiteStatement storeEnumElmStmt;
+ SQLiteStatement deleteFunctionDeclByFilenameStmt;
+ SQLiteStatement deleteStructDeclByFilenameStmt;
+ SQLiteStatement deleteEnumDeclByFilenameStmt;
+ // TODO
+ // SQLiteStatement getRootClassStmt;
+
+ void InitializeDatabase() {
+ char* errMsg = nullptr;
+
+ int result = sqlite3_exec(database, "PRAGMA user_version = " STRINGIFY(CURRENT_DATABASE_VERSION), nullptr, nullptr, &errMsg);
+ PrintErrMsgIfPresent(errMsg);
+ assert(result == SQLITE_OK);
+
+ // TODO unique with overloading, and container structs
+ result = sqlite3_exec(database, R"""(
+BEGIN TRANSACTION;
+CREATE TABLE Files(
+ -- NOTE: SQLite forbids foreign keys referencing the implicit `rowid` column, we have to create an alias for it
+ Id INTEGER PRIMARY KEY,
+ FileName TEXT,
+ UNIQUE (FileName)
+);
+
+CREATE TABLE Namespaces(
+ Id INTEGER PRIMARY KEY,
+ ParentNamespaceId INTEGER REFERENCES Namespaces(Id),
+ Name TEXT,
+ UNIQUE (ParentNamespaceId, Name)
+);
+
+CREATE TABLE DeclFunctions(
+ Id INTEGER PRIMARY KEY,
+ FileId INTEGER REFERENCES Files(Id) ON DELETE CASCADE,
+ NamespaceId INTEGER REFERENCES Namespaces(Id),
+ Name TEXT
+);
+CREATE TABLE DeclFunctionParameters(
+ FunctionId INTEGER REFERENCES DeclFunctions(Id) ON DELETE CASCADE,
+ Name TEXT,
+ Type TEXT,
+ UNIQUE (FunctionId, Name)
+);
+
+CREATE TABLE DeclStructs(
+ Id INTEGER PRIMARY KEY,
+ FileId INTEGER REFERENCES Files(Id) ON DELETE CASCADE,
+ NamespaceId INTEGER REFERENCES Namespaces(Id),
+ Name TEXT,
+ IsMetadataMarked INTEGER
+);
+CREATE TABLE DeclStructBaseClassRelations(
+ StructId INTEGER REFERENCES DeclStructs(Id) ON DELETE CASCADE,
+ -- NOTE: intentionally not foreign keys, because we want relations to still exist even if the base class is deleted
+ -- we do validation after a complete regeneration pass, on reads
+ ParentStructNamespaceId INTEGER,
+ ParentStructName TEXT,
+ UNIQUE (StructId, ParentStructNamespaceId, ParentStructName)
+);
+CREATE TABLE DeclStructProperties(
+ StructId INTEGER REFERENCES DeclStructs(Id) ON DELETE CASCADE,
+ Name TEXT,
+ Type TEXT,
+ -- NOTE: getter and setter may or may not be methods; search the DeclStructMethods table if needed
+ GetterName TEXT,
+ SetterName TEXT,
+ IsPlainField INTEGER GENERATED ALWAYS AS (GetterName = '' AND SetterName = '') VIRTUAL,
+ IsMetadataMarked INTEGER
+);
+CREATE TABLE DeclStructMethods(
+ Id INTEGER PRIMARY KEY,
+ StructId INTEGER REFERENCES DeclStructs(Id) ON DELETE CASCADE,
+ Name TEXT,
+ Type TEXT,
+ IsConst INTEGER,
+ IsMetadataMarked INTEGER
+);
+CREATE TABLE DeclStructMethodParameters(
+ MethodId INTEGER REFERENCES DeclStructMethods(Id) ON DELETE CASCADE,
+ Name TEXT,
+ Type TEXT,
+ UNIQUE (MethodId, Name)
+);
+
+CREATE TABLE DeclEnums(
+ Id INTEGER PRIMARY KEY,
+FileId INTEGER REFERENCES Files(Id) ON DELETE CASCADE,
+ NamespaceId INTEGER REFERENCES Namespaces(Id),
+ Name TEXT,
+ UnderlyingType TEXT
+);
+CREATE TABLE DeclEnumElements(
+ EnumId INTEGER REFERENCES DeclEnums(Id) ON DELETE CASCADE,
+ Name TEXT,
+ Value INTEGER,
+ UNIQUE (EnumId, Name)
+);
+
+CREATE INDEX Index_DeclFunctions_FileId ON DeclFunctions(FileId);
+CREATE INDEX Index_DeclStructs_FileId ON DeclStructs(FileId);
+CREATE INDEX Index_DeclEnums_FileId ON DeclEnums(FileId);
+
+CREATE UNIQUE INDEX Index_DeclFunctions_Identity ON DeclFunctions(NamespaceId, Name);
+
+CREATE UNIQUE INDEX Index_DeclStruct_Identity ON DeclStructs(NamespaceId, Name);
+CREATE UNIQUE INDEX Index_DeclStructProperties_Identity ON DeclStructProperties(StructId, Name);
+CREATE UNIQUE INDEX Index_DeclStructMethods_Identity ON DeclStructMethods(StructId, Name);
+
+CREATE UNIQUE INDEX Index_DeclEnums_Identity ON DeclEnums(NamespaceId, Name);
+
+-- Special global namespace that has no parent, and Id should always be 1
+INSERT INTO Namespaces(Id, ParentNamespaceId, Name)
+VALUES (1, NULL, '<global namespace>');
+
+COMMIT TRANSACTION;
+)""",
+ nullptr,
+ nullptr,
+ &errMsg);
+ PrintErrMsgIfPresent(errMsg);
+ assert(result == SQLITE_OK);
+ }
+
+ void BeginTransaction() {
+ int result = sqlite3_step(beginTransactionStmt);
+ assert(result == SQLITE_DONE);
+ sqlite3_reset(beginTransactionStmt);
+ }
+
+ void CommitTransaction() {
+ int result = sqlite3_step(commitTransactionStmt);
+ assert(result == SQLITE_DONE);
+ sqlite3_reset(commitTransactionStmt);
+ }
+
+ void RollbackTransaction() {
+ int result = sqlite3_step(rollbackTransactionStmt);
+ assert(result == SQLITE_DONE);
+ sqlite3_reset(rollbackTransactionStmt);
+ }
+
+ /// \return Row ID of the namespace, or 0 if it currently doesn't exist.
+ int64_t FindNamespace(const DeclNamespace* ns) {
+ if (!ns) {
+ return kGlobalNamespaceId;
+ }
+
+ return FindNamespaceImpl(*ns);
+ }
+
+ /// \return Row ID of the namespace.
+ int64_t FindOrStoreNamespace(const DeclNamespace* ns) {
+ if (!ns) {
+ return kGlobalNamespaceId;
+ }
+
+ if (auto rowId = FindNamespaceImpl(*ns); rowId != 0) {
+ return rowId;
+ }
+
+ SQLiteRunningStatement rt(storeNamespaceStmt);
+ rt.BindArguments(FindOrStoreNamespace(ns->container), ns->name);
+
+ rt.StepAndCheck(SQLITE_ROW);
+
+ auto [nsId] = rt.ResultColumns<int64_t>();
+ return nsId;
+ }
+
+ std::string GetNamespaceFullName(int64_t nsId) const {
+ return GetNamespaceFullNameImpl(nsId, nullptr, 0);
+ }
+
+ std::string GetDeclFullName(int64_t nsId, std::string_view declName) const {
+ return GetNamespaceFullNameImpl(nsId, declName.data(), declName.size());
+ }
+
+ /// \return Row ID of the file, or 0 if it currently doesn't exist.
+ int64_t FindFile(std::string_view filename) {
+ SQLiteRunningStatement rt(findFileStmt);
+ rt.BindArguments(filename);
+
+ int result = rt.Step();
+
+ if (result == SQLITE_ROW) {
+ auto [fileId] = rt.ResultColumns<int64_t>();
+ return fileId;
+ } else {
+ return 0;
+ }
+ }
+
+ /// \return Row ID of the file
+ int64_t FindOrStoreFile(std::string_view filename) {
+ if (auto id = FindFile(filename); id != 0) {
+ return id;
+ }
+
+ SQLiteRunningStatement rt(storeFileStmt);
+ rt.BindArguments(filename);
+
+ rt.StepAndCheck(SQLITE_ROW);
+
+ auto [fileId] = rt.ResultColumns<int64_t>();
+ return fileId;
+ }
+
+ /// \return Row ID of the file, or 0 if not found.
+ int64_t FindOrStoreFile(/*nullable*/ const SourceFile* file) {
+ if (!file) {
+ return 0;
+ }
+ return FindOrStoreFile(file->filename);
+ }
+
+private:
+ // TODO maybe merge with Utils::MakeFullName?
+ std::string GetNamespaceFullNameImpl(int64_t nsId, const char* append, size_t appendLength) const {
+ std::vector<std::string> namespaceNames;
+ size_t fullnameLength = 0;
+
+ sqlite3_stmt* stmt = getNamespaceStmt;
+ int64_t currentNsId = nsId;
+ while (true) {
+ SQLiteRunningStatement rt(getNamespaceStmt);
+ rt.BindArguments(currentNsId);
+
+ rt.StepAndCheck(SQLITE_ROW);
+
+ auto [id, parentNamespaceId, name] = rt.ResultColumns<int64_t, int64_t, std::string>();
+ currentNsId = parentNamespaceId;
+ fullnameLength += name.size() + 2;
+ namespaceNames.push_back(std::move(name));
+
+ if (parentNamespaceId == kGlobalNamespaceId) {
+ break;
+ }
+ }
+ if (append) {
+ // Already has the '::' at the end
+ fullnameLength += appendLength;
+ } else {
+ fullnameLength -= 2;
+ }
+
+ std::string fullname;
+ fullname.reserve(fullnameLength);
+
+ for (auto it = namespaceNames.rbegin(); it != namespaceNames.rend(); ++it) {
+ fullname.append(*it);
+ if (append || std::next(it) != namespaceNames.rend()) {
+ fullname.append("::");
+ }
+ }
+ if (append) {
+ fullname += std::string_view(append, appendLength);
+ }
+
+ return fullname;
+ }
+
+ int64_t FindNamespaceImpl(const DeclNamespace& ns) {
+ int64_t parentNsRowId;
+ if (ns.container) {
+ parentNsRowId = FindNamespaceImpl(*ns.container);
+ if (parentNsRowId == 0) {
+ // Parent namespace doesn't exist in database, shortcircuit
+ return 0;
+ }
+ } else {
+ parentNsRowId = kGlobalNamespaceId;
+ }
+
+ return FindNamespaceImpl(ns, parentNsRowId);
+ }
+
+ int64_t FindNamespaceImpl(const DeclNamespace& ns, int64_t parentNsRowId) {
+ sqlite3_stmt* stmt = findNamespaceStmt;
+ SQLiteRunningStatement rt(findNamespaceStmt);
+ rt.BindArguments(parentNsRowId, ns.name);
+
+ int result = rt.Step();
+ if (result == SQLITE_ROW) {
+ auto [nsId] = rt.ResultColumns<int64_t>();
+ return nsId;
+ } else {
+ return 0;
+ }
+ }
+};
+
+CodegenRuntimeModel::CodegenRuntimeModel()
+ : m{ new Private() } //
+{
+}
+
+CodegenRuntimeModel::~CodegenRuntimeModel() {
+ delete m;
+}
+
+#define STORE_DECL_OF_TYPE(DeclType, fullname, decl) \
+ auto [iter, success] = m->decls.try_emplace(std::move(fullname), SomeDecl{ .v = std::move(decl) }); \
+ auto& key = iter->first; \
+ auto& val = iter->second; \
+ auto& declRef = std::get<DeclType>(val.v); \
+ declRef.fullname = &key; \
+ return &declRef
+
+DeclEnum* CodegenRuntimeModel::AddEnum(std::string fullname, DeclEnum decl) {
+#if CODEGEN_DEBUG_PRINT
+ printf("Committed enum '%s'\n", decl.name.c_str());
+ for (auto& elm : decl.elements) {
+ printf(" - element %s = %" PRId64 "\n", elm.name.c_str(), elm.value);
+ }
+#endif
+
+ STORE_DECL_OF_TYPE(DeclEnum, fullname, decl);
+}
+
+DeclStruct* CodegenRuntimeModel::AddStruct(std::string fullname, DeclStruct decl) {
+#if CODEGEN_DEBUG_PRINT
+ printf("Committed struct '%s'\n", decl.name.c_str());
+ printf(" Base classes:\n");
+ for (auto& base : decl.baseClasses) {
+ printf(" - %.*s\n", PRINTF_STRING_VIEW(base->name));
+ }
+#endif
+
+ STORE_DECL_OF_TYPE(DeclStruct, fullname, decl);
+}
+
+#define FIND_DECL_OF_TYPE(DeclType) \
+ auto iter = m->decls.find(name); \
+ if (iter != m->decls.end()) { \
+ auto& some = iter->second.v; \
+ if (auto decl = std::get_if<DeclType>(&some)) { \
+ return decl; \
+ } \
+ } \
+ return nullptr
+
+const DeclEnum* CodegenRuntimeModel::FindEnum(std::string_view name) const {
+ FIND_DECL_OF_TYPE(DeclEnum);
+}
+
+const DeclStruct* CodegenRuntimeModel::FindStruct(std::string_view name) const {
+ FIND_DECL_OF_TYPE(DeclStruct);
+}
+
+DeclNamespace* CodegenRuntimeModel::AddNamespace(DeclNamespace ns) {
+ auto path = Utils::MakeFullName(""sv, &ns);
+ auto [iter, success] = m->namespaces.try_emplace(std::move(path), std::move(ns));
+ auto& nsRef = iter->second;
+ if (success) {
+ nsRef.fullname = &iter->first;
+ }
+ return &nsRef;
+}
+
+const DeclNamespace* CodegenRuntimeModel::FindNamespace(std::string_view fullname) const {
+ auto iter = m->namespaces.find(fullname);
+ if (iter != m->namespaces.end()) {
+ return &iter->second;
+ } else {
+ return nullptr;
+ }
+}
+
+DeclNamespace* CodegenRuntimeModel::FindNamespace(std::string_view name) {
+ return const_cast<DeclNamespace*>(const_cast<const CodegenRuntimeModel*>(this)->FindNamespace(name));
+}
+
+CodegenArchiveModel::CodegenArchiveModel(std::string_view dbPath)
+ : m{ new Private() } //
+{
+ std::string zstrPath(dbPath);
+ int reuslt = sqlite3_open(zstrPath.c_str(), &m->database);
+ if (reuslt != SQLITE_OK) {
+ std::string msg;
+ msg += "Failed to open SQLite3 database, error message:\n";
+ msg += sqlite3_errmsg(m->database);
+ throw std::runtime_error(msg);
+ }
+
+ // NOTE: These pragmas are not persistent, so we need to set them every time
+ // As of SQLite3 3.38.5, it defaults to foreign_keys = OFF, so we need this to be on for ON DELETE CASCADE and etc. to work
+ sqlite3_exec(m->database, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr);
+ // This database is used for a buildsystem and can be regenerated at any time. We don't care for the slightest about data integrity, we just want fast updates
+ sqlite3_exec(m->database, "PRAGMA synchronous = OFF", nullptr, nullptr, nullptr);
+ sqlite3_exec(m->database, "PRAGMA journal_mode = MEMORY", nullptr, nullptr, nullptr);
+
+ {
+ SQLiteStatement readVersionStmt;
+ readVersionStmt.InitializeLazily(m->database, "PRAGMA user_version"sv);
+
+ int result = sqlite3_step(readVersionStmt);
+ assert(result == SQLITE_ROW);
+ int currentDatabaseVersion = sqlite3_column_int(readVersionStmt, 0);
+
+ result = sqlite3_step(readVersionStmt);
+ assert(result == SQLITE_DONE);
+
+ if (currentDatabaseVersion == 0) {
+ // Newly created database, initialize it
+ m->InitializeDatabase();
+ } else if (currentDatabaseVersion == CURRENT_DATABASE_VERSION) {
+ // Same version, no need to do anything
+ } else {
+ INPLACE_FMT(msg, "Incompatbile database versions %d (in file) vs %d (expected).", currentDatabaseVersion, CURRENT_DATABASE_VERSION);
+ throw std::runtime_error(msg);
+ }
+ }
+
+ // Initialize core statements
+ m->beginTransactionStmt.Initialize(m->database, "BEGIN TRANSACTION");
+ m->commitTransactionStmt.Initialize(m->database, "COMMIT TRANSACTION");
+ m->rollbackTransactionStmt.Initialize(m->database, "ROLLBACK TRANSACTION");
+ m->findFileStmt.Initialize(m->database, "SELECT Id FROM Files WHERE FileName = ?1");
+ m->storeFileStmt.Initialize(m->database, "INSERT INTO Files(FileName) VALUES (?1) RETURNING Id");
+ m->findNamespaceStmt.Initialize(m->database, "SELECT Id FROM Namespaces WHERE ParentNamespaceId = ?1 AND Name = ?2");
+ m->getNamespaceStmt.Initialize(m->database, "SELECT * FROM Namespaces WHERE Id = ?1"sv);
+ m->storeNamespaceStmt.Initialize(m->database, "INSERT INTO Namespaces(ParentNamespaceId, Name) VALUES (?1, ?2) RETURNING Id");
+}
+
+CodegenArchiveModel::~CodegenArchiveModel() {
+ delete m;
+}
+
+void CodegenArchiveModel::DeleteDeclsRelatedToFile(std::string_view filename) {
+ // -Argument- -Description-
+ // ?1 The filename to delete
+ m->deleteFunctionDeclByFilenameStmt.InitializeLazily(m->database, "DELETE FROM DeclFunctions WHERE FileId = (SELECT Id FROM Files WHERE FileName = ?1)"sv);
+ m->deleteStructDeclByFilenameStmt.InitializeLazily(m->database, "DELETE FROM DeclStructs WHERE FileId = (SELECT Id FROM Files WHERE FileName = ?1);"sv);
+ m->deleteEnumDeclByFilenameStmt.InitializeLazily(m->database, "DELETE FROM DeclEnums WHERE FileId = (SELECT Id FROM Files WHERE FileName = ?1);"sv);
+
+ m->BeginTransaction();
+ auto stmtList = {
+ m->deleteFunctionDeclByFilenameStmt.stmt,
+ m->deleteStructDeclByFilenameStmt.stmt,
+ m->deleteEnumDeclByFilenameStmt.stmt,
+ };
+ for (auto& stmt : stmtList) {
+ SQLiteRunningStatement rt(stmt);
+ rt.BindArguments(filename);
+ rt.StepUntilDone();
+ }
+ m->CommitTransaction();
+}
+
+void CodegenArchiveModel::Store(const CodegenRuntimeModel& cgModel) {
+ auto& cgm = cgModel.GetPimpl();
+
+ struct Visiter {
+ CodegenArchiveModel* self;
+
+ void operator()(const DeclStruct& decl) const {
+ self->StoreStruct(decl);
+ }
+ void operator()(const DeclFunction& decl) const {
+ self->StoreFunction(decl);
+ }
+ void operator()(const DeclEnum& decl) const {
+ self->StoreEnum(decl);
+ }
+ } visiter;
+ visiter.self = this;
+
+ m->BeginTransaction();
+
+ for (auto&& [DISCARD, ns] : cgm.namespaces) {
+ // This will insert the namespace if it doesn't exist, or no-op (fetches data) if it already exists
+ m->FindOrStoreNamespace(&ns);
+ }
+ for (auto&& [DISCARD, value] : cgm.decls) {
+ std::visit(visiter, value.v);
+ }
+
+ m->CommitTransaction();
+}
+
+void CodegenArchiveModel::LoadInto(CodegenRuntimeModel& model) const {
+ // TODO
+}
+
+CodegenRuntimeModel CodegenArchiveModel::Load() const {
+ CodegenRuntimeModel cgModel;
+
+ // TODO files
+ // TODO namespaces
+
+ robin_hood::unordered_map<int64_t, DeclStruct*> structsById;
+ robin_hood::unordered_map<int64_t, DeclMemberVariable*> propertiesById;
+ robin_hood::unordered_map<int64_t, DeclMemberFunction*> methodsById;
+
+ { // Load structs
+ SQLiteStatement stmt(m->database, "SELECT * FROM DeclStructs"sv);
+ SQLiteRunningStatement rt(stmt);
+ while (true) {
+ int result = rt.StepAndCheckError();
+ if (result == SQLITE_DONE) break;
+ assert(result == SQLITE_ROW);
+
+ auto [id, fileId, nsId, name] = rt.ResultColumns<int64_t, int64_t, int64_t, std::string_view>();
+
+ auto decl = cgModel.AddStruct(m->GetDeclFullName(nsId, name), DeclStruct{});
+ structsById.try_emplace(id, decl);
+ }
+ }
+ { // Load struct's base classes
+ SQLiteStatement stmt(m->database, "SELECT * FROM DeclStructBaseClassRelations");
+ SQLiteRunningStatement rt(stmt);
+ while (true) {
+ int result = rt.StepAndCheckError();
+ if (result == SQLITE_DONE) break;
+ assert(result == SQLITE_ROW);
+
+ auto [structId, parentStructNsId, parentStructName] = rt.ResultColumns<int64_t, int64_t, std::string_view>();
+
+ auto declThis = structsById.at(structId);
+ auto declParent = cgModel.FindStruct(parentStructName); // TODO namespace
+ declThis->baseClasses.push_back(declParent);
+ }
+ }
+ { // Load struct properties
+ SQLiteStatement stmt(m->database, "SELECT * FROM DeclStructProperties"sv);
+ SQLiteRunningStatement rt(stmt);
+ while (true) {
+ int result = rt.StepAndCheckError();
+ if (result == SQLITE_DONE) break;
+ assert(result == SQLITE_ROW);
+
+ // TODO
+ }
+ }
+ { // Load struct methods
+ SQLiteStatement stmt(m->database, "SELECT * FROM DeclStructMethods"sv);
+ SQLiteRunningStatement rt(stmt);
+ while (true) {
+ int result = rt.StepAndCheckError();
+ if (result == SQLITE_DONE) break;
+ assert(result == SQLITE_ROW);
+
+ // TODO
+ }
+ }
+ { // Load method params
+ SQLiteStatement stmt(m->database, "SELECT * FROM DeclStructMethodParameters"sv);
+ SQLiteRunningStatement rt(stmt);
+ while (true) {
+ int result = rt.StepAndCheckError();
+ if (result == SQLITE_DONE) break;
+ assert(result == SQLITE_ROW);
+
+ // TODO
+ }
+ }
+
+ return cgModel;
+}
+
+void CodegenArchiveModel::StoreStruct(const DeclStruct& decl) {
+ // -Argument- -Description-
+ // ?1 Namespace ID
+ // ?2 Struct name
+ // ?3 File ID containing the struct
+ // ?4 Is this struct marked for metadata generation?
+ m->storeStructStmt.InitializeLazily(m->database, R"""(
+INSERT INTO DeclStructs(NamespaceId, Name, FileId, IsMetadataMarked)
+VALUES (?1, ?2, ?3, ?4)
+ON CONFLICT DO UPDATE SET
+ FileId = ?3,
+ IsMetadataMarked = ?4
+RETURNING Id
+)"""sv);
+
+ // -Argument- -Description-
+ // ?1 Struct ID
+ // ?2 Parent struct's namespace ID
+ // ?3 Parent struct's name
+ m->storeStructBaseClassStmt.InitializeLazily(m->database, R"""(
+INSERT INTO DeclStructBaseClassRelations(StructId, ParentStructNamespaceId, ParentStructName)
+VALUES (?1, ?2, ?3)
+)"""sv);
+
+ // -Argument- -Description-
+ // ?1 Struct ID
+ // ?2 Property name
+ // ?3 Property type
+ // ?4 Getter name (optional)
+ // ?5 Setter name (optional)
+ // ?6 Is this property marked for metadata generation?
+ m->storeStructPropertyStmt.InitializeLazily(m->database, R"""(
+INSERT INTO DeclStructProperties(StructId, Name, Type, GetterName, SetterName, IsMetadataMarked)
+VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+)"""sv);
+
+ SQLiteRunningStatement rt(m->storeStructStmt);
+ rt.BindArguments(m->FindOrStoreNamespace(decl.container), decl.name, m->FindOrStoreFile(decl.sourceFile), decl.generating);
+ rt.StepAndCheck(SQLITE_ROW);
+ auto [structId] = rt.ResultColumns<int64_t>();
+
+ for (auto& baseClass : decl.baseClasses) {
+ SQLiteRunningStatement rt(m->storeStructBaseClassStmt);
+ rt.BindArguments(structId, m->FindOrStoreNamespace(baseClass->container), baseClass->name);
+ rt.StepUntilDone();
+ }
+
+ for (auto& property : decl.memberVariables) {
+ SQLiteRunningStatement rt(m->storeStructPropertyStmt);
+ rt.BindArguments(
+ structId,
+ property.name,
+ property.type,
+ property.getterName,
+ property.setterName,
+ // Since DeclMemberVariable entries currently only exist if it's marked BRUSSEL_PROPERTY
+ true);
+ rt.StepUntilDone();
+ }
+
+ for (auto& method : decl.memberFunctions) {
+ // TODO
+ }
+}
+
+void CodegenArchiveModel::StoreFunction(const DeclFunction& decl) {
+ // TODO
+}
+
+void CodegenArchiveModel::StoreEnum(const DeclEnum& decl) {
+ // -Argument- -Description-
+ // ?1 Namespace ID
+ // ?2 Enum name
+ // ?3 Enum underlying type
+ // ?4 File ID containing the enum
+ m->storeEnumStmt.InitializeLazily(m->database, R"""(
+INSERT INTO DeclEnums(NamespaceId, Name, UnderlyingType, FileId)
+VALUES (?1, ?2, ?3, ?4)
+ON CONFLICT DO UPDATE SET
+ UnderlyingType = ?3,
+ FileId = ?4
+RETURNING Id
+)"""sv);
+
+ // -Argument- -Description-
+ // ?1 Container enum's id
+ // ?2 Enum element name
+ // ?3 Enum element value
+ m->storeEnumElmStmt.InitializeLazily(m->database, R"""(
+INSERT INTO DeclEnumElements(EnumId, Name, Value)
+VALUES (?1, ?2, ?3)
+ON CONFLICT DO UPDATE SET Value=?3
+)"""sv);
+
+ SQLiteRunningStatement rt(m->storeEnumStmt);
+ rt.BindArguments(m->FindOrStoreNamespace(decl.container), decl.name, decl.GetUnderlyingTypeName(), m->FindOrStoreFile(decl.sourceFile));
+ rt.StepAndCheck(SQLITE_ROW);
+ auto [enumId] = rt.ResultColumns<int64_t>();
+
+ for (auto& elm : decl.elements) {
+ SQLiteRunningStatement rt(m->storeEnumElmStmt);
+ rt.BindArguments(enumId, elm.name, elm.value);
+ rt.StepUntilDone();
+ }
+}
diff --git a/source/20-codegen-compiler/CodegenModel.hpp b/source/20-codegen-compiler/CodegenModel.hpp
new file mode 100644
index 0000000..99c345d
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenModel.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "CodegenConfig.hpp"
+#include "CodegenDecl.hpp"
+#include "CodegenModel.hpp"
+#include "CodegenUtils.hpp"
+
+#include <sqlite3.h>
+#include <cinttypes>
+#include <cstdint>
+#include <string>
+#include <string_view>
+
+using namespace std::literals;
+
+class CodegenRuntimeModel {
+private:
+ class Private;
+ Private* m;
+
+public:
+ CodegenRuntimeModel();
+ ~CodegenRuntimeModel();
+
+ // Implementation detail helper, don't use outside
+ Private& GetPimpl() const { return *m; }
+
+ DeclEnum* AddEnum(std::string fullname, DeclEnum decl);
+ DeclStruct* AddStruct(std::string fullname, DeclStruct decl);
+
+ const DeclEnum* FindEnum(std::string_view name) const;
+ const DeclStruct* FindStruct(std::string_view name) const;
+
+ DeclNamespace* AddNamespace(DeclNamespace ns);
+
+ const DeclNamespace* FindNamespace(std::string_view fullname) const;
+ DeclNamespace* FindNamespace(std::string_view name);
+};
+
+class CodegenArchiveModel {
+private:
+ class Private;
+ Private* m;
+
+public:
+ CodegenArchiveModel(std::string_view dbPath);
+ ~CodegenArchiveModel();
+
+ // Implementation detail helper, don't use outside
+ Private& GetPimpl() const { return *m; }
+
+ void DeleteDeclsRelatedToFile(std::string_view filename);
+
+ void Store(const CodegenRuntimeModel& model);
+ void LoadInto(CodegenRuntimeModel& model) const;
+ CodegenRuntimeModel Load() const;
+
+ void StoreStruct(const DeclStruct& decl);
+ void StoreFunction(const DeclFunction& decl);
+ void StoreEnum(const DeclEnum& decl);
+};
diff --git a/source/20-codegen-compiler/CodegenOutput.cpp b/source/20-codegen-compiler/CodegenOutput.cpp
new file mode 100644
index 0000000..d85feac
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenOutput.cpp
@@ -0,0 +1,39 @@
+#include "CodegenOutput.hpp"
+
+#include "CodegenUtils.hpp"
+
+void CodegenOutput::AddRequestInclude(std::string_view include) {
+ if (!mRequestIncludes.contains(include)) {
+ mRequestIncludes.insert(std::string(include));
+ }
+}
+
+void CodegenOutput::AddOutputThing(CodegenOutputThing thing, int placementLocation) {
+ if (placementLocation < 0 || placementLocation >= mOutThings.size()) {
+ mOutThings.push_back(std::move(thing));
+ } else {
+ int maxIndex = (int)mOutThings.size() - 1;
+ if (placementLocation > maxIndex) {
+ placementLocation = maxIndex;
+ }
+
+ auto placementIter = mOutThings.begin() + placementLocation;
+ mOutThings.insert(placementIter, std::move(thing));
+ }
+}
+
+void CodegenOutput::MergeContents(CodegenOutput other) {
+ std::move(other.mOutThings.begin(), other.mOutThings.end(), std::back_inserter(this->mOutThings));
+}
+
+void CodegenOutput::Write(FILE* file) const {
+ for (auto& include : mRequestIncludes) {
+ // TODO how to resolve to the correct include paths?
+ WRITE_FMT_LN(file, "#include <%s>", include.c_str());
+ }
+
+ for (auto& thing : mOutThings) {
+ fwrite(thing.text.c_str(), sizeof(char), thing.text.size(), file);
+ WRITE_LIT(file, "\n");
+ }
+}
diff --git a/source/20-codegen-compiler/CodegenOutput.hpp b/source/20-codegen-compiler/CodegenOutput.hpp
new file mode 100644
index 0000000..df949f5
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenOutput.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <Utils.hpp>
+
+#include <robin_hood.h>
+#include <algorithm>
+#include <cstdio>
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+// A generic "thing" (could be anything, comments, string-concated functionsm, etc.) to spit into the output file
+struct CodegenOutputThing {
+ std::string text;
+};
+
+class CodegenOutput {
+private:
+ robin_hood::unordered_set<std::string, StringHash, StringEqual> mRequestIncludes;
+ std::vector<CodegenOutputThing> mOutThings;
+
+public:
+ std::string optionOutPrefix;
+ // Whether to add prefixes mOutPrefix to all global names or not
+ bool optionAutoAddPrefix : 1 = false;
+
+public:
+ void AddRequestInclude(std::string_view include);
+ void AddOutputThing(CodegenOutputThing thing, int placementLocation = -1);
+
+ void MergeContents(CodegenOutput other);
+
+ void Write(FILE* file) const;
+};
diff --git a/source/20-codegen-compiler/CodegenUtils.cpp b/source/20-codegen-compiler/CodegenUtils.cpp
new file mode 100644
index 0000000..5bc5d79
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenUtils.cpp
@@ -0,0 +1,171 @@
+#include "CodegenUtils.hpp"
+
+#include <Macros.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <cstdio>
+#include <cstdlib>
+
+using namespace std::literals;
+
+bool Utils::WriteOutputFile(const CodegenOutput& output, const char* path) {
+ auto outputFile = Utils::OpenCstdioFile(path, Utils::WriteTruncate);
+ if (!outputFile) {
+ printf("[ERROR] unable to open output file %s\n", path);
+ return false;
+ }
+ DEFER {
+ fclose(outputFile);
+ };
+
+ DEBUG_PRINTF("Writing output %s\n", path);
+ output.Write(outputFile);
+
+ return true;
+}
+
+std::string Utils::JoinNames(DeclNamespace* ns, std::string_view prefix, std::string_view suffix, std::string_view delimiter) {
+ size_t length = 0;
+ if (!prefix.empty()) {
+ length += prefix.length() + delimiter.length();
+ }
+ if (!suffix.empty()) {
+ length += suffix.length() + delimiter.length();
+ }
+ size_t nsCount = 0;
+ {
+ DeclNamespace* curr = ns;
+ while (curr) {
+ length += curr->name.length() + delimiter.length();
+
+ curr = curr->container;
+ ++nsCount;
+ }
+ }
+ length -= delimiter.length();
+
+ std::string joined;
+ joined.reserve(length);
+
+ if (!prefix.empty()) {
+ joined += prefix;
+ joined += delimiter;
+ }
+ {
+ DeclNamespace* curr = ns;
+ size_t i = 0;
+ while (curr) {
+ joined += curr->name;
+ if (!suffix.empty() || i != (nsCount - 1)) {
+ joined += delimiter;
+ }
+
+ curr = curr->container;
+ ++i;
+ }
+ }
+ if (!suffix.empty()) {
+ joined += suffix;
+ }
+
+ return joined;
+}
+
+std::string Utils::MakeFullName(std::string_view name, DeclNamespace* ns) {
+ return JoinNames(ns, ""sv, name, "::"sv);
+}
+
+std::string Utils::MakeMangledName(std::string_view name, DeclNamespace* ns) {
+ return JoinNames(ns, ""sv, name, "_"sv);
+}
+
+// NOTE: assuming we are only dealing with ASCII characters
+static bool IsLowerCase(char c) {
+ return c >= 'a' && c <= 'z';
+}
+static bool IsUpperCase(char c) {
+ return c >= 'A' && c <= 'Z';
+}
+static bool IsAlphabetic(char c) {
+ return IsLowerCase(c) || IsUpperCase(c);
+}
+static char MakeUpperCase(char c) {
+ if (IsAlphabetic(c)) {
+ return IsUpperCase(c)
+ ? c
+ : ('A' + (c - 'a'));
+ }
+ return c;
+}
+
+std::vector<std::string_view> Utils::SplitIdentifier(std::string_view name) {
+ // TODO handle SCREAMING_CASE
+
+ size_t chunkStart = 0;
+ size_t chunkEnd = 0;
+ std::vector<std::string_view> result;
+ auto PushChunk = [&]() { result.push_back(std::string_view(name.begin() + chunkStart, name.begin() + chunkEnd)); };
+ while (chunkEnd < name.size()) {
+ char c = name[chunkEnd];
+ if (IsUpperCase(c)) {
+ // Start of next chunk, using camelCase or PascalCase
+ PushChunk();
+ chunkStart = chunkEnd;
+ chunkEnd = chunkStart + 1;
+ continue;
+ } else if (c == '_') {
+ // End of this chunk, using snake_case
+ PushChunk();
+ chunkStart = chunkEnd + 1;
+ chunkEnd = chunkStart + 1;
+ continue;
+ } else if (c == '-') {
+ // End of this chunk, using kebab-case
+ PushChunk();
+ chunkStart = chunkEnd + 1;
+ chunkEnd = chunkStart + 1;
+ continue;
+ }
+ ++chunkEnd;
+ }
+
+ if ((chunkEnd - chunkStart) >= 1) {
+ PushChunk();
+ }
+
+ return result;
+}
+
+std::string Utils::MakePascalCase(std::string_view name) {
+ std::string result;
+ for (auto part : SplitIdentifier(name)) {
+ result += MakeUpperCase(part[0]);
+ result += part.substr(1);
+ }
+ return result;
+}
+
+void Utils::ProduceGeneratedHeader(const char* headerFilename, CodegenOutput& header, const char* sourceFilename, CodegenOutput& source) {
+ CodegenOutputThing headerOut;
+ headerOut.text += &R"""(
+// This file is generated. Any changes will be overidden when building.
+#include <MetadataBase.hpp>
+#include <cstddef>
+#include <cstdint>
+)"""[1];
+
+ CodegenOutputThing sourceOut;
+ APPEND_LIT_LN(sourceOut.text, "// This file is generated. Any changes will be overidden when building.");
+ APPEND_FMT_LN(sourceOut.text, "#include \"%s\"", headerFilename);
+ sourceOut.text += &R"""(
+#include <frozen/string.h>
+#include <frozen/unordered_map.h>
+#include <MetadataDetails.hpp>
+using namespace std::literals;
+using namespace Metadata;
+)"""[1];
+
+ header.AddOutputThing(std::move(headerOut), 0);
+ source.AddOutputThing(std::move(sourceOut), 0);
+}
diff --git a/source/20-codegen-compiler/CodegenUtils.hpp b/source/20-codegen-compiler/CodegenUtils.hpp
new file mode 100644
index 0000000..2d5b684
--- /dev/null
+++ b/source/20-codegen-compiler/CodegenUtils.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "CodegenConfig.hpp"
+#include "CodegenDecl.hpp"
+#include "CodegenOutput.hpp"
+
+#include <algorithm>
+#include <string>
+#include <string_view>
+
+// I give up, hopefully nothing overflows this buffer
+// TODO handle buffer sizing properly
+
+#define INPLACE_FMT(varName, format, ...) \
+ char varName[2048]; \
+ snprintf(varName, sizeof(varName), format, __VA_ARGS__);
+
+#define APPEND_LIT(out, str) \
+ out += str
+
+#define APPEND_FMT(out, format, ...) \
+ { \
+ char buffer[65536]; \
+ snprintf(buffer, sizeof(buffer), format, __VA_ARGS__); \
+ out += buffer; \
+ }
+
+#define WRITE_LIT(file, str) \
+ fwrite(str, sizeof(char), sizeof(str) - 1, file)
+
+// NOTE: snprintf() returns the size written (given an infinite buffer) not including \0
+#define WRITE_FMT(file, format, ...) \
+ { \
+ char buffer[65536]; \
+ int size = snprintf(buffer, sizeof(buffer), format, __VA_ARGS__); \
+ fwrite(buffer, sizeof(char), std::min<int>(size, sizeof(buffer)), file); \
+ }
+
+#define APPEND_LIT_LN(out, str) APPEND_LIT(out, (str "\n"))
+#define APPEND_FMT_LN(out, format, ...) APPEND_FMT(out, (format "\n"), __VA_ARGS__)
+#define WRITE_LIT_LN(out, str) WRITE_LIT(out, (str "\n"))
+#define WRITE_FMT_LN(out, format, ...) WRITE_FMT(out, (format "\n"), __VA_ARGS__)
+
+namespace Utils {
+
+bool WriteOutputFile(const CodegenOutput& output, const char* path);
+
+std::string JoinNames(DeclNamespace* ns, std::string_view prefix, std::string_view suffix, std::string_view delimiter);
+std::string MakeFullName(std::string_view name, DeclNamespace* ns = nullptr);
+std::string MakeMangledName(std::string_view name, DeclNamespace* ns = nullptr);
+
+std::vector<std::string_view> SplitIdentifier(std::string_view name);
+std::string MakePascalCase(std::string_view name);
+
+void ProduceGeneratedHeader(const char* headerFilename, CodegenOutput& header, const char* sourceFilename, CodegenOutput& source);
+
+} // namespace Utils
diff --git a/source/20-codegen-compiler/SQLiteHelper.hpp b/source/20-codegen-compiler/SQLiteHelper.hpp
new file mode 100644
index 0000000..c24e476
--- /dev/null
+++ b/source/20-codegen-compiler/SQLiteHelper.hpp
@@ -0,0 +1,220 @@
+#pragma once
+
+#include <fmt/format.h>
+#include <sqlite3.h>
+#include <cassert>
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <sstream>
+#include <stdexcept>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+
+struct SQLiteDatabase {
+ sqlite3* database = nullptr;
+
+ ~SQLiteDatabase() {
+ // NOTE: calling with NULL is a harmless no-op
+ int result = sqlite3_close(database);
+ assert(result == SQLITE_OK);
+ }
+
+ operator sqlite3*() const { return database; }
+ sqlite3** operator&() { return &database; }
+};
+
+struct SQLiteStatement {
+ sqlite3_stmt* stmt = nullptr;
+
+ SQLiteStatement(const SQLiteStatement&) = delete;
+ SQLiteStatement& operator=(const SQLiteStatement&) = delete;
+
+ SQLiteStatement() = default;
+
+ SQLiteStatement(sqlite3* database, std::string_view sql) {
+ Initialize(database, sql);
+ }
+
+ ~SQLiteStatement() {
+ // NOTE: calling with NULL is a harmless no-op
+ // NOTE: we don't care about the error code, because they are returned if the statement has errored in the most recent execution
+ // but deleting it will succeeed anyways
+ sqlite3_finalize(stmt);
+ }
+
+ operator sqlite3_stmt*() const { return stmt; }
+ sqlite3_stmt** operator&() { return &stmt; }
+
+ void Initialize(sqlite3* database, std::string_view sql) {
+ int result = sqlite3_prepare_v2(database, sql.data(), sql.size(), &stmt, nullptr);
+ if (result != SQLITE_OK) {
+ auto msg = fmt::format(
+ "Failed to prepare SQLite3 statement, error message: {}",
+ sqlite3_errmsg(sqlite3_db_handle(stmt)));
+ throw std::runtime_error(msg);
+ }
+ }
+
+ bool InitializeLazily(sqlite3* database, std::string_view sql) {
+ if (!stmt) {
+ Initialize(database, sql);
+ return true;
+ }
+ return false;
+ }
+};
+
+struct SQLiteRunningStatement {
+ sqlite3_stmt* stmt;
+
+ SQLiteRunningStatement(sqlite3_stmt* stmt)
+ : stmt{ stmt } {
+ }
+
+ SQLiteRunningStatement(const SQLiteStatement& stmt)
+ : stmt{ stmt.stmt } {
+ }
+
+ ~SQLiteRunningStatement() {
+ sqlite3_clear_bindings(stmt);
+ sqlite3_reset(stmt);
+ }
+
+ void BindArgument(int index, int32_t value) {
+ sqlite3_bind_int(stmt, index, (int)value);
+ }
+
+ void BindArgument(int index, uint32_t value) {
+ sqlite3_bind_int(stmt, index, (int)value);
+ }
+
+ void BindArgument(int index, int64_t value) {
+ sqlite3_bind_int64(stmt, index, value);
+ }
+
+ void BindArgument(int index, uint64_t value) {
+ sqlite3_bind_int64(stmt, index, (int64_t)value);
+ }
+
+ void BindArgument(int index, const char* value) {
+ sqlite3_bind_text(stmt, index, value, -1, nullptr);
+ }
+
+ void BindArgument(int index, std::string_view value) {
+ sqlite3_bind_text(stmt, index, value.data(), value.size(), nullptr);
+ }
+
+ void BindArgument(int index, std::nullptr_t) {
+ // Noop
+ }
+
+ template <typename... Ts>
+ void BindArguments(Ts&&... args) {
+ // NOTE: SQLite3 argument index starts at 1
+ size_t idx = 1;
+ auto HandleEachArgument = [this, &idx]<typename T>(T&& arg) {
+ BindArgument(idx, std::forward<T>(arg));
+ ++idx;
+ };
+ (HandleEachArgument(std::forward<Ts>(args)), ...);
+ }
+
+ int Step() {
+ return sqlite3_step(stmt);
+ }
+
+ void StepAndCheck(int forErr) {
+ int err = sqlite3_step(stmt);
+ assert(err == forErr);
+ }
+
+ int StepAndCheckError() {
+ int err = sqlite3_step(stmt);
+ if (err != SQLITE_DONE || err != SQLITE_ROW) {
+ auto msg = fmt::format(
+ "Error {} executing SQLite3 statement, error message: {}",
+ sqlite3_errstr(err),
+ sqlite3_errmsg(sqlite3_db_handle(stmt)));
+ throw std::runtime_error(msg);
+ }
+ return err;
+ }
+
+ void StepUntilDone() {
+ while (true) {
+ int err = sqlite3_step(stmt);
+ // SQLITE_OK is never returned for sqlite3_step() //TODO fact check this
+ if (err == SQLITE_DONE) {
+ break;
+ }
+ if (err == SQLITE_ROW) {
+ continue;
+ }
+
+ auto msg = fmt::format(
+ "Error {} executing SQLite3 statement, error message: {}",
+ sqlite3_errstr(err),
+ sqlite3_errmsg(sqlite3_db_handle(stmt)));
+ throw std::runtime_error(msg);
+ }
+ }
+
+ using TimePoint = std::chrono::time_point<std::chrono::system_clock>;
+ using TpFromUnixTimestamp = std::pair<TimePoint, int64_t>;
+ using TpFromDateTime = std::pair<TimePoint, const char*>;
+
+ // TODO replace with overloads?
+ template <typename T>
+ auto ResultColumn(int column) const {
+ if constexpr (std::is_enum_v<T>) {
+ auto value = sqlite3_column_int64(stmt, column);
+ return static_cast<T>(value);
+ } else if constexpr (std::is_same_v<T, int> || std::is_same_v<T, bool>) {
+ return (T)sqlite3_column_int(stmt, column);
+ } else if constexpr (std::is_same_v<T, int64_t>) {
+ return (T)sqlite3_column_int64(stmt, column);
+ } else if constexpr (std::is_same_v<T, const char*>) {
+ return (const char*)sqlite3_column_text(stmt, column);
+ } else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>) {
+ // SQLite3 uses `unsigned char` to represent UTF-8 code units, on all platforms we care about this is the same as plain `char`
+ auto cstr = (const char*)sqlite3_column_text(stmt, column);
+ // For std::string_view, this finds size based on null terminator and stores reference to pointer
+ // For std::string, this also allocates buffer and copies `cstr` content
+ return T(cstr);
+ } else if constexpr (std::is_same_v<T, TpFromUnixTimestamp>) {
+ auto unixTimestamp = sqlite3_column_int64(stmt, column);
+ auto chrono = std::chrono::seconds(unixTimestamp);
+ return TimePoint(chrono);
+ } else if constexpr (std::is_same_v<T, TpFromDateTime>) {
+ // TODO wait for libstdc++ and libc++ implement c++20 std::chrono addition
+#ifdef _MSC_VER
+ auto datetime = (const char*)sqlite3_column_text(stmt, column);
+ if (datetime) {
+ std::stringstream ss(datetime);
+ TimePoint timepoint;
+ ss >> std::chrono::parse("%F %T", timepoint);
+ return timepoint;
+ } else {
+ return TimePoint();
+ }
+#else
+ static_assert(false && sizeof(T), "Unimplemented");
+#endif
+ } else {
+ static_assert(false && sizeof(T), "Unknown type");
+ }
+ }
+
+ template <typename... Ts>
+ auto ResultColumns() {
+ // NOTE: SQLite3 column index starts at 0
+ // NOTE: ((size_t)-1) + 1 == 0
+ size_t idx = -1;
+ // NOTE: std::make_tuple() -- variadic template function
+ // std::tuple() -- CTAD constructor
+ // Both of these cause make the comma operator unsequenced, not viable here
+ return std::tuple{ (++idx, ResultColumn<Ts>(idx))... };
+ }
+};
diff --git a/source/20-codegen-compiler/main.cpp b/source/20-codegen-compiler/main.cpp
new file mode 100644
index 0000000..a2e50f5
--- /dev/null
+++ b/source/20-codegen-compiler/main.cpp
@@ -0,0 +1,1443 @@
+#include "CodegenConfig.hpp"
+#include "CodegenDecl.hpp"
+#include "CodegenLexer.hpp"
+#include "CodegenModel.hpp"
+#include "CodegenOutput.hpp"
+#include "CodegenUtils.hpp"
+
+#include <Enum.hpp>
+#include <LookupTable.hpp>
+#include <Macros.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <robin_hood.h>
+#include <stb_c_lexer.h>
+#include <cinttypes>
+#include <cstdlib>
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+
+using namespace std::literals;
+namespace fs = std::filesystem;
+
+// TODO support codegen target in .cpp files
+// TOOD maybe switch to libclang, maintaining this parser is just too painful
+
+struct AppState {
+ CodegenRuntimeModel* runtimeModel;
+ CodegenArchiveModel* archiveModel;
+ // NOTE: decl objects reference the SourceFile objects by pointer
+ robin_hood::unordered_node_map<std::string, SourceFile, StringHash, StringEqual> sourceFiles;
+ std::vector<DeclEnum*> enumsToRevisit;
+ std::vector<DeclStruct*> structsToRevisit;
+ std::string_view outputDir;
+ std::string_view databaseFilePath;
+
+ SourceFile& GetOrCreateSourceFile(std::string_view filename) {
+ auto iter = sourceFiles.find(filename);
+ if (iter != sourceFiles.end()) {
+ return iter->second;
+ } else {
+ auto [iter, success] = sourceFiles.try_emplace(std::string(filename), SourceFile{});
+ // NOTE: "persistent" means pointer stable below
+ auto& persistentFilename = iter->first;
+ auto& persistentSourceFile = iter->second;
+ persistentSourceFile.filename = persistentFilename;
+ return persistentSourceFile;
+ }
+ }
+};
+
+FSTR_LUT_DECL(ClexNames, CLEX_eof, CLEX_ext_COUNT) {
+ FSTR_LUT_MAP_FOR(ClexNames);
+ FSTR_LUT_MAP_ENUM(CLEX_intlit);
+ FSTR_LUT_MAP_ENUM(CLEX_floatlit);
+ FSTR_LUT_MAP_ENUM(CLEX_id);
+ FSTR_LUT_MAP_ENUM(CLEX_dqstring);
+ FSTR_LUT_MAP_ENUM(CLEX_sqstring);
+ FSTR_LUT_MAP_ENUM(CLEX_charlit);
+ FSTR_LUT_MAP_ENUM(CLEX_eq);
+ FSTR_LUT_MAP_ENUM(CLEX_noteq);
+ FSTR_LUT_MAP_ENUM(CLEX_lesseq);
+ FSTR_LUT_MAP_ENUM(CLEX_greatereq);
+ FSTR_LUT_MAP_ENUM(CLEX_andand);
+ FSTR_LUT_MAP_ENUM(CLEX_oror);
+ FSTR_LUT_MAP_ENUM(CLEX_shl);
+ FSTR_LUT_MAP_ENUM(CLEX_shr);
+ FSTR_LUT_MAP_ENUM(CLEX_plusplus);
+ FSTR_LUT_MAP_ENUM(CLEX_minusminus);
+ FSTR_LUT_MAP_ENUM(CLEX_pluseq);
+ FSTR_LUT_MAP_ENUM(CLEX_minuseq);
+ FSTR_LUT_MAP_ENUM(CLEX_muleq);
+ FSTR_LUT_MAP_ENUM(CLEX_diveq);
+ FSTR_LUT_MAP_ENUM(CLEX_modeq);
+ FSTR_LUT_MAP_ENUM(CLEX_andeq);
+ FSTR_LUT_MAP_ENUM(CLEX_oreq);
+ FSTR_LUT_MAP_ENUM(CLEX_xoreq);
+ FSTR_LUT_MAP_ENUM(CLEX_arrow);
+ FSTR_LUT_MAP_ENUM(CLEX_eqarrow);
+ FSTR_LUT_MAP_ENUM(CLEX_shleq);
+ FSTR_LUT_MAP_ENUM(CLEX_shreq);
+ FSTR_LUT_MAP_ENUM(CLEX_ext_single_char);
+ FSTR_LUT_MAP_ENUM(CLEX_ext_double_colon);
+ FSTR_LUT_MAP_ENUM(CLEX_ext_dot_dot_dot);
+}
+
+FSTR_LUT_DECL(EnumUnderlyingType, 0, EUT_COUNT) {
+ FSTR_LUT_MAP_FOR(EnumUnderlyingType);
+ FSTR_LUT_MAP(EUT_Int8, "int8_t");
+ FSTR_LUT_MAP(EUT_Int16, "int16_t");
+ FSTR_LUT_MAP(EUT_Int32, "int32_t");
+ FSTR_LUT_MAP(EUT_Int64, "int64_t");
+ FSTR_LUT_MAP(EUT_Uint8, "uint8_t");
+ FSTR_LUT_MAP(EUT_Uint16, "uint16_t");
+ FSTR_LUT_MAP(EUT_Uint32, "uint32_t");
+ FSTR_LUT_MAP(EUT_Uint64, "uint64_t");
+}
+
+RSTR_LUT_DECL(EnumUnderlyingType, 0, EUT_COUNT) {
+ RSTR_LUT_MAP_FOR(EnumUnderlyingType);
+
+ // Platform-dependent types
+ RSTR_LUT_MAP(EUT_Int16, "short");
+ RSTR_LUT_MAP(EUT_Int16, "short int");
+ RSTR_LUT_MAP(EUT_Uint16, "unsigned short");
+ RSTR_LUT_MAP(EUT_Uint16, "unsigned short int");
+ RSTR_LUT_MAP(EUT_Int32, "int");
+ RSTR_LUT_MAP(EUT_Uint32, "unsigned");
+ RSTR_LUT_MAP(EUT_Uint32, "unsigned int");
+#ifdef _WIN32
+ RSTR_LUT_MAP(EUT_Int32, "long");
+ RSTR_LUT_MAP(EUT_Int32, "long int");
+ RSTR_LUT_MAP(EUT_Uint32, "unsigned long");
+ RSTR_LUT_MAP(EUT_Uint32, "unsigned long int");
+#else
+ RSTR_LUT_MAP(EUT_Int64, "long");
+ RSTR_LUT_MAP(EUT_Int64, "long int");
+ RSTR_LUT_MAP(EUT_Uint64, "unsigned long");
+ RSTR_LUT_MAP(EUT_Uint64, "unsigned long int");
+#endif
+ RSTR_LUT_MAP(EUT_Int64, "long long");
+ RSTR_LUT_MAP(EUT_Int64, "long long int");
+ RSTR_LUT_MAP(EUT_Uint64, "unsigned long long");
+ RSTR_LUT_MAP(EUT_Uint64, "unsigned long long int");
+
+ // Sized types
+ RSTR_LUT_MAP(EUT_Int8, "int8_t");
+ RSTR_LUT_MAP(EUT_Int16, "int16_t");
+ RSTR_LUT_MAP(EUT_Int32, "int32_t");
+ RSTR_LUT_MAP(EUT_Int64, "int64_t");
+ RSTR_LUT_MAP(EUT_Uint8, "uint8_t");
+ RSTR_LUT_MAP(EUT_Uint16, "uint16_t");
+ RSTR_LUT_MAP(EUT_Uint32, "uint32_t");
+ RSTR_LUT_MAP(EUT_Uint64, "uint64_t");
+}
+
+FSTR_LUT_DECL(EnumValuePattern, 0, EVP_COUNT) {
+ FSTR_LUT_MAP_FOR(EnumValuePattern);
+ FSTR_LUT_MAP_ENUM(EVP_Continuous);
+ FSTR_LUT_MAP_ENUM(EVP_Bits);
+ FSTR_LUT_MAP_ENUM(EVP_Random);
+}
+
+enum CppKeyword {
+ CKw_Namespace,
+ CKw_Struct,
+ CKw_Class,
+ CKw_Enum,
+ CKw_Public,
+ CKw_Protected,
+ CKw_Private,
+ CKw_Virtual,
+ CKw_Using,
+ CKw_Template,
+ CKw_COUNT,
+};
+
+RSTR_LUT_DECL(CppKeyword, 0, CKw_COUNT) {
+ RSTR_LUT_MAP_FOR(CppKeyword);
+ RSTR_LUT_MAP(CKw_Namespace, "namespace");
+ RSTR_LUT_MAP(CKw_Struct, "struct");
+ RSTR_LUT_MAP(CKw_Class, "class");
+ RSTR_LUT_MAP(CKw_Enum, "enum");
+ RSTR_LUT_MAP(CKw_Public, "public");
+ RSTR_LUT_MAP(CKw_Protected, "protected");
+ RSTR_LUT_MAP(CKw_Private, "private");
+ RSTR_LUT_MAP(CKw_Virtual, "virtual");
+ RSTR_LUT_MAP(CKw_Using, "using");
+ RSTR_LUT_MAP(CKw_Template, "template");
+}
+
+enum CodegenDirective {
+ CD_Class,
+ CD_ClassProperty,
+ CD_ClassMethod,
+ CD_Enum,
+ CD_XGlobalVar,
+ CD_XGlobalVarCtor,
+ CD_XGlobalVarDtor,
+ CD_COUNT,
+};
+
+RSTR_LUT_DECL(CodegenDirective, 0, CD_COUNT) {
+ RSTR_LUT_MAP_FOR(CodegenDirective);
+ RSTR_LUT_MAP(CD_Class, "BRUSSEL_CLASS");
+ RSTR_LUT_MAP(CD_ClassProperty, "BRUSSEL_PROPERTY");
+ RSTR_LUT_MAP(CD_ClassMethod, "BRUSSEL_METHOD");
+ RSTR_LUT_MAP(CD_Enum, "BRUSSEL_ENUM");
+ RSTR_LUT_MAP(CD_XGlobalVar, "BRUSSEL_GLOBAL_DECL");
+ RSTR_LUT_MAP(CD_XGlobalVarCtor, "BRUSSEL_GLOBAL_CTOR");
+ RSTR_LUT_MAP(CD_XGlobalVarDtor, "BRUSSEL_GLOBAL_DTOR");
+}
+
+std::vector<std::vector<const StbLexerToken*>>
+TryConsumeDirectiveArgumentList(CodegenLexer& lexer) {
+ std::vector<std::vector<const StbLexerToken*>> result;
+ decltype(result)::value_type currentArg;
+
+ size_t i = lexer.idx;
+ int parenDepth = 0;
+ for (; i < lexer.tokens.size(); ++i) {
+ auto& token = lexer.tokens[i];
+ if (token.text[0] == '(') {
+ if (parenDepth > 0) {
+ currentArg.push_back(&token);
+ }
+ ++parenDepth;
+ } else if (token.text[0] == ')') {
+ --parenDepth;
+ if (parenDepth == 0) {
+ // End of argument list
+ ++i; // Consume the ')' token
+ break;
+ }
+ } else if (parenDepth > 0) {
+ // Parse these only if we are inside the argument list
+ if (token.text[0] == ',') {
+ result.push_back(std::move(currentArg));
+ currentArg = {};
+ } else {
+ currentArg.push_back(&token);
+ }
+ }
+ }
+
+ if (!currentArg.empty()) {
+ result.push_back(std::move(currentArg));
+ }
+
+ lexer.idx = i;
+ return result;
+}
+
+bool TryConsumeKeyword(CodegenLexer& lexer, CppKeyword keyword) {
+ auto& token = lexer.Current();
+ if (token.type == CLEX_id) {
+ auto iter = RSTR_LUT(CppKeyword).find(token.text);
+ if (iter != RSTR_LUT(CppKeyword).end()) {
+ ++lexer.idx;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TryConsumeAnyKeyword(CodegenLexer& lexer) {
+ auto& token = lexer.Current();
+ if (token.type == CLEX_id &&
+ RSTR_LUT(CppKeyword).contains(token.text))
+ {
+ ++lexer.idx;
+ return true;
+ }
+ return false;
+}
+
+std::optional<DeclMemberVariable>
+TryConsumeMemberVariable(CodegenLexer& lexer) {
+ // The identifier/name will always be one single token, right before the 1st '=' (if has initializer) or ';' (no initializer)
+ // NOTE: we assume there is no (a == b) stuff in the templates
+
+ auto& tokens = lexer.tokens;
+ auto& idx = lexer.idx;
+
+ size_t idenTokIdx;
+ size_t typeStart = idx;
+ size_t typeEnd;
+ for (; idx < tokens.size(); ++idx) {
+ auto& token = tokens[idx];
+ if (token.type == CLEX_ext_single_char) {
+ if (token.text[0] == '=') {
+ typeEnd = idx - 1;
+ idenTokIdx = idx - 1;
+ lexer.SkipUntilTokenSingleChar(';');
+ goto found;
+ } else if (token.text[0] == ';') {
+ typeEnd = idx - 1;
+ idenTokIdx = idx - 1;
+ goto found;
+ }
+ }
+ }
+ // We reached end of input but still no end of statement
+ return {};
+
+found:
+ if (tokens[idenTokIdx].type != CLEX_id) {
+ // Expected identifier, found something else
+ return {};
+ }
+
+ DeclMemberVariable result;
+ result.name = tokens[idenTokIdx].text;
+ result.type = CombineTokens(std::span(&tokens[typeStart], &tokens[typeEnd]));
+
+ // Consume the '=' or ';' token
+ ++idx;
+
+ return result;
+}
+
+EnumUnderlyingType TryConsumeEnumUnderlyingType(CodegenLexer& lexer) {
+ // Try 1, 2, 3, 4 tokens from the current position
+ // NOTE: see the FSTR map initialization code for reference that there is max 4 tokens
+ for (int i = 4; i >= 1; --i) {
+ auto text = CombineTokens(std::span(&lexer.Current(), i), " "sv);
+ auto iter = RSTR_LUT(EnumUnderlyingType).find(text);
+ if (iter != RSTR_LUT(EnumUnderlyingType).end()) {
+ lexer.idx += i;
+ return iter->second;
+ }
+ }
+ return EUT_COUNT;
+}
+
+// Also includes the ':' token in the front
+EnumUnderlyingType TryConsumeEnumUnderlyingTypeClause(CodegenLexer& lexer) {
+ if (lexer.Current().text != ":") {
+ return EUT_COUNT;
+ }
+
+ ++lexer.idx;
+ return TryConsumeEnumUnderlyingType(lexer);
+}
+
+enum StructMetaGenOptions {
+ SMGO_InheritanceHiearchy,
+ SMGO_COUNT,
+};
+
+RSTR_LUT_DECL(StructMetaGenOptions, 0, SMGO_COUNT) {
+ RSTR_LUT_MAP_FOR(StructMetaGenOptions);
+ RSTR_LUT_MAP(SMGO_InheritanceHiearchy, "InheritanceHiearchy");
+}
+
+enum StructPropertyOptions {
+ SPO_Getter,
+ SPO_Setter,
+ SPO_COUNT,
+};
+
+RSTR_LUT_DECL(StructPropertyOptions, 0, SPO_COUNT) {
+ RSTR_LUT_MAP_FOR(StructPropertyOptions);
+ RSTR_LUT_MAP(SPO_Getter, "GETTER");
+ RSTR_LUT_MAP(SPO_Setter, "SETTER");
+}
+
+enum EnumMetaGenOptions {
+ EMGO_ToString,
+ EMGO_FromString,
+ EMGO_ExcludeUseHeuristics,
+ EMGO_RemovePrefix,
+ EMGO_AddPrefix,
+ EMGO_COUNT,
+};
+
+RSTR_LUT_DECL(EnumMetaGenOptions, 0, EMGO_COUNT) {
+ RSTR_LUT_MAP_FOR(EnumMetaGenOptions);
+ RSTR_LUT_MAP(EMGO_ToString, "ToString");
+ RSTR_LUT_MAP(EMGO_FromString, "FromString");
+ RSTR_LUT_MAP(EMGO_ExcludeUseHeuristics, "ExcludeHeuristics");
+ RSTR_LUT_MAP(EMGO_RemovePrefix, "RemovePrefix");
+ RSTR_LUT_MAP(EMGO_AddPrefix, "AddPrefix");
+}
+
+void GenerateEnumStringArray(CodegenOutput& out, const DeclEnum& decl, const char* arrayName, const std::vector<DeclEnumElement>& filteredElements) {
+ CodegenOutputThing thing;
+ APPEND_FMT_LN(thing.text, "const char* %s[] = {", arrayName);
+ for (auto& elm : filteredElements) {
+ APPEND_FMT_LN(thing.text, "\"%s\",", elm.name.c_str());
+ }
+ APPEND_LIT_LN(thing.text, "};");
+ out.AddOutputThing(std::move(thing));
+}
+
+void GenerateEnumStringMap(CodegenOutput& out, const DeclEnum& decl, const char* mapName, const std::vector<DeclEnumElement>& filteredElements) {
+ CodegenOutputThing thing;
+ // TODO
+ out.AddOutputThing(std::move(thing));
+}
+
+void GenerateForEnum(CodegenOutput& headerOut, CodegenOutput& sourceOut, const DeclEnum& decl) {
+ auto& enumName = decl.GetFullName();
+ auto& mangledName = decl.GetMangledName();
+
+ auto useExcludeHeuristics = decl.generateExcludeUseHeuristics;
+ auto filteredElements = [&]() {
+ if (useExcludeHeuristics) {
+ decltype(decl.elements) result;
+ for (auto& elm : decl.elements) {
+ if (elm.name.ends_with("COUNT")) continue;
+
+ std::string_view trimmedName = elm.name;
+ if (!decl.generateRemovingPrefix.empty() &&
+ elm.name.starts_with(decl.generateRemovingPrefix))
+ {
+ trimmedName = trimmedName.substr(decl.generateRemovingPrefix.size());
+ }
+
+ result.push_back(DeclEnumElement{
+ .name = decl.generatingAddingPrefix + std::string(trimmedName),
+ .value = elm.value,
+ });
+ }
+ return result;
+ } else {
+ return decl.elements;
+ }
+ }();
+
+ if (decl.generateToString) {
+ // Generate value -> string lookup table and function
+ INPLACE_FMT(val2StrName, "gCG_%s_Val2Str", mangledName.c_str());
+
+ switch (decl.GetPattern()) {
+ case EVP_Continuous: {
+ GenerateEnumStringArray(sourceOut, decl, val2StrName, filteredElements);
+ int minVal = filteredElements.empty() ? 0 : filteredElements.front().value;
+ int maxVal = filteredElements.empty() ? 0 : filteredElements.back().value;
+
+ CodegenOutputThing lookupFunctionDecl;
+ {
+ auto& o = lookupFunctionDecl.text;
+ APPEND_LIT_LN(o, "template <>");
+ APPEND_FMT_LN(o, "std::string_view Metadata::EnumToString<%s>(%s value);", enumName.c_str(), enumName.c_str());
+ }
+
+ CodegenOutputThing lookupFunctionDef;
+ {
+ auto& o = lookupFunctionDef.text;
+ APPEND_LIT_LN(o, "template <>");
+ APPEND_FMT_LN(o, "std::string_view Metadata::EnumToString<%s>(%s value) {", enumName.c_str(), enumName.c_str());
+ APPEND_FMT_LN(o, " auto intVal = (%s)value;", FSTR_LUT_LOOKUP(EnumUnderlyingType, decl.underlyingType));
+ APPEND_FMT_LN(o, " if (intVal < %d || intVal > %d) return {};", minVal, maxVal);
+ APPEND_FMT_LN(o, " return %s[intVal - %d];", val2StrName, minVal);
+ APPEND_LIT_LN(o, "}");
+ }
+
+ headerOut.AddOutputThing(std::move(lookupFunctionDecl));
+ sourceOut.AddOutputThing(std::move(lookupFunctionDef));
+ } break;
+
+ case EVP_Bits: {
+ GenerateEnumStringArray(sourceOut, decl, val2StrName, filteredElements);
+ // TODO
+ } break;
+
+ case EVP_Random: {
+ GenerateEnumStringMap(sourceOut, decl, val2StrName, filteredElements);
+ // TODO
+ } break;
+
+ case EVP_COUNT: break;
+ }
+ }
+
+ if (decl.generateFromString) {
+ // Generate string -> value lookup table
+ INPLACE_FMT(str2ValName, "gCG_%s_Str2Val", mangledName.c_str());
+
+ CodegenOutputThing lookupTable;
+ {
+ auto& o = lookupTable.text;
+ // TODO use correct underlying type
+ APPEND_FMT_LN(o, "constinit frozen::unordered_map<frozen::string, %s, %" PRId64 "> %s = {", FSTR_LUT_LOOKUP(EnumUnderlyingType, decl.underlyingType), filteredElements.size(), str2ValName);
+ for (auto& elm : filteredElements) {
+ APPEND_FMT_LN(o, "{\"%s\", %" PRId64 "},", elm.name.c_str(), elm.value);
+ }
+ APPEND_LIT_LN(o, "};");
+ }
+
+ // Generate lookup function
+ CodegenOutputThing lookupFunctionDecl;
+ {
+ auto& o = lookupFunctionDecl.text;
+ APPEND_LIT_LN(o, "template <>");
+ APPEND_FMT_LN(o, "std::optional<%s> Metadata::EnumFromString<%s>(std::string_view value);", enumName.c_str(), enumName.c_str());
+ }
+
+ CodegenOutputThing lookupFunctionDef;
+ {
+ auto& o = lookupFunctionDef.text;
+ APPEND_LIT_LN(o, "template <>");
+ APPEND_FMT_LN(o, "std::optional<%s> Metadata::EnumFromString<%s>(std::string_view value) {", enumName.c_str(), enumName.c_str());
+ APPEND_FMT_LN(o, " auto iter = %s.find(value);", str2ValName);
+ APPEND_FMT_LN(o, " if (iter != %s.end()) {", str2ValName);
+ APPEND_FMT_LN(o, " return (%s)iter->second;", enumName.c_str());
+ APPEND_LIT_LN(o, " } else {");
+ APPEND_LIT_LN(o, " return {};");
+ APPEND_LIT_LN(o, " }");
+ APPEND_LIT_LN(o, "}");
+ }
+
+ sourceOut.AddOutputThing(std::move(lookupTable));
+ headerOut.AddOutputThing(std::move(lookupFunctionDecl));
+ sourceOut.AddOutputThing(std::move(lookupFunctionDef));
+ }
+}
+
+void GenerateClassProperty(CodegenOutput& headerOutput, CodegenOutput& sourceOutput) {
+ CodegenOutputThing thing;
+ APPEND_FMT_LN(thing.text, "TypePropertyInfo gCGtype_%s_%s_Property = {", "TODO", "TODO");
+ APPEND_LIT_LN(thing.text, "};");
+
+ sourceOutput.AddOutputThing(std::move(thing));
+}
+
+void GenerateClassFunction(CodegenOutput& headerOutput, CodegenOutput& sourceOutput) {
+ // TODO
+}
+
+void GenerateForClassMetadata(
+ CodegenOutput& headerOutput,
+ CodegenOutput& sourceOutput,
+ const DeclStruct& decl //
+) {
+ auto& mangedName = decl.GetMangledName();
+ auto mangedNameCstr = mangedName.c_str();
+
+ CodegenOutputThing data;
+ // TODO generate type id, this needs global scanning
+
+ if (!decl.baseClasses.empty()) {
+ // Forward declare the variables (which may appear before this section, after this section, or in another TU)
+ for (auto& baseClass : decl.baseClasses) {
+ auto baseClassIdName = baseClass->name.c_str();
+ APPEND_FMT_LN(data.text, "extern const TypeInfo gCGtype_%s_TypeInfo;", baseClassIdName);
+ }
+ APPEND_FMT_LN(data.text, "const TypeInfo* const gCGtype_%s_BaseClasses[] = {", mangedNameCstr);
+ for (auto& baseClass : decl.baseClasses) {
+ auto baseClassIdName = baseClass->name.c_str();
+ APPEND_FMT_LN(data.text, "gCGtype_%s_TypeInfo,", baseClassIdName);
+ }
+ APPEND_LIT_LN(data.text, "};");
+ }
+
+ if (!decl.memberVariables.empty()) {
+ APPEND_FMT_LN(data.text, "const TypePropertyInfo gCGtype_%s_Properties[] = {", mangedNameCstr);
+ for (auto& property : decl.memberVariables) {
+ APPEND_FMT_LN(data.text, "{.name=\"%s\"sv, .getterName=\"%s\"sv, .setterName=\"%s\"sv},", property.name.c_str(), property.getterName.c_str(), property.setterName.c_str());
+ }
+ APPEND_LIT_LN(data.text, "};");
+ }
+
+ if (!decl.memberFunctions.empty()) {
+ APPEND_FMT_LN(data.text, "const TypeMethodInfo gCGtype_%s_Methods[] = {", mangedNameCstr);
+ for (auto& method : decl.memberFunctions) {
+ // TODO
+ }
+ APPEND_LIT_LN(data.text, "};");
+ }
+
+ APPEND_FMT_LN(data.text, "const TypeInfo gCGtype_%s_TypeInfo = {", mangedNameCstr);
+ APPEND_FMT_LN(data.text, ".name = \"%s\"sv,", mangedNameCstr);
+ if (!decl.baseClasses.empty()) APPEND_FMT_LN(data.text, ".parents = gCGtype_%s_BaseClasses,", mangedNameCstr);
+ if (!decl.memberVariables.empty()) APPEND_FMT_LN(data.text, ".properties = gCGtype_%s_Properties,", mangedNameCstr);
+ if (!decl.memberFunctions.empty()) APPEND_FMT_LN(data.text, ".methods = gCGtype_%s_Methods,", mangedNameCstr);
+ APPEND_LIT_LN(data.text, "};");
+
+ CodegenOutputThing queryFunc;
+ APPEND_FMT(queryFunc.text,
+ "template <>\n"
+ "const TypeInfo* Metadata::GetTypeInfo<%s>() {\n"
+ " return &gCGtype_%s_TypeInfo;\n"
+ "}\n",
+ decl.fullname->c_str(),
+ mangedNameCstr);
+
+ sourceOutput.AddOutputThing(std::move(data));
+ sourceOutput.AddOutputThing(std::move(queryFunc));
+}
+
+struct NamespaceStackframe {
+ // The current namespace that owns the brace level, see example
+ DeclNamespace* ns = nullptr;
+ // Brace depth `ns` was created at (e.g. [std::details].depth == 0)
+ int depth = 0;
+};
+
+struct ParserState {
+ // TODO
+};
+
+struct ParserOutput {
+ // Example:
+ // namespace std::details {
+ // /* [stack top].ns = std::details */
+ // /* [stack top].depth = std */
+ // }
+ // namespace foo {
+ // /* [stack top].ns = foo */
+ // /* [stack top].depth = foo */
+ // namespace details {
+ // /* [stack top].ns = foo::details */
+ // /* [stack top].depth = foo::details */
+ // }
+ // }
+ std::vector<NamespaceStackframe> nsStack;
+ // The current effective namespace, see example
+ DeclNamespace* currentNamespace = nullptr;
+
+ DeclStruct* currentStruct = nullptr;
+ DeclEnum* currentEnum = nullptr;
+ int currentBraceDepth = 0;
+ int currentStructBraceDepth = -1;
+ int currentEnumBraceDepth = -1;
+};
+
+void HandleDirectiveEnum(AppState& as, ParserOutput& ps, CodegenLexer& lexer) {
+ // Consume the directive
+ ++lexer.idx;
+
+ if (!ps.currentEnum) {
+ printf("[ERROR] BRUSSEL_ENUM must be used within a enum\n");
+ return;
+ }
+
+ auto argList = TryConsumeDirectiveArgumentList(lexer);
+ auto& lut = RSTR_LUT(EnumMetaGenOptions);
+ for (auto& arg : argList) {
+ if (arg.empty()) {
+ printf("[ERROR] empty argument is invalid in BRUSSEL_ENUM\n");
+ continue;
+ }
+
+ auto& optionDirective = arg[0]->text;
+ auto iter = lut.find(optionDirective);
+ if (iter == lut.end()) {
+ printf("[ERROR] BRUSSEL_ENUM: invalid option '%s'\n", optionDirective.c_str());
+ }
+
+ auto option = iter->second;
+ switch (option) {
+ case EMGO_ToString: ps.currentEnum->generateToString = true; break;
+ case EMGO_FromString: ps.currentEnum->generateFromString = true; break;
+ case EMGO_ExcludeUseHeuristics: ps.currentEnum->generateExcludeUseHeuristics = true; break;
+
+ case EMGO_RemovePrefix: {
+ if (argList.size() <= 1) {
+ printf("[ERROR] missing argument for RemovePrefix");
+ break;
+ }
+ ps.currentEnum->generateRemovingPrefix = arg[1]->text;
+ } break;
+ case EMGO_AddPrefix: {
+ if (argList.size() <= 1) {
+ printf("[ERROR] missing argument for AddPrefix");
+ break;
+ }
+ ps.currentEnum->generatingAddingPrefix = arg[1]->text;
+ } break;
+
+ case EMGO_COUNT: break;
+ }
+ }
+
+ ps.currentEnum->generating = true;
+}
+
+CodegenLexer LexInputFile(AppState& as, std::string_view source) {
+ CodegenLexer result;
+ result.InitializeFrom(source);
+ return result;
+}
+
+void ParseInputFileAndGenerate(AppState& as, CodegenLexer& /*lexingState*/ ls, std::string_view filenameStem) {
+#if CODEGEN_DEBUG_PRINT
+ printf("BEGIN tokens\n");
+ for (auto& token : ls.tokens) {
+ switch (token.type) {
+ case CLEX_intlit: {
+ printf(" token %-32s = %ld\n", FSTR_LUT_LOOKUP(ClexNames, token.type), token.lexerIntNumber);
+ } break;
+
+ case CLEX_floatlit: {
+ printf(" token %-32s = %f\n", FSTR_LUT_LOOKUP(ClexNames, token.type), token.lexerRealNumber);
+ } break;
+
+ default: {
+ printf(" token %-32s '%s'\n", FSTR_LUT_LOOKUP(ClexNames, token.type), token.text.c_str());
+ } break;
+ }
+ }
+ printf("END tokens\n");
+#endif
+
+ auto& sourceFile = as.GetOrCreateSourceFile(filenameStem);
+ sourceFile.header = true;
+ sourceFile.reprocessing = true;
+
+ // TODO move lexedTokens and consumption related functions to ParserState struct
+
+ ParserOutput po;
+
+ auto& tokens = ls.tokens;
+ auto& idx = ls.idx;
+ while (ls.idx < ls.tokens.size()) {
+ auto& token = ls.Current();
+
+ bool incrementTokenIdx = true;
+ switch (token.Reamalgamate()) {
+ case CLEX_id: {
+ CppKeyword keyword;
+ {
+ auto& map = RSTR_LUT(CppKeyword);
+ auto iter = map.find(token.text);
+ if (iter != map.end()) {
+ keyword = iter->second;
+ } else {
+ keyword = CKw_COUNT; // Skip keyword section
+ }
+ }
+ switch (keyword) {
+ case CKw_Namespace: {
+ ++idx;
+ incrementTokenIdx = false;
+
+ int nestingCount = 0;
+ while (true) {
+ if (tokens[idx].type != CLEX_id) {
+ // TODO better error recovery
+ // TODO handle annoymous namespaces
+ printf("[ERROR] invalid syntax for namespace\n");
+ break;
+ }
+
+ po.currentNamespace = as.runtimeModel->AddNamespace(DeclNamespace{
+ .container = po.currentNamespace,
+ .name = tokens[idx].text,
+ });
+
+ // Consume the identifier token
+ ++idx;
+
+ if (tokens[idx].type == CLEX_ext_double_colon) {
+ // Consume the "::" token
+ ++idx;
+ } else {
+ break;
+ }
+ }
+
+ po.nsStack.push_back(NamespaceStackframe{
+ .ns = po.currentNamespace,
+ .depth = po.currentBraceDepth,
+ });
+
+ goto endCaseCLEX_id;
+ }
+
+ case CKw_Struct:
+ case CKw_Class: {
+ // Consume the 'class' or 'struct' keyword
+ ++idx;
+ incrementTokenIdx = false;
+
+ // For forward declarations, there are always 2 tokens after `class`: an identifier, and the ';' token
+ // Example:
+ // class MyClass;
+ if (tokens[idx + 0].type == CLEX_id &&
+ tokens[idx + 1].text == ";")
+ {
+ // Skip class forward declarations
+ idx += 2;
+ break;
+ }
+
+ auto& idenTok = tokens[idx];
+ if (idenTok.type != CLEX_id) {
+ printf("[ERROR] invalid syntax for struct or class\n");
+ break;
+ }
+
+ DEBUG_PRINTF("[DEBUG] found struct named %s\n", idenTok.text.c_str());
+
+ auto& name = idenTok.text;
+ auto fullname = Utils::MakeFullName(name, po.currentNamespace);
+ DeclStruct structDecl;
+ structDecl.sourceFile = &sourceFile;
+ structDecl.container = po.currentNamespace;
+ structDecl.name = name;
+
+ // Consume the identifier token
+ ++idx;
+
+ if (ls.TryConsumeSingleCharToken(':')) {
+ while (true) {
+ // Public, protected, etc.
+ TryConsumeAnyKeyword(ls);
+
+ auto& idenTok = tokens[idx];
+ if (idenTok.type != CLEX_id) {
+ printf("[ERROR] invalid syntax for class inheritance list\n");
+ goto endCase;
+ }
+
+ // TODO support namespace qualified names
+ auto baseClassFullname = Utils::MakeFullName(idenTok.text, po.currentNamespace);
+ auto baseClassDecl = as.runtimeModel->FindStruct(baseClassFullname);
+ if (baseClassDecl) {
+ // TODO retreive class from database
+ // ---- Or just silent create it, and assume the code was valid?
+ // We silently ignore a non-existent base class, because they may reside in a file that we didn't scan
+ structDecl.baseClasses.push_back(baseClassDecl);
+ }
+
+ // Consume the identifier token
+ ++idx;
+
+ if (ls.TryConsumeSingleCharToken('{')) {
+ // End of base class list
+ --idx; // Give the '{' token back to the main loop
+ break;
+ } else if (!ls.TryConsumeSingleCharToken(',')) {
+ // If the list didn't end, we expect a comma (then followed by more entries)
+ printf("[ERROR] invalid syntax for class inheritance list\n");
+ goto endCase;
+ }
+
+ // NOTE: we currently only scan one base class to workaround some code inherits from template classes after their initial base class
+ // TODO remove this hack
+ break;
+ }
+ }
+
+ {
+ // Get a pointer to the decl inside CodegenInput's storage
+ auto decl = as.runtimeModel->AddStruct(std::move(fullname), std::move(structDecl));
+ po.currentStruct = decl;
+ po.currentStructBraceDepth = po.currentBraceDepth;
+ }
+
+ endCase:
+ goto endCaseCLEX_id;
+ }
+
+ case CKw_Enum: {
+ if (po.currentStruct) {
+ // TODO parsing enums inside classes is currently broken (1. model database is not modeled for this 2. codegen logic is not modeled)
+ break;
+ }
+
+ // Consume the "enum" keyword
+ ++idx;
+ incrementTokenIdx = false;
+
+ StbLexerToken* idenTok;
+ if (tokens[idx].text == "class") {
+ // Consume the "class" keyword
+ ++idx;
+ idenTok = &tokens[idx];
+ DEBUG_PRINTF("[DEBUG] found enum class named %s\n", idenTok->text.c_str());
+ } else {
+ idenTok = &tokens[idx];
+ DEBUG_PRINTF("[DEBUG] found enum named %s\n", idenTok->text.c_str());
+ }
+
+ auto& name = tokens[idx].text;
+ auto fullname = Utils::MakeFullName(name, po.currentNamespace);
+ DeclEnum enumDecl;
+ enumDecl.sourceFile = &sourceFile;
+ enumDecl.container = po.currentNamespace;
+ enumDecl.name = name;
+ // Setting underlying type: see below
+
+ // Temporarily bind the pointers to local variable, HandleDirectiveEnum() and other functions expect these to the set
+ po.currentEnum = &enumDecl;
+ po.currentEnumBraceDepth = po.currentBraceDepth;
+
+ // Consume the enum name identifier
+ ++idx;
+
+ // Setting underlying type
+ if (auto eut = TryConsumeEnumUnderlyingTypeClause(ls);
+ eut != EUT_COUNT)
+ {
+ enumDecl.underlyingType = eut;
+ } else {
+ enumDecl.underlyingType = EUT_Int32;
+ }
+
+ int enumClosingBraceCount = 0;
+ int enumBraceDepth = 0;
+ while (enumClosingBraceCount == 0 && idx < tokens.size()) {
+ auto& token = tokens[idx];
+ switch (token.Reamalgamate()) {
+ case CLEX_id: {
+ if (token.text == "BRUSSEL_ENUM") {
+ // Consume the argument list and skip advancing index: this function already consumed all the tokens about BRUSSEL_ENUM
+ HandleDirectiveEnum(as, po, ls);
+ continue;
+ } else {
+ auto& vec = enumDecl.elements;
+ // Set to the previous enum element's value + 1, or starting from 0 if this is the first
+ // Also overridden in the CLEX_intlit branch
+ auto value = vec.empty() ? 0 : vec.back().value + 1;
+ vec.push_back(DeclEnumElement{
+ .name = token.text,
+ .value = value,
+ });
+ }
+ } break;
+
+ case CLEX_intlit: {
+ auto& vec = enumDecl.elements;
+ if (!vec.empty()) {
+ auto& lastElm = vec.back();
+ lastElm.value = token.lexerIntNumber;
+ }
+ } break;
+
+ case '{': {
+ ++enumBraceDepth;
+ } break;
+
+ case '}': {
+ --enumBraceDepth;
+ ++enumClosingBraceCount;
+ } break;
+ }
+
+ ++idx;
+ }
+
+ {
+ auto decl = as.runtimeModel->AddEnum(std::move(fullname), std::move(enumDecl));
+ // Fix pointers
+ po.currentEnum = decl;
+ po.currentEnumBraceDepth = po.currentBraceDepth;
+ }
+
+ if (po.currentEnum->generating) {
+ as.enumsToRevisit.push_back(po.currentEnum);
+ }
+
+ // NOTE: we parse the whole enum at once (above code), the enum ends right here after the closing brace '}'
+ po.currentEnum = nullptr;
+ po.currentEnumBraceDepth = -1;
+
+ goto endCaseCLEX_id;
+ }
+
+ // Consume the whole statement, because this statement may contain `enum` or `class` keywords that will pollute the parser
+ case CKw_Template: {
+ // `template` is either a template list which we don't care about, or a part of a type which we don't care about,
+ // unless it's a part of a function declaration, where the tokens are handled inside CG_ClassMethod parsing
+ // TODO handle nested templates or operator> inside template expression
+ ls.SkipUntilTokenSingleChar('>');
+ } break;
+ case CKw_Using: {
+ // `using` indicates a type alias or namespace import which we don't care about
+ ls.SkipUntilTokenSingleChar(';');
+ } break;
+
+ // We don't care about these keywords
+ case CKw_Public:
+ case CKw_Protected:
+ case CKw_Private:
+ case CKw_Virtual:
+ case CKw_COUNT: break;
+ }
+
+ CodegenDirective directive;
+ {
+ auto& map = RSTR_LUT(CodegenDirective);
+ auto iter = map.find(token.text);
+ if (iter != map.end()) {
+ directive = iter->second;
+ } else {
+ directive = CD_COUNT; // Skip directive section
+ }
+ }
+ switch (directive) {
+ case CD_Class: {
+ // Consume the directive
+ ++idx;
+ incrementTokenIdx = false;
+
+ if (!po.currentStruct) {
+ printf("[ERROR] BRUSSEL_CLASS must be used within a class or struct\n");
+ break;
+ }
+
+ // Always-on option
+ po.currentStruct->generating = true;
+
+ auto argList = TryConsumeDirectiveArgumentList(ls);
+ auto& lut = RSTR_LUT(StructMetaGenOptions);
+ for (auto& arg : argList) {
+ if (arg.empty()) {
+ printf("[ERROR] empty argument is invalid in BRUSSEL_CLASS\n");
+ continue;
+ }
+
+ auto& optionDirective = arg[0]->text;
+ auto iter = lut.find(optionDirective);
+ if (iter == lut.end()) continue;
+ switch (iter->second) {
+ case SMGO_InheritanceHiearchy: po.currentStruct->generatingInheritanceHiearchy = true; break;
+ case SMGO_COUNT: break;
+ }
+ }
+
+ goto endCaseCLEX_id;
+ }
+
+ case CD_ClassProperty: {
+ // Consume the directive
+ ++idx;
+ incrementTokenIdx = false;
+
+ if (!po.currentStruct ||
+ !po.currentStruct->generating)
+ {
+ printf("[ERROR] BRUSSEL_PROPERTY must be used within a class or struct, that has the BRUSSEL_CLASS directive\n");
+ break;
+ }
+
+ auto argList = TryConsumeDirectiveArgumentList(ls);
+ auto declOpt = TryConsumeMemberVariable(ls);
+ if (!declOpt.has_value()) {
+ printf("[ERROR] a member variable must immediately follow a BRUSSEL_PROPERTY\n");
+ break;
+ }
+ auto& decl = declOpt.value();
+ decl.containerStruct = po.currentStruct;
+
+ // Different option's common logic
+ std::string pascalCaseName;
+ auto GetPascalCasedName = [&]() -> const std::string& {
+ if (pascalCaseName.empty()) {
+ pascalCaseName = Utils::MakePascalCase(decl.name);
+ }
+ return pascalCaseName;
+ };
+
+ auto& lut = RSTR_LUT(StructPropertyOptions);
+ for (auto& arg : argList) {
+ if (arg.empty()) {
+ printf("[ERROR] empty argument is invalid in BRUSSEL_PROPERTY\n");
+ continue;
+ }
+
+ auto& optionDirective = arg[0]->text;
+ auto iter = lut.find(optionDirective);
+ if (iter == lut.end()) continue;
+ switch (iter->second) {
+ case SPO_Getter: {
+ // NOTE: I'm too lazy to write error checks, just let the codegen crash if syntax is invalid
+ auto& getterName = arg.at(1)->text;
+ if (getterName == "auto") {
+ // NOTE: intentionally shadowing
+ INPLACE_FMT(getterName, "Get%s", GetPascalCasedName().c_str());
+
+ decl.getterName = getterName;
+ decl.isGetterGenerated = true;
+ } else {
+ decl.getterName = getterName;
+ }
+ } break;
+
+ case SPO_Setter: {
+ // NOTE: I'm too lazy to write error checks, just let the codegen crash if syntax is invalid
+ auto& setterName = arg.at(1)->text;
+ if (setterName == "auto") {
+ // NOTE: intentionally shadowing
+ INPLACE_FMT(setterName, "Set%s", GetPascalCasedName().c_str());
+
+ decl.setterName = setterName;
+ decl.isSetterGenerated = true;
+ } else {
+ decl.setterName = setterName;
+ }
+ } break;
+
+ case SPO_COUNT: break;
+ }
+ }
+
+ po.currentStruct->memberVariables.push_back(std::move(decl));
+
+ goto endCaseCLEX_id;
+ }
+
+ case CD_ClassMethod: {
+ // Consume the directive
+ ++idx;
+ incrementTokenIdx = false;
+
+ goto endCaseCLEX_id;
+ }
+
+ case CD_XGlobalVar: {
+ // TODO
+ goto endCaseCLEX_id;
+ }
+
+ case CD_XGlobalVarCtor: {
+ // TODO
+ goto endCaseCLEX_id;
+ }
+
+ case CD_XGlobalVarDtor: {
+ // TODO
+ goto endCaseCLEX_id;
+ }
+
+ // This directive always appear inside a enum{} block, which is handled above in the keywords section
+ case CD_Enum:
+ case CD_COUNT: break;
+ }
+
+ endCaseCLEX_id:;
+ } break;
+
+ case '{': {
+ po.currentBraceDepth++;
+ if (po.currentBraceDepth < 0) {
+ printf("[WARNING] unbalanced brace\n");
+ }
+ } break;
+
+ case '}': {
+ po.currentBraceDepth--;
+ if (po.currentBraceDepth < 0) {
+ printf("[WARNING] unbalanced brace\n");
+ }
+
+ if (!po.nsStack.empty()) {
+ auto& ns = po.nsStack.back();
+ if (ns.depth == po.currentBraceDepth) {
+ po.nsStack.pop_back();
+
+ if (!po.nsStack.empty()) {
+ po.currentNamespace = po.nsStack.back().ns;
+ } else {
+ po.currentNamespace = nullptr;
+ }
+ }
+ }
+
+ if (po.currentStruct && po.currentBraceDepth == po.currentStructBraceDepth) {
+ // Exit struct
+
+ if (po.currentStruct->generating) {
+ as.structsToRevisit.push_back(po.currentStruct);
+ }
+
+ po.currentStruct = nullptr;
+ po.currentStructBraceDepth = -1;
+ }
+ if (po.currentEnum && po.currentBraceDepth == po.currentEnumBraceDepth) {
+ // Exit enum
+
+ // TODO this is unused currently, see CKw_Enum branch
+ if (po.currentEnum->generating) {
+ as.enumsToRevisit.push_back(po.currentEnum);
+ }
+
+ po.currentEnum = nullptr;
+ po.currentEnumBraceDepth = -1;
+ }
+ } break;
+ }
+
+ if (incrementTokenIdx) {
+ ++idx;
+ }
+ }
+
+ if (po.currentBraceDepth != 0) {
+ printf("[WARNING] unbalanced brace at end of file\n");
+ }
+
+ as.archiveModel->DeleteDeclsRelatedToFile(filenameStem);
+ // as.modelArchive->Store(po.model);
+}
+
+void HandleInputFile(AppState& as, const fs::path& path) {
+ auto filenameStem = path.stem().string();
+ auto lexingState = LexInputFile(as, Utils::ReadFileAsString(path));
+ ParseInputFileAndGenerate(as, lexingState, filenameStem);
+}
+
+enum InputOpcode {
+ IOP_ProcessSingleFile,
+ IOP_ProcessRecursively,
+ IOP_ProcessFileList,
+ IOP_COUNT,
+};
+
+void HandleArgument(AppState& as, InputOpcode opcode, std::string_view operand) {
+ switch (opcode) {
+ case IOP_ProcessSingleFile: {
+ DEBUG_PRINTF("Processing single file %.*s\n", PRINTF_STRING_VIEW(operand));
+ HandleInputFile(as, fs::path(operand));
+ } break;
+
+ case IOP_ProcessRecursively: {
+ DEBUG_PRINTF("Recursively processing folder %.*s\n", PRINTF_STRING_VIEW(operand));
+
+ fs::path startPath(operand);
+ for (auto& item : fs::recursive_directory_iterator(startPath)) {
+ if (!item.is_regular_file()) {
+ continue;
+ }
+
+ auto& path = item.path();
+ if (auto pathExt = path.extension();
+ pathExt != ".h" &&
+ pathExt != ".hpp")
+ {
+ continue;
+ }
+
+ DEBUG_PRINTF("Processing subfile %s\n", path.string().c_str());
+ HandleInputFile(as, path);
+ }
+ } break;
+
+ case IOP_ProcessFileList: {
+ DEBUG_PRINTF("Processing file list %.*s\n", PRINTF_STRING_VIEW(operand));
+
+ fs::path fileListPath(operand);
+ auto fileList = Utils::OpenCstdioFile(fileListPath, Utils::Read);
+ if (!fileList) {
+ // NOTE: need this because our dirty-file-list generation algorithm in CMakeLists.txt doesn't produce a file when nothing is changed
+ DEBUG_PRINTF("File-list file does not exist, silently skipping.\n");
+ break;
+ }
+ DEFER {
+ fclose(fileList);
+ };
+
+ std::string line;
+ while (Utils::ReadCstdioLine(fileList, line)) {
+ // Remove '\n'
+ line.pop_back();
+
+ DEBUG_PRINTF("Processing file in list %.*s\n", line.c_str());
+ HandleInputFile(as, fs::path(line));
+ }
+ } break;
+
+ case IOP_COUNT: break;
+ }
+}
+
+InputOpcode ParseInputOpcode(std::string_view text) {
+ if (text == "single"sv) {
+ return IOP_ProcessSingleFile;
+ } else if (text == "rec"sv) {
+ return IOP_ProcessRecursively;
+ } else if (text == "fileList"sv) {
+ return IOP_ProcessFileList;
+ } else {
+ INPLACE_FMT(msg, "Unknown input opcode %s\n", text.data());
+ throw std::runtime_error(msg);
+ }
+}
+
+int main(int argc, char* argv[]) {
+ FSTR_LUT_INIT(ClexNames);
+ FSTR_LUT_INIT(EnumUnderlyingType);
+ RSTR_LUT_INIT(EnumUnderlyingType);
+ FSTR_LUT_INIT(EnumValuePattern);
+ RSTR_LUT_INIT(CppKeyword);
+ RSTR_LUT_INIT(CodegenDirective);
+ RSTR_LUT_INIT(StructMetaGenOptions);
+ RSTR_LUT_INIT(StructPropertyOptions);
+ RSTR_LUT_INIT(EnumMetaGenOptions);
+
+ // TODO better arg parser
+ // option 1: use cxxopts and positional arguments
+ // option 2: take one argument only, being a json objecet
+
+ AppState as;
+
+ // If no cli is provided (argv[0] conventionally but not mandatorily the cli), this will do thing
+ // Otherwise, start with the 2nd element in the array, which is the 1st actual argument
+ if (argc <= 1) {
+ // NOTE: keep in sync with various enum options and parser code
+ printf(&R"""(
+USAGE: codegen.exe --output-dir=<path> [--database=<path>] [<opcode>:<input path>]...
+where --output-dir=<path>: the *directory* to write generated contents to. This will NOT automatically create the directory.
+ --database=<path>: the *file* to use for the code model database.
+ <opcode> is one of:
+ "single" process this <input path> file only
+ "rec" starting at the given directory <input path>, recursively process all .h .hpp files
+ "fileList" read <input path> as a text file, and process each line as a separate file path
+)"""[1]);
+ return -1;
+ }
+
+ // Named argument pass
+ robin_hood::unordered_map<std::string_view, std::string_view*> namedArguments{
+ { "output-dir"sv, &as.outputDir },
+ { "database"sv, &as.databaseFilePath },
+ };
+ for (int i = 1; i < argc; ++i) {
+ std::string_view arg(argv[i]);
+ if (!arg.starts_with("--")) {
+ // Convention: a "--" argument indicates everything afterwords are positional arguments
+ if (arg.size() == 2) {
+ break;
+ } else {
+ continue;
+ }
+ }
+
+ size_t equalLoc = arg.find('=');
+ auto oper = arg.substr(/*--*/ 2, equalLoc - 2);
+ auto iter = namedArguments.find(oper);
+ if (iter != namedArguments.end()) {
+ auto storage = iter->second;
+ if (storage) {
+ if (equalLoc == std::string_view::npos) {
+ *storage = ""sv;
+ } else {
+ *storage = arg.substr(equalLoc + 1);
+ }
+ }
+ }
+ }
+
+ DEBUG_PRINTF("Outputting to directory %.*s.\n", PRINTF_STRING_VIEW(as.outputDir));
+ DEBUG_PRINTF("Databse file: %.*s.\n", PRINTF_STRING_VIEW(as.databaseFilePath));
+
+ // TODO move the actual output logic after processing all input commands, based on SQLite batabase model instead of the in-memory CodegenModel model
+ // this allows better consistency between direct in-file entities (like enums) vs. multi-file entities (like struct inheritance hierarchy)
+ // this would also mean almost rewriting the whole codegen logic, to work on a changelist fetched from SQLite database instead of being embedded inside the parser loop
+ // TODO how do we detect the case of
+ // 1. has: Foo.hpp Bar.hpp
+ // 2. struct Foo; struct Bar : Foo;
+ // 3. struct Foo is removed from Foo.hpp, but our parser only recieves Foo.hpp as file changed--and can't figure out that there is still a reference to Foo in Bar.hpp
+ // possible solutions
+ // - use some kind of database scanner to review all references to a class when removing (e.g. detect for logic error on foreign key linked columns)
+ // - follow the file links in database, and propagate parsing to those files in the hierarchy
+ // - pretty much defeats the purpose of using an incremental parser: some classes like GameObject will have links throughout a very large portion of the project code
+ // - [x] out of parser generation
+ // - [ ] database readback
+ // - [ ] full database based generation (tentative)
+ CodegenRuntimeModel runtimeModel;
+ CodegenArchiveModel archiveModel(as.databaseFilePath);
+
+ as.runtimeModel = &runtimeModel;
+ as.archiveModel = &archiveModel;
+
+ // Positional argument pass
+ for (int i = 1; i < argc; ++i) {
+ std::string_view arg(argv[i]);
+ if (arg.starts_with("--")) {
+ continue;
+ }
+
+ DEBUG_PRINTF("Processing input command %s\n", argv[i]);
+
+ auto separatorLoc = arg.find(':');
+ if (separatorLoc != std::string_view::npos) {
+ auto opcodeString = arg.substr(0, separatorLoc);
+ auto opcode = ParseInputOpcode(opcodeString);
+ auto operand = arg.substr(separatorLoc + 1);
+
+ HandleArgument(as, opcode, operand);
+ }
+ }
+
+ for (auto decl : as.enumsToRevisit) {
+ if (!decl->generating) {
+ continue;
+ }
+
+ auto& headerOutput = decl->sourceFile->postHeaderOutput;
+ auto& sourceOutput = decl->sourceFile->postSourceOutput;
+ GenerateForEnum(headerOutput, sourceOutput, *decl);
+ }
+ for (auto decl : as.structsToRevisit) {
+ if (!decl->generating) {
+ continue;
+ }
+
+ auto& headerOutput = decl->sourceFile->postHeaderOutput;
+ auto& sourceOutput = decl->sourceFile->postSourceOutput;
+
+ // Always-on metdata
+ GenerateForClassMetadata(headerOutput, sourceOutput, *decl);
+
+ if (decl->generatingInheritanceHiearchy) {
+ // TODO
+ }
+
+ for (auto& property : decl->memberVariables) {
+ if (property.isGetterGenerated) {
+ // TODO work with pass-by-value vs pass-by-reference
+ // this probably needs libclang to detect the size and existance of trivial copy-ctors
+ CodegenOutputThing data;
+ APPEND_FMT_LN(data.text, "const %s& %s::%s() const {", property.type.c_str(), property.containerStruct->fullname->c_str(), property.getterName.c_str());
+ APPEND_FMT_LN(data.text, " return %s;", property.name.c_str());
+ APPEND_LIT_LN(data.text, "}");
+
+ sourceOutput.AddOutputThing(std::move(data));
+ }
+ if (property.isSetterGenerated) {
+ CodegenOutputThing data;
+ APPEND_FMT_LN(data.text, "void %s::%s(const %s& value) const {", property.containerStruct->fullname->c_str(), property.setterName.c_str(), property.type.c_str());
+ APPEND_FMT_LN(data.text, " this->%s = value;", property.name.c_str());
+ APPEND_LIT_LN(data.text, "}");
+
+ sourceOutput.AddOutputThing(std::move(data));
+ }
+ }
+ for (auto& method : decl->memberFunctions) {
+ // TODO
+ }
+ }
+
+ archiveModel.Store(runtimeModel);
+
+ // Write output files
+ for (auto&& [_, sourceFile] : as.sourceFiles) {
+ INPLACE_FMT(hpp, "%.*s.gh.inl", PRINTF_STRING_VIEW(sourceFile.filename));
+ INPLACE_FMT(cpp, "%.*s.gs.inl", PRINTF_STRING_VIEW(sourceFile.filename));
+ Utils::ProduceGeneratedHeader(hpp, sourceFile.postHeaderOutput, cpp, sourceFile.postSourceOutput);
+
+ INPLACE_FMT(generatedHeaderInlName, "%.*s/%s", PRINTF_STRING_VIEW(as.outputDir), hpp);
+ Utils::WriteOutputFile(sourceFile.postHeaderOutput, generatedHeaderInlName);
+ INPLACE_FMT(generatedSourceInlName, "%.*s/%s", PRINTF_STRING_VIEW(as.outputDir), cpp);
+ Utils::WriteOutputFile(sourceFile.postSourceOutput, generatedSourceInlName);
+ INPLACE_FMT(generatedCppName, "%.*s/%.*s.g.cpp", PRINTF_STRING_VIEW(as.outputDir), PRINTF_STRING_VIEW(sourceFile.filename));
+ Utils::WriteOutputFile(sourceFile.tuOutput, generatedCppName);
+ }
+
+ return 0;
+}
+
+// TODO move this function to CodegenDecl.cpp, after making LUT able to cross TUs
+std::string_view DeclEnum::GetUnderlyingTypeName() const {
+ return FSTR_LUT_LOOKUP(EnumUnderlyingType, underlyingType);
+}
diff --git a/source/20-codegen-compiler/test/examples/TestClass.hpp.txt b/source/20-codegen-compiler/test/examples/TestClass.hpp.txt
new file mode 100644
index 0000000..3eed8db
--- /dev/null
+++ b/source/20-codegen-compiler/test/examples/TestClass.hpp.txt
@@ -0,0 +1,38 @@
+#include <TestClass.gph.inl>
+
+class MyClass {
+ BRUSSEL_CLASS()
+
+public:
+ BRUSSEL_PROPERTY(GETTER GetName, SETTER SetName)
+ std::string name;
+
+ BRUSSEL_PROPERTY(GETTER auto, SETTER auto)
+ std::string tag;
+
+ BRUSSEL_PROPERTY()
+ int foo;
+
+ BRUSSEL_PROPERTY()
+ int bar;
+
+public:
+ const std::string& GetName() const { return name; }
+ void SetName(std::string name) { this->name = std::move(name); }
+};
+
+namespace MyNamespace {
+struct Base {
+ BRUSSEL_CLASS(InheritanceHiearchy)
+};
+
+struct DerviedFoo : public Base {
+ BRUSSEL_CLASS()
+};
+
+struct DerviedBar : Base {
+ BRUSSEL_CLASS()
+};
+}
+
+#include <TestClass.gh.inl>
diff --git a/source/20-codegen-compiler/test/examples/TestEnum.hpp.txt b/source/20-codegen-compiler/test/examples/TestEnum.hpp.txt
new file mode 100644
index 0000000..132bac0
--- /dev/null
+++ b/source/20-codegen-compiler/test/examples/TestEnum.hpp.txt
@@ -0,0 +1,44 @@
+enum MyEnum {
+ BRUSSEL_ENUM(ToString, FromString)
+ EnumElement1,
+ EnumElement2,
+ EnumElement3,
+};
+
+// Let's also test enum class
+enum class CountedEnumAll {
+ BRUSSEL_ENUM(ToString, FromString)
+ CEA_Foo,
+ CEA_Bar,
+ CEA_COUNT,
+};
+
+enum CountedEnum : unsigned short int {
+ BRUSSEL_ENUM(ToString, FromString, RemovePrefix CE_, AddPrefix CustomPrefix, ExcludeHeuristics)
+ CE_Foo,
+ CE_Bar,
+ CE_FooBar,
+ CE_COUNT,
+};
+
+namespace MyNamespace {
+ enum class MyNamespacedEnum {
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ MNE_Foo,
+ MNE_Bar,
+ };
+
+ namespace details {
+ enum MyNamespacedEnum {
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ MNE_Foo,
+ MNE_Bar,
+ };
+ }
+}
+
+namespace foo::details {
+ enum Enum {
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ };
+}
diff --git a/source/20-codegen-runtime/MacrosCodegen.hpp b/source/20-codegen-runtime/MacrosCodegen.hpp
new file mode 100644
index 0000000..43ae99c
--- /dev/null
+++ b/source/20-codegen-runtime/MacrosCodegen.hpp
@@ -0,0 +1,10 @@
+// NOTE: Contents of this file is coupled with the codegen compiler.
+// When updating, change both sides at the same time.
+
+#pragma once
+
+#define BRUSSEL_CLASS(...)
+#define BRUSSEL_PROPERTY(...)
+#define BRUSSEL_METHOD(...)
+
+#define BRUSSEL_ENUM(...)
diff --git a/source/20-codegen-runtime/Metadata.cpp b/source/20-codegen-runtime/Metadata.cpp
new file mode 100644
index 0000000..0d640da
--- /dev/null
+++ b/source/20-codegen-runtime/Metadata.cpp
@@ -0,0 +1,45 @@
+#include "Metadata.hpp"
+
+auto Metadata::TypeInfoList::Iterator::operator*() const -> const TypeInfo& {
+ // TODO
+}
+
+auto Metadata::TypeInfoList::Iterator::operator++() -> Iterator& {
+ // TODO
+}
+
+auto Metadata::TypeInfoList::Iterator::operator++(int) -> Iterator {
+ auto copy = *this;
+ ++copy;
+ return copy;
+}
+
+bool Metadata::TypeInfoList::Iterator::operator==(const Iterator& that) const {
+ return this->data == that.data;
+}
+
+bool Metadata::TypeInfoList::Iterator::operator==(const Sentinel&) const {
+ // TODO
+}
+
+auto Metadata::TypeInfoList::begin() const -> Iterator {
+ return Iterator();
+}
+
+auto Metadata::TypeInfoList::end() const -> Sentinel {
+ return Sentinel();
+}
+
+auto Metadata::GetTypeInfoList() -> const TypeInfoList& {
+ // TODO
+}
+
+auto Metadata::QueryTypeInfo(TypeId id) -> const TypeInfo* {
+ // TODO
+ return nullptr;
+}
+
+auto Metadata::QueryTypeInfo(std::string_view id) -> const TypeInfo* {
+ // TODO
+ return nullptr;
+}
diff --git a/source/20-codegen-runtime/Metadata.hpp b/source/20-codegen-runtime/Metadata.hpp
new file mode 100644
index 0000000..e89fd8f
--- /dev/null
+++ b/source/20-codegen-runtime/Metadata.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "MacrosCodegen.hpp"
+#include "MetadataBase.hpp"
+
+namespace Metadata {
+
+struct TypeInfoList {
+ struct Sentinel {
+ };
+
+ struct Iterator {
+ void* data;
+
+ const TypeInfo& operator*() const;
+ Iterator& operator++();
+ Iterator operator++(int);
+
+ bool operator==(const Iterator&) const;
+ bool operator==(const Sentinel&) const;
+ };
+
+ Iterator begin() const;
+ Sentinel end() const;
+};
+
+/// Get a list of all type infos present.
+const TypeInfoList& GetTypeInfoList();
+
+const TypeInfo* QueryTypeInfo(TypeId id);
+const TypeInfo* QueryTypeInfo(std::string_view name);
+
+} // namespace Metadata
diff --git a/source/20-codegen-runtime/MetadataBase.cpp b/source/20-codegen-runtime/MetadataBase.cpp
new file mode 100644
index 0000000..2f2ef94
--- /dev/null
+++ b/source/20-codegen-runtime/MetadataBase.cpp
@@ -0,0 +1,5 @@
+#include "MetadataBase.hpp"
+
+bool Metadata::TypePropertyInfo::IsDirectAccess() const {
+ return getterName.empty() && setterName.empty();
+}
diff --git a/source/20-codegen-runtime/MetadataBase.hpp b/source/20-codegen-runtime/MetadataBase.hpp
new file mode 100644
index 0000000..f84a152
--- /dev/null
+++ b/source/20-codegen-runtime/MetadataBase.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <cstddef>
+#include <optional>
+#include <span>
+#include <string_view>
+
+namespace Metadata {
+
+struct TypeId {
+ size_t id;
+};
+
+struct TypeInfo;
+
+struct TypePropertyInfo {
+ std::string_view name;
+ const TypeInfo* type;
+ std::string_view getterName;
+ std::string_view setterName;
+
+ bool IsDirectAccess() const;
+};
+
+struct TypeMethodInfo {
+ std::string_view name;
+ // TODO
+};
+
+struct TypeInfo {
+ TypeId typeId;
+ std::string_view name;
+ std::span<const TypeInfo* const> parents;
+ std::span<const TypePropertyInfo> properties;
+ std::span<const TypeMethodInfo> methods;
+
+ /// Whether this object is registered at runtime or statically compiled
+ bool dynamic = false;
+};
+
+// NOTE: implemented by generating specializations; not-generated ones should generated an error in the linking phase
+template <typename T>
+const TypeInfo* GetTypeInfo();
+
+// NOTE: implemented by generating specializations
+template <typename TEnum>
+std::string_view EnumToString(TEnum value) = delete;
+
+// NOTE: implemented by generating specializations
+template <typename TEnum>
+std::optional<TEnum> EnumFromString(std::string_view str) = delete;
+
+} // namespace Metadata
diff --git a/source/20-codegen-runtime/MetadataDetails.hpp b/source/20-codegen-runtime/MetadataDetails.hpp
new file mode 100644
index 0000000..09b71ff
--- /dev/null
+++ b/source/20-codegen-runtime/MetadataDetails.hpp
@@ -0,0 +1,7 @@
+// This file contains implementation details used by the outputs of the code generaetor. Consumers do not use.
+#pragma once
+
+#include "MetadataBase.hpp"
+
+namespace Metadata::Details {
+} // namespace Metadata::Details
diff --git a/source/30-game/App.cpp b/source/30-game/App.cpp
new file mode 100644
index 0000000..8328589
--- /dev/null
+++ b/source/30-game/App.cpp
@@ -0,0 +1,168 @@
+#include "App.hpp"
+
+#include "ScopeGuard.hpp"
+#include "Utils.hpp"
+
+#include <rapidjson/document.h>
+#include <rapidjson/filereadstream.h>
+#include <string>
+#include <utility>
+
+using namespace std::literals;
+
+App::App()
+ : mActiveCamera{ &mMainCamera } {
+ auto& worldRoot = mWorld.GetRoot();
+
+ constexpr int kPlayerCount = 2;
+ for (int i = 0; i < kPlayerCount; ++i) {
+ auto player = new Player(&mWorld, i);
+ worldRoot.AddChild(player);
+ mPlayers.push_back(player);
+ };
+
+#if defined(BRUSSEL_DEV_ENV)
+ SetGameRunning(false);
+ SetEditorVisible(true);
+#else
+ SetGameRunning(true);
+#endif
+
+ mMainCamera.name = "Main Camera"s;
+ mMainCamera.SetEyePos(glm::vec3(0, 0, 1));
+ mMainCamera.SetTargetDirection(glm::vec3(0, 0, -1));
+ mMainCamera.SetHasPerspective(false);
+
+ do {
+ auto file = Utils::OpenCstdioFile("assets/GameRendererBindings.json", Utils::Read);
+ if (!file) break;
+ DEFER { fclose(file); };
+
+ char readerBuffer[65536];
+ rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer));
+
+ rapidjson::Document root;
+ root.ParseStream(stream);
+
+ mWorldRenderer.LoadBindings(root);
+ } while (false);
+}
+
+App::~App() {
+}
+
+Camera* App::GetActiveCamera() const {
+ return mActiveCamera;
+}
+
+void App::BindActiveCamera(Camera* camera) {
+ mActiveCamera = camera;
+}
+
+void App::UnbindActiveCamera() {
+ mActiveCamera = &mMainCamera;
+}
+
+bool App::IsGameRunning() const {
+ return mGameRunning;
+}
+
+void App::SetGameRunning(bool running) {
+ if (mGameRunning != running) {
+ mGameRunning = running;
+ if (running) {
+ mWorld.Awaken();
+ } else {
+ mWorld.Resleep();
+ }
+ }
+}
+
+bool App::IsEditorVisible() const {
+ return mEditorVisible;
+}
+
+void App::SetEditorVisible(bool visible) {
+ if (mEditorVisible != visible) {
+ if (visible) {
+#if BRUSSEL_ENABLE_EDITOR
+ mEditorVisible = true;
+ if (mEditor == nullptr) {
+ mEditor = IEditor::CreateInstance(this);
+ }
+#endif
+ } else {
+ mEditorVisible = false;
+ }
+ }
+}
+
+void App::Show() {
+ if (mEditorVisible) {
+ mEditor->Show();
+ }
+}
+
+void App::Update() {
+ if (IsGameRunning()) {
+ mWorld.Update();
+ }
+}
+
+void App::Draw(float currentTime, float deltaTime) {
+ mWorldRenderer.BeginFrame(*mActiveCamera, currentTime, deltaTime);
+
+ PodVector<GameObject*> stack;
+ stack.push_back(&mWorld.GetRoot());
+
+ while (!stack.empty()) {
+ auto obj = stack.back();
+ stack.pop_back();
+
+ for (auto child : obj->GetChildren()) {
+ stack.push_back(child);
+ }
+
+ auto renderObjects = obj->GetRenderObjects();
+ mWorldRenderer.Draw(renderObjects.data(), obj, renderObjects.size());
+ }
+
+ mWorldRenderer.EndFrame();
+}
+
+void App::HandleMouse(int button, int action) {
+}
+
+void App::HandleMouseMotion(double xOff, double yOff) {
+}
+
+void App::HandleKey(GLFWkeyboard* keyboard, int key, int action) {
+ if (!mKeyCaptureCallbacks.empty()) {
+ auto& callback = mKeyCaptureCallbacks.front();
+ bool remove = callback(key, action);
+ if (remove) {
+ mKeyCaptureCallbacks.pop_front();
+ }
+ }
+
+ switch (key) {
+ case GLFW_KEY_F3: {
+ if (action == GLFW_PRESS) {
+ SetEditorVisible(!IsEditorVisible());
+ }
+ return;
+ }
+ }
+
+ for (auto& player : mPlayers) {
+ for (auto playerKeyboard : player->boundKeyboards) {
+ if (playerKeyboard == keyboard) {
+ player->HandleKeyInput(key, action);
+ }
+ }
+ }
+}
+
+void App::PushKeyCaptureCallback(KeyCaptureCallback callback) {
+ mKeyCaptureCallbacks.push_back(std::move(callback));
+}
diff --git a/source/30-game/App.hpp b/source/30-game/App.hpp
new file mode 100644
index 0000000..c73c5a1
--- /dev/null
+++ b/source/30-game/App.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "Camera.hpp"
+#include "EditorCore.hpp"
+#include "Player.hpp"
+#include "PodVector.hpp"
+#include "Renderer.hpp"
+#include "World.hpp"
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <deque>
+#include <functional>
+#include <memory>
+#include <vector>
+
+using KeyCaptureCallback = std::function<bool(int, int)>;
+
+class App {
+private:
+ std::deque<KeyCaptureCallback> mKeyCaptureCallbacks;
+ PodVector<Player*> mPlayers;
+ std::unique_ptr<IEditor> mEditor;
+ GameWorld mWorld;
+ Renderer mWorldRenderer;
+ Camera mMainCamera;
+ Camera* mActiveCamera;
+ // NOTE: should only be true when mEditor != nullptr
+ bool mEditorVisible = false;
+ bool mGameRunning = false;
+
+public:
+ App();
+ ~App();
+
+ IEditor* GetEditor() { return mEditor.get(); }
+ GameWorld* GetWorld() { return &mWorld; }
+ Renderer* GetWorldRenderer() { return &mWorldRenderer; }
+
+ Camera* GetActiveCamera() const;
+ void BindActiveCamera(Camera* camera);
+ void UnbindActiveCamera();
+
+ bool IsGameRunning() const;
+ void SetGameRunning(bool running);
+
+ bool IsEditorVisible() const;
+ void SetEditorVisible(bool visible);
+
+ // Do ImGui calls
+ void Show();
+ // Do regular calls
+ void Update();
+ void Draw(float currentTime, float deltaTime);
+
+ void HandleMouse(int button, int action);
+ void HandleMouseMotion(double xOff, double yOff);
+ void HandleKey(GLFWkeyboard* keyboard, int key, int action);
+
+ void PushKeyCaptureCallback(KeyCaptureCallback callback);
+};
diff --git a/source/30-game/AppConfig.hpp b/source/30-game/AppConfig.hpp
new file mode 100644
index 0000000..794bee5
--- /dev/null
+++ b/source/30-game/AppConfig.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <imgui.h>
+#include <filesystem>
+#include <string>
+
+namespace AppConfig {
+constexpr std::string_view kAppName = "ProjectBrussel";
+// Since kAppName is initialized by a C string literal, we know it's null termianted
+constexpr const char* kAppNameC = kAppName.data();
+
+inline float mainWindowWidth;
+inline float mainWindowHeight;
+inline float mainWindowAspectRatio;
+
+// TODO add a bold font
+inline ImFont* fontRegular = nullptr;
+inline ImFont* fontBold = nullptr;
+
+// Duplicate each as path and string so that on non-UTF-8 platforms (e.g. Windows) we can easily do string manipulation on the paths
+// NOTE: even though non-const, these should not be modified outside of main()
+inline std::filesystem::path dataDirPath;
+inline std::string dataDir;
+inline std::filesystem::path assetDirPath;
+inline std::string assetDir;
+} // namespace AppConfig
diff --git a/source/30-game/Camera.cpp b/source/30-game/Camera.cpp
new file mode 100644
index 0000000..39f0369
--- /dev/null
+++ b/source/30-game/Camera.cpp
@@ -0,0 +1,46 @@
+#include "Camera.hpp"
+
+#include "AppConfig.hpp"
+
+#include <glm/gtc/matrix_transform.hpp>
+
+Camera::Camera()
+ : eye(0.0f, 0.0f, 0.0f)
+ , target(0.0, 0.0f, -2.0f)
+ , pixelsPerMeter{ 50.0f } // Basic default
+ , fov{ M_PI / 4 } // 45deg is the convention
+ , perspective{ false } //
+{
+}
+
+void Camera::SetEyePos(glm::vec3 pos) {
+ auto lookVector = this->target - /*Old pos*/ this->eye;
+ this->eye = pos;
+ this->target = pos + lookVector;
+}
+
+void Camera::SetTargetPos(glm::vec3 pos) {
+ this->target = pos;
+}
+
+void Camera::SetTargetDirection(glm::vec3 lookVector) {
+ this->target = this->eye + lookVector;
+}
+
+void Camera::SetHasPerspective(bool perspective) {
+ this->perspective = perspective;
+}
+
+glm::mat4 Camera::CalcViewMatrix() const {
+ return glm::lookAt(eye, target, glm::vec3(0, 1, 0));
+}
+
+glm::mat4 Camera::CalcProjectionMatrix() const {
+ if (perspective) {
+ return glm::perspective(fov, AppConfig::mainWindowAspectRatio, 0.1f, 1000.0f);
+ } else {
+ float widthMeters = AppConfig::mainWindowWidth / pixelsPerMeter;
+ float heightMeters = AppConfig::mainWindowHeight / pixelsPerMeter;
+ return glm::ortho(-widthMeters / 2, +widthMeters / 2, -heightMeters / 2, +heightMeters / 2);
+ }
+}
diff --git a/source/30-game/Camera.hpp b/source/30-game/Camera.hpp
new file mode 100644
index 0000000..7bf0a6c
--- /dev/null
+++ b/source/30-game/Camera.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <glm/glm.hpp>
+#include <string>
+
+class Camera {
+public:
+ std::string name;
+ glm::vec3 eye;
+ glm::vec3 target;
+
+ // --- Orthographic settings ---
+ float pixelsPerMeter;
+ // --- Orthographic settings ---
+
+ // ---- Perspective settings ---
+ /// In radians
+ float fov;
+ // ---- Perspective settings ---
+
+ bool perspective;
+
+public:
+ Camera();
+
+ void SetEyePos(glm::vec3 pos);
+ void SetTargetPos(glm::vec3 pos);
+ void SetTargetDirection(glm::vec3 lookVector);
+
+ bool HasPerspective() const { return perspective; }
+ void SetHasPerspective(bool perspective);
+
+ glm::mat4 CalcViewMatrix() const;
+ glm::mat4 CalcProjectionMatrix() const;
+};
diff --git a/source/30-game/CommonVertexIndex.cpp b/source/30-game/CommonVertexIndex.cpp
new file mode 100644
index 0000000..e9a3ce6
--- /dev/null
+++ b/source/30-game/CommonVertexIndex.cpp
@@ -0,0 +1,152 @@
+#include "CommonVertexIndex.hpp"
+
+template <typename TNumber>
+static void AssignIndices(TNumber indices[6], TNumber startIdx) {
+ // Triangle #1
+ indices[0] = startIdx + 1; // Top right
+ indices[1] = startIdx + 0; // Top left
+ indices[2] = startIdx + 3; // Bottom left
+ // Triangle #2
+ indices[3] = startIdx + 1; // Top right
+ indices[4] = startIdx + 3; // Bottom left
+ indices[5] = startIdx + 2; // Bottom right
+}
+
+template <typename TNumber>
+static void AssignIndices(TNumber indices[6], TNumber topLeft, TNumber topRight, TNumber bottomRight, TNumber bottomLeft) {
+ // Triangle #1
+ indices[0] = topRight;
+ indices[1] = topLeft;
+ indices[2] = bottomLeft;
+ // Triangle #2
+ indices[3] = topRight;
+ indices[4] = bottomLeft;
+ indices[5] = bottomRight;
+}
+
+template <typename TVertex>
+static void AssignPositions(TVertex vertices[4], const Rect<float>& rect) {
+ // Top left
+ vertices[0].x = rect.x0();
+ vertices[0].y = rect.y0();
+ // Top right
+ vertices[1].x = rect.x1();
+ vertices[1].y = rect.y0();
+ // Bottom right
+ vertices[2].x = rect.x1();
+ vertices[2].y = rect.y1();
+ // Bottom left
+ vertices[3].x = rect.x0();
+ vertices[3].y = rect.y1();
+}
+
+template <typename TVertex>
+static void AssignPositions(TVertex vertices[4], glm::vec2 bl, glm::vec2 tr) {
+ // Top left
+ vertices[0].x = bl.x;
+ vertices[0].y = tr.y;
+ // Top right
+ vertices[1].x = tr.x;
+ vertices[1].y = tr.y;
+ // Bottom right
+ vertices[2].x = tr.x;
+ vertices[2].y = bl.y;
+ // Bottom left
+ vertices[3].x = bl.x;
+ vertices[3].y = bl.y;
+}
+
+template <typename TVertex>
+static void AssignDepths(TVertex vertices[4], float z) {
+ for (int i = 0; i < 4; ++i) {
+ auto& vert = vertices[i];
+ vert.z = z;
+ }
+}
+
+template <typename TVertex>
+static void AssignTexCoords(TVertex vertices[4], const Subregion& texcoords) {
+ // Top left
+ vertices[0].u = texcoords.u0;
+ vertices[0].v = texcoords.v1;
+ // Top right
+ vertices[1].u = texcoords.u1;
+ vertices[1].v = texcoords.v1;
+ // Bottom right
+ vertices[2].u = texcoords.u1;
+ vertices[2].v = texcoords.v0;
+ // Bottom left
+ vertices[3].u = texcoords.u0;
+ vertices[3].v = texcoords.v0;
+}
+
+template <typename TVertex>
+static void AssignColors(TVertex vertices[4], RgbaColor color) {
+ for (int i = 0; i < 4; ++i) {
+ auto& vert = vertices[i];
+ vert.r = color.r;
+ vert.g = color.g;
+ vert.b = color.b;
+ vert.a = color.a;
+ }
+}
+
+void Index_U16::Assign(uint16_t indices[6], uint16_t startIdx) {
+ ::AssignIndices(indices, startIdx);
+}
+
+void Index_U16::Assign(uint16_t indices[6], uint16_t startIdx, uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft) {
+ ::AssignIndices<uint16_t>(indices, startIdx + topLeft, startIdx + topRight, startIdx + bottomRight, startIdx + bottomLeft);
+}
+
+void Index_U16::Assign(uint16_t indices[6], uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft) {
+ ::AssignIndices<uint16_t>(indices, topLeft, topRight, bottomRight, bottomLeft);
+}
+
+void Index_U32::Assign(uint32_t indices[6], uint32_t startIdx) {
+ ::AssignIndices(indices, startIdx);
+}
+
+void Index_U32::Assign(uint32_t indices[6], uint32_t startIdx, uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft) {
+ ::AssignIndices<uint32_t>(indices, startIdx + topLeft, startIdx + topRight, startIdx + bottomRight, startIdx + bottomLeft);
+}
+
+void Index_U32::Assign(uint32_t indices[6], uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft) {
+ ::AssignIndices<uint32_t>(indices, topLeft, topRight, bottomRight, bottomLeft);
+}
+
+void Vertex_PC::Assign(Vertex_PC vertices[4], const Rect<float>& rect) {
+ ::AssignPositions(vertices, rect);
+}
+
+void Vertex_PC::Assign(Vertex_PC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight) {
+ ::AssignPositions(vertices, bottomLeft, topRight);
+}
+
+void Vertex_PC::Assign(Vertex_PC vertices[4], float z) {
+ ::AssignDepths(vertices, z);
+}
+
+void Vertex_PC::Assign(Vertex_PC vertices[4], RgbaColor color) {
+ ::AssignColors(vertices, color);
+}
+
+void Vertex_PTC::Assign(Vertex_PTC vertices[4], const Rect<float>& rect) {
+ ::AssignPositions(vertices, rect);
+}
+
+void Vertex_PTC::Assign(Vertex_PTC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight) {
+ ::AssignPositions(vertices, bottomLeft, topRight);
+}
+
+void Vertex_PTC::Assign(Vertex_PTC vertices[4], float z) {
+ ::AssignDepths(vertices, z);
+}
+
+void Vertex_PTC::Assign(Vertex_PTC vertices[4], const Subregion& texcoords) {
+ ::AssignTexCoords(vertices, texcoords);
+}
+
+void Vertex_PTC::Assign(Vertex_PTC vertices[4], RgbaColor color) {
+ ::AssignColors(vertices, color);
+}
diff --git a/source/30-game/CommonVertexIndex.hpp b/source/30-game/CommonVertexIndex.hpp
new file mode 100644
index 0000000..7e6aa66
--- /dev/null
+++ b/source/30-game/CommonVertexIndex.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "Color.hpp"
+#include "RcPtr.hpp"
+#include "Rect.hpp"
+#include "Texture.hpp"
+#include "VertexIndex.hpp"
+
+#include <cstdint>
+
+// Initialized in main()
+inline RcPtr<VertexFormat> gVformatStandard{};
+inline RcPtr<VertexFormat> gVformatStandardSplit{};
+inline RcPtr<VertexFormat> gVformatLines{};
+
+// Suffixes:
+// - _P_osition
+// - _T_exture coordiantes
+// - _C_olor
+// - _N_ormal
+// When an number is attached to some suffix, it means there are N number of this element
+
+struct Index_U16 {
+ uint16_t value;
+
+ static void Assign(uint16_t indices[6], uint16_t startIdx);
+ static void Assign(uint16_t indices[6], uint16_t startIdx, uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft);
+ static void Assign(uint16_t indices[6], uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft);
+};
+
+struct Index_U32 {
+ uint32_t value;
+
+ static void Assign(uint32_t indices[6], uint32_t startIdx);
+ static void Assign(uint32_t indices[6], uint32_t startIdx, uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft);
+ static void Assign(uint32_t indices[6], uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft);
+};
+
+struct Vertex_PC {
+ float x, y, z;
+ uint8_t r, g, b, a;
+
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PC vertices[4], const Rect<float>& rect);
+ /// Assign position in regular cartesian coordinate space (x increases from left to right, y increases from top to bottom).
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight);
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PC vertices[4], float z);
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PC vertices[4], RgbaColor color);
+};
+
+struct Vertex_PTC {
+ float x, y, z;
+ float u, v;
+ uint8_t r, g, b, a;
+
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PTC vertices[4], const Rect<float>& rect);
+ /// Assign position in regular cartesian coordinate space (x increases from left to right, y increases from top to bottom).
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PTC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight);
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PTC vertices[4], float z);
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PTC vertices[4], const Subregion& uvs);
+ /// Assumes the 4 vertices come in TL, TR, BR, BL order.
+ static void Assign(Vertex_PTC vertices[4], RgbaColor color);
+};
+
+struct Vertex_PTNC {
+ float x, y, z;
+ float nx, ny, nz;
+ float u, v;
+ uint8_t r, g, b, a;
+};
diff --git a/source/30-game/EditorAccessories.cpp b/source/30-game/EditorAccessories.cpp
new file mode 100644
index 0000000..821d41e
--- /dev/null
+++ b/source/30-game/EditorAccessories.cpp
@@ -0,0 +1,26 @@
+#include "EditorAccessories.hpp"
+
+#include "Input.hpp"
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <imgui.h>
+
+void EditorKeyboardViewer::Show(bool* open) {
+ ImGui::Begin("Keyboards", open);
+
+ int count;
+ GLFWkeyboard** keyboards = glfwGetKeyboards(&count);
+
+ for (int i = 0; i < count; ++i) {
+ GLFWkeyboard* keyboard = keyboards[i];
+ auto attachment = static_cast<GlfwKeyboardAttachment*>(glfwGetKeyboardUserPointer(keyboard));
+
+ ImGui::BulletText("%s", glfwGetKeyboardName(keyboard));
+ ImGui::Indent();
+ ImGui::Unindent();
+ }
+
+ ImGui::End();
+}
diff --git a/source/30-game/EditorAccessories.hpp b/source/30-game/EditorAccessories.hpp
new file mode 100644
index 0000000..56a8238
--- /dev/null
+++ b/source/30-game/EditorAccessories.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "Player.hpp"
+
+class EditorKeyboardViewer {
+public:
+ void Show(bool* open = nullptr);
+};
diff --git a/source/30-game/EditorAttachment.hpp b/source/30-game/EditorAttachment.hpp
new file mode 100644
index 0000000..61b824b
--- /dev/null
+++ b/source/30-game/EditorAttachment.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <string>
+
+class EditorAttachment {
+public:
+ std::string name;
+
+public:
+ EditorAttachment();
+ virtual ~EditorAttachment() = default;
+};
diff --git a/source/30-game/EditorAttachmentImpl.cpp b/source/30-game/EditorAttachmentImpl.cpp
new file mode 100644
index 0000000..b09c133
--- /dev/null
+++ b/source/30-game/EditorAttachmentImpl.cpp
@@ -0,0 +1,23 @@
+#include "EditorAttachmentImpl.hpp"
+#include "EditorAttachment.hpp"
+
+#include <Metadata.hpp>
+
+EditorAttachment::EditorAttachment() {
+}
+
+std::unique_ptr<EditorAttachment> EaGameObject::Create(GameObject* object) {
+ EaGameObject* result;
+
+ auto kind = object->GetKind();
+ switch (kind) {
+ case GameObject::KD_Player: result = new EaPlayer(); break;
+ case GameObject::KD_LevelWrapper: result = new EaLevelWrapper(); break;
+
+ default: result = new EaGameObject(); break;
+ }
+
+ result->name = Metadata::EnumToString(kind);
+ result->eulerAnglesRotation = glm::eulerAngles(object->GetRotation());
+ return std::unique_ptr<EditorAttachment>(result);
+}
diff --git a/source/30-game/EditorAttachmentImpl.hpp b/source/30-game/EditorAttachmentImpl.hpp
new file mode 100644
index 0000000..53bcd37
--- /dev/null
+++ b/source/30-game/EditorAttachmentImpl.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "EditorAttachment.hpp"
+#include "GameObject.hpp"
+#include "Material.hpp"
+#include "Player.hpp"
+#include "Sprite.hpp"
+
+#include <memory>
+
+class EaGameObject : public EditorAttachment {
+public:
+ // NOTE: in degrees
+ glm::vec3 eulerAnglesRotation;
+
+public:
+ static std::unique_ptr<EditorAttachment> Create(GameObject* object);
+};
+
+class EaPlayer : public EaGameObject {
+public:
+ RcPtr<IresSpritesheet> confSprite;
+ RcPtr<IresMaterial> confMaterial;
+};
+
+class EaLevelWrapper : public EaGameObject {
+public:
+};
+
+class EaIresObject : public EditorAttachment {
+public:
+ std::string nameEditingScratch;
+ bool isEditingName = false;
+};
diff --git a/source/30-game/EditorCommandPalette.cpp b/source/30-game/EditorCommandPalette.cpp
new file mode 100644
index 0000000..0e7b894
--- /dev/null
+++ b/source/30-game/EditorCommandPalette.cpp
@@ -0,0 +1,406 @@
+#include "EditorCommandPalette.hpp"
+
+#include "AppConfig.hpp"
+#include "EditorUtils.hpp"
+#include "FuzzyMatch.hpp"
+#include "Utils.hpp"
+
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <algorithm>
+#include <limits>
+#include <utility>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui_internal.h>
+
+using namespace std::literals;
+
+bool EditorCommandExecuteContext::IsInitiated() const {
+ return mCommand != nullptr;
+}
+
+const EditorCommand* EditorCommandExecuteContext::GetCurrentCommand() const {
+ return mCommand;
+}
+
+void EditorCommandExecuteContext::Initiate(const EditorCommand& command) {
+ if (mCommand == nullptr) {
+ mCommand = &command;
+ }
+}
+
+void EditorCommandExecuteContext::Prompt(std::vector<std::string> options) {
+ assert(mCommand != nullptr);
+ mCurrentOptions = std::move(options);
+ ++mDepth;
+}
+
+void EditorCommandExecuteContext::Finish() {
+ assert(mCommand != nullptr);
+ mCommand = nullptr;
+ mCurrentOptions.clear();
+ mDepth = 0;
+}
+
+int EditorCommandExecuteContext::GetExecutionDepth() const {
+ return mDepth;
+}
+
+struct EditorCommandPalette::SearchResult {
+ int itemIndex;
+ int score;
+ int matchCount;
+ uint8_t matches[32];
+};
+
+struct EditorCommandPalette::Item {
+ bool hovered = false;
+ bool held = false;
+};
+
+EditorCommandPalette::EditorCommandPalette() = default;
+EditorCommandPalette::~EditorCommandPalette() = default;
+
+namespace P6503_UNITY_ID {
+std::string MakeCommandName(std::string_view category, std::string_view name) {
+ std::string result;
+ constexpr auto infix = ": "sv;
+ result.reserve(category.size() + infix.size() + name.size());
+ result.append(category);
+ result.append(infix);
+ result.append(name);
+ return result;
+}
+} // namespace P6503_UNITY_ID
+
+void EditorCommandPalette::AddCommand(std::string_view category, std::string_view name, EditorCommand command) {
+ command.name = P6503_UNITY_ID::MakeCommandName(category, name);
+
+ auto location = std::lower_bound(
+ mCommands.begin(),
+ mCommands.end(),
+ command,
+ [](const EditorCommand& a, const EditorCommand& b) -> bool {
+ return a.name < b.name;
+ });
+ auto iter = mCommands.insert(location, std::move(command));
+
+ InvalidateSearchResults();
+}
+
+void EditorCommandPalette::RemoveCommand(std::string_view category, std::string_view name) {
+ auto commandName = P6503_UNITY_ID::MakeCommandName(category, name);
+ RemoveCommand(commandName);
+}
+
+void EditorCommandPalette::RemoveCommand(const std::string& commandName) {
+ struct Comparator {
+ bool operator()(const EditorCommand& command, const std::string& str) const {
+ return command.name < str;
+ }
+
+ bool operator()(const std::string& str, const EditorCommand& command) const {
+ return str < command.name;
+ }
+ };
+
+ auto range = std::equal_range(mCommands.begin(), mCommands.end(), commandName, Comparator{});
+ mCommands.erase(range.first, range.second);
+
+ InvalidateSearchResults();
+}
+
+void EditorCommandPalette::Show(bool* open) {
+ // Center window horizontally, align top vertically
+ ImGui::SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->Size.x / 2, 0), ImGuiCond_Always, ImVec2(0.5f, 0.0f));
+ ImGui::SetNextWindowSizeRelScreen(0.3f, 0.0f);
+
+ ImGui::Begin("Command Palette", open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar);
+ float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x;
+
+ if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || mShouldCloseNextFrame) {
+ // Close popup when user unfocused the command palette window (clicking elsewhere)
+ // or some action requested closing this window
+ mShouldCloseNextFrame = false;
+ if (open) {
+ *open = false;
+ }
+ }
+
+ if (ImGui::IsWindowAppearing() || mFocusSearchBox) {
+ mFocusSearchBox = false;
+
+ // Focus the search box when user first brings command palette window up
+ // Note: this only affects the next frame
+ ImGui::SetKeyboardFocusHere(0);
+ }
+ ImGui::SetNextItemWidth(width);
+ if (ImGui::InputText("##", &mSearchText)) {
+ // Search string updated, update search results
+
+ mFocusedItemId = 0;
+ mSearchResults.clear();
+
+ size_t itemCount;
+ if (mExeCtx.GetExecutionDepth() == 0) {
+ itemCount = mCommands.size();
+ } else {
+ itemCount = mExeCtx.mCurrentOptions.size();
+ }
+
+ for (size_t i = 0; i < itemCount; ++i) {
+ const char* text;
+ if (mExeCtx.GetExecutionDepth() == 0) {
+ text = mCommands[i].name.c_str();
+ } else {
+ text = mExeCtx.mCurrentOptions[i].c_str();
+ }
+
+ SearchResult result{
+ .itemIndex = (int)i,
+ };
+ if (FuzzyMatch::Search(mSearchText.c_str(), text, result.score, result.matches, std::size(result.matches), result.matchCount)) {
+ mSearchResults.push_back(result);
+ }
+ }
+
+ std::sort(
+ mSearchResults.begin(),
+ mSearchResults.end(),
+ [](const SearchResult& a, const SearchResult& b) -> bool {
+ // We want the biggest element first
+ return a.score > b.score;
+ });
+ }
+
+ ImGui::BeginChild("SearchResults", ImVec2(width, 300), false, ImGuiWindowFlags_AlwaysAutoResize);
+ auto window = ImGui::GetCurrentWindow();
+
+ auto& io = ImGui::GetIO();
+ auto dlSharedData = ImGui::GetDrawListSharedData();
+
+ auto textColor = ImGui::GetColorU32(ImGuiCol_Text);
+ auto itemHoveredColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
+ auto itemActiveColor = ImGui::GetColorU32(ImGuiCol_HeaderActive);
+ auto itemSelectedColor = ImGui::GetColorU32(ImGuiCol_Header);
+
+ int itemCount = GetItemCount();
+ if (mItems.size() < itemCount) {
+ mItems.resize(itemCount);
+ }
+
+ // Flag used to delay item selection until after the loop ends
+ bool selectFocusedItem = false;
+ for (size_t i = 0; i < itemCount; ++i) {
+ auto id = window->GetID(static_cast<int>(i));
+
+ ImVec2 size{
+ ImGui::GetContentRegionAvail().x,
+ dlSharedData->Font->FontSize,
+ };
+ ImRect rect{
+ window->DC.CursorPos,
+ window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f),
+ };
+
+ bool& hovered = mItems[i].hovered;
+ bool& held = mItems[i].held;
+ if (held && hovered) {
+ window->DrawList->AddRectFilled(rect.Min, rect.Max, itemActiveColor);
+ } else if (hovered) {
+ window->DrawList->AddRectFilled(rect.Min, rect.Max, itemHoveredColor);
+ } else if (mFocusedItemId == i) {
+ window->DrawList->AddRectFilled(rect.Min, rect.Max, itemSelectedColor);
+ }
+
+ auto item = GetItem(i);
+ if (item.indexType == SearchResultIndex) {
+ // Iterating search results: draw text with highlights at matched chars
+
+ auto& searchResult = mSearchResults[i];
+ auto textPos = window->DC.CursorPos;
+ int rangeBegin;
+ int rangeEnd;
+ int lastRangeEnd = 0;
+
+ auto DrawCurrentRange = [&]() -> void {
+ if (rangeBegin != lastRangeEnd) {
+ // Draw normal text between last highlighted range end and current highlighted range start
+ auto begin = item.text + lastRangeEnd;
+ auto end = item.text + rangeBegin;
+ window->DrawList->AddText(textPos, textColor, begin, end);
+
+ auto segmentSize = dlSharedData->Font->CalcTextSizeA(dlSharedData->Font->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end);
+ textPos.x += segmentSize.x;
+ }
+
+ auto begin = item.text + rangeBegin;
+ auto end = item.text + rangeEnd;
+ window->DrawList->AddText(AppConfig::fontBold, AppConfig::fontBold->FontSize, textPos, textColor, begin, end);
+
+ auto segmentSize = AppConfig::fontBold->CalcTextSizeA(AppConfig::fontBold->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end);
+ textPos.x += segmentSize.x;
+ };
+
+ assert(searchResult.matchCount >= 1);
+ rangeBegin = searchResult.matches[0];
+ rangeEnd = rangeBegin;
+
+ int lastCharIdx = -1;
+ for (int j = 0; j < searchResult.matchCount; ++j) {
+ int charIdx = searchResult.matches[j];
+
+ if (charIdx == lastCharIdx + 1) {
+ // These 2 indices are equal, extend our current range by 1
+ ++rangeEnd;
+ } else {
+ DrawCurrentRange();
+ lastRangeEnd = rangeEnd;
+ rangeBegin = charIdx;
+ rangeEnd = charIdx + 1;
+ }
+
+ lastCharIdx = charIdx;
+ }
+
+ // Draw the remaining range (if any)
+ if (rangeBegin != rangeEnd) {
+ DrawCurrentRange();
+ }
+
+ // Draw the text after the last range (if any)
+ window->DrawList->AddText(textPos, textColor, item.text + rangeEnd); // Draw until \0
+ } else {
+ // Iterating everything else: draw text as-is, there is no highlights
+
+ window->DrawList->AddText(window->DC.CursorPos, textColor, item.text);
+ }
+
+ ImGui::ItemSize(rect);
+ if (!ImGui::ItemAdd(rect, id)) {
+ continue;
+ }
+ if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) {
+ mFocusedItemId = i;
+ selectFocusedItem = true;
+ }
+ }
+
+ if (ImGui::IsKeyPressed(GLFW_KEY_UP)) {
+ mFocusedItemId = std::max(mFocusedItemId - 1, 0);
+ } else if (ImGui::IsKeyPressed(GLFW_KEY_DOWN)) {
+ mFocusedItemId = std::min(mFocusedItemId + 1, itemCount - 1);
+ }
+ if (ImGui::IsKeyPressed(GLFW_KEY_ENTER) || selectFocusedItem) {
+ SelectFocusedItem();
+ }
+
+ ImGui::EndChild();
+
+ ImGui::End();
+}
+
+size_t EditorCommandPalette::GetItemCount() const {
+ int depth = mExeCtx.GetExecutionDepth();
+ if (depth == 0) {
+ if (mSearchText.empty()) {
+ return mCommands.size();
+ } else {
+ return mSearchResults.size();
+ }
+ } else {
+ if (mSearchText.empty()) {
+ return mExeCtx.mCurrentOptions.size();
+ } else {
+ return mSearchResults.size();
+ }
+ }
+}
+
+EditorCommandPalette::ItemInfo EditorCommandPalette::GetItem(size_t idx) const {
+ ItemInfo option;
+
+ int depth = mExeCtx.GetExecutionDepth();
+ if (depth == 0) {
+ if (mSearchText.empty()) {
+ option.text = mCommands[idx].name.c_str();
+ option.command = &mCommands[idx];
+ option.itemId = idx;
+ option.indexType = DirectIndex;
+ } else {
+ auto id = mSearchResults[idx].itemIndex;
+ option.text = mCommands[id].name.c_str();
+ option.command = &mCommands[id];
+ option.itemId = id;
+ option.indexType = SearchResultIndex;
+ }
+ option.itemType = CommandItem;
+ } else {
+ assert(mExeCtx.GetCurrentCommand() != nullptr);
+ if (mSearchText.empty()) {
+ option.text = mExeCtx.mCurrentOptions[idx].c_str();
+ option.command = mExeCtx.GetCurrentCommand();
+ option.itemId = idx;
+ option.indexType = DirectIndex;
+ } else {
+ auto id = mSearchResults[idx].itemIndex;
+ option.text = mExeCtx.mCurrentOptions[id].c_str();
+ option.command = mExeCtx.GetCurrentCommand();
+ option.itemId = id;
+ option.indexType = SearchResultIndex;
+ }
+ option.itemType = CommandOptionItem;
+ }
+
+ return option;
+}
+
+void EditorCommandPalette::SelectFocusedItem() {
+ if (mFocusedItemId < 0 || mFocusedItemId >= GetItemCount()) {
+ return;
+ }
+
+ auto selectedItem = GetItem(mFocusedItemId);
+ auto& command = *selectedItem.command;
+
+ int depth = mExeCtx.GetExecutionDepth();
+ if (depth == 0) {
+ assert(!mExeCtx.IsInitiated());
+
+ mExeCtx.Initiate(*selectedItem.command);
+ if (command.callback) {
+ command.callback(mExeCtx);
+
+ mFocusSearchBox = true;
+ // Don't invalidate search results if no further actions have been requested (returning to global list of commands)
+ if (mExeCtx.IsInitiated()) {
+ InvalidateSearchResults();
+ }
+ } else {
+ mExeCtx.Finish();
+ }
+ } else {
+ assert(mExeCtx.IsInitiated());
+ assert(command.subsequentCallback);
+ command.subsequentCallback(mExeCtx, selectedItem.itemId);
+
+ mFocusSearchBox = true;
+ InvalidateSearchResults();
+ }
+
+ // This action terminated execution, close command palette window
+ if (!mExeCtx.IsInitiated()) {
+ if (command.terminate) {
+ command.terminate();
+ }
+ mShouldCloseNextFrame = true;
+ }
+}
+
+void EditorCommandPalette::InvalidateSearchResults() {
+ mSearchText.clear();
+ mSearchResults.clear();
+ mFocusedItemId = 0;
+}
diff --git a/source/30-game/EditorCommandPalette.hpp b/source/30-game/EditorCommandPalette.hpp
new file mode 100644
index 0000000..101344d
--- /dev/null
+++ b/source/30-game/EditorCommandPalette.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <imgui.h>
+#include <cstddef>
+#include <functional>
+#include <string>
+#include <string_view>
+
+class EditorCommandExecuteContext;
+class EditorCommand {
+public:
+ std::string name;
+ std::function<void(EditorCommandExecuteContext& ctx)> callback;
+ std::function<void(EditorCommandExecuteContext& ctx, size_t selectedOptionId)> subsequentCallback;
+ std::function<void()> terminate;
+};
+
+class EditorCommandExecuteContext {
+ friend class EditorCommandPalette;
+
+private:
+ const EditorCommand* mCommand = nullptr;
+ std::vector<std::string> mCurrentOptions;
+ int mDepth = 0;
+
+public:
+ bool IsInitiated() const;
+ const EditorCommand* GetCurrentCommand() const;
+ void Initiate(const EditorCommand& command);
+
+ void Prompt(std::vector<std::string> options);
+ void Finish();
+
+ /// Return the number of prompts that the user is currently completing. For example, when the user opens command
+ /// palette fresh and selects a command, 0 is returned. If the command asks some prompt, and then the user selects
+ /// again, 1 is returned.
+ int GetExecutionDepth() const;
+};
+
+class EditorCommandPalette {
+private:
+ struct SearchResult;
+ struct Item;
+
+ std::vector<EditorCommand> mCommands;
+ std::vector<Item> mItems;
+ std::vector<SearchResult> mSearchResults;
+ std::string mSearchText;
+ EditorCommandExecuteContext mExeCtx;
+ int mFocusedItemId = 0;
+ bool mFocusSearchBox = false;
+ bool mShouldCloseNextFrame = false;
+
+public:
+ EditorCommandPalette();
+ ~EditorCommandPalette();
+
+ EditorCommandPalette(const EditorCommandPalette&) = delete;
+ EditorCommandPalette& operator=(const EditorCommandPalette&) = delete;
+ EditorCommandPalette(EditorCommandPalette&&) = default;
+ EditorCommandPalette& operator=(EditorCommandPalette&&) = default;
+
+ void AddCommand(std::string_view category, std::string_view name, EditorCommand command);
+ void RemoveCommand(std::string_view category, std::string_view name);
+ void RemoveCommand(const std::string& commandName);
+
+ void Show(bool* open = nullptr);
+
+ enum ItemType {
+ CommandItem,
+ CommandOptionItem,
+ };
+
+ enum IndexType {
+ DirectIndex,
+ SearchResultIndex,
+ };
+
+ struct ItemInfo {
+ const char* text;
+ const EditorCommand* command;
+ int itemId;
+ ItemType itemType;
+ IndexType indexType;
+ };
+
+ size_t GetItemCount() const;
+ ItemInfo GetItem(size_t idx) const;
+
+ void SelectFocusedItem();
+
+private:
+ void InvalidateSearchResults();
+};
diff --git a/source/30-game/EditorCore.hpp b/source/30-game/EditorCore.hpp
new file mode 100644
index 0000000..726f43e
--- /dev/null
+++ b/source/30-game/EditorCore.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <memory>
+
+class App;
+class SpriteDefinition;
+
+class IEditorInspector {
+public:
+ enum TargetType {
+ ITT_GameObject,
+ ITT_Ires,
+ ITT_Level,
+ ITT_None,
+ };
+
+public:
+ virtual ~IEditorInspector() = default;
+ virtual void SelectTarget(TargetType type, void* object) = 0;
+};
+
+class IEditorContentBrowser {
+public:
+ virtual ~IEditorContentBrowser() = default;
+};
+
+class IEditor {
+public:
+ static std::unique_ptr<IEditor> CreateInstance(App* app);
+ virtual ~IEditor() = default;
+
+ virtual void OnGameStateChanged(bool running) = 0;
+ virtual void Show() = 0;
+
+ virtual IEditorInspector& GetInspector() = 0;
+ virtual IEditorContentBrowser& GetContentBrowser() = 0;
+
+ virtual void OpenSpriteViewer(SpriteDefinition* sprite) = 0;
+};
diff --git a/source/30-game/EditorCorePrivate.cpp b/source/30-game/EditorCorePrivate.cpp
new file mode 100644
index 0000000..3efa33c
--- /dev/null
+++ b/source/30-game/EditorCorePrivate.cpp
@@ -0,0 +1,1171 @@
+#include "EditorCorePrivate.hpp"
+
+#include "App.hpp"
+#include "AppConfig.hpp"
+#include "EditorAccessories.hpp"
+#include "EditorAttachmentImpl.hpp"
+#include "EditorCommandPalette.hpp"
+#include "EditorUtils.hpp"
+#include "GameObject.hpp"
+#include "Mesh.hpp"
+#include "Player.hpp"
+#include "SceneThings.hpp"
+#include "VertexIndex.hpp"
+
+#include <ImGuiNotification.hpp>
+#include <Macros.hpp>
+#include <Metadata.hpp>
+#include <ScopeGuard.hpp>
+#include <YCombinator.hpp>
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <imgui.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <rapidjson/document.h>
+#include <rapidjson/filereadstream.h>
+#include <rapidjson/filewritestream.h>
+#include <rapidjson/writer.h>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <functional>
+#include <glm/gtc/quaternion.hpp>
+#include <glm/gtc/type_ptr.hpp>
+#include <glm/gtx/quaternion.hpp>
+#include <limits>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+
+using namespace std::literals;
+
+namespace ProjectBrussel_UNITY_ID {
+// TODO handle internal state internally and move this to EditorUtils.hpp
+enum RenamableSelectableAction {
+ RSA_None,
+ RSA_Selected,
+ RSA_RenameCommitted,
+ RSA_RenameCancelled,
+};
+RenamableSelectableAction RenamableSelectable(const char* displayName, bool selected, bool& renaming, std::string& renamingScratchBuffer) //
+{
+ RenamableSelectableAction result = RSA_None;
+
+ ImGuiSelectableFlags flags = 0;
+ // When renaming, disable all other entries that is not the one being renamed
+ if (renaming && !selected) {
+ flags |= ImGuiSelectableFlags_Disabled;
+ }
+
+ if (renaming && selected) {
+ // State: being renamed
+
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 });
+ ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
+ ImGui::SetKeyboardFocusHere();
+ if (ImGui::InputText("##Rename", &renamingScratchBuffer, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue)) {
+ // Confirm
+ renaming = false;
+ result = RSA_RenameCommitted;
+ }
+ ImGui::PopStyleVar(2);
+
+ if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
+ // Cancel
+ renaming = false;
+ result = RSA_RenameCancelled;
+ }
+ } else {
+ // State: normal
+
+ if (ImGui::Selectable(displayName, selected, flags)) {
+ result = RSA_Selected;
+ }
+ }
+
+ return result;
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+void EditorInspector::SelectTarget(TargetType type, void* object) {
+ selectedItt = type;
+ selectedItPtr = object;
+ renaming = false;
+ renamingScratchBuffer.clear();
+}
+
+EditorContentBrowser::EditorContentBrowser(EditorInspector* inspector)
+ : mInspector{ inspector } {
+}
+
+EditorContentBrowser::~EditorContentBrowser() {
+}
+
+void EditorContentBrowser::Show(bool* open) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ ImGuiWindowFlags windowFlags;
+ if (mDocked) {
+ // Center window horizontally, align bottom vertically
+ auto& viewportSize = ImGui::GetMainViewport()->Size;
+ ImGui::SetNextWindowPos(ImVec2(viewportSize.x / 2, viewportSize.y), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
+ ImGui::SetNextWindowSizeRelScreen(0.8f, mBrowserHeight);
+ windowFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse;
+ } else {
+ windowFlags = 0;
+ }
+ ImGui::Begin("Content Browser", open, windowFlags);
+
+ ImGui::Splitter(true, kSplitterThickness, &mSplitterLeft, &mSplitterRight, kLeftPaneMinWidth, kRightPaneMinWidth);
+
+ ImGui::BeginChild("LeftPane", ImVec2(mSplitterLeft - kPadding, 0.0f));
+ {
+ if (ImGui::Selectable("Settings", mPane == P_Settings)) {
+ mPane = P_Settings;
+ }
+ if (ImGui::Selectable("Ires", mPane == P_Ires)) {
+ mPane = P_Ires;
+ }
+ if (ImGui::Selectable("Levels", mPane == P_Level)) {
+ mPane = P_Level;
+ }
+ }
+ ImGui::EndChild();
+
+ ImGui::SameLine(0.0f, kPadding + kSplitterThickness + kPadding);
+ ImGui::BeginChild("RightPane"); // Fill remaining space
+ auto origItt = mInspector->selectedItt;
+ auto origItPtr = mInspector->selectedItPtr;
+ switch (mPane) {
+ case P_Settings: {
+ ImGui::Checkbox("Docked", &mDocked);
+ ImGui::SliderFloat("Height", &mBrowserHeight, 0.1f, 1.0f);
+ } break;
+
+ case P_Ires: {
+ bool isIttIres = origItt == EditorInspector::ITT_Ires;
+
+ if (ImGui::Button("New")) {
+ ImGui::OpenPopup("New Ires");
+ }
+ if (ImGui::BeginPopup("New Ires")) {
+ for (int i = 0; i < (int)IresObject::KD_COUNT; ++i) {
+ auto kind = static_cast<IresObject::Kind>(i);
+ if (ImGui::MenuItem(Metadata::EnumToString(kind).data())) {
+ auto ires = IresObject::Create(kind);
+ auto [DISCARD, success] = IresManager::instance->Add(ires.get());
+ if (success) {
+ (void)ires.release();
+ }
+ }
+ }
+ ImGui::EndPopup();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Refresh list") ||
+ ImGui::IsKeyPressed(ImGuiKey_F5))
+ {
+ // TODO
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Save", !isIttIres)) {
+ auto ires = static_cast<IresObject*>(origItPtr);
+ IresManager::instance->Save(ires);
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Reload", !isIttIres)) {
+ auto ires = static_cast<IresObject*>(origItPtr);
+ IresManager::instance->Reload(ires);
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Rename", !isIttIres) ||
+ (isIttIres && ImGui::IsKeyPressed(ImGuiKey_F2, false)))
+ {
+ auto ires = static_cast<IresObject*>(origItPtr);
+ mInspector->renaming = true;
+ mInspector->renamingScratchBuffer = ires->GetName();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Delete", !isIttIres) ||
+ (isIttIres && ImGui::IsKeyPressed(ImGuiKey_Delete, false)))
+ {
+ ImGui::OpenPopup("Delete Ires");
+ }
+ bool openedDummy = true;
+ if (ImGui::BeginPopupModal("Delete Ires", &openedDummy, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) {
+ if (ImGui::Button("Confirm")) {
+ auto ires = static_cast<IresObject*>(origItPtr);
+ IresManager::instance->Delete(ires);
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Cancel")) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("...")) {
+ ImGui::OpenPopup("More Actions");
+ }
+ if (ImGui::BeginPopup("More Actions")) {
+ if (ImGui::MenuItem("Rewrite all Ires to disk")) {
+ IresManager::instance->OverwriteAllToDisk();
+ }
+ ImGui::EndPopup();
+ }
+
+ auto& objects = IresManager::instance->GetObjects();
+ for (auto it = objects.begin(); it != objects.end(); ++it) {
+ auto ires = it->second.Get();
+ auto& name = ires->GetName();
+
+ bool selected = origItPtr == ires;
+
+ switch (RenamableSelectable(name.c_str(), selected, mInspector->renaming, mInspector->renamingScratchBuffer)) {
+ case RSA_Selected: {
+ mInspector->SelectTarget(EditorInspector::ITT_Ires, ires);
+ } break;
+
+ case RSA_RenameCommitted: {
+ ires->SetName(std::move(mInspector->renamingScratchBuffer));
+ } break;
+
+ // Do nothing
+ case RSA_RenameCancelled:
+ case RSA_None: break;
+ }
+ if (!mInspector->renaming) {
+ if (ImGui::BeginDragDropSource()) {
+ auto kindName = Metadata::EnumToString(ires->GetKind());
+ // Reason: intentionally using pointer as payload
+ ImGui::SetDragDropPayload(kindName.data(), &ires, sizeof(ires)); // NOLINT(bugprone-sizeof-expression)
+ ImGui::Text("%s '%s'", kindName.data(), name.c_str());
+ ImGui::EndDragDropSource();
+ }
+ }
+ }
+ } break;
+
+ case P_Level: {
+ bool isIttLevel = origItt == EditorInspector::ITT_Level;
+
+ if (ImGui::Button("New")) {
+ auto uid = Uid::Create();
+ auto& ldObj = LevelManager::instance->AddLevel(uid);
+ mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj);
+ mInspector->renaming = true;
+ mInspector->renamingScratchBuffer = ldObj.name;
+ }
+
+ if (ImGui::Button("Save", !isIttLevel)) {
+ auto ldObj = static_cast<LevelManager::LoadableObject*>(origItPtr);
+ LevelManager::instance->SaveLevel(ldObj->level->GetUid());
+ }
+
+ auto& objects = LevelManager::instance->mObjByUid;
+ for (auto it = objects.begin(); it != objects.end(); ++it) {
+ auto& uid = it->first;
+ auto& ldObj = it->second;
+ auto* level = ldObj.level.Get();
+ bool selected = origItPtr == &ldObj;
+ const char* displayName = ldObj.name.c_str();
+ if (strcmp(displayName, "") == 0) {
+ displayName = "<unnamed level>";
+ }
+
+ switch (RenamableSelectable(displayName, selected, mInspector->renaming, mInspector->renamingScratchBuffer)) {
+ case RSA_Selected: {
+ mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj);
+ } break;
+
+ case RSA_RenameCommitted: {
+ ldObj.name = std::move(mInspector->renamingScratchBuffer);
+ } break;
+
+ // Do nothing
+ case RSA_RenameCancelled:
+ case RSA_None: break;
+ }
+ if (!mInspector->renaming) {
+ if (ImGui::BeginDragDropSource()) {
+ // Reason: intentionally using pointer as payload
+ ImGui::SetDragDropPayload(BRUSSEL_TAG_Level, &ldObj, sizeof(ldObj)); // NOLINT(bugprone-sizeof-expression)
+ ImGui::Text(BRUSSEL_Uid_FORMAT_STR, BRUSSEL_Uid_FORMAT_EXPAND(uid));
+ ImGui::TextUnformatted(ldObj.name.c_str());
+ ImGui::EndDragDropSource();
+ }
+ }
+ }
+ } break;
+ }
+ ImGui::EndChild();
+
+ ImGui::End();
+}
+
+namespace ProjectBrussel_UNITY_ID {
+glm::quat CalcQuaternionFromDegreesEulerAngle(glm::vec3 eulerAngleDegrees) {
+ glm::vec3 eulerAngleRadians;
+ eulerAngleRadians.x = eulerAngleDegrees.x / 180 * M_PI;
+ eulerAngleRadians.y = eulerAngleDegrees.y / 180 * M_PI;
+ eulerAngleRadians.z = eulerAngleDegrees.z / 180 * M_PI;
+ return glm::quat(eulerAngleRadians);
+}
+
+void PushKeyCodeRecorder(App* app, int* writeKey, bool* writeKeyStatus) {
+ app->PushKeyCaptureCallback([=](int key, int action) {
+ // Allow the user to cancel by pressing Esc
+ if (key == GLFW_KEY_ESCAPE) {
+ return true;
+ }
+
+ if (action == GLFW_PRESS) {
+ *writeKey = key;
+ *writeKeyStatus = writeKeyStatus;
+ return true;
+ }
+ return false;
+ });
+}
+
+struct GobjTreeNodeShowInfo {
+ EditorInstance* in_editor;
+ GameObject* out_openPopup = nullptr;
+};
+
+void GobjTreeNode(GobjTreeNodeShowInfo& showInfo, GameObject* object) {
+ auto& inspector = showInfo.in_editor->GetInspector();
+
+ auto attachment = object->GetEditorAttachment();
+ if (!attachment) {
+ attachment = EaGameObject::Create(object).release();
+ object->SetEditorAttachment(attachment); // NOTE: takes ownership
+ }
+
+ ImGuiTreeNodeFlags flags =
+ ImGuiTreeNodeFlags_DefaultOpen |
+ ImGuiTreeNodeFlags_OpenOnDoubleClick |
+ ImGuiTreeNodeFlags_OpenOnArrow |
+ ImGuiTreeNodeFlags_SpanAvailWidth |
+ ImGuiTreeNodeFlags_NoTreePushOnOpen;
+ if (inspector.selectedItPtr == object) {
+ flags |= ImGuiTreeNodeFlags_Selected;
+ }
+
+ ImGui::PushID(reinterpret_cast<uintptr_t>(object));
+ // BEGIN tree node
+
+ bool opened = ImGui::TreeNodeEx(attachment->name.c_str(), flags);
+ if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
+ inspector.SelectTarget(EditorInspector::ITT_GameObject, object);
+ }
+ if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
+ ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup))
+ {
+ showInfo.out_openPopup = object;
+ }
+
+ if (opened) {
+ ImGui::Indent();
+ for (auto& child : object->GetChildren()) {
+ GobjTreeNode(showInfo, child);
+ }
+ ImGui::Unindent();
+ }
+
+ // END tree node
+ ImGui::PopID();
+};
+
+#define GAMEOBJECT_CONSTRUCTOR(ClassName) [](GameWorld* world) -> GameObject* { return new ClassName(world); }
+struct CreatableGameObject {
+ GameObject* (*factory)(GameWorld*);
+ const char* name;
+ GameObject::Kind kind;
+} creatableGameObjects[] = {
+ {
+ .factory = GAMEOBJECT_CONSTRUCTOR(GameObject),
+ .name = "GameObject",
+ .kind = GameObject::KD_Generic,
+ },
+ {
+ .factory = GAMEOBJECT_CONSTRUCTOR(SimpleGeometryObject),
+ .name = "Simple Geometry",
+ .kind = GameObject::KD_SimpleGeometry,
+ },
+ {
+ .factory = GAMEOBJECT_CONSTRUCTOR(BuildingObject),
+ .name = "Building",
+ .kind = GameObject::KD_Building,
+ },
+ {
+ .factory = GAMEOBJECT_CONSTRUCTOR(LevelWrapperObject),
+ .name = "Level Wrapper",
+ .kind = GameObject::KD_LevelWrapper,
+ },
+};
+#undef GAMEOBJECT_CONSTRUCTOR
+
+void SaveRendererBindings(const Renderer& renderer) {
+ auto file = Utils::OpenCstdioFile("assets/GameRendererBindings.json", Utils::WriteTruncate);
+ if (!file) return;
+ DEFER {
+ fclose(file);
+ };
+
+ char writerBuffer[65536];
+ rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer));
+ rapidjson::Writer writer(stream);
+
+ rapidjson::Document root(rapidjson::kObjectType);
+ renderer.SaveBindings(root, root);
+
+ root.Accept(writer);
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+std::unique_ptr<IEditor> IEditor::CreateInstance(App* app) {
+ return std::make_unique<EditorInstance>(app);
+}
+
+EditorInstance::EditorInstance(App* app)
+ : mApp{ app }
+ , mEdContentBrowser(&mEdInspector)
+ , mEdGuides(app, this) //
+{
+ mEditorCamera.name = "Editor Camera"s;
+ mEditorCamera.SetEyePos(glm::vec3(0, 0, 1));
+ mEditorCamera.SetTargetDirection(glm::vec3(0, 0, -1));
+ app->BindActiveCamera(&mEditorCamera);
+}
+
+EditorInstance::~EditorInstance() {
+}
+
+void EditorInstance::OnGameStateChanged(bool running) {
+ if (running) {
+ mApp->UnbindActiveCamera();
+ } else {
+ mApp->BindActiveCamera(&mEditorCamera);
+ }
+}
+
+void EditorInstance::Show() {
+ using namespace ProjectBrussel_UNITY_ID;
+ using namespace Tags;
+
+ auto world = mApp->GetWorld();
+ auto& io = ImGui::GetIO();
+
+ ImGui::BeginMainMenuBar();
+ if (ImGui::BeginMenu("View")) {
+ ImGui::MenuItem("ImGui Demo", nullptr, &mWindowVisible_ImGuiDemo);
+ ImGui::MenuItem("Command Palette", "Ctrl+Shift+P", &mWindowVisible_CommandPalette);
+ ImGui::MenuItem("Inspector", nullptr, &mWindowVisible_Inspector);
+ ImGui::MenuItem("Content Browser", "Ctrl+Space", &mWindowVisible_ContentBrowser);
+ ImGui::MenuItem("Keyboard Viewer", nullptr, &mWindowVisible_KeyboardViewer);
+ ImGui::MenuItem("World Structure", nullptr, &mWindowVisible_WorldStructure);
+ ImGui::MenuItem("World Properties", nullptr, &mWindowVisible_WorldProperties);
+ ImGui::EndMenu();
+ }
+ ImGui::EndMainMenuBar();
+
+ if (mWindowVisible_ImGuiDemo) {
+ ImGui::ShowDemoWindow(&mWindowVisible_ImGuiDemo);
+ }
+
+ if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(GLFW_KEY_P, false)) {
+ mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette;
+ }
+ if (mWindowVisible_CommandPalette) {
+ mEdCommandPalette.Show(&mWindowVisible_CommandPalette);
+ }
+
+ if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) {
+ mWindowVisible_ContentBrowser = !mWindowVisible_ContentBrowser;
+ }
+ if (mWindowVisible_ContentBrowser) {
+ mEdContentBrowser.Show(&mWindowVisible_ContentBrowser);
+ }
+
+ if (mWindowVisible_KeyboardViewer) {
+ mEdKbViewer.Show(&mWindowVisible_KeyboardViewer);
+ }
+
+ auto& camera = *mApp->GetActiveCamera();
+ ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y);
+ ImGuizmo::SetDrawlist(ImGui::GetBackgroundDrawList());
+
+ if (IsCurrentCameraEditor() && mEcm == ECM_Side3D) {
+ float viewManipulateRight = io.DisplaySize.x;
+ float viewManipulateTop = 0;
+
+ // TODO get rid of this massive hack: how to manage const better for intuitively read-only, but write doesn't-care data?
+ auto& lastFrameInfo = const_cast<RendererFrameInfo&>(mApp->GetWorldRenderer()->GetLastFrameInfo());
+ auto& view = lastFrameInfo.matrixView;
+ auto& proj = lastFrameInfo.matrixProj;
+
+#if 0
+ ImGuizmo::ViewManipulate(
+ glm::value_ptr(view),
+ 200.0f, // TODO
+ ImVec2(viewManipulateRight - 128, viewManipulateTop),
+ ImVec2(128, 128),
+ 0x10101010);
+ // Extract eye and target position from view matrix
+ // - View matrix transforms world space to view space
+ // - Inverse view matrix should transform view space into world space
+ // - In view space, camera's pos is (0,0,0) and the look/forward vector should be (0,0,-1)
+ auto invView = glm::inverse(view);
+ camera.eye = invView * glm::vec4(0, 0, 0, 1);
+ camera.target = camera.eye + glm::vec3(invView * glm::vec4(0, 0, -1, 1));
+#endif
+
+ // TODO draw this as a part of the world so it doesn't block objects
+#if 0
+ glm::mat4 identity(1.00f);
+ ImGuizmo::DrawGrid(
+ glm::value_ptr(view),
+ glm::value_ptr(proj),
+ glm::value_ptr(identity),
+ 100.f);
+#endif
+
+ { // Camera controls
+ auto cameraPos = camera.eye;
+ auto cameraForward = glm::normalize(camera.target - camera.eye);
+ // Always move on the horzontal flat plane
+ cameraForward.y = 0.0f;
+
+ if (mMoveCamKeyboard) {
+ if (ImGui::IsKeyDown(ImGuiKey_W)) {
+ cameraPos += mMoveCamSlideSpeed * cameraForward;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_S)) {
+ auto cameraBack = glm::normalize(-cameraForward);
+ cameraPos += mMoveCamSlideSpeed * cameraBack;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_A)) {
+ auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0)));
+ auto cameraLeft = -cameraRight;
+ cameraPos += mMoveCamSlideSpeed * cameraLeft;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_D)) {
+ auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0)));
+ cameraPos += mMoveCamSlideSpeed * cameraRight;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_Space)) {
+ cameraPos.y += mMoveCamSlideSpeed;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) {
+ cameraPos.y -= mMoveCamSlideSpeed;
+ }
+ }
+
+ if (mMoveCamScrollWheel) {
+ cameraPos += cameraForward * io.MouseWheel * mMoveCamScrollSpeed;
+ }
+
+ camera.SetEyePos(cameraPos);
+ }
+ } else {
+ { // Camera controls
+ auto cameraPos = camera.eye;
+
+ if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !io.WantCaptureMouse && !mDragCam_Happening) {
+ mDragCam_CamInitial = camera.eye;
+ mDragCam_CursorInitial = ImGui::GetMousePos();
+ mDragCam_Happening = true;
+ }
+ if (mDragCam_Happening) {
+ auto newPos = ImGui::GetMousePos();
+ // NOTE: we are emulating as if the mouse is dragging the "canvas", through moving the camera in the opposite direction of the natural position delta
+ float deltaX = mDragCam_CursorInitial.x - newPos.x;
+ cameraPos.x = mDragCam_CamInitial.x + deltaX / camera.pixelsPerMeter;
+ float deltaY = -(mDragCam_CursorInitial.y - newPos.y); // Invert Y delta because ImGui uses top-left origin (mouse moving down translates to positive value, but in our coordinate system down is negative)
+ cameraPos.y = mDragCam_CamInitial.y + deltaY / camera.pixelsPerMeter;
+ }
+ if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
+ mDragCam_Happening = false;
+ }
+
+ if (mMoveCamKeyboard) {
+ if (ImGui::IsKeyDown(ImGuiKey_W)) {
+ cameraPos.y += mMoveCamSlideSpeed;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_S)) {
+ cameraPos.y -= mMoveCamSlideSpeed;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_A)) {
+ cameraPos.x -= mMoveCamSlideSpeed;
+ }
+ if (ImGui::IsKeyDown(ImGuiKey_D)) {
+ cameraPos.x += mMoveCamSlideSpeed;
+ }
+ }
+
+ if (mMoveCamScrollWheel) {
+ cameraPos.z = std::clamp(cameraPos.z + io.MouseWheel, 0.1f, 100.0f);
+ }
+
+ camera.SetEyePos(cameraPos);
+ }
+ }
+
+ if (mWindowVisible_Inspector) {
+ ImGui::Begin("Inspector");
+ switch (mEdInspector.selectedItt) {
+ case EditorInspector::ITT_GameObject: {
+ auto object = static_cast<GameObject*>(mEdInspector.selectedItPtr);
+ ShowInspector(object);
+ } break;
+
+ case EditorInspector::ITT_Ires: {
+ auto ires = static_cast<IresObject*>(mEdInspector.selectedItPtr);
+ ShowInspector(ires);
+ } break;
+
+ case EditorInspector::ITT_Level: {
+ auto ldObj = static_cast<LevelManager::LoadableObject*>(mEdInspector.selectedItPtr);
+ ShowInspector(ldObj);
+ } break;
+
+ case EditorInspector::ITT_None: break;
+ }
+ ImGui::End();
+ }
+
+ if (mWindowVisible_WorldProperties) {
+ ImGui::Begin("World properties");
+ ShowWorldProperties();
+ ImGui::End();
+ }
+
+ if (mWindowVisible_WorldStructure) {
+ ImGui::Begin("World structure");
+ GobjTreeNodeShowInfo showInfo{
+ .in_editor = this,
+ };
+ GobjTreeNode(showInfo, &world->GetRoot());
+
+ if (showInfo.out_openPopup) {
+ mPopupCurrent_GameObject = showInfo.out_openPopup;
+
+ ImGui::OpenPopup("GameObject Popup");
+ ImGui::SetNextWindowPos(ImGui::GetMousePos());
+ }
+ if (ImGui::BeginPopup("GameObject Popup")) {
+ // Target no longer selected during popup open
+ if (!mPopupCurrent_GameObject) {
+ ImGui::CloseCurrentPopup();
+ }
+
+ if (ImGui::BeginMenu("Add child")) {
+ for (size_t i = 0; i < std::size(creatableGameObjects); ++i) {
+ auto& info = creatableGameObjects[i];
+ if (ImGui::MenuItem(info.name)) {
+ auto object = info.factory(world);
+ mPopupCurrent_GameObject->AddChild(object);
+ }
+ }
+ ImGui::EndMenu();
+ }
+ ImGui::Separator();
+ if (ImGui::MenuItem("Remove")) {
+ ImGui::DialogConfirmation("Are you sure you want to delete this GameObject?", [object = mPopupCurrent_GameObject](bool yes) {
+ object->RemoveSelfFromParent();
+ delete object;
+ });
+ }
+ ImGui::EndPopup();
+ }
+ ImGui::End();
+ }
+
+ ShowSpriteViewer();
+
+ ImGui::ShowDialogs();
+ ImGui::ShowNotifications();
+}
+
+void EditorInstance::ShowWorldProperties() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (mApp->IsGameRunning()) {
+ if (ImGui::Button("Pause")) {
+ mApp->SetGameRunning(false);
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("The game is currently running. Click to pause.");
+ }
+ } else {
+ if (ImGui::Button("Play")) {
+ mApp->SetGameRunning(true);
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("The game is currently paused. Click to run.");
+ }
+ }
+
+ auto& renderer = *mApp->GetWorldRenderer();
+ auto& camera = *mApp->GetActiveCamera();
+
+ if (ImGui::CollapsingHeader("Renderer settings")) {
+ if (ImGui::Checkbox("Draw shaded", &mRenderer_DrawShaded)) {
+ renderer.SetRenderOption(Renderer::RO_Shading, mRenderer_DrawShaded);
+ }
+ if (ImGui::Checkbox("Draw wireframe", &mRenderer_DrawWireFrame)) {
+ renderer.SetRenderOption(Renderer::RO_Wireframe, mRenderer_DrawWireFrame);
+ }
+
+ if (auto ires = Utils::SimpleIresReceptor<IresMaterial>(renderer.binding_WireframeMaterial->GetIres(), *this, IresObject::KD_Material)) {
+ renderer.binding_WireframeMaterial.Attach(ires->GetInstance());
+ SaveRendererBindings(renderer);
+ }
+ }
+
+ if (ImGui::CollapsingHeader("Camera settings")) {
+ // vvv Camera settings (per instance)
+ ImGui::TextUnformatted("Active camera:");
+ ImGui::Indent();
+
+ ImGui::TextUnformatted(camera.name.c_str());
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("Object at <%p>", &camera);
+ }
+
+ ImGui::InputFloat3("Eye", glm::value_ptr(camera.eye));
+ if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) {
+ ImGui::OpenPopup("##CTXMENU");
+ }
+ ImGui::InputFloat3("Target", glm::value_ptr(camera.target));
+ if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) {
+ ImGui::OpenPopup("##CTXMENU");
+ }
+ if (ImGui::BeginPopup("##CTXMENU", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) {
+ if (ImGui::MenuItem("Reset to origin")) {
+ camera.SetEyePos(glm::vec3(1.0f, 1.0f, 1.0f));
+ }
+ if (ImGui::MenuItem("Reset completely")) {
+ camera.eye = glm::vec3(0, 0, 1);
+ camera.target = glm::vec3(0, 0, 0);
+ }
+ ImGui::EndPopup();
+ }
+
+ if (IsCurrentCameraEditor()) {
+ const char* preview;
+ switch (mEcm) {
+ case ECM_2D: preview = "2D view"; break;
+ case ECM_Side3D: preview = "Side 3D view"; break;
+ }
+ if (ImGui::BeginCombo("Mode", preview)) {
+ if (ImGui::Selectable("2D view", mEcm == ECM_2D)) {
+ if (mEcm != ECM_2D) {
+ mEcm = ECM_2D;
+
+ // TODO project eye to world plane
+
+ camera.SetHasPerspective(false);
+ }
+ }
+ if (ImGui::Selectable("Side 3D view", mEcm == ECM_Side3D, ImGuiSelectableFlags_None)) {
+ if (mEcm != ECM_Side3D) {
+ mEcm = ECM_Side3D;
+
+ auto origEye = camera.eye;
+ auto origTarget = camera.target;
+
+ // New setup: focus on the point of world plane that we were originally "hovering above"
+ camera.target = origEye;
+ camera.target.z = 0.0f;
+
+ // New setup: move the eye back at an angle
+ camera.eye = camera.target;
+ camera.eye.x += 4.0f * std::cos(60.0f);
+ camera.eye.y += 0.0f;
+ camera.eye.z += 4.0f * std::sin(60.0f);
+
+ camera.SetHasPerspective(true);
+ }
+ }
+ ImGui::EndCombo();
+ }
+ }
+
+ ImGui::Checkbox("Perspective", &camera.perspective);
+ if (camera.perspective) {
+ float fovDegress = camera.fov / M_PI * 180.0f;
+ if (ImGui::SliderFloat("FOV", &fovDegress, 1.0f, 180.0f)) {
+ camera.fov = fovDegress / 180.0f * M_PI;
+ }
+ } else {
+ if (ImGui::InputFloat("Pixels per meter", &camera.pixelsPerMeter)) {
+ camera.pixelsPerMeter = std::max(camera.pixelsPerMeter, 0.0f);
+ }
+ }
+
+ ImGui::Unindent();
+ // ^^^ Camera settings (per instance)
+ // vvv Camera control settings
+ ImGui::TextUnformatted("Camera controls:");
+ ImGui::Indent();
+
+ ImGui::Checkbox("Move camera with WASD", &mMoveCamKeyboard);
+ ImGui::SliderFloat("Slide speed", &mMoveCamSlideSpeed, 0.1f, 10.0f);
+
+ ImGui::Checkbox("Move camera with scoll wheel", &mMoveCamScrollWheel);
+ ImGui::SliderFloat("Scroll speed", &mMoveCamScrollSpeed, 0.01, 10.0f);
+
+ ImGui::Unindent();
+ // ^^^ Camera control settings
+ }
+}
+
+// TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism
+
+void EditorInstance::ShowInspector(IresObject* ires) {
+ ires->ShowEditor(*this);
+}
+
+void EditorInstance::ShowInspector(LevelManager::LoadableObject* ldObj) {
+ using namespace Tags;
+ using namespace ProjectBrussel_UNITY_ID;
+
+ ImGui::InputText("Name", &ldObj->name);
+ ImGui::InputTextMultiline("Desciption", &ldObj->description);
+
+ if (ImGui::CollapsingHeader("Instanciation Entries")) {
+ ldObj->level->ShowInstanciationEntries(*this);
+ }
+}
+
+void EditorInstance::ShowInspector(GameObject* object) {
+ using namespace Tags;
+ using namespace ProjectBrussel_UNITY_ID;
+
+ auto& io = ImGui::GetIO();
+
+ auto objectEa = static_cast<EaGameObject*>(object->GetEditorAttachment());
+
+ auto ShowInspector = [&](/*array[6]*/ float* bounds = nullptr) {
+ glm::mat4 identityMatrix(1.00f);
+
+ if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_T))
+ mGuizmo.currOperation = ImGuizmo::TRANSLATE;
+ if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_R))
+ mGuizmo.currOperation = ImGuizmo::ROTATE;
+ if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_S))
+ mGuizmo.currOperation = ImGuizmo::SCALE;
+ if (ImGui::RadioButton("Translate", mGuizmo.currOperation == ImGuizmo::TRANSLATE))
+ mGuizmo.currOperation = ImGuizmo::TRANSLATE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Rotate", mGuizmo.currOperation == ImGuizmo::ROTATE))
+ mGuizmo.currOperation = ImGuizmo::ROTATE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Scale", mGuizmo.currOperation == ImGuizmo::SCALE))
+ mGuizmo.currOperation = ImGuizmo::SCALE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Universal", mGuizmo.currOperation == ImGuizmo::UNIVERSAL))
+ mGuizmo.currOperation = ImGuizmo::UNIVERSAL;
+
+ if (mGuizmo.currOperation != ImGuizmo::SCALE) {
+ if (ImGui::RadioButton("Local", mGuizmo.currMode == ImGuizmo::LOCAL))
+ mGuizmo.currMode = ImGuizmo::LOCAL;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("World", mGuizmo.currMode == ImGuizmo::WORLD))
+ mGuizmo.currMode = ImGuizmo::WORLD;
+ }
+
+ ImGui::Checkbox("##UseSnap", &mGuizmo.useSnap);
+ ImGui::SameLine();
+ switch (mGuizmo.currOperation) {
+ case ImGuizmo::TRANSLATE:
+ ImGui::InputFloat3("Pos Snap", &mGuizmo.snap[0]);
+ break;
+ case ImGuizmo::ROTATE:
+ ImGui::InputFloat("Angle Snap", &mGuizmo.snap[0]);
+ break;
+ case ImGuizmo::SCALE:
+ ImGui::InputFloat("Scale Snap", &mGuizmo.snap[0]);
+ break;
+ default: break;
+ }
+
+ if (bounds) {
+ ImGui::Checkbox("Bound Sizing", &mGuizmo.boundSizing);
+ if (mGuizmo.boundSizing) {
+ ImGui::PushID(3);
+ ImGui::Checkbox("##BoundSizing", &mGuizmo.boundSizingSnap);
+ ImGui::SameLine();
+ ImGui::InputFloat3("Bound Snap", mGuizmo.boundsSnap);
+ ImGui::PopID();
+ }
+ }
+
+ ImGui::Separator();
+
+ auto objectPos = object->GetPos();
+ if (ImGui::InputFloat3("Translation", &objectPos.x)) {
+ object->SetPos(objectPos);
+ }
+
+ auto objectRot = object->GetRotation();
+ if (ImGui::InputFloat3("Rotation", &objectEa->eulerAnglesRotation.x)) {
+ objectRot = CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation);
+ object->SetRotation(objectRot);
+ }
+
+ auto objectScale = object->GetScale();
+ if (ImGui::InputFloat3("Scale", &objectScale.x)) {
+ object->SetScale(objectScale);
+ }
+ };
+
+ auto ShowGuizmo = [&](/*array[6]*/ float* bounds = nullptr) {
+ glm::mat4 identityMatrix(1.00f);
+
+ auto& lastFrameInfo = mApp->GetWorldRenderer()->GetLastFrameInfo();
+ auto& cameraViewMat = lastFrameInfo.matrixView;
+ const float* cameraView = &cameraViewMat[0][0];
+ auto& cameraProjMat = lastFrameInfo.matrixProj;
+ const float* cameraProj = &cameraProjMat[0][0];
+
+ auto objectPos = object->GetPos();
+ auto objectScale = object->GetScale();
+
+ float matrix[16];
+ ImGuizmo::RecomposeMatrixFromComponents(&objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x, matrix);
+
+ bool edited = ImGuizmo::Manipulate(
+ cameraView,
+ cameraProj,
+ mGuizmo.currOperation,
+ mGuizmo.currMode,
+ matrix,
+ nullptr,
+ mGuizmo.useSnap ? mGuizmo.snap : nullptr,
+ mGuizmo.boundSizing ? bounds : nullptr,
+ (bounds && mGuizmo.boundSizingSnap) ? mGuizmo.boundsSnap : nullptr);
+
+ if (edited) {
+ ImGuizmo::DecomposeMatrixToComponents(matrix, &objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x);
+ object->SetPos(objectPos);
+ object->SetRotation(CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation));
+ object->SetScale(objectScale);
+ }
+ };
+
+ auto type = object->GetKind();
+ switch (type) {
+ case GameObject::KD_Player: {
+ ShowInspector();
+ ShowGuizmo();
+ ImGui::Separator();
+
+ auto player = static_cast<Player*>(object);
+ auto ea = static_cast<EaPlayer*>(objectEa);
+ auto& kb = player->keybinds;
+
+ ImGui::Text("Player #%d", player->GetId());
+
+ ImGui::TextUnformatted("Spritesheet: ");
+ ImGui::SameLine();
+ IresObject::ShowReferenceSafe(*this, ea->confSprite.Get());
+ if (ImGui::BeginDragDropTarget()) {
+ if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(IresObject::KD_Spritesheet).data())) {
+ auto spritesheet = *static_cast<IresSpritesheet* const*>(payload->Data);
+ ea->confSprite.Attach(spritesheet);
+ auto def = spritesheet->GetInstance();
+ player->sprite.SetDefinition(def);
+ player->renderObject.autofill_TextureAtlas.Attach(def->GetAtlas());
+ }
+ ImGui::EndDragDropTarget();
+ }
+
+ ImGui::TextUnformatted("Material: ");
+ ImGui::SameLine();
+ IresObject::ShowReferenceSafe(*this, ea->confMaterial.Get());
+ if (ImGui::BeginDragDropTarget()) {
+ if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(IresObject::KD_Material).data())) {
+ auto material = *static_cast<IresMaterial* const*>(payload->Data);
+ ea->confMaterial.Attach(material);
+ player->SetMaterial(material->GetInstance());
+ }
+ ImGui::EndDragDropTarget();
+ }
+
+ if (ImGui::Button("Load config")) {
+ bool success = player->LoadFromFile();
+ if (success) {
+ ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully loaded player config"));
+ }
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Save config")) {
+ bool success = player->SaveToFile();
+ if (success) {
+ ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully saved player config"));
+ }
+ }
+
+ ImGui::Text("Move left (%s)", ImGui::GetKeyNameGlfw(kb.keyLeft));
+ ImGui::SameLine();
+ if (ImGui::Button("Change##Move left")) {
+ PushKeyCodeRecorder(mApp, &kb.keyLeft, &kb.pressedLeft);
+ }
+
+ ImGui::Text("Move right (%s)", ImGui::GetKeyNameGlfw(kb.keyRight));
+ ImGui::SameLine();
+ if (ImGui::Button("Change##Move right")) {
+ PushKeyCodeRecorder(mApp, &kb.keyRight, &kb.pressedRight);
+ }
+
+ ImGui::Text("Jump (%s)", ImGui::GetKeyNameGlfw(kb.keyJump));
+ ImGui::SameLine();
+ if (ImGui::Button("Change##Jump")) {
+ PushKeyCodeRecorder(mApp, &kb.keyJump, &kb.pressedJump);
+ }
+
+ ImGui::Text("Attack (%s)", ImGui::GetKeyNameGlfw(kb.keyAttack));
+ ImGui::SameLine();
+ if (ImGui::Button("Change##Attack")) {
+ PushKeyCodeRecorder(mApp, &kb.keyAttack, &kb.pressedAttack);
+ }
+ } break;
+
+ case GameObject::KD_SimpleGeometry: {
+ auto sg = static_cast<SimpleGeometryObject*>(object);
+
+ float bounds[6] = {
+ /*x1*/ -sg->GetSize().x / 2.0f,
+ /*y1*/ -sg->GetSize().y / 2.0f,
+ /*z1*/ -sg->GetSize().z / 2.0f,
+ /*x2*/ +sg->GetSize().x / 2.0f,
+ /*y2*/ +sg->GetSize().y / 2.0f,
+ /*z2*/ +sg->GetSize().z / 2.0f,
+ };
+
+ ShowInspector(bounds);
+ ShowGuizmo(bounds);
+ ImGui::Separator();
+
+ sg->SetSize(glm::vec3(bounds[3] - bounds[0], bounds[4] - bounds[1], bounds[5] - bounds[2]));
+
+ auto size = sg->GetSize();
+ if (ImGui::InputFloat3("Size", &size.x)) {
+ sg->SetSize(size);
+ }
+
+ auto xFaceColor = sg->GetXFaceColor();
+ if (ImGui::ColorEdit4("X color", &xFaceColor)) {
+ sg->SetXFaceColor(xFaceColor);
+ }
+ auto yFaceColor = sg->GetYFaceColor();
+ if (ImGui::ColorEdit4("Y color", &yFaceColor)) {
+ sg->SetYFaceColor(yFaceColor);
+ }
+ auto zFaceColor = sg->GetZFaceColor();
+ if (ImGui::ColorEdit4("Z color", &zFaceColor)) {
+ sg->SetZFaceColor(zFaceColor);
+ }
+
+ if (ImGui::Button("Sync from X color")) {
+ sg->SetYFaceColor(xFaceColor);
+ sg->SetZFaceColor(xFaceColor);
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Reset")) {
+ sg->SetXFaceColor(kXAxisColor);
+ sg->SetYFaceColor(kYAxisColor);
+ sg->SetZFaceColor(kZAxisColor);
+ }
+ } break;
+
+ case GameObject::KD_Building: {
+ ShowInspector();
+ ShowGuizmo();
+ ImGui::Separator();
+
+ auto b = static_cast<BuildingObject*>(object);
+ // TODO
+ } break;
+
+ case GameObject::KD_LevelWrapper: {
+ ShowInspector();
+ ShowGuizmo();
+ ImGui::Separator();
+
+ auto lwo = static_cast<LevelWrapperObject*>(object);
+ // TODO
+ } break;
+
+ default: {
+ ShowInspector();
+ ShowGuizmo();
+ } break;
+ }
+}
+
+void EditorInstance::OpenSpriteViewer(SpriteDefinition* sprite) {
+ mSpriteView_Instance.Attach(sprite);
+ mSpriteView_Frame = 0;
+ mSpriteView_OpenNextFrame = true;
+}
+
+void EditorInstance::ShowSpriteViewer() {
+ if (mSpriteView_Instance == nullptr) return;
+ if (!mSpriteView_Instance->IsValid()) return;
+
+ if (mSpriteView_OpenNextFrame) {
+ mSpriteView_OpenNextFrame = false;
+ ImGui::OpenPopup("Sprite Viewer");
+ }
+
+ bool windowOpen = true;
+ if (ImGui::BeginPopupModal("Sprite Viewer", &windowOpen)) {
+ auto atlas = mSpriteView_Instance->GetAtlas();
+ auto atlasSize = atlas->GetInfo().size;
+ auto& frames = mSpriteView_Instance->GetFrames();
+
+ int frameCount = mSpriteView_Instance->GetFrames().size();
+ if (ImGui::Button("<")) {
+ --mSpriteView_Frame;
+ }
+ ImGui::SameLine();
+ ImGui::Text("%d/%d", mSpriteView_Frame + 1, frameCount);
+ ImGui::SameLine();
+ if (ImGui::Button(">")) {
+ ++mSpriteView_Frame;
+ }
+ // Scrolling down (negative value) should advance, so invert the sign
+ mSpriteView_Frame += -std::round(ImGui::GetIO().MouseWheel);
+ // Clamp mSpriteView_Frame to range [0, frameCount)
+ if (mSpriteView_Frame < 0) {
+ mSpriteView_Frame = frameCount - 1;
+ } else if (mSpriteView_Frame >= frameCount) {
+ mSpriteView_Frame = 0;
+ }
+
+ auto& currFrame = frames[mSpriteView_Frame];
+ auto boundingBox = mSpriteView_Instance->GetBoundingBox();
+ ImGui::Text("Frame size: (%d, %d)", boundingBox.x, boundingBox.y);
+ ImGui::Text("Frame location: (%f, %f) to (%f, %f)", currFrame.u0, currFrame.v0, currFrame.u1, currFrame.v1);
+ ImGui::Image(
+ (ImTextureID)(uintptr_t)atlas->GetHandle(),
+ Utils::FitImage(atlasSize),
+ ImVec2(currFrame.u0, currFrame.v0),
+ ImVec2(currFrame.u1, currFrame.v1));
+
+ ImGui::EndPopup();
+ }
+}
diff --git a/source/30-game/EditorCorePrivate.hpp b/source/30-game/EditorCorePrivate.hpp
new file mode 100644
index 0000000..4071e7a
--- /dev/null
+++ b/source/30-game/EditorCorePrivate.hpp
@@ -0,0 +1,136 @@
+#pragma once
+
+#include "App.hpp"
+#include "EditorAccessories.hpp"
+#include "EditorAttachment.hpp"
+#include "EditorCommandPalette.hpp"
+#include "EditorCore.hpp"
+#include "EditorUtils.hpp"
+#include "EditorWorldGuides.hpp"
+#include "GameObject.hpp"
+#include "Ires.hpp"
+#include "Level.hpp"
+#include "RcPtr.hpp"
+#include "Sprite.hpp"
+#include "World.hpp"
+
+#include <memory>
+#include <string>
+
+// TODO move inspector drawing to this class
+class EditorInspector final : public IEditorInspector {
+public:
+ std::string renamingScratchBuffer;
+ void* selectedItPtr = nullptr;
+ TargetType selectedItt = ITT_None;
+ bool renaming = false;
+
+ void SelectTarget(TargetType type, void* object) override;
+};
+
+class EditorContentBrowser final : public IEditorContentBrowser {
+private:
+ enum Pane {
+ P_Settings,
+ P_Ires,
+ P_Level,
+ };
+
+ static constexpr float kSplitterThickness = 3.0f;
+ static constexpr float kPadding = 4.0f;
+
+ // <root>
+ static constexpr float kLeftPaneMinWidth = 200.0f;
+ static constexpr float kRightPaneMinWidth = 200.0f;
+
+ EditorInspector* mInspector;
+ Pane mPane = P_Settings;
+ float mBrowserHeight = 0.5f;
+ float mSplitterLeft = kLeftPaneMinWidth;
+ float mSplitterRight = 0.0f;
+ bool mDocked = true;
+
+public:
+ EditorContentBrowser(EditorInspector* inspector);
+ ~EditorContentBrowser() override;
+
+ void Show(bool* open = nullptr);
+};
+
+struct GuizmoState {
+ ImGuizmo::OPERATION currOperation = ImGuizmo::TRANSLATE;
+ ImGuizmo::MODE currMode = ImGuizmo::LOCAL;
+ glm::mat4 cubeMatrix;
+ float snap[3] = { 1.f, 1.f, 1.f };
+ float boundsSnap[3] = { 0.1f, 0.1f, 0.1f };
+ bool useSnap = false;
+ bool boundSizing = false;
+ bool boundSizingSnap = false;
+};
+
+// TODO editor undo stack
+class EditorInstance : public IEditor {
+public:
+ enum EditorCameraMode {
+ // "Game mode": the camera views from the side, where the forward vector is perpendicular to the world plane
+ ECM_2D,
+ // "Editor mode": equivalent to Unity's 3D mode, the camera is completely free and controlled as a 3d camera
+ ECM_Side3D,
+ };
+
+private:
+ App* mApp;
+ GameObject* mPopupCurrent_GameObject = nullptr;
+ Camera mEditorCamera;
+ RcPtr<SpriteDefinition> mSpriteView_Instance;
+ EditorCommandPalette mEdCommandPalette;
+ EditorInspector mEdInspector;
+ EditorContentBrowser mEdContentBrowser;
+ EditorKeyboardViewer mEdKbViewer;
+ EditorWorldGuides mEdGuides;
+ GuizmoState mGuizmo;
+ glm::vec3 mDragCam_CamInitial;
+ ImVec2 mDragCam_CursorInitial;
+ int mSpriteView_Frame;
+ float mMoveCamScrollSpeed = 1.0f;
+ float mMoveCamSlideSpeed = 0.1f;
+ EditorCameraMode mEcm = ECM_2D;
+ bool mSpriteView_OpenNextFrame = false;
+ bool mWindowVisible_ImGuiDemo = false;
+ bool mWindowVisible_CommandPalette = false;
+ bool mWindowVisible_Inspector = true;
+ bool mWindowVisible_ContentBrowser = true;
+ bool mWindowVisible_WorldStructure = true;
+ bool mWindowVisible_WorldProperties = true;
+ bool mWindowVisible_KeyboardViewer = false;
+ bool mDragCam_Happening = false;
+ bool mMoveCamKeyboard = false;
+ bool mMoveCamScrollWheel = false;
+ bool mRenderer_DrawShaded = true;
+ bool mRenderer_DrawWireFrame = false;
+
+public:
+ EditorInstance(App* app);
+ ~EditorInstance() override;
+
+ void OnGameStateChanged(bool running) override;
+ void Show() override;
+
+ EditorInspector& GetInspector() override { return mEdInspector; }
+ EditorContentBrowser& GetContentBrowser() override { return mEdContentBrowser; }
+
+ void OpenSpriteViewer(SpriteDefinition* sprite) override;
+
+private:
+ bool IsCurrentCameraEditor() const {
+ return !mApp->IsGameRunning();
+ }
+
+ void ShowWorldProperties();
+
+ void ShowInspector(IresObject* ires);
+ void ShowInspector(LevelManager::LoadableObject* ldObj);
+ void ShowInspector(GameObject* object);
+
+ void ShowSpriteViewer();
+};
diff --git a/source/30-game/EditorUtils.cpp b/source/30-game/EditorUtils.cpp
new file mode 100644
index 0000000..20caef7
--- /dev/null
+++ b/source/30-game/EditorUtils.cpp
@@ -0,0 +1,447 @@
+#include "EditorUtils.hpp"
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui_internal.h>
+
+#include <backends/imgui_impl_glfw.h>
+#include <cmath>
+#include <glm/gtc/quaternion.hpp>
+#include <glm/gtx/quaternion.hpp>
+#include <numbers>
+
+const char* ImGui::GetKeyNameGlfw(int key) {
+ return GetKeyName(ImGui_ImplGlfw_KeyToImGuiKey(key));
+}
+
+void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond) {
+ auto vs = ImGui::GetMainViewport()->Size;
+ ImGui::SetNextWindowSize({ vs.x * xPercent, vs.y * yPercent }, cond);
+}
+
+void ImGui::SetNextWindowCentered(ImGuiCond cond) {
+ auto vs = ImGui::GetMainViewport()->Size;
+ ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, cond, { 0.5f, 0.5f });
+}
+
+void ImGui::PushDisabled() {
+ ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha);
+}
+
+void ImGui::PopDisabled() {
+ ImGui::PopItemFlag();
+ ImGui::PopStyleVar();
+}
+
+bool ImGui::Button(const char* label, bool disabled) {
+ return Button(label, ImVec2{}, disabled);
+}
+
+bool ImGui::Button(const char* label, const ImVec2& sizeArg, bool disabled) {
+ if (disabled) PushDisabled();
+ bool res = ImGui::Button(label, sizeArg);
+ if (disabled) PopDisabled();
+
+ return res;
+}
+
+#define EDIT_RGBA_COLOR(EditorFunction, kUsesAlpha) \
+ float components[4]; \
+ components[0] = color->GetNormalizedRed(); \
+ components[1] = color->GetNormalizedGreen(); \
+ components[2] = color->GetNormalizedBlue(); \
+ if constexpr (kUsesAlpha) components[3] = color->GetNormalizedAlpha(); \
+ if (EditorFunction(label, components, flags)) { \
+ color->r = components[0] * 255; \
+ color->g = components[1] * 255; \
+ color->b = components[2] * 255; \
+ if constexpr (kUsesAlpha) color->a = components[3] * 255; \
+ return true; \
+ } else { \
+ return false; \
+ }
+
+bool ImGui::ColorEdit3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) {
+ EDIT_RGBA_COLOR(ColorEdit3, false);
+}
+
+bool ImGui::ColorEdit4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) {
+ EDIT_RGBA_COLOR(ColorEdit4, true);
+}
+
+bool ImGui::ColorPicker3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) {
+ EDIT_RGBA_COLOR(ColorPicker3, false);
+}
+
+bool ImGui::ColorPicker4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) {
+ EDIT_RGBA_COLOR(ColorPicker4, true);
+}
+
+#undef EDIT_RGBA_COLOR
+
+struct InputTextCallbackUserData {
+ std::string* str;
+ ImGuiInputTextCallback chainCallback;
+ void* chainCallbackUserData;
+};
+
+static int InputTextCallback(ImGuiInputTextCallbackData* data) {
+ auto user_data = (InputTextCallbackUserData*)data->UserData;
+ if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
+ // Resize string callback
+ // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want.
+ auto str = user_data->str;
+ IM_ASSERT(data->Buf == str->c_str());
+ str->resize(data->BufTextLen);
+ data->Buf = (char*)str->c_str();
+ } else if (user_data->chainCallback) {
+ // Forward to user callback, if any
+ data->UserData = user_data->chainCallbackUserData;
+ return user_data->chainCallback(data);
+ }
+ return 0;
+}
+
+bool ImGui::Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize) {
+ // Adapted from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/blueprints-example.cpp
+ // ::Splitter
+
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ ImGuiID id = window->GetID("##Splitter");
+ ImRect bb;
+ bb.Min = window->DC.CursorPos + (splitVertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1));
+ bb.Max = bb.Min + CalcItemSize(splitVertically ? ImVec2(thickness, splitterLongAxisSize) : ImVec2(splitterLongAxisSize, thickness), 0.0f, 0.0f);
+
+ // Adapted from ImGui::SplitterBehavior, changes:
+ // - Simplified unneeded logic (hover_extend and hover_visibility_delay)
+ // - Changed clamped delta to clamping result size1 and deriving size2 from size1, allowing automatically adapting to the latest window content region width
+
+ auto itemFlagsBackup = g.CurrentItemFlags;
+ g.CurrentItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus;
+ bool itemAdd = ItemAdd(bb, id);
+ g.CurrentItemFlags = itemFlagsBackup;
+ if (!itemAdd) {
+ return false;
+ }
+
+ bool hovered, held;
+ auto bbInteract = bb;
+ ButtonBehavior(bbInteract, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap);
+ if (hovered) {
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect;
+ } // for IsItemHovered(), because bbInteract is larger than bb
+ if (g.ActiveId != id) {
+ SetItemAllowOverlap();
+ }
+
+ if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= 0.0f)) {
+ SetMouseCursor((splitVertically ? ImGuiAxis_X : ImGuiAxis_Y) == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
+ }
+
+ float contentSize = splitVertically ? window->ContentRegionRect.GetWidth() : window->ContentRegionRect.GetHeight();
+ if (held) {
+ ImVec2 mouseDelta2D = g.IO.MousePos - g.ActiveIdClickOffset - bbInteract.Min;
+ float mouseDelta = ((splitVertically ? ImGuiAxis_X : ImGuiAxis_Y) == ImGuiAxis_Y) ? mouseDelta2D.y : mouseDelta2D.x;
+
+ // Apply resize
+ if (mouseDelta != 0.0f) {
+ *size1 = ImClamp(*size1 + mouseDelta, minSize1, contentSize - minSize2 - thickness);
+ *size2 = contentSize - *size1 - thickness;
+ MarkItemEdited(id);
+ }
+ }
+
+ ImU32 col;
+ if (held) {
+ col = GetColorU32(ImGuiCol_SeparatorActive);
+ } else if (hovered && g.HoveredIdTimer >= 0.0f) {
+ col = GetColorU32(ImGuiCol_SeparatorHovered);
+ } else {
+ col = GetColorU32(ImGuiCol_Separator);
+ }
+ window->DrawList->AddRectFilled(bb.Min, bb.Max, col, 0.0f);
+
+ return held;
+}
+
+void ImGui::AddUnderLine(ImColor col) {
+ auto min = ImGui::GetItemRectMin();
+ auto max = ImGui::GetItemRectMax();
+ min.y = max.y;
+ ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0f);
+}
+
+void ImGui::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) {
+ // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/drawing.cpp
+ // ax::NodeEditor::DrawIcon
+
+ auto rect = ImRect(a, b);
+ auto rect_x = rect.Min.x;
+ auto rect_y = rect.Min.y;
+ auto rect_w = rect.Max.x - rect.Min.x;
+ auto rect_h = rect.Max.y - rect.Min.y;
+ auto rect_center_x = (rect.Min.x + rect.Max.x) * 0.5f;
+ auto rect_center_y = (rect.Min.y + rect.Max.y) * 0.5f;
+ auto rect_center = ImVec2(rect_center_x, rect_center_y);
+ const auto outline_scale = rect_w / 24.0f;
+ const auto extra_segments = static_cast<int>(2 * outline_scale); // for full circle
+
+ if (type == IconType::Flow) {
+ const auto origin_scale = rect_w / 24.0f;
+
+ const auto offset_x = 1.0f * origin_scale;
+ const auto offset_y = 0.0f * origin_scale;
+ const auto margin = 2.0f * origin_scale;
+ const auto rounding = 0.1f * origin_scale;
+ const auto tip_round = 0.7f; // percentage of triangle edge (for tip)
+ // const auto edge_round = 0.7f; // percentage of triangle edge (for corner)
+ const auto canvas = ImRect(
+ rect.Min.x + margin + offset_x,
+ rect.Min.y + margin + offset_y,
+ rect.Max.x - margin + offset_x,
+ rect.Max.y - margin + offset_y);
+ const auto canvas_x = canvas.Min.x;
+ const auto canvas_y = canvas.Min.y;
+ const auto canvas_w = canvas.Max.x - canvas.Min.x;
+ const auto canvas_h = canvas.Max.y - canvas.Min.y;
+
+ const auto left = canvas_x + canvas_w * 0.5f * 0.3f;
+ const auto right = canvas_x + canvas_w - canvas_w * 0.5f * 0.3f;
+ const auto top = canvas_y + canvas_h * 0.5f * 0.2f;
+ const auto bottom = canvas_y + canvas_h - canvas_h * 0.5f * 0.2f;
+ const auto center_y = (top + bottom) * 0.5f;
+ // const auto angle = AX_PI * 0.5f * 0.5f * 0.5f;
+
+ const auto tip_top = ImVec2(canvas_x + canvas_w * 0.5f, top);
+ const auto tip_right = ImVec2(right, center_y);
+ const auto tip_bottom = ImVec2(canvas_x + canvas_w * 0.5f, bottom);
+
+ drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding));
+ drawList->PathBezierCubicCurveTo(
+ ImVec2(left, top),
+ ImVec2(left, top),
+ ImVec2(left, top) + ImVec2(rounding, 0));
+ drawList->PathLineTo(tip_top);
+ drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round);
+ drawList->PathBezierCubicCurveTo(
+ tip_right,
+ tip_right,
+ tip_bottom + (tip_right - tip_bottom) * tip_round);
+ drawList->PathLineTo(tip_bottom);
+ drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0));
+ drawList->PathBezierCubicCurveTo(
+ ImVec2(left, bottom),
+ ImVec2(left, bottom),
+ ImVec2(left, bottom) - ImVec2(0, rounding));
+
+ if (!filled) {
+ if (innerColor & 0xFF000000) {
+ drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
+ }
+
+ drawList->PathStroke(color, true, 2.0f * outline_scale);
+ } else {
+ drawList->PathFillConvex(color);
+ }
+ } else {
+ auto triangleStart = rect_center_x + 0.32f * rect_w;
+
+ auto rect_offset = -static_cast<int>(rect_w * 0.25f * 0.25f);
+
+ rect.Min.x += rect_offset;
+ rect.Max.x += rect_offset;
+ rect_x += rect_offset;
+ rect_center_x += rect_offset * 0.5f;
+ rect_center.x += rect_offset * 0.5f;
+
+ if (type == IconType::Circle) {
+ const auto c = rect_center;
+
+ if (!filled) {
+ const auto r = 0.5f * rect_w / 2.0f - 0.5f;
+
+ if (innerColor & 0xFF000000)
+ drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments);
+ drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale);
+ } else {
+ drawList->AddCircleFilled(c, 0.5f * rect_w / 2.0f, color, 12 + extra_segments);
+ }
+ }
+
+ if (type == IconType::Square) {
+ if (filled) {
+ const auto r = 0.5f * rect_w / 2.0f;
+ const auto p0 = rect_center - ImVec2(r, r);
+ const auto p1 = rect_center + ImVec2(r, r);
+
+ drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments);
+ } else {
+ const auto r = 0.5f * rect_w / 2.0f - 0.5f;
+ const auto p0 = rect_center - ImVec2(r, r);
+ const auto p1 = rect_center + ImVec2(r, r);
+
+ if (innerColor & 0xFF000000)
+ drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments);
+
+ drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale);
+ }
+ }
+
+ if (type == IconType::Grid) {
+ const auto r = 0.5f * rect_w / 2.0f;
+ const auto w = ceilf(r / 3.0f);
+
+ const auto baseTl = ImVec2(floorf(rect_center_x - w * 2.5f), floorf(rect_center_y - w * 2.5f));
+ const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w));
+
+ auto tl = baseTl;
+ auto br = baseBr;
+ for (int i = 0; i < 3; ++i) {
+ tl.x = baseTl.x;
+ br.x = baseBr.x;
+ drawList->AddRectFilled(tl, br, color);
+ tl.x += w * 2;
+ br.x += w * 2;
+ if (i != 1 || filled)
+ drawList->AddRectFilled(tl, br, color);
+ tl.x += w * 2;
+ br.x += w * 2;
+ drawList->AddRectFilled(tl, br, color);
+
+ tl.y += w * 2;
+ br.y += w * 2;
+ }
+
+ triangleStart = br.x + w + 1.0f / 24.0f * rect_w;
+ }
+
+ if (type == IconType::RoundSquare) {
+ if (filled) {
+ const auto r = 0.5f * rect_w / 2.0f;
+ const auto cr = r * 0.5f;
+ const auto p0 = rect_center - ImVec2(r, r);
+ const auto p1 = rect_center + ImVec2(r, r);
+
+ drawList->AddRectFilled(p0, p1, color, cr, 15);
+ } else {
+ const auto r = 0.5f * rect_w / 2.0f - 0.5f;
+ const auto cr = r * 0.5f;
+ const auto p0 = rect_center - ImVec2(r, r);
+ const auto p1 = rect_center + ImVec2(r, r);
+
+ if (innerColor & 0xFF000000)
+ drawList->AddRectFilled(p0, p1, innerColor, cr, 15);
+
+ drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale);
+ }
+ } else if (type == IconType::Diamond) {
+ if (filled) {
+ const auto r = 0.607f * rect_w / 2.0f;
+ const auto c = rect_center;
+
+ drawList->PathLineTo(c + ImVec2(0, -r));
+ drawList->PathLineTo(c + ImVec2(r, 0));
+ drawList->PathLineTo(c + ImVec2(0, r));
+ drawList->PathLineTo(c + ImVec2(-r, 0));
+ drawList->PathFillConvex(color);
+ } else {
+ const auto r = 0.607f * rect_w / 2.0f - 0.5f;
+ const auto c = rect_center;
+
+ drawList->PathLineTo(c + ImVec2(0, -r));
+ drawList->PathLineTo(c + ImVec2(r, 0));
+ drawList->PathLineTo(c + ImVec2(0, r));
+ drawList->PathLineTo(c + ImVec2(-r, 0));
+
+ if (innerColor & 0xFF000000)
+ drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
+
+ drawList->PathStroke(color, true, 2.0f * outline_scale);
+ }
+ } else {
+ const auto triangleTip = triangleStart + rect_w * (0.45f - 0.32f);
+
+ drawList->AddTriangleFilled(
+ ImVec2(ceilf(triangleTip), rect_y + rect_h * 0.5f),
+ ImVec2(triangleStart, rect_center_y + 0.15f * rect_h),
+ ImVec2(triangleStart, rect_center_y - 0.15f * rect_h),
+ color);
+ }
+ }
+}
+
+void ImGui::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color, const ImVec4& innerColor) {
+ // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/widgets.cpp
+ // ax::NodeEditor::Icon
+
+ if (ImGui::IsRectVisible(size)) {
+ auto cursorPos = ImGui::GetCursorScreenPos();
+ auto drawList = ImGui::GetWindowDrawList();
+ DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor));
+ }
+
+ ImGui::Dummy(size);
+}
+
+void ImGui::DrawArrow(ImDrawList* drawList, ImVec2 from, ImVec2 to, ImU32 color, float lineThickness) {
+ // Adapted from https://stackoverflow.com/a/6333775
+
+ using namespace std::numbers;
+ constexpr float kHeadLength = 10;
+
+ auto angle = std::atan2(to.y - from.y, to.x - from.x);
+ drawList->AddLine(from, to, color, lineThickness);
+ drawList->AddLine(to, ImVec2(to.x - kHeadLength * std::cos(angle - pi / 6), to.y - kHeadLength * std::sin(angle - pi / 6)), color, lineThickness);
+ drawList->AddLine(to, ImVec2(to.x - kHeadLength * std::cos(angle + pi / 6), to.y - kHeadLength * std::sin(angle + pi / 6)), color, lineThickness);
+}
+
+struct DialogObject {
+ std::string message;
+ std::function<void(int)> callback;
+};
+
+static DialogObject gConfirmationDialog{};
+
+void ImGui::DialogConfirmation(std::string message, std::function<void(bool)> callback) {
+ gConfirmationDialog.message = std::move(message);
+ // TODO is converting void(bool) to void(int) fine?
+ gConfirmationDialog.callback = std::move(callback);
+}
+
+void ImGui::ShowDialogs() {
+ if (gConfirmationDialog.callback) {
+ if (ImGui::BeginPopupModal("Confirmation")) {
+ ImGui::Text("%s", gConfirmationDialog.message.c_str());
+ if (ImGui::Button("Cancel")) {
+ gConfirmationDialog.callback(false);
+ gConfirmationDialog.callback = {};
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Confirm")) {
+ gConfirmationDialog.callback(true);
+ gConfirmationDialog.callback = {};
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+ }
+}
+
+float Utils::CalcImageHeight(glm::vec2 original, int targetWidth) {
+ // Xorig / Yorig = Xnew / Ynew
+ // Ynew = Xnew * Yorig / Xorig
+ return targetWidth * original.y / original.x;
+}
+
+float Utils::CalcImageWidth(glm::vec2 original, float targetHeight) {
+ // Xorig / Yorig = Xnew / Ynew
+ // Xnew = Xorig / Yorig * Ynew
+ return original.x / original.y * targetHeight;
+}
+
+ImVec2 Utils::FitImage(glm::vec2 original) {
+ float newWidth = ImGui::GetContentRegionAvail().x;
+ return ImVec2(newWidth, CalcImageHeight(original, newWidth));
+}
diff --git a/source/30-game/EditorUtils.hpp b/source/30-game/EditorUtils.hpp
new file mode 100644
index 0000000..96e92d3
--- /dev/null
+++ b/source/30-game/EditorUtils.hpp
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "EditorCore.hpp"
+#include "ImGuiGuizmo.hpp"
+#include "Ires.hpp"
+
+#include <Color.hpp>
+#include <Metadata.hpp>
+
+#include <imgui.h>
+#include <string>
+
+// To check whether a payload is of this type, use starts_with()
+#define BRUSSEL_TAG_PREFIX_GameObject "GameObject"
+#define BRUSSEL_TAG_PREFIX_Ires "Ires"
+
+#define BRUSSEL_TAG_Level "Level"
+
+namespace ImGui {
+
+const char* GetKeyNameGlfw(int key);
+
+void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond = ImGuiCond_None);
+void SetNextWindowCentered(ImGuiCond cond = ImGuiCond_None);
+
+void PushDisabled();
+void PopDisabled();
+
+bool Button(const char* label, bool disabled);
+bool Button(const char* label, const ImVec2& sizeArg, bool disabled);
+
+bool ColorEdit3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0);
+bool ColorEdit4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0);
+bool ColorPicker3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0);
+bool ColorPicker4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0);
+
+bool Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize = -1.0f);
+
+void AddUnderLine(ImColor col);
+
+enum class IconType {
+ Flow,
+ Circle,
+ Square,
+ Grid,
+ RoundSquare,
+ Diamond,
+};
+
+void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor);
+void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0));
+
+void DrawArrow(ImDrawList* drawList, ImVec2 from, ImVec2 to, ImU32 color, float lineThickness = 1.0f);
+
+// NOTE: string is copied into an internal storage
+void DialogConfirmation(std::string message, std::function<void(bool)> callback);
+void ShowDialogs();
+
+} // namespace ImGui
+
+namespace Utils {
+
+float CalcImageHeight(glm::vec2 original, int targetWidth);
+float CalcImageWidth(glm::vec2 original, float targetHeight);
+ImVec2 FitImage(glm::vec2 original);
+
+// TODO get kind from T
+template <typename T>
+T* SimpleIresReceptor(T* existing, IEditor& editor, IresObject::Kind kind) {
+ if (existing) {
+ existing->ShowReference(editor);
+ } else {
+ IresObject::ShowReferenceNull(editor);
+ }
+ if (ImGui::BeginDragDropTarget()) {
+ if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(kind).data())) {
+ return *static_cast<T* const*>(payload->Data);
+ }
+ ImGui::EndDragDropTarget();
+ }
+ return nullptr;
+}
+
+} // namespace Utils
diff --git a/source/30-game/EditorWorldGuides.cpp b/source/30-game/EditorWorldGuides.cpp
new file mode 100644
index 0000000..f0d66b8
--- /dev/null
+++ b/source/30-game/EditorWorldGuides.cpp
@@ -0,0 +1,26 @@
+#include "EditorWorldGuides.hpp"
+
+#include "App.hpp"
+#include "CommonVertexIndex.hpp"
+#include "GraphicsTags.hpp"
+#include "VertexIndex.hpp"
+
+EditorWorldGuides::EditorWorldGuides(App* app, IEditor* editor)
+ : mApp{ app }
+ , mEditor{ editor } //
+{
+ using namespace Tags;
+
+ mRoGrid.SetFormat(gVformatLines.Get(), IT_16Bit);
+ mRoGrid.SetMaterial(gDefaultMaterial.Get());
+ mRoGrid.RebuildIfNecessary();
+
+ mRoDebugSkybox.SetFormat(gVformatStandard.Get(), IT_16Bit);
+ mRoDebugSkybox.SetMaterial(gDefaultMaterial.Get());
+ mRoDebugSkybox.RebuildIfNecessary();
+}
+
+void EditorWorldGuides::Update() {
+ auto& camera = *mApp->GetActiveCamera();
+ // TODO
+}
diff --git a/source/30-game/EditorWorldGuides.hpp b/source/30-game/EditorWorldGuides.hpp
new file mode 100644
index 0000000..0dfdea2
--- /dev/null
+++ b/source/30-game/EditorWorldGuides.hpp
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "Renderer.hpp"
+
+class EditorWorldGuides {
+private:
+ App* mApp;
+ IEditor* mEditor;
+ RenderObject mRoGrid;
+ RenderObject mRoDebugSkybox;
+
+public:
+ EditorWorldGuides(App* app, IEditor* editor);
+
+ void Update();
+};
diff --git a/source/30-game/FuzzyMatch.cpp b/source/30-game/FuzzyMatch.cpp
new file mode 100644
index 0000000..0ab604d
--- /dev/null
+++ b/source/30-game/FuzzyMatch.cpp
@@ -0,0 +1,174 @@
+// Adapted from https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h
+#include "FuzzyMatch.hpp"
+
+#include <cctype>
+#include <cstring>
+
+namespace FuzzyMatch {
+
+namespace P6503_UNITY_ID {
+ bool SearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit);
+} // namespace P6503_UNITY_ID
+
+bool SearchSimple(char const* pattern, char const* haystack) {
+ while (*pattern != '\0' && *haystack != '\0') {
+ if (tolower(*pattern) == tolower(*haystack)) {
+ ++pattern;
+ }
+ ++haystack;
+ }
+
+ return *pattern == '\0';
+}
+
+bool Search(char const* pattern, char const* haystack, int& outScore) {
+ uint8_t matches[256];
+ int matchCount = 0;
+ return Search(pattern, haystack, outScore, matches, sizeof(matches), matchCount);
+}
+
+bool Search(char const* pattern, char const* haystack, int& outScore, uint8_t matches[], int maxMatches, int& outMatches) {
+ int recursionCount = 0;
+ int recursionLimit = 10;
+ int newMatches = 0;
+ bool result = P6503_UNITY_ID::SearchRecursive(pattern, haystack, outScore, haystack, nullptr, matches, maxMatches, newMatches, recursionCount, recursionLimit);
+ outMatches = newMatches;
+ return result;
+}
+
+namespace P6503_UNITY_ID {
+ bool SearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit) {
+ // Count recursions
+ ++recursionCount;
+ if (recursionCount >= recursionLimit) {
+ return false;
+ }
+
+ // Detect end of strings
+ if (*pattern == '\0' || *src == '\0') {
+ return false;
+ }
+
+ // Recursion params
+ bool recursiveMatch = false;
+ uint8_t bestRecursiveMatches[256];
+ int bestRecursiveScore = 0;
+
+ // Loop through pattern and str looking for a match
+ bool firstMatch = true;
+ while (*pattern != '\0' && *src != '\0') {
+ // Found match
+ if (tolower(*pattern) == tolower(*src)) {
+ // Supplied matches buffer was too short
+ if (nextMatch >= maxMatches) {
+ return false;
+ }
+
+ // "Copy-on-Write" srcMatches into matches
+ if (firstMatch && srcMatches) {
+ memcpy(newMatches, srcMatches, nextMatch);
+ firstMatch = false;
+ }
+
+ // Recursive call that "skips" this match
+ uint8_t recursiveMatches[256];
+ int recursiveScore;
+ int recursiveNextMatch = nextMatch;
+ if (SearchRecursive(pattern, src + 1, recursiveScore, strBegin, newMatches, recursiveMatches, sizeof(recursiveMatches), recursiveNextMatch, recursionCount, recursionLimit)) {
+ // Pick the best recursive score
+ if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
+ memcpy(bestRecursiveMatches, recursiveMatches, 256);
+ bestRecursiveScore = recursiveScore;
+ }
+ recursiveMatch = true;
+ }
+
+ // Advance
+ newMatches[nextMatch++] = (uint8_t)(src - strBegin);
+ ++pattern;
+ }
+ ++src;
+ }
+
+ // Determine if full pattern was matched
+ bool matched = *pattern == '\0';
+
+ // Calculate score
+ if (matched) {
+ const int sequentialBonus = 15; // bonus for adjacent matches
+ const int separatorBonus = 30; // bonus if match occurs after a separator
+ const int camelBonus = 30; // bonus if match is uppercase and prev is lower
+ const int firstLetterBonus = 15; // bonus if the first letter is matched
+
+ const int leadingLetterPenalty = -5; // penalty applied for every letter in str before the first match
+ const int maxLeadingLetterPenalty = -15; // maximum penalty for leading letters
+ const int unmatchedLetterPenalty = -1; // penalty for every letter that doesn't matter
+
+ // Iterate str to end
+ while (*src != '\0') {
+ ++src;
+ }
+
+ // Initialize score
+ outScore = 100;
+
+ // Apply leading letter penalty
+ int penalty = leadingLetterPenalty * newMatches[0];
+ if (penalty < maxLeadingLetterPenalty) {
+ penalty = maxLeadingLetterPenalty;
+ }
+ outScore += penalty;
+
+ // Apply unmatched penalty
+ int unmatched = (int)(src - strBegin) - nextMatch;
+ outScore += unmatchedLetterPenalty * unmatched;
+
+ // Apply ordering bonuses
+ for (int i = 0; i < nextMatch; ++i) {
+ uint8_t currIdx = newMatches[i];
+
+ if (i > 0) {
+ uint8_t prevIdx = newMatches[i - 1];
+
+ // Sequential
+ if (currIdx == (prevIdx + 1))
+ outScore += sequentialBonus;
+ }
+
+ // Check for bonuses based on neighbor character value
+ if (currIdx > 0) {
+ // Camel case
+ char neighbor = strBegin[currIdx - 1];
+ char curr = strBegin[currIdx];
+ if (::islower(neighbor) && ::isupper(curr)) {
+ outScore += camelBonus;
+ }
+
+ // Separator
+ bool neighborSeparator = neighbor == '_' || neighbor == ' ';
+ if (neighborSeparator) {
+ outScore += separatorBonus;
+ }
+ } else {
+ // First letter
+ outScore += firstLetterBonus;
+ }
+ }
+ }
+
+ // Return best result
+ if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
+ // Recursive score is better than "this"
+ memcpy(newMatches, bestRecursiveMatches, maxMatches);
+ outScore = bestRecursiveScore;
+ return true;
+ } else if (matched) {
+ // "this" score is better than recursive
+ return true;
+ } else {
+ // no match
+ return false;
+ }
+ }
+} // namespace P6503_UNITY_ID
+} // namespace FuzzyMatch
diff --git a/source/30-game/FuzzyMatch.hpp b/source/30-game/FuzzyMatch.hpp
new file mode 100644
index 0000000..7a26b7e
--- /dev/null
+++ b/source/30-game/FuzzyMatch.hpp
@@ -0,0 +1,10 @@
+// Adapted from https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h
+#pragma once
+
+#include <cstdint>
+
+namespace FuzzyMatch {
+bool SearchSimple(char const* pattern, char const* haystack);
+bool Search(char const* pattern, char const* haystack, int& outScore);
+bool Search(char const* pattern, char const* haystack, int& outScore, uint8_t matches[], int maxMatches, int& outMatches);
+} // namespace FuzzyMatch
diff --git a/source/30-game/GameObject.cpp b/source/30-game/GameObject.cpp
new file mode 100644
index 0000000..3b15111
--- /dev/null
+++ b/source/30-game/GameObject.cpp
@@ -0,0 +1,230 @@
+#include "GameObject.hpp"
+
+#include "Level.hpp"
+#include "Player.hpp"
+#include "SceneThings.hpp"
+#include "World.hpp"
+
+#include <Metadata.hpp>
+#include <RapidJsonHelper.hpp>
+
+#include <rapidjson/document.h>
+#include <cassert>
+#include <string_view>
+#include <utility>
+
+using namespace std::literals;
+
+namespace ProjectBrussel_UNITY_ID {
+GameObject* CreateGameObject(GameObject::Kind kind, GameWorld* world) {
+ using enum Tags::GameObjectKind;
+ switch (kind) {
+ case KD_Generic: return new GameObject(world);
+ case KD_SimpleGeometry: return new SimpleGeometryObject(world);
+ case KD_Building: return new BuildingObject(world);
+ case KD_LevelWrapper: return new LevelWrapperObject(world);
+ default: break;
+ }
+ return nullptr;
+}
+
+bool ValidateGameObjectChild(GameObject* parent, GameObject* child) {
+ return parent->GetWorld() == child->GetWorld();
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+void GameObject::FreeRecursive(GameObject* obj) {
+ if (!obj->mStopFreePropagation) {
+ for (auto child : obj->GetChildren()) {
+ FreeRecursive(obj);
+ }
+ }
+ delete obj;
+}
+
+GameObject::GameObject(GameWorld* world)
+ : GameObject(KD_Generic, world) {
+}
+
+GameObject::GameObject(Kind kind, GameWorld* world)
+ : mEditorAttachment{ nullptr }
+ , mWorld{ world }
+ , mParent{ nullptr }
+ , mRot(1.0f, 0.0f, 0.0f, 0.0f)
+ , mPos(0.0f, 0.0f, 0.0f)
+ , mScale(1.0f, 1.0f, 1.0f)
+ , mKind{ kind } {
+}
+
+GameObject::~GameObject() {
+ RemoveAllChildren();
+ if (mParent) {
+ mParent->RemoveChild(this);
+ // NOTE: from this point on, mParent will be nullptr
+ }
+}
+
+GameObject::Kind GameObject::GetKind() const {
+ return mKind;
+}
+
+GameWorld* GameObject::GetWorld() const {
+ return mWorld;
+}
+
+GameObject* GameObject::GetParent() const {
+ return mParent;
+}
+
+const PodVector<GameObject*>& GameObject::GetChildren() const {
+ return mChildren;
+}
+
+void GameObject::AddChild(GameObject* child) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (child->mParent) {
+ return;
+ }
+ if (!ValidateGameObjectChild(this, child)) {
+ return;
+ }
+
+ mChildren.push_back(child);
+ child->SetParent(this);
+}
+
+GameObject* GameObject::RemoveChild(int index) {
+ if (index < 0 || index >= mChildren.size()) {
+ return nullptr;
+ }
+
+ auto it = mChildren.begin() + index;
+ auto child = *it;
+
+ // cancelUpdate(ret);
+
+ std::swap(*it, mChildren.back());
+ mChildren.pop_back();
+ child->SetParent(nullptr);
+ return child;
+}
+
+GameObject* GameObject::RemoveChild(GameObject* child) {
+ if (child) {
+ for (auto it = mChildren.begin(); it != mChildren.end(); ++it) {
+ if (*it == child) {
+ // cancelUpdate(child);
+
+ std::swap(*it, mChildren.back());
+ mChildren.pop_back();
+ child->SetParent(nullptr);
+ return child;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void GameObject::RemoveSelfFromParent() {
+ if (mParent) {
+ mParent->RemoveChild(this);
+ }
+}
+
+PodVector<GameObject*> GameObject::RemoveAllChildren() {
+ for (auto& child : mChildren) {
+ child->SetParent(nullptr);
+ }
+
+ auto result = std::move(mChildren);
+ // Moving from STL object leaves it in a valid but _unspecified_ state, call std::vector::clear() to guarantee it's empty
+ // NOTE: even though we have the source code of PodVector<T>, we still do this to follow convention
+ mChildren.clear();
+ return result;
+}
+
+const glm::vec3& GameObject::GetPos() const {
+ return mPos;
+}
+
+void GameObject::SetPos(const glm::vec3& pos) {
+ mPos = pos;
+}
+
+const glm::quat& GameObject::GetRotation() const {
+ return mRot;
+}
+
+void GameObject::SetRotation(const glm::quat& rotation) {
+ mRot = rotation;
+}
+
+const glm::vec3& GameObject::GetScale() const {
+ return mScale;
+}
+
+void GameObject::SetScale(const glm::vec3& scale) {
+ mScale = scale;
+}
+
+std::span<const RenderObject> GameObject::GetRenderObjects() const {
+ return {};
+}
+
+void GameObject::OnInitialized() {
+}
+
+void GameObject::Awaken() {
+}
+
+void GameObject::Resleep() {
+}
+
+void GameObject::Update() {
+}
+
+rapidjson::Value GameObject::Serialize(GameObject* obj, rapidjson::Document& root) {
+ rapidjson::Value result(rapidjson::kObjectType);
+
+ result.AddMember("Type", rapidjson::StringRef(Metadata::EnumToString(obj->GetKind())), root.GetAllocator());
+
+ rapidjson::Value rvValue(rapidjson::kObjectType);
+ obj->WriteSaveFormat(rvValue, root);
+ result.AddMember("Value", rvValue, root.GetAllocator());
+
+ return result;
+}
+
+std::unique_ptr<GameObject> GameObject::Deserialize(const rapidjson::Value& value, GameWorld* world) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ auto rvType = rapidjson::GetProperty(value, rapidjson::kStringType, "Type"sv);
+ if (!rvType) return nullptr;
+
+ auto rvValue = rapidjson::GetProperty(value, rapidjson::kObjectType, "Value"sv);
+ if (!rvValue) return nullptr;
+
+ auto kind = Metadata::EnumFromString<Kind>(rapidjson::AsStringView(*rvType));
+ assert(kind.has_value());
+ auto obj = std::unique_ptr<GameObject>(CreateGameObject(kind.value(), world));
+ if (!obj) return nullptr;
+ obj->ReadSaveFormat(*rvValue);
+
+ return obj;
+}
+
+void GameObject::ReadSaveFormat(const rapidjson::Value& value) {
+}
+
+void GameObject::WriteSaveFormat(rapidjson::Value& value, rapidjson::Document& root) {
+}
+
+void GameObject::SetParent(GameObject* parent) {
+ if (mParent != parent) {
+ mParent = parent;
+ // needUpdate();
+ }
+}
+
+#include <generated/GameObject.gs.inl>
diff --git a/source/30-game/GameObject.hpp b/source/30-game/GameObject.hpp
new file mode 100644
index 0000000..40c52e7
--- /dev/null
+++ b/source/30-game/GameObject.hpp
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "EditorAttachment.hpp"
+#include "Material.hpp"
+#include "Renderer.hpp"
+#include "VertexIndex.hpp"
+
+#include <MacrosCodegen.hpp>
+#include <PodVector.hpp>
+
+#include <rapidjson/fwd.h>
+#include <glm/glm.hpp>
+#include <glm/gtc/quaternion.hpp>
+#include <span>
+#include <vector>
+
+namespace Tags {
+enum class GameObjectKind {
+ // clang-format off
+ BRUSSEL_ENUM(ToString, FromString, RemovePrefix KD_, AddPrefix GameObject, ExcludeHeuristics)
+ // clang-format on
+
+ KD_Generic,
+ KD_Player,
+ KD_SimpleGeometry,
+ KD_Building,
+ KD_LevelWrapper,
+ KD_COUNT,
+};
+} // namespace Tags
+
+class GameWorld;
+class GameObject {
+ BRUSSEL_CLASS(InheritanceHiearchy)
+
+public:
+ using Kind = Tags::GameObjectKind;
+ using enum Tags::GameObjectKind;
+
+private:
+ std::unique_ptr<EditorAttachment> mEditorAttachment;
+ GameWorld* mWorld;
+ GameObject* mParent;
+ PodVector<GameObject*> mChildren;
+ glm::quat mRot;
+ glm::vec3 mPos;
+ glm::vec3 mScale;
+ Kind mKind;
+
+protected:
+ bool mStopFreePropagation : 1 = false;
+
+public:
+ static void FreeRecursive(GameObject* object);
+
+ // TODO allow moving between worlds
+ GameObject(GameWorld* world);
+ GameObject(Kind kind, GameWorld* world);
+ virtual ~GameObject();
+
+ GameObject(const GameObject&) = delete;
+ GameObject& operator=(const GameObject&) = delete;
+ GameObject(GameObject&&) = default;
+ GameObject& operator=(GameObject&&) = default;
+
+ Kind GetKind() const;
+
+ GameWorld* GetWorld() const;
+ GameObject* GetParent() const;
+ const PodVector<GameObject*>& GetChildren() const;
+ void AddChild(GameObject* child);
+ GameObject* RemoveChild(int index);
+ GameObject* RemoveChild(GameObject* child);
+ void RemoveSelfFromParent();
+ PodVector<GameObject*> RemoveAllChildren();
+
+ EditorAttachment* GetEditorAttachment() const { return mEditorAttachment.get(); }
+ void SetEditorAttachment(EditorAttachment* attachment) { mEditorAttachment.reset(attachment); }
+
+ const glm::vec3& GetPos() const;
+ void SetPos(const glm::vec3& pos);
+
+ const glm::quat& GetRotation() const;
+ void SetRotation(const glm::quat& rotation);
+
+ const glm::vec3& GetScale() const;
+ void SetScale(const glm::vec3& scale);
+
+ // Visuals
+ virtual std::span<const RenderObject> GetRenderObjects() const;
+
+ // Lifetime hooks
+ virtual void OnInitialized();
+ virtual void Awaken();
+ virtual void Resleep();
+ virtual void Update();
+
+ static rapidjson::Value Serialize(GameObject* obj, rapidjson::Document& root);
+ static std::unique_ptr<GameObject> Deserialize(const rapidjson::Value& value, GameWorld* world);
+ virtual void ReadSaveFormat(const rapidjson::Value& value);
+ virtual void WriteSaveFormat(rapidjson::Value& value, rapidjson::Document& root);
+
+protected:
+ void SetParent(GameObject* parent);
+};
+
+#include <generated/GameObject.gh.inl>
diff --git a/source/30-game/GraphicsTags.cpp b/source/30-game/GraphicsTags.cpp
new file mode 100644
index 0000000..83d52f8
--- /dev/null
+++ b/source/30-game/GraphicsTags.cpp
@@ -0,0 +1,273 @@
+#include "GraphicsTags.hpp"
+
+#include <robin_hood.h>
+#include <cstddef>
+#include <cstdint>
+
+using namespace std::literals;
+
+int Tags::SizeOf(VertexElementType type) {
+ switch (type) {
+ case VET_Float1:
+ return sizeof(float);
+ case VET_Float2:
+ return sizeof(float) * 2;
+ case VET_Float3:
+ return sizeof(float) * 3;
+ case VET_Float4:
+ return sizeof(float) * 4;
+ case VET_Double1:
+ return sizeof(double);
+ case VET_Double2:
+ return sizeof(double) * 2;
+ case VET_Double3:
+ return sizeof(double) * 3;
+ case VET_Double4:
+ return sizeof(double) * 4;
+ case VET_Short2:
+ case VET_Short2Norm:
+ case VET_Ushort2:
+ case VET_Ushort2Norm:
+ return sizeof(short) * 2;
+ case VET_Short4:
+ case VET_Short4Norm:
+ case VET_Ushort4:
+ case VET_Ushort4Norm:
+ return sizeof(short) * 4;
+ case VET_Int1:
+ case VET_Uint1:
+ return sizeof(int);
+ case VET_Int2:
+ case VET_Uint2:
+ return sizeof(int) * 2;
+ case VET_Int3:
+ case VET_Uint3:
+ return sizeof(int) * 3;
+ case VET_Int4:
+ case VET_Uint4:
+ return sizeof(int) * 4;
+ case VET_Byte4:
+ case VET_Byte4Norm:
+ case VET_Ubyte4:
+ case VET_Ubyte4Norm:
+ return sizeof(char) * 4;
+ }
+ return 0;
+}
+
+int Tags::VectorLenOf(VertexElementType type) {
+ switch (type) {
+ case VET_Float1:
+ case VET_Double1:
+ case VET_Int1:
+ case VET_Uint1:
+ return 1;
+ case VET_Float2:
+ case VET_Double2:
+ case VET_Short2:
+ case VET_Short2Norm:
+ case VET_Ushort2:
+ case VET_Ushort2Norm:
+ case VET_Int2:
+ case VET_Uint2:
+ return 2;
+ case VET_Float3:
+ case VET_Double3:
+ case VET_Int3:
+ case VET_Uint3:
+ return 3;
+ case VET_Float4:
+ case VET_Double4:
+ case VET_Short4:
+ case VET_Short4Norm:
+ case VET_Ushort4:
+ case VET_Ushort4Norm:
+ case VET_Int4:
+ case VET_Uint4:
+ case VET_Byte4:
+ case VET_Byte4Norm:
+ case VET_Ubyte4:
+ case VET_Ubyte4Norm:
+ return 4;
+ }
+ return 0;
+}
+
+GLenum Tags::FindGLType(VertexElementType type) {
+ switch (type) {
+ case VET_Float1:
+ case VET_Float2:
+ case VET_Float3:
+ case VET_Float4:
+ return GL_FLOAT;
+ case VET_Double1:
+ case VET_Double2:
+ case VET_Double3:
+ case VET_Double4:
+ return GL_DOUBLE;
+ case VET_Short2:
+ case VET_Short2Norm:
+ case VET_Short4:
+ case VET_Short4Norm:
+ return GL_SHORT;
+ case VET_Ushort2:
+ case VET_Ushort2Norm:
+ case VET_Ushort4:
+ case VET_Ushort4Norm:
+ return GL_UNSIGNED_SHORT;
+ case VET_Int1:
+ case VET_Int2:
+ case VET_Int3:
+ case VET_Int4:
+ return GL_INT;
+ case VET_Uint1:
+ case VET_Uint2:
+ case VET_Uint3:
+ case VET_Uint4:
+ return GL_UNSIGNED_INT;
+ case VET_Byte4:
+ case VET_Byte4Norm:
+ return GL_BYTE;
+ case VET_Ubyte4:
+ case VET_Ubyte4Norm:
+ return GL_UNSIGNED_BYTE;
+ }
+ return 0;
+}
+
+bool Tags::IsNormalized(VertexElementType type) {
+ return type >= VET_NORM_BEGIN && type <= VET_NORM_END;
+}
+
+int Tags::SizeOf(IndexType type) {
+ switch (type) {
+ case IT_16Bit: return sizeof(uint16_t);
+ case IT_32Bit: return sizeof(uint32_t);
+ }
+ return 0;
+}
+
+GLenum Tags::FindGLType(IndexType type) {
+ switch (type) {
+ case IT_16Bit: return GL_UNSIGNED_SHORT;
+ case IT_32Bit: return GL_UNSIGNED_BYTE;
+ }
+ return 0;
+}
+
+namespace ProjectBrussel_UNITY_ID {
+struct GLTypeInfo {
+ robin_hood::unordered_flat_map<GLenum, std::string_view> enum2Name;
+ robin_hood::unordered_flat_map<std::string_view, GLenum> name2Enum;
+
+ GLTypeInfo() {
+ InsertEntry("float"sv, GL_FLOAT);
+ InsertEntry("double"sv, GL_DOUBLE);
+ InsertEntry("int"sv, GL_INT);
+ InsertEntry("uint"sv, GL_UNSIGNED_INT);
+ InsertEntry("bool"sv, GL_BOOL);
+
+ InsertEntry("vec2"sv, GL_FLOAT_VEC2);
+ InsertEntry("vec3"sv, GL_FLOAT_VEC3);
+ InsertEntry("vec4"sv, GL_FLOAT_VEC4);
+ InsertEntry("dvec2"sv, GL_DOUBLE_VEC2);
+ InsertEntry("dvec3"sv, GL_DOUBLE_VEC3);
+ InsertEntry("dvec4"sv, GL_DOUBLE_VEC4);
+ InsertEntry("ivec2"sv, GL_INT_VEC2);
+ InsertEntry("ivec3"sv, GL_INT_VEC3);
+ InsertEntry("ivec4"sv, GL_INT_VEC4);
+ InsertEntry("uvec2"sv, GL_UNSIGNED_INT_VEC2);
+ InsertEntry("uvec3"sv, GL_UNSIGNED_INT_VEC3);
+ InsertEntry("uvec4"sv, GL_UNSIGNED_INT_VEC4);
+ InsertEntry("bvec2"sv, GL_BOOL_VEC2);
+ InsertEntry("bvec3"sv, GL_BOOL_VEC3);
+ InsertEntry("bvec4"sv, GL_BOOL_VEC4);
+
+ InsertEntry("mat2"sv, GL_FLOAT_MAT2);
+ InsertEntry("mat3"sv, GL_FLOAT_MAT3);
+ InsertEntry("mat4"sv, GL_FLOAT_MAT4);
+ InsertEntry("mat2x3"sv, GL_FLOAT_MAT2x3);
+ InsertEntry("mat2x4"sv, GL_FLOAT_MAT2x4);
+ InsertEntry("mat3x2"sv, GL_FLOAT_MAT3x2);
+ InsertEntry("mat3x4"sv, GL_FLOAT_MAT3x4);
+ InsertEntry("mat4x2"sv, GL_FLOAT_MAT4x2);
+ InsertEntry("mat4x3"sv, GL_FLOAT_MAT4x3);
+
+ InsertEntry("dmat2"sv, GL_DOUBLE_MAT2);
+ InsertEntry("dmat3"sv, GL_DOUBLE_MAT3);
+ InsertEntry("dmat4"sv, GL_DOUBLE_MAT4);
+ InsertEntry("dmat2x3"sv, GL_DOUBLE_MAT2x3);
+ InsertEntry("dmat2x4"sv, GL_DOUBLE_MAT2x4);
+ InsertEntry("dmat3x2"sv, GL_DOUBLE_MAT3x2);
+ InsertEntry("dmat3x4"sv, GL_DOUBLE_MAT3x4);
+ InsertEntry("dmat4x2"sv, GL_DOUBLE_MAT4x2);
+ InsertEntry("dmat4x3"sv, GL_DOUBLE_MAT4x3);
+
+ InsertEntry("sampler1D"sv, GL_SAMPLER_1D);
+ InsertEntry("sampler2D"sv, GL_SAMPLER_2D);
+ InsertEntry("sampler3D"sv, GL_SAMPLER_3D);
+ InsertEntry("samplerCube"sv, GL_SAMPLER_CUBE);
+ InsertEntry("sampler1DShadow"sv, GL_SAMPLER_1D_SHADOW);
+ InsertEntry("sampler2DShadow"sv, GL_SAMPLER_2D_SHADOW);
+ InsertEntry("sampler1DArray"sv, GL_SAMPLER_1D_ARRAY);
+ InsertEntry("sampler2DArray"sv, GL_SAMPLER_2D_ARRAY);
+ InsertEntry("sampler1DArrayShadow"sv, GL_SAMPLER_1D_ARRAY_SHADOW);
+ InsertEntry("sampler2DArrayShadow"sv, GL_SAMPLER_2D_ARRAY_SHADOW);
+ InsertEntry("sampler2DMultisample"sv, GL_SAMPLER_2D_MULTISAMPLE);
+ InsertEntry("sampler2DMultisampleArray"sv, GL_SAMPLER_2D_MULTISAMPLE_ARRAY);
+ InsertEntry("samplerCubeShadow"sv, GL_SAMPLER_CUBE_SHADOW);
+ InsertEntry("samplerBuffer"sv, GL_SAMPLER_BUFFER);
+ InsertEntry("sampler2DRect"sv, GL_SAMPLER_2D_RECT);
+ InsertEntry("sampler2DRectShadow"sv, GL_SAMPLER_2D_RECT_SHADOW);
+
+ InsertEntry("isampler1D"sv, GL_INT_SAMPLER_1D);
+ InsertEntry("isampler2D"sv, GL_INT_SAMPLER_2D);
+ InsertEntry("isampler3D"sv, GL_INT_SAMPLER_3D);
+ InsertEntry("isamplerCube"sv, GL_INT_SAMPLER_CUBE);
+ InsertEntry("isampler1DArray"sv, GL_INT_SAMPLER_1D_ARRAY);
+ InsertEntry("isampler2DArray"sv, GL_INT_SAMPLER_2D_ARRAY);
+ InsertEntry("isampler2DMultisample"sv, GL_INT_SAMPLER_2D_MULTISAMPLE);
+ InsertEntry("isampler2DMultisampleArray"sv, GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY);
+ InsertEntry("isamplerBuffer"sv, GL_INT_SAMPLER_BUFFER);
+ InsertEntry("isampler2DRect"sv, GL_INT_SAMPLER_2D_RECT);
+
+ InsertEntry("usampler1D"sv, GL_UNSIGNED_INT_SAMPLER_1D);
+ InsertEntry("usampler2D"sv, GL_UNSIGNED_INT_SAMPLER_2D);
+ InsertEntry("usampler3D"sv, GL_UNSIGNED_INT_SAMPLER_3D);
+ InsertEntry("usamplerCube"sv, GL_UNSIGNED_INT_SAMPLER_CUBE);
+ InsertEntry("usampler1DArray"sv, GL_UNSIGNED_INT_SAMPLER_1D_ARRAY);
+ InsertEntry("usampler2DArray"sv, GL_UNSIGNED_INT_SAMPLER_2D_ARRAY);
+ InsertEntry("usampler2DMultisample"sv, GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE);
+ InsertEntry("usampler2DMultisampleArray"sv, GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY);
+ InsertEntry("usamplerBuffer"sv, GL_UNSIGNED_INT_SAMPLER_BUFFER);
+ InsertEntry("usampler2DRect"sv, GL_UNSIGNED_INT_SAMPLER_2D_RECT);
+ }
+
+ void InsertEntry(std::string_view name, GLenum value) {
+ enum2Name.try_emplace(value, name);
+ name2Enum.try_emplace(name, value);
+ }
+} const kGLTypeInfo;
+} // namespace ProjectBrussel_UNITY_ID
+
+std::string_view Tags::GLTypeToString(GLenum value) {
+ using namespace ProjectBrussel_UNITY_ID;
+ auto iter = kGLTypeInfo.enum2Name.find(value);
+ if (iter != kGLTypeInfo.enum2Name.end()) {
+ return iter->second;
+ } else {
+ return std::string_view();
+ }
+}
+
+GLenum Tags::GLTypeFromString(std::string_view name) {
+ using namespace ProjectBrussel_UNITY_ID;
+ auto iter = kGLTypeInfo.name2Enum.find(name);
+ if (iter != kGLTypeInfo.name2Enum.end()) {
+ return iter->second;
+ } else {
+ return 0;
+ }
+}
+
+#include <generated/GraphicsTags.gs.inl>
diff --git a/source/30-game/GraphicsTags.hpp b/source/30-game/GraphicsTags.hpp
new file mode 100644
index 0000000..f9628b2
--- /dev/null
+++ b/source/30-game/GraphicsTags.hpp
@@ -0,0 +1,111 @@
+#pragma once
+
+#include <MacrosCodegen.hpp>
+
+#include <glad/glad.h>
+#include <limits>
+#include <string>
+#include <string_view>
+
+namespace Tags {
+/// Vertex element semantics, used to identify the meaning of vertex buffer contents
+enum VertexElementSemantic {
+ // clang-format off
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ // clang-format on
+
+ /// Position, typically VET_Float3
+ VES_Position,
+ /// Blending weights
+ VES_BlendWeights,
+ /// Blending indices
+ VES_BlendIndices,
+ /// Normal, typically VET_Float3
+ VES_Normal,
+ /// Colour, typically VET_Ubyte4
+ VES_Color1,
+ VES_Color2,
+ VES_Color3,
+ /// Texture coordinates, typically VET_Float2
+ VES_TexCoords1,
+ VES_TexCoords2,
+ VES_TexCoords3,
+ /// Binormal (Y axis if normal is Z)
+ VES_Binormal,
+ /// Tangent (X axis if normal is Z)
+ VES_Tangent,
+ /// Default semantic
+ VES_Generic,
+ VES_COUNT,
+};
+
+enum VertexElementType {
+ // clang-format off
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ // clang-format on
+
+ VET_Float1,
+ VET_Float2,
+ VET_Float3,
+ VET_Float4,
+
+ VET_Short2,
+ VET_Short4,
+ VET_Ubyte4,
+
+ // the following are not universally supported on all hardware:
+ VET_Double1,
+ VET_Double2,
+ VET_Double3,
+ VET_Double4,
+ VET_Ushort2,
+ VET_Ushort4,
+ VET_Int1,
+ VET_Int2,
+ VET_Int3,
+ VET_Int4,
+ VET_Uint1,
+ VET_Uint2,
+ VET_Uint3,
+ VET_Uint4,
+ VET_Byte4, /// signed bytes
+
+ VET_Byte4Norm, /// signed bytes (normalized to -1..1)
+ VET_Ubyte4Norm, /// unsigned bytes (normalized to 0..1)
+ VET_Short2Norm, /// signed shorts (normalized to -1..1)
+ VET_Short4Norm,
+ VET_Ushort2Norm, /// unsigned shorts (normalized to 0..1)
+ VET_Ushort4Norm,
+};
+constexpr auto VET_NORM_BEGIN = VET_Byte4Norm;
+constexpr auto VET_NORM_END = VET_Ushort4Norm;
+
+int SizeOf(VertexElementType type);
+int VectorLenOf(VertexElementType type);
+GLenum FindGLType(VertexElementType type);
+bool IsNormalized(VertexElementType type);
+
+enum IndexType {
+ IT_16Bit,
+ IT_32Bit,
+};
+
+int SizeOf(IndexType type);
+GLenum FindGLType(IndexType type);
+
+enum TexFilter {
+ // clang-format off
+ BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics)
+ // clang-format on
+
+ TF_Linear,
+ TF_Nearest,
+};
+
+std::string_view GLTypeToString(GLenum);
+GLenum GLTypeFromString(std::string_view name);
+
+constexpr auto kInvalidLocation = std::numeric_limits<GLuint>::max();
+} // namespace Tags
+
+#include <generated/GraphicsTags.gh.inl>
diff --git a/source/30-game/Image.cpp b/source/30-game/Image.cpp
new file mode 100644
index 0000000..3673acc
--- /dev/null
+++ b/source/30-game/Image.cpp
@@ -0,0 +1,101 @@
+#include "Image.hpp"
+
+#include <stb_image.h>
+#include <stb_rect_pack.h>
+#include <cstring>
+
+Image::Image()
+ : mSize{}
+ , mChannels{ 0 } {
+}
+
+bool Image::InitFromImageFile(const char* filePath, int desiredChannels) {
+ // Dimensions of the image in
+ int width, height;
+ // Number of channels that the image has, we'll get `desiredChannels` channels in our output (if it's non-0, which is the default argument)
+ int channels;
+
+ // NOTE: don't free, the data is passed to std::unique_ptr
+ auto result = (uint8_t*)stbi_load(filePath, &width, &height, &channels, desiredChannels);
+ if (!result) {
+ return false;
+ }
+
+ mData.reset(result);
+ mSize = { width, height };
+ mChannels = desiredChannels == 0 ? channels : desiredChannels;
+ return true;
+}
+
+bool Image::InitFromImageData(std::span<uint8_t> data, int desiredChannels) {
+ int width, height;
+ int channels;
+
+ // NOTE: don't free, the data is passed to std::unique_ptr
+ auto result = (uint8_t*)stbi_load_from_memory(data.data(), data.size(), &width, &height, &channels, desiredChannels);
+ if (!result) {
+ return false;
+ }
+
+ mData.reset(result);
+ mSize = { width, height };
+ mChannels = desiredChannels == 0 ? channels : desiredChannels;
+ return true;
+}
+
+bool Image::InitFromPixels(std::span<uint8_t> pixels, glm::ivec2 dimensions, int channels) {
+ mData = std::make_unique<uint8_t[]>(pixels.size());
+ std::memcpy(mData.get(), pixels.data(), pixels.size());
+ mSize = dimensions;
+ mChannels = channels;
+ return true;
+}
+
+bool Image::InitFromPixels(std::unique_ptr<uint8_t[]> pixels, glm::ivec2 dimensions, int channels) {
+ mData = std::move(pixels);
+ mSize = dimensions;
+ mChannels = channels;
+ return true;
+}
+
+RgbaColor Image::GetPixel(int x, int y) const {
+ size_t offset = (y * mSize.x + x) * mChannels;
+ RgbaColor color;
+ color.r = mData.get()[offset + 0];
+ color.g = mData.get()[offset + 1];
+ color.b = mData.get()[offset + 2];
+ color.a = mData.get()[offset + 3];
+ return color;
+}
+
+void Image::SetPixel(int x, int y, RgbaColor color) {
+ size_t offset = (y * mSize.x + x) * mChannels;
+ mData.get()[offset + 0] = color.r;
+ mData.get()[offset + 1] = color.g;
+ mData.get()[offset + 2] = color.b;
+ mData.get()[offset + 3] = color.a;
+}
+
+uint8_t* Image::GetDataPtr() const {
+ return mData.get();
+}
+
+size_t Image::GetDataLength() const {
+ return mSize.x * mSize.y * mChannels * sizeof(uint8_t);
+}
+
+std::span<uint8_t> Image::GetData() const {
+ return { mData.get(), GetDataLength() };
+}
+
+glm::ivec2 Image::GetSize() const {
+ return mSize;
+}
+
+int Image::GetChannels() const {
+ return mChannels;
+}
+
+bool Image::IsEmpty() const {
+ return mSize.x == 0 || mSize.y == 0;
+}
diff --git a/source/30-game/Image.hpp b/source/30-game/Image.hpp
new file mode 100644
index 0000000..c577c24
--- /dev/null
+++ b/source/30-game/Image.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "Color.hpp"
+#include "RcPtr.hpp"
+
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <memory>
+#include <span>
+
+/// Image is a 2d array of pixels, stored as a continuous array in memory, with the first pixel
+/// being the top-left pixel. If a vertically flipped image data is needed, load using stb_image
+/// yourself, or flip the data here.
+class Image : public RefCounted {
+private:
+ std::unique_ptr<uint8_t[]> mData;
+ glm::ivec2 mSize;
+ int mChannels;
+
+public:
+ Image();
+
+ bool InitFromImageFile(const char* filePath, int desiredChannels = 0);
+ bool InitFromImageData(std::span<uint8_t> data, int desiredChannels = 0);
+ bool InitFromPixels(std::span<uint8_t> pixels, glm::ivec2 dimensions, int channels);
+ bool InitFromPixels(std::unique_ptr<uint8_t[]> pixels, glm::ivec2 dimensions, int channels);
+
+ /// Get the pixel at the given location.
+ RgbaColor GetPixel(int x, int y) const;
+ void SetPixel(int x, int y, RgbaColor color);
+
+ uint8_t* GetDataPtr() const;
+ size_t GetDataLength() const;
+ std::span<uint8_t> GetData() const;
+ glm::ivec2 GetSize() const;
+ int GetChannels() const;
+ bool IsEmpty() const;
+};
diff --git a/source/30-game/Input.cpp b/source/30-game/Input.cpp
new file mode 100644
index 0000000..9f304ff
--- /dev/null
+++ b/source/30-game/Input.cpp
@@ -0,0 +1,23 @@
+#include "Input.hpp"
+
+#include <cassert>
+
+GLFWkeyboard* InputState::FindKeyboard(std::string_view name) {
+ assert(false);
+ // TODO
+}
+
+GlfwKeyboardAttachment* InputState::ConnectKeyboard(GLFWkeyboard* keyboard) {
+ auto attachment = new GlfwKeyboardAttachment();
+ glfwSetKeyboardUserPointer(keyboard, attachment);
+
+ return attachment;
+}
+
+void InputState::DisconnectKeyboard(GLFWkeyboard* keyboard) {
+ InputState::instance->DisconnectKeyboard(keyboard);
+ auto attachment = static_cast<GlfwKeyboardAttachment*>(glfwGetKeyboardUserPointer(keyboard));
+ // Defensive: this GLFWkeyboard* will be deleted after this callback ends anyways
+ glfwSetKeyboardUserPointer(keyboard, nullptr);
+ delete attachment;
+}
diff --git a/source/30-game/Input.hpp b/source/30-game/Input.hpp
new file mode 100644
index 0000000..feb50f0
--- /dev/null
+++ b/source/30-game/Input.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <string_view>
+#include <utility>
+#include <vector>
+
+/// Extra data attached to a GLFWkeyboard object
+struct GlfwKeyboardAttachment {
+};
+
+class InputState {
+public:
+ static inline InputState* instance = nullptr;
+
+public:
+ GLFWkeyboard* FindKeyboard(std::string_view name);
+
+ GlfwKeyboardAttachment* ConnectKeyboard(GLFWkeyboard* keyboard);
+ void DisconnectKeyboard(GLFWkeyboard* keyboard);
+};
diff --git a/source/30-game/Ires.cpp b/source/30-game/Ires.cpp
new file mode 100644
index 0000000..0529395
--- /dev/null
+++ b/source/30-game/Ires.cpp
@@ -0,0 +1,436 @@
+#include "Ires.hpp"
+
+#include "AppConfig.hpp"
+#include "EditorCore.hpp"
+#include "EditorUtils.hpp"
+#include "Material.hpp"
+#include "Shader.hpp"
+#include "Sprite.hpp"
+#include "Texture.hpp"
+
+#include <Macros.hpp>
+#include <Metadata.hpp>
+#include <RapidJsonHelper.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <imgui.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <rapidjson/document.h>
+#include <rapidjson/filereadstream.h>
+#include <rapidjson/filewritestream.h>
+#include <rapidjson/prettywriter.h>
+#include <rapidjson/writer.h>
+#include <algorithm>
+#include <cassert>
+
+namespace fs = std::filesystem;
+using namespace std::literals;
+
+IresObject::IresObject(Kind kind)
+ : mKind{ kind } {
+}
+
+std::unique_ptr<IresObject> IresObject::Create(Kind kind) {
+ switch (kind) {
+ case KD_Texture: return std::make_unique<IresTexture>();
+ case KD_Shader: return std::make_unique<IresShader>();
+ case KD_Material: return std::make_unique<IresMaterial>();
+ case KD_SpriteFiles: return std::make_unique<IresSpriteFiles>();
+ case KD_Spritesheet: return std::make_unique<IresSpritesheet>();
+ case KD_COUNT: break;
+ }
+ return nullptr;
+}
+
+bool IresObject::IsAnnoymous() const {
+ return mName.empty();
+}
+
+void IresObject::SetName(std::string name) {
+ if (mMan) {
+ mMan->Rename(this, std::move(name));
+ } else {
+ mName = std::move(name);
+ }
+}
+
+void IresObject::ShowNameSafe(IresObject* ires) {
+ if (ires) {
+ ires->ShowName();
+ } else {
+ ShowNameNull();
+ }
+}
+
+void IresObject::ShowNameNull() {
+ ImGui::Text("<null>");
+}
+
+void IresObject::ShowName() const {
+ if (IsAnnoymous()) {
+ ImGui::Text("<annoymous %p>", (void*)this);
+ } else {
+ ImGui::Text("%s", mName.c_str());
+ }
+}
+
+void IresObject::ShowReferenceSafe(IEditor& editor, IresObject* ires) {
+ if (ires) {
+ ires->ShowReference(editor);
+ } else {
+ ShowReferenceNull(editor);
+ }
+}
+
+void IresObject::ShowReferenceNull(IEditor& editor) {
+ ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]);
+ ImGui::Text("<null>");
+ ImGui::PopStyleColor();
+ ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_Button]);
+}
+
+void IresObject::ShowReference(IEditor& editor) {
+ ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]);
+ if (IsAnnoymous()) {
+ ImGui::Text("<annoymous %p>", (void*)this);
+ } else {
+ ImGui::Text("%s", mName.c_str());
+ }
+ ImGui::PopStyleColor();
+ if (ImGui::IsItemHovered()) {
+ if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
+ editor.GetInspector().SelectTarget(IEditorInspector::ITT_Ires, this);
+ }
+ ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]);
+ } else {
+ ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_Button]);
+ }
+}
+
+void IresObject::ShowEditor(IEditor& editor) {
+ ImGui::Text("%.*s", PRINTF_STRING_VIEW(Metadata::EnumToString(mKind)));
+
+ bool isAnnoymous = mName.empty();
+ if (isAnnoymous) {
+ ImGui::Text("Name: <annoymous: %p>", (void*)this);
+ } else {
+ ImGui::Text("Name: %s", mName.c_str());
+ }
+
+ if (mUid.IsNull()) {
+ ImGui::TextUnformatted("Uid: <none>");
+ } else {
+ ImGui::Text("Uid: %lx-%lx", mUid.upper, mUid.lower);
+ }
+}
+
+void IresObject::WriteFull(IresWritingContext& ctx, IresObject* ires, rapidjson::Value& value, rapidjson::Document& root) {
+ rapidjson::Value rvIres(rapidjson::kObjectType);
+ ires->Write(ctx, rvIres, root);
+
+ value.AddMember("Type", rapidjson::StringRef(Metadata::EnumToString(ires->GetKind())), root.GetAllocator());
+ value.AddMember("Uid", ires->mUid.Write(root), root.GetAllocator());
+ value.AddMember("Value", rvIres, root.GetAllocator());
+}
+
+std::unique_ptr<IresObject> IresObject::ReadFull(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ auto ires = ReadBasic(value);
+ if (!ires) {
+ return nullptr;
+ }
+
+ if (!ReadPartial(ctx, ires.get(), value)) {
+ return nullptr;
+ }
+
+ return ires;
+}
+
+std::unique_ptr<IresObject> IresObject::ReadBasic(const rapidjson::Value& value) {
+ std::unique_ptr<IresObject> ires;
+ Uid uid;
+ for (auto it = value.MemberBegin(); it != value.MemberEnd(); ++it) {
+ if (it->name == "Type"_rj_sv) {
+ if (it->value.GetType() != rapidjson::kStringType)
+ return nullptr;
+ auto kind = Metadata::EnumFromString<Kind>(rapidjson::AsStringView(it->value));
+ if (!kind.has_value())
+ return nullptr;
+ if ((ires = Create(*kind)) == nullptr)
+ return nullptr;
+ } else if (it->name == "Uid"_rj_sv) {
+ // FIXME this needs to do type checks
+ uid.Read(it->value);
+ }
+ }
+ if (ires == nullptr || uid.IsNull())
+ return nullptr;
+
+ ires->mUid = uid;
+ return ires;
+}
+
+bool IresObject::ReadPartial(IresLoadingContext& ctx, IresObject* ires, const rapidjson::Value& value) {
+ auto rvValue = rapidjson::GetProperty(value, "Value"sv);
+ if (!rvValue) return false;
+ ires->Read(ctx, *rvValue);
+ return true;
+}
+
+void IresObject::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+}
+
+void IresObject::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+}
+
+void IresManager::DiscoverFilesDesignatedLocation() {
+ auto path = AppConfig::assetDirPath / "Ires";
+ DiscoverFiles(path);
+}
+
+void IresManager::DiscoverFiles(const fs::path& dir) {
+ struct LoadCandidate {
+ fs::path path;
+ rapidjson::Document data;
+ RcPtr<IresObject> ires;
+ };
+
+ class IresLoadTimeContext final : public IresLoadingContext {
+ public:
+ // NOTE: pointer stability required
+ robin_hood::unordered_node_map<Uid, LoadCandidate> candidates;
+ std::vector<std::vector<LoadCandidate*>> candidatesByKind;
+
+ IresLoadTimeContext() {
+ candidatesByKind.resize((int)IresObject::KD_COUNT);
+ }
+
+ std::vector<LoadCandidate*>& GetByKind(IresObject::Kind kind) {
+ int i = static_cast<int>(kind);
+ return candidatesByKind[i];
+ }
+
+ virtual IresObject* FindIres(const Uid& uid) const override {
+ auto iter = candidates.find(uid);
+ if (iter != candidates.end()) {
+ auto& cand = iter->second;
+ return cand.ires.Get();
+ } else {
+ return nullptr;
+ }
+ }
+ } ctx;
+
+ for (auto& item : fs::directory_iterator(dir)) {
+ if (!item.is_regular_file()) {
+ continue;
+ }
+ if (item.path().extension() != ".json") {
+ continue;
+ }
+
+ auto file = Utils::OpenCstdioFile(item.path(), Utils::Read);
+ if (!file) {
+ fprintf(stderr, "Ires file [" PLATFORM_PATH_STR "] Failed to open file.", item.path().c_str());
+ continue;
+ }
+ DEFER {
+ fclose(file);
+ };
+
+ char readerBuffer[65536];
+ rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer));
+
+ rapidjson::Document root;
+ root.ParseStream(stream);
+
+ auto ires = IresObject::ReadBasic(root);
+ if (!ires) {
+ fprintf(stderr, "Ires file: [" PLATFORM_PATH_STR "] Failed to parse header.\n", item.path().c_str());
+ continue;
+ }
+
+ // Load name from filename
+ ires->mName = item.path().filename().replace_extension().string();
+ auto& iresName = ires->mName;
+
+ // Load uid should be handled by IresObject::ReadBasic
+ assert(!ires->mUid.IsNull());
+ auto iresUid = ires->GetUid();
+
+ auto iresKind = ires->GetKind();
+
+ auto&& [iter, DISCARD] = ctx.candidates.try_emplace(
+ iresUid,
+ LoadCandidate{
+ .path = item.path(),
+ .data = std::move(root),
+ .ires = RcPtr(ires.release()),
+ });
+ auto& cand = iter->second;
+
+ ctx.GetByKind(iresKind).push_back(&cand);
+ }
+
+ // Load Ires in order by type, there are dependencies between them
+ // TODO full arbitary dependency between Ires
+ for (int i = 0; i < (int)IresObject::KD_COUNT; ++i) {
+ auto kind = static_cast<IresObject::Kind>(i);
+ auto& list = ctx.GetByKind(kind);
+ for (auto cand : list) {
+ auto& ires = cand->ires;
+
+ if (!IresObject::ReadPartial(ctx, ires.Get(), cand->data)) {
+ fprintf(stderr, "Ires file: [" PLATFORM_PATH_STR "] Failed to parse object data.\n", cand->path.c_str());
+ continue;
+ }
+
+ ires->mMan = this;
+ mObjByUid.try_emplace(ires->GetUid(), ires);
+ }
+ }
+}
+
+std::pair<IresObject*, bool> IresManager::Add(IresObject* ires) {
+ auto& name = ires->mName;
+ if (name.empty()) {
+ name = Utils::MakeRandomNumberedName(Metadata::EnumToString(ires->GetKind()).data());
+ }
+
+ auto& uid = ires->mUid;
+ if (uid.IsNull()) {
+ uid = Uid::Create();
+ }
+
+ auto [iter, inserted] = mObjByUid.try_emplace(uid, ires);
+ if (inserted) {
+ ires->mMan = this;
+ // TODO handle full path
+ return { ires, true };
+ } else {
+ return { iter->second.Get(), false };
+ }
+}
+
+IresObject* IresManager::Load(const fs::path& filePath) {
+ auto file = Utils::OpenCstdioFile(filePath, Utils::Read);
+ if (!file) return nullptr;
+ DEFER {
+ fclose(file);
+ };
+
+ char readerBuffer[65536];
+ rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer));
+
+ rapidjson::Document root;
+ root.ParseStream(stream);
+
+ auto ires = IresObject::ReadFull(*this, root);
+ if (!ires) {
+ return nullptr;
+ }
+
+ // Load uid should be handled by IresObject::ReadFull
+ assert(!ires->mUid.IsNull());
+ // Load name from filename
+ ires->mName = filePath.filename().replace_extension().string();
+ Add(ires.get());
+
+ return ires.release();
+}
+
+static fs::path GetDesignatedPath(IresObject* ires) {
+ return AppConfig::assetDirPath / "Ires" / fs::path(ires->GetName()).replace_extension(".json");
+}
+
+void IresManager::Delete(IresObject* ires) {
+ // TODO
+}
+
+bool IresManager::Rename(IresObject* ires, std::string newName) {
+ auto oldPath = GetDesignatedPath(ires);
+ ires->mName = std::move(newName);
+ auto newPath = GetDesignatedPath(ires);
+ if (fs::exists(oldPath)) {
+ fs::rename(oldPath, newPath);
+ }
+
+ // TODO validate no name duplication
+#if 0
+ if (mObjByPath.contains(newName)) {
+ return false;
+ }
+
+ // Keep the material from being deleted, in case the old entry in map is the only one existing
+ RcPtr rc(ires);
+
+ // Remove old entry (must do before replacing Material::mName, because the std::string_view in the map is a reference to it)
+ mObjByPath.erase(ires->GetName());
+
+ // Add new entry
+ ires->mName = std::move(newName);
+ // TODO handle full path
+ mObjByPath.try_emplace(ires->GetName(), ires);
+#endif
+ return true;
+}
+
+void IresManager::Reload(IresObject* ires) {
+ auto file = Utils::OpenCstdioFile(GetDesignatedPath(ires), Utils::Read);
+ if (!file) return;
+ DEFER {
+ fclose(file);
+ };
+
+ char readerBuffer[65536];
+ rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer));
+
+ rapidjson::Document root;
+ root.ParseStream(stream);
+
+ IresObject::ReadPartial(*this, ires, root);
+}
+
+void IresManager::Save(IresObject* ires) {
+ Save(ires, GetDesignatedPath(ires));
+}
+
+void IresManager::Save(IresObject* ires, const fs::path& filePath) {
+ rapidjson::Document root(rapidjson::kObjectType);
+
+ IresObject::WriteFull(*this, ires, root, root);
+
+ auto file = Utils::OpenCstdioFile(filePath, Utils::WriteTruncate);
+ if (!file) return;
+ DEFER {
+ fclose(file);
+ };
+
+ char writerBuffer[65536];
+ rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer));
+ rapidjson::PrettyWriter<rapidjson::FileWriteStream> writer(stream);
+ // We no longer need this after disabling BRUSSEL_Uid_WRITE_USE_ARRAY
+// writer.SetFormatOptions(rapidjson::PrettyFormatOptions::kFormatSingleLineArray);
+#if BRUSSEL_Uid_WRITE_USE_ARRAY
+# warning "Writing Uid in array format but single line formatting isn't enabled, this might cause excessively long ires files."
+#endif
+ root.Accept(writer);
+}
+
+void IresManager::OverwriteAllToDisk() {
+ for (const auto& [DISCARD, ires] : mObjByUid) {
+ Save(ires.Get());
+ }
+}
+
+IresObject* IresManager::FindIres(const Uid& uid) const {
+ auto iter = mObjByUid.find(uid);
+ if (iter != mObjByUid.end()) {
+ return iter->second.Get();
+ } else {
+ return nullptr;
+ }
+}
+
+#include <generated/Ires.gs.inl>
diff --git a/source/30-game/Ires.hpp b/source/30-game/Ires.hpp
new file mode 100644
index 0000000..e2e79bd
--- /dev/null
+++ b/source/30-game/Ires.hpp
@@ -0,0 +1,130 @@
+#pragma once
+
+#include "EditorAttachment.hpp"
+#include "EditorCore.hpp"
+
+#include <MacrosCodegen.hpp>
+#include <RcPtr.hpp>
+#include <Uid.hpp>
+#include <Utils.hpp>
+
+#include <rapidjson/fwd.h>
+#include <robin_hood.h>
+#include <filesystem>
+#include <memory>
+#include <string_view>
+
+// Forward declarations
+class IresManager;
+class IresWritingContext;
+class IresLoadingContext;
+
+namespace Tags {
+enum class IresObjectKind {
+ // clang-format off
+ BRUSSEL_ENUM(ToString, FromString, RemovePrefix KD_, AddPrefix Ires, ExcludeHeuristics)
+ // clang-format on
+
+ KD_Texture,
+ KD_Shader,
+ KD_Material,
+ KD_SpriteFiles,
+ KD_Spritesheet,
+ KD_COUNT,
+};
+} // namespace Tags
+
+class IresObject : public RefCounted {
+ friend class IresManager;
+
+public:
+ using Kind = Tags::IresObjectKind;
+ using enum Tags::IresObjectKind;
+
+private:
+ std::string mName; // Serialized as filename
+ Uid mUid; // Serialized in full mode
+ std::unique_ptr<EditorAttachment> mEditorAttachment; // Transient
+ IresManager* mMan = nullptr; // Transient
+ Kind mKind; // Serialized in full mode
+
+public:
+ IresObject(Kind kind);
+ virtual ~IresObject() = default;
+
+ static std::unique_ptr<IresObject> Create(Kind kind);
+ Kind GetKind() const { return mKind; }
+
+ IresManager* GetAssociatedManager() const { return mMan; }
+ bool IsAnnoymous() const;
+ const std::string& GetName() const { return mName; }
+ void SetName(std::string name);
+ const Uid& GetUid() const { return mUid; }
+
+ static void ShowNameSafe(IresObject* ires);
+ static void ShowNameNull();
+ void ShowName() const;
+
+ static void ShowReferenceSafe(IEditor& editor, IresObject* ires);
+ static void ShowReferenceNull(IEditor& editor);
+ void ShowReference(IEditor& editor);
+
+ virtual void ShowEditor(IEditor& editor);
+
+ EditorAttachment* GetEditorAttachment() const { return mEditorAttachment.get(); }
+ void SetEditorAttachment(EditorAttachment* attachment) { mEditorAttachment.reset(attachment); }
+
+ static void WriteFull(IresWritingContext& ctx, IresObject* ires, rapidjson::Value& value, rapidjson::Document& root);
+
+ /// Sequentially call ReadBasic() and then ReadPartial()
+ static std::unique_ptr<IresObject> ReadFull(IresLoadingContext& ctx, const rapidjson::Value& value);
+ /// Reads the type and UID of the object from data, and return a newly constructed Ires object.
+ static std::unique_ptr<IresObject> ReadBasic(const rapidjson::Value& value);
+ /// Reads all object-speific data from the root-level data object into the given Ires object.
+ static bool ReadPartial(IresLoadingContext& ctx, IresObject* ires, const rapidjson::Value& value);
+
+ virtual void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const;
+ virtual void Read(IresLoadingContext& ctx, const rapidjson::Value& value);
+
+protected:
+ rapidjson::Value WriteIresRef(IresObject* object);
+};
+
+class IresWritingContext {
+public:
+ virtual ~IresWritingContext() = default;
+};
+
+class IresLoadingContext {
+public:
+ virtual ~IresLoadingContext() = default;
+ virtual IresObject* FindIres(const Uid& uid) const = 0;
+};
+
+class IresManager final : public IresWritingContext, public IresLoadingContext {
+public:
+ static inline IresManager* instance = nullptr;
+
+private:
+ robin_hood::unordered_map<Uid, RcPtr<IresObject>> mObjByUid;
+
+public:
+ void DiscoverFilesDesignatedLocation();
+ void DiscoverFiles(const std::filesystem::path& dir);
+
+ std::pair<IresObject*, bool> Add(IresObject* mat);
+ IresObject* Load(const std::filesystem::path& filePath);
+ void Delete(IresObject* ires);
+ bool Rename(IresObject* ires, std::string newName);
+
+ void Reload(IresObject* ires);
+ void Save(IresObject* ires);
+ void Save(IresObject* ires, const std::filesystem::path& filePath);
+
+ void OverwriteAllToDisk();
+
+ const auto& GetObjects() const { return mObjByUid; }
+ virtual IresObject* FindIres(const Uid& uid) const override;
+};
+
+#include <generated/Ires.gh.inl>
diff --git a/source/30-game/Level.cpp b/source/30-game/Level.cpp
new file mode 100644
index 0000000..076e5d5
--- /dev/null
+++ b/source/30-game/Level.cpp
@@ -0,0 +1,228 @@
+#include "Level.hpp"
+
+#include "AppConfig.hpp"
+
+#include <PodVector.hpp>
+#include <RapidJsonHelper.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <imgui.h>
+#include <rapidjson/document.h>
+#include <rapidjson/filereadstream.h>
+#include <rapidjson/filewritestream.h>
+#include <rapidjson/prettywriter.h>
+#include <cstdio>
+#include <filesystem>
+
+using namespace std::literals;
+namespace fs = std::filesystem;
+
+constexpr auto kParentToRootObject = std::numeric_limits<size_t>::max();
+constexpr auto kInvalidEntryId = std::numeric_limits<size_t>::max();
+
+struct Level::InstanciationEntry {
+ // If set to std::numeric_limits<size_t>::max(), this object is parented to the "root" provided when instanciating
+ size_t parentId;
+ rapidjson::Document data;
+};
+
+Level::Level() = default;
+
+Level::~Level() = default;
+
+void Level::Instanciate(GameObject* relRoot) const {
+ auto objectsLut = std::make_unique<GameObject*[]>(mEntries.size());
+ for (auto& entry : mEntries) {
+ GameObject* parent;
+ if (entry.parentId == kParentToRootObject) {
+ parent = relRoot;
+ } else {
+ parent = objectsLut[entry.parentId];
+ }
+
+ // TODO deser object
+ }
+}
+
+void Level::ShowInstanciationEntries(IEditor& editor) {
+ for (auto& entry : mEntries) {
+ // TODO
+ }
+}
+
+void LevelManager::DiscoverFilesDesignatedLocation() {
+ auto path = AppConfig::assetDirPath / "Levels";
+ DiscoverFiles(path);
+}
+
+void LevelManager::DiscoverFiles(const std::filesystem::path& dir) {
+ for (auto& item : fs::directory_iterator(dir)) {
+ auto& path = item.path();
+ if (!item.is_regular_file()) {
+ continue;
+ }
+ if (path.extension() != ".json") {
+ continue;
+ }
+
+ // Parse uid from filename, map key
+ Uid uid;
+ uid.ReadString(path.filename().string());
+
+ // Map value
+ LoadableObject obj;
+ obj.filePath = path;
+
+ mObjByUid.try_emplace(uid, std::move(obj));
+ }
+}
+
+Level* LevelManager::FindLevel(const Uid& uid) const {
+ auto iter = mObjByUid.find(uid);
+ if (iter != mObjByUid.end()) {
+ return iter->second.level.Get();
+ } else {
+ return nullptr;
+ }
+}
+
+#define BRUSSEL_DEF_LEVEL_NAME "New Level"
+#define BRUSSEL_DEF_LEVEL_DESC "No description."
+
+Level* LevelManager::LoadLevel(const Uid& uid) {
+ auto iter = mObjByUid.find(uid);
+ if (iter != mObjByUid.end()) {
+ auto& ldObj = iter->second;
+ if (ldObj.level != nullptr) {
+ auto file = Utils::OpenCstdioFile(ldObj.filePath, Utils::Read, false);
+ if (!file) {
+ fprintf(stderr, "Cannot open file level file %s that was discovered on game startup.", ldObj.filePath.string().c_str());
+ return nullptr;
+ }
+ DEFER { fclose(file); };
+
+ char readerBuffer[65536];
+ rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer));
+
+ rapidjson::Document root;
+ root.ParseStream(stream);
+
+ Level* level;
+ ldObj.level.Attach(level = new Level());
+
+ level->mMan = this;
+ level->mUid = uid;
+
+#if defined(BRUSSEL_DEV_ENV)
+ BRUSSEL_JSON_GET_DEFAULT(root, "Name", std::string, ldObj.name, BRUSSEL_DEF_LEVEL_NAME);
+ BRUSSEL_JSON_GET_DEFAULT(root, "Description", std::string, ldObj.description, BRUSSEL_DEF_LEVEL_DESC)
+#endif
+
+ auto rvEntries = rapidjson::GetProperty(root, rapidjson::kArrayType, "DataEntries"sv);
+ if (!rvEntries) return nullptr;
+ for (auto iter = rvEntries->Begin(); iter != rvEntries->End(); ++iter) {
+ Level::InstanciationEntry entry;
+
+ BRUSSEL_JSON_GET_DEFAULT(*iter, "ParentId", int, entry.parentId, kInvalidEntryId);
+
+ auto rvDataEntry = rapidjson::GetProperty(*iter, "Data"sv);
+ if (!rvDataEntry) return nullptr;
+ entry.data.CopyFrom(*iter, entry.data.GetAllocator());
+
+ level->mEntries.push_back(std::move(entry));
+ }
+ }
+ return ldObj.level.Get();
+ } else {
+ return nullptr;
+ }
+}
+
+void LevelManager::PrepareLevel(const Uid& uid) {
+ // TODO
+}
+
+LevelManager::LoadableObject& LevelManager::AddLevel(const Uid& uid) {
+ auto&& [iter, inserted] = mObjByUid.try_emplace(uid);
+ auto& ldObj = iter->second;
+ ldObj.level->mUid = uid;
+#if defined(BRUSSEL_DEV_ENV)
+ ldObj.name = BRUSSEL_DEF_LEVEL_NAME;
+ ldObj.description = BRUSSEL_DEF_LEVEL_DESC;
+#endif
+ return ldObj;
+}
+
+void LevelManager::SaveLevel(const Uid& uid) const {
+ auto iter = mObjByUid.find(uid);
+ if (iter == mObjByUid.end()) return;
+ auto& obj = iter->second;
+
+ SaveLevelImpl(obj, obj.filePath);
+}
+
+void LevelManager::SaveLevel(const Uid& uid, const std::filesystem::path& path) const {
+ auto iter = mObjByUid.find(uid);
+ if (iter == mObjByUid.end()) return;
+ auto& obj = iter->second;
+
+ SaveLevelImpl(obj, path);
+}
+
+void LevelManager::SaveLevelImpl(const LoadableObject& obj, const std::filesystem::path& path) const {
+ rapidjson::Document root;
+
+ // TODO
+
+ auto file = Utils::OpenCstdioFile(path, Utils::WriteTruncate);
+ if (!file) return;
+ DEFER { fclose(file); };
+
+ char writerBuffer[65536];
+ rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer));
+ rapidjson::Writer<rapidjson::FileWriteStream> writer(stream);
+ root.Accept(writer);
+}
+
+LevelWrapperObject::LevelWrapperObject(GameWorld* world)
+ : GameObject(KD_LevelWrapper, world) //
+{
+ mStopFreePropagation = true;
+}
+
+LevelWrapperObject::~LevelWrapperObject() {
+ // Destruction/freeing of this object is handled by our parent
+ for (auto child : GetChildren()) {
+ FreeRecursive(child);
+ }
+}
+
+void LevelWrapperObject::SetBoundLevel(Level* level) {
+ if (mLevel != level) {
+ mLevel.Attach(level);
+
+ // Cleanup old children
+ // TODO needs to Resleep()?
+ auto children = RemoveAllChildren();
+ for (auto child : children) {
+ FreeRecursive(child);
+ }
+ }
+
+ level->Instanciate(this);
+
+ PodVector<GameObject*> stack;
+ stack.push_back(this);
+
+ while (!stack.empty()) {
+ auto obj = stack.back();
+ stack.pop_back();
+
+ for (auto child : obj->GetChildren()) {
+ stack.push_back(child);
+ }
+
+ obj->Awaken();
+ }
+}
diff --git a/source/30-game/Level.hpp b/source/30-game/Level.hpp
new file mode 100644
index 0000000..c030b8e
--- /dev/null
+++ b/source/30-game/Level.hpp
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "EditorCore.hpp"
+#include "GameObject.hpp"
+
+#include <MacrosCodegen.hpp>
+#include <RcPtr.hpp>
+#include <Uid.hpp>
+
+#include <robin_hood.h>
+#include <filesystem>
+#include <vector>
+
+// Forward declarations
+class Level;
+class LevelManager;
+
+/// Represents a seralized GameObject tree.
+class Level : public RefCounted {
+ friend class LevelManager;
+
+private:
+ struct InstanciationEntry;
+
+ LevelManager* mMan;
+ Uid mUid;
+ std::vector<InstanciationEntry> mEntries;
+
+public:
+ Level();
+ ~Level();
+
+ void Instanciate(GameObject* relRoot) const;
+
+ LevelManager* GetLinkedLevelManager() const { return mMan; }
+ const Uid& GetUid() const { return mUid; }
+
+ // Editor stuff
+ void ShowInstanciationEntries(IEditor& editor);
+};
+
+class LevelManager {
+public:
+ static inline LevelManager* instance = nullptr;
+
+public: // NOTE: public for the editor; actual game components should not modify the map using this
+ // TODO maybe cut this struct to only the first RcPtr<Level> field in release mode?
+ struct LoadableObject {
+ RcPtr<Level> level; // TODO make weak pointer
+ std::filesystem::path filePath;
+ // NOTE: these fields are only loaded in dev mode
+ std::string name;
+ std::string description;
+
+ // Editor book keeping fields
+ bool edited = false;
+ };
+ // We want pointer stability here for the editor (inspector object)
+ robin_hood::unordered_node_map<Uid, LoadableObject> mObjByUid;
+
+public:
+ void DiscoverFilesDesignatedLocation();
+ void DiscoverFiles(const std::filesystem::path& dir);
+
+ Level* FindLevel(const Uid& uid) const;
+ /// Get or load the given level
+ Level* LoadLevel(const Uid& uid);
+ /// Send the given level to be loaded on another thread
+ void PrepareLevel(const Uid& uid);
+
+ /// Create and add a new level object with the given uid.
+ /// Should only be used by the editor.
+ LoadableObject& AddLevel(const Uid& uid);
+ /// Should only be used by the editor.
+ void SaveLevel(const Uid& uid) const;
+ /// Should only be used by the editor.
+ void SaveLevel(const Uid& uid, const std::filesystem::path& path) const;
+
+private:
+ void SaveLevelImpl(const LoadableObject& obj, const std::filesystem::path& path) const;
+};
+
+class LevelWrapperObject : public GameObject {
+ BRUSSEL_CLASS()
+
+private:
+ RcPtr<Level> mLevel;
+
+public:
+ LevelWrapperObject(GameWorld* world);
+ ~LevelWrapperObject() override;
+
+ Level* GetBoundLevel() const;
+ void SetBoundLevel(Level* level);
+};
diff --git a/source/30-game/Material.cpp b/source/30-game/Material.cpp
new file mode 100644
index 0000000..4443ae5
--- /dev/null
+++ b/source/30-game/Material.cpp
@@ -0,0 +1,526 @@
+#include "Material.hpp"
+
+#include "AppConfig.hpp"
+#include "EditorCore.hpp"
+#include "EditorUtils.hpp"
+
+#include <Metadata.hpp>
+#include <RapidJsonHelper.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <imgui.h>
+#include <rapidjson/document.h>
+#include <cstdlib>
+#include <cstring>
+#include <utility>
+
+using namespace std::literals;
+
+Material::Material() {
+}
+
+namespace ProjectBrussel_UNITY_ID {
+bool TryFindShaderId(Shader* shader, std::string_view name, int& out) {
+ auto& info = shader->GetInfo();
+ auto iter = info.things.find(name);
+ if (iter == info.things.end()) return false;
+ auto& id = iter->second;
+
+ if (id.kind != ShaderThingId::KD_Uniform) return false;
+
+ out = id.index;
+ return true;
+}
+
+template <typename TUniform>
+TUniform& ObtainUniform(Shader* shader, const char* name, std::vector<TUniform>& uniforms, GLint location) {
+ for (auto& uniform : uniforms) {
+ if (uniform.location == location) {
+ return uniform;
+ }
+ }
+
+ auto& uniform = uniforms.emplace_back();
+ uniform.location = location;
+ if (!TryFindShaderId(shader, name, uniform.infoUniformIndex)) {
+ uniform.infoUniformIndex = -1;
+ }
+
+ return uniform;
+}
+
+rapidjson::Value MakeVectorJson(const Material::VectorUniform& vector, rapidjson::Document& root) {
+ int len = vector.actualLength;
+
+ rapidjson::Value result(rapidjson::kArrayType);
+ result.Reserve(len, root.GetAllocator());
+
+ for (int i = 0; i < len; ++i) {
+ result.PushBack(vector.value[i], root.GetAllocator());
+ }
+
+ return result;
+}
+
+Material::VectorUniform ReadVectorFromJson(const rapidjson::Value& rv) {
+ assert(rv.IsArray());
+ Material::VectorUniform result;
+ int len = result.actualLength = rv.Size();
+ for (int i = 0; i < len; ++i) {
+ result.value[i] = rv[i].GetFloat();
+ }
+ return result;
+}
+
+rapidjson::Value MakeMatrixJson(const Material::MatrixUniform& matrix, rapidjson::Document& root) {
+ int w = matrix.actualWidth;
+ int h = matrix.actualHeight;
+
+ rapidjson::Value result(rapidjson::kArrayType);
+ result.Reserve(h, root.GetAllocator());
+
+ for (int y = 0; y < h; ++y) {
+ rapidjson::Value row(rapidjson::kArrayType);
+ row.Reserve(w, root.GetAllocator());
+
+ for (int x = 0; x < w; ++x) {
+ // Each item in a column is consecutive in memory in glm::mat<> structs
+ row.PushBack(matrix.value[x * h + y], root.GetAllocator());
+ }
+
+ result.PushBack(row, root.GetAllocator());
+ }
+
+ return result;
+}
+
+Material::MatrixUniform ReadMatrixFromjson(const rapidjson::Value& rv) {
+ assert(rv.IsArray());
+ assert(rv.Size() > 0);
+ assert(rv[0].IsArray());
+ Material::MatrixUniform result;
+ int w = result.actualWidth = rv[0].Size();
+ int h = result.actualHeight = rv.Size();
+ for (int y = 0; y < h; ++y) {
+ auto& row = rv[y];
+ assert(row.IsArray());
+ assert(row.Size() == w);
+ for (int x = 0; x < w; ++x) {
+ auto& val = row[x];
+ assert(val.IsNumber());
+ result.value[x * h + y] = val.GetFloat();
+ }
+ }
+ return result;
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+void Material::SetFloat(const char* name, float value) {
+ assert(IsValid());
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+ auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location);
+ uniform.floatValue = value;
+ uniform.actualType = GL_FLOAT;
+}
+
+void Material::SetInt(const char* name, int32_t value) {
+ assert(IsValid());
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+ auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location);
+ uniform.intValue = value;
+ uniform.actualType = GL_INT;
+}
+
+void Material::SetUInt(const char* name, uint32_t value) {
+ assert(IsValid());
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+ auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location);
+ uniform.uintValue = value;
+ uniform.actualType = GL_UNSIGNED_INT;
+}
+
+template <int length>
+void Material::SetVector(const char* name, const glm::vec<length, float>& vec) {
+ assert(IsValid());
+
+ static_assert(length >= 1 && length <= 4);
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+ auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundVectors, location);
+ uniform.actualLength = length;
+ std::memset(uniform.value, 0, sizeof(uniform.value));
+ std::memcpy(uniform.value, &vec[0], length * sizeof(float));
+}
+
+template void Material::SetVector<1>(const char*, const glm::vec<1, float>&);
+template void Material::SetVector<2>(const char*, const glm::vec<2, float>&);
+template void Material::SetVector<3>(const char*, const glm::vec<3, float>&);
+template void Material::SetVector<4>(const char*, const glm::vec<4, float>&);
+
+template <int width, int height>
+void Material::SetMatrix(const char* name, const glm::mat<width, height, float>& mat) {
+ static_assert(width >= 1 && width <= 4);
+ static_assert(height >= 1 && height <= 4);
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+ auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundMatrices, location);
+ uniform.actualWidth = width;
+ uniform.actualHeight = height;
+ std::memset(uniform.value, 0, sizeof(uniform.value));
+ std::memcpy(uniform.value, &mat[0][0], width * height * sizeof(float));
+}
+
+template void Material::SetMatrix<2, 2>(const char*, const glm::mat<2, 2, float>&);
+template void Material::SetMatrix<3, 3>(const char*, const glm::mat<3, 3, float>&);
+template void Material::SetMatrix<4, 4>(const char*, const glm::mat<4, 4, float>&);
+
+template void Material::SetMatrix<2, 3>(const char*, const glm::mat<2, 3, float>&);
+template void Material::SetMatrix<3, 2>(const char*, const glm::mat<3, 2, float>&);
+
+template void Material::SetMatrix<2, 4>(const char*, const glm::mat<2, 4, float>&);
+template void Material::SetMatrix<4, 2>(const char*, const glm::mat<4, 2, float>&);
+
+template void Material::SetMatrix<3, 4>(const char*, const glm::mat<3, 4, float>&);
+template void Material::SetMatrix<4, 3>(const char*, const glm::mat<4, 3, float>&);
+
+void Material::SetTexture(const char* name, Texture* texture) {
+ assert(IsValid());
+
+ GLint location = glGetUniformLocation(mShader->GetProgram(), name);
+
+ for (auto& uniform : mBoundTextures) {
+ if (uniform.location == location) {
+ uniform.value.Attach(texture);
+ return;
+ }
+ }
+
+ auto& uniform = mBoundTextures.emplace_back();
+ uniform.value.Attach(texture);
+ uniform.location = location;
+}
+
+std::span<const Material::VectorUniform> Material::GetVectors() const {
+ return mBoundVectors;
+}
+
+std::span<const Material::MatrixUniform> Material::GetMatrices() const {
+ return mBoundMatrices;
+}
+
+std::span<const Material::TextureUniform> Material::GetTextures() const {
+ return mBoundTextures;
+}
+
+Shader* Material::GetShader() const {
+ return mShader.Get();
+}
+
+void Material::SetShader(Shader* shader) {
+ mShader.Attach(shader);
+ auto& info = shader->GetInfo();
+
+ mBoundScalars.clear();
+ mBoundVectors.clear();
+ mBoundMatrices.clear();
+ mBoundTextures.clear();
+ for (int i = 0; i < info.uniforms.size(); ++i) {
+ auto& decl = info.uniforms[i];
+ switch (decl->kind) {
+ case ShaderVariable::KD_Math: {
+ auto& mathDecl = static_cast<ShaderMathVariable&>(*decl);
+ if (mathDecl.width == 1) {
+ if (mathDecl.height == 1) {
+ // Scalar
+ auto& scalar = mBoundScalars.emplace_back();
+ scalar.location = decl->location;
+ scalar.infoUniformIndex = i;
+ } else {
+ // Vector
+ auto& vec = mBoundVectors.emplace_back();
+ vec.location = decl->location;
+ vec.infoUniformIndex = i;
+ vec.actualLength = mathDecl.height;
+ }
+ } else {
+ // Matrix
+ auto& mat = mBoundMatrices.emplace_back();
+ mat.location = decl->location;
+ mat.infoUniformIndex = i;
+ mat.actualWidth = mathDecl.width;
+ mat.actualHeight = mathDecl.height;
+ }
+ } break;
+
+ case ShaderVariable::KD_Sampler: {
+ auto& uniform = mBoundTextures.emplace_back();
+ uniform.location = decl->location;
+ uniform.infoUniformIndex = i;
+ } break;
+ }
+ }
+}
+
+bool Material::IsValid() const {
+ return mShader != nullptr;
+}
+
+static constexpr int IdentifyMatrixSize(int width, int height) {
+ return width * 10 + height;
+}
+
+void Material::UseUniforms() const {
+ for (auto& uniform : mBoundScalars) {
+ switch (uniform.actualType) {
+ case GL_FLOAT: glUniform1f(uniform.location, uniform.intValue); break;
+ case GL_INT: glUniform1i(uniform.location, uniform.intValue); break;
+ case GL_UNSIGNED_INT: glUniform1ui(uniform.location, uniform.intValue); break;
+ default: break;
+ }
+ }
+
+ for (auto& uniform : mBoundVectors) {
+ switch (uniform.actualLength) {
+ case 1: glUniform1fv(uniform.location, 1, &uniform.value[0]); break;
+ case 2: glUniform2fv(uniform.location, 1, &uniform.value[0]); break;
+ case 3: glUniform3fv(uniform.location, 1, &uniform.value[0]); break;
+ case 4: glUniform4fv(uniform.location, 1, &uniform.value[0]); break;
+ default: break;
+ }
+ }
+
+ for (auto& uniform : mBoundMatrices) {
+ switch (IdentifyMatrixSize(uniform.actualWidth, uniform.actualHeight)) {
+ case IdentifyMatrixSize(2, 2): glUniformMatrix2fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+ case IdentifyMatrixSize(3, 3): glUniformMatrix3fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+ case IdentifyMatrixSize(4, 4): glUniformMatrix4fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+
+ case IdentifyMatrixSize(2, 3): glUniformMatrix2x3fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+ case IdentifyMatrixSize(3, 2): glUniformMatrix3x2fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+
+ case IdentifyMatrixSize(2, 4): glUniformMatrix2x4fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+ case IdentifyMatrixSize(4, 2): glUniformMatrix4x2fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+
+ case IdentifyMatrixSize(3, 4): glUniformMatrix3x4fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+ case IdentifyMatrixSize(4, 3): glUniformMatrix4x3fv(uniform.location, 1, GL_FALSE, uniform.value); break;
+
+ default: break;
+ }
+ }
+
+ int i = 0;
+ for (auto& uniform : mBoundTextures) {
+ glActiveTexture(GL_TEXTURE0 + i);
+ glBindTexture(GL_TEXTURE_2D, uniform.value->GetHandle());
+ glUniform1i(uniform.location, i);
+ ++i;
+ }
+}
+
+IresMaterial::IresMaterial()
+ : IresObject(KD_Material)
+ , mInstance(new Material()) {
+ mInstance->mIres = this;
+}
+
+Material* IresMaterial::GetInstance() const {
+ return mInstance.Get();
+}
+
+void IresMaterial::InvalidateInstance() {
+ if (mInstance != nullptr) {
+ mInstance->mIres = nullptr;
+ }
+ mInstance.Attach(new Material());
+ mInstance->mIres = this;
+}
+
+void IresMaterial::ShowEditor(IEditor& editor) {
+ using namespace Tags;
+
+ IresObject::ShowEditor(editor);
+
+ auto shader = mInstance->GetShader();
+ if (shader) {
+ shader->GetIres()->ShowReference(editor);
+ } else {
+ IresObject::ShowReferenceNull(editor);
+ }
+ if (ImGui::BeginDragDropTarget()) {
+ if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(KD_Shader).data())) {
+ auto shader = *static_cast<IresShader* const*>(payload->Data);
+ mInstance->SetShader(shader->GetInstance());
+ }
+ ImGui::EndDragDropTarget();
+ }
+
+ if (!shader) return;
+ auto& shaderInfo = shader->GetInfo();
+ auto shaderIres = shader->GetIres();
+
+ for (auto& field : mInstance->mBoundScalars) {
+ auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]);
+ decl.ShowInfo();
+
+ ImGui::Indent();
+ switch (decl.scalarType) {
+ case GL_FLOAT: ImGui::InputFloat("##", &field.floatValue); break;
+ case GL_INT: ImGui::InputInt("##", &field.intValue); break;
+ // TODO proper uint edit?
+ case GL_UNSIGNED_INT: ImGui::InputInt("##", (int32_t*)(&field.uintValue), 0, std::numeric_limits<int32_t>::max()); break;
+ default: ImGui::TextUnformatted("Unsupported scalar type"); break;
+ }
+ ImGui::Unindent();
+ }
+ for (auto& field : mInstance->mBoundVectors) {
+ auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]);
+ decl.ShowInfo();
+
+ ImGui::Indent();
+ switch (decl.semantic) {
+ case VES_Color1:
+ case VES_Color2: {
+ ImGui::ColorEdit4("##", field.value);
+ } break;
+
+ default: {
+ ImGui::InputFloat4("##", field.value);
+ } break;
+ }
+ ImGui::Unindent();
+ }
+ for (auto& field : mInstance->mBoundMatrices) {
+ auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]);
+ decl.ShowInfo();
+
+ // TODO
+ }
+ for (auto& field : mInstance->mBoundTextures) {
+ auto& decl = static_cast<ShaderSamplerVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]);
+ decl.ShowInfo();
+
+ // TODO
+ }
+}
+
+void IresMaterial::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ IresObject::Write(ctx, value, root);
+
+ if (!mInstance->IsValid()) {
+ return;
+ }
+
+ auto& shaderInfo = mInstance->mShader->GetInfo();
+ auto shaderUid = mInstance->mShader->GetIres()->GetUid();
+ value.AddMember("Shader", shaderUid.Write(root), root.GetAllocator());
+
+ rapidjson::Value fields(rapidjson::kArrayType);
+ for (auto& scalar : mInstance->mBoundScalars) {
+ rapidjson::Value rvField(rapidjson::kObjectType);
+ rvField.AddMember("Name", shaderInfo.uniforms[scalar.infoUniformIndex]->name, root.GetAllocator());
+ rvField.AddMember("Type", "Scalar", root.GetAllocator());
+ switch (scalar.actualType) {
+ case GL_FLOAT: rvField.AddMember("Value", scalar.floatValue, root.GetAllocator()); break;
+ case GL_INT: rvField.AddMember("Value", scalar.intValue, root.GetAllocator()); break;
+ case GL_UNSIGNED_INT: rvField.AddMember("Value", scalar.uintValue, root.GetAllocator()); break;
+ }
+ fields.PushBack(rvField, root.GetAllocator());
+ }
+ for (auto& vector : mInstance->mBoundVectors) {
+ rapidjson::Value rvField(rapidjson::kObjectType);
+ rvField.AddMember("Name", shaderInfo.uniforms[vector.infoUniformIndex]->name, root.GetAllocator());
+ rvField.AddMember("Type", "Vector", root.GetAllocator());
+ rvField.AddMember("Value", MakeVectorJson(vector, root).Move(), root.GetAllocator());
+ fields.PushBack(rvField, root.GetAllocator());
+ }
+ for (auto& matrix : mInstance->mBoundMatrices) {
+ rapidjson::Value rvField(rapidjson::kObjectType);
+ rvField.AddMember("Name", shaderInfo.uniforms[matrix.infoUniformIndex]->name, root.GetAllocator());
+ rvField.AddMember("Type", "Matrix", root.GetAllocator());
+ rvField.AddMember("Value", MakeMatrixJson(matrix, root).Move(), root.GetAllocator());
+ fields.PushBack(rvField, root.GetAllocator());
+ }
+ for (auto& texture : mInstance->mBoundTextures) {
+ // TODO
+ }
+ value.AddMember("Fields", fields, root.GetAllocator());
+}
+
+void IresMaterial::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ IresObject::Read(ctx, value);
+
+ {
+ auto rvShader = rapidjson::GetProperty(value, "Shader"sv);
+ if (!rvShader) return;
+
+ Uid uid;
+ uid.Read(*rvShader);
+
+ auto ires = ctx.FindIres(uid);
+ if (!ires) return;
+ if (ires->GetKind() != KD_Shader) return;
+ auto shader = static_cast<IresShader*>(ires);
+
+ mInstance->mShader.Attach(shader->GetInstance());
+ }
+ auto shader = mInstance->mShader.Get();
+ auto& shaderInfo = shader->GetInfo();
+
+ auto fields = rapidjson::GetProperty(value, rapidjson::kArrayType, "Fields"sv);
+ if (!fields) return;
+
+ for (auto& rvField : fields->GetArray()) {
+ if (!rvField.IsObject()) continue;
+
+ auto rvName = rapidjson::GetProperty(rvField, rapidjson::kStringType, "Name"sv);
+
+ auto rvType = rapidjson::GetProperty(rvField, rapidjson::kStringType, "Type"sv);
+ if (!rvType) continue;
+ auto type = rapidjson::AsStringView(*rvType);
+
+ auto rvValue = rapidjson::GetProperty(rvField, "Value"sv);
+
+ if (type == "Scalar"sv) {
+ Material::ScalarUniform uniform;
+ if (rvName) {
+ TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex);
+ uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location;
+ }
+ if (rvValue->IsFloat()) {
+ uniform.actualType = GL_FLOAT;
+ uniform.floatValue = rvValue->GetFloat();
+ } else if (rvValue->IsInt()) {
+ uniform.actualType = GL_INT;
+ uniform.intValue = rvValue->GetInt();
+ } else if (rvValue->IsUint()) {
+ uniform.actualType = GL_UNSIGNED_INT;
+ uniform.uintValue = rvValue->GetUint();
+ }
+ mInstance->mBoundScalars.push_back(std::move(uniform));
+ } else if (type == "Vector"sv) {
+ auto uniform = ReadVectorFromJson(*rvValue);
+ if (rvName) {
+ TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex);
+ uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location;
+ }
+ mInstance->mBoundVectors.push_back(std::move(uniform));
+ } else if (type == "Matrix"sv) {
+ auto uniform = ReadMatrixFromjson(*rvValue);
+ if (rvName) {
+ TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex);
+ uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location;
+ }
+ mInstance->mBoundMatrices.push_back(uniform);
+ } else if (type == "Texture"sv) {
+ // TODO
+ }
+ }
+}
diff --git a/source/30-game/Material.hpp b/source/30-game/Material.hpp
new file mode 100644
index 0000000..f1cd7dd
--- /dev/null
+++ b/source/30-game/Material.hpp
@@ -0,0 +1,125 @@
+#pragma once
+
+#include "Ires.hpp"
+#include "RcPtr.hpp"
+#include "Shader.hpp"
+#include "Texture.hpp"
+
+#include <glad/glad.h>
+#include <robin_hood.h>
+#include <cstddef>
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <memory>
+#include <span>
+#include <string_view>
+#include <vector>
+
+// Forward declarations
+class Material;
+class IresMaterial;
+
+class Material : public RefCounted {
+ friend class IresMaterial;
+
+public:
+ // NOTE: specialize between scalar vs matrix vs vector to save memory
+
+ enum UniformType : uint16_t {
+ UT_Scalar,
+ UT_Vector,
+ UT_Matrix,
+ };
+
+ struct UniformIndex {
+ UniformType type;
+ uint16_t index;
+ };
+
+ struct ScalarUniform {
+ union {
+ float floatValue;
+ int32_t intValue;
+ uint32_t uintValue;
+ };
+ GLenum actualType;
+ /* Transient */ int infoUniformIndex;
+ /* Transient */ GLint location;
+ };
+
+ struct VectorUniform {
+ float value[4];
+ int actualLength;
+ /* Transient */ int infoUniformIndex;
+ /* Transient */ GLint location;
+ };
+
+ struct MatrixUniform {
+ float value[16];
+ int actualWidth;
+ int actualHeight;
+ /* Transient */ int infoUniformIndex;
+ /* Transient */ GLint location;
+ };
+
+ struct TextureUniform {
+ RcPtr<Texture> value;
+ /* Transient */ int infoUniformIndex;
+ /* Transient */ GLint location;
+ };
+
+ IresMaterial* mIres = nullptr;
+ RcPtr<Shader> mShader;
+ std::vector<ScalarUniform> mBoundScalars;
+ std::vector<VectorUniform> mBoundVectors;
+ std::vector<MatrixUniform> mBoundMatrices;
+ std::vector<TextureUniform> mBoundTextures;
+
+public:
+ Material();
+
+ void SetFloat(const char* name, float value);
+ void SetInt(const char* name, int32_t value);
+ void SetUInt(const char* name, uint32_t value);
+
+ /// Instanciated for length == 1, 2, 3, 4
+ template <int length>
+ void SetVector(const char* name, const glm::vec<length, float>& vec);
+
+ /// Instanciated for sizes (2,2) (3,3) (4,4) (2,3) (3,2) (2,4) (4,2) (3,4) (4,3)
+ template <int width, int height>
+ void SetMatrix(const char* name, const glm::mat<width, height, float>& mat);
+
+ void SetTexture(const char* name, Texture* texture);
+
+ std::span<const VectorUniform> GetVectors() const;
+ std::span<const MatrixUniform> GetMatrices() const;
+ std::span<const TextureUniform> GetTextures() const;
+ Shader* GetShader() const;
+ void SetShader(Shader* shader);
+
+ IresMaterial* GetIres() const { return mIres; }
+
+ bool IsValid() const;
+
+ void UseUniforms() const;
+};
+
+// Initialized in main()
+inline RcPtr<Material> gDefaultMaterial;
+
+class IresMaterial : public IresObject {
+private:
+ RcPtr<Material> mInstance;
+
+public:
+ IresMaterial();
+
+ Material* GetInstance() const;
+ void InvalidateInstance();
+
+ void ShowEditor(IEditor& editor) override;
+
+ void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override;
+ void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override;
+};
diff --git a/source/30-game/Mesh.cpp b/source/30-game/Mesh.cpp
new file mode 100644
index 0000000..244e2e3
--- /dev/null
+++ b/source/30-game/Mesh.cpp
@@ -0,0 +1,54 @@
+#include "Mesh.hpp"
+
+#include <cstring>
+
+// StandardCpuMesh::StandardCpuMesh()
+// : mGpuMesh(new GpuMesh()) {
+// mGpuMesh->vertFormat = gVformatStandard;
+// mGpuMesh->vertBufBindings.SetBinding(0, new GpuVertexBuffer());
+// mGpuMesh->vertBufBindings.SetBinding(1, new GpuVertexBuffer());
+// mGpuMesh->indexBuf.Attach(new GpuIndexBuffer());
+// }
+
+// StandardCpuMesh::~StandardCpuMesh() {
+// delete mData;
+// }
+
+// void StandardCpuMesh::CreateCpuData() {
+// if (!mData) {
+// mData = new StandardCpuMeshData();
+// }
+// }
+
+// GpuVertexBuffer* StandardCpuMesh::GetPosBuffer() const {
+// return mGpuMesh->vertBufBindings.bindings[0].Get();
+// }
+
+// GpuVertexBuffer* StandardCpuMesh::GetExtraBuffer() const {
+// return mGpuMesh->vertBufBindings.bindings[1].Get();
+// }
+
+// bool StandardCpuMesh::UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex) {
+// if (mData) {
+// std::memcpy(&mData->vertPositions[startVertIndex], pos, count * sizeof(glm::vec3));
+// }
+// auto posBuf = GetPosBuffer();
+// glBindBuffer(GL_ARRAY_BUFFER, posBuf->handle);
+// glBufferSubData(GL_ARRAY_BUFFER, startVertIndex * mGpuMesh->vertFormat->vertexSize, count * sizeof(glm::vec3), pos);
+// return true;
+// }
+
+// bool StandardCpuMesh::UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex) {
+// if (!mData) return false;
+// // TODO
+// }
+
+// bool StandardCpuMesh::UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex) {
+// if (!mData) return false;
+// // TODO
+// }
+
+// bool StandardCpuMesh::UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex) {
+// if (!mData) return false;
+// // TODO
+// }
diff --git a/source/30-game/Mesh.hpp b/source/30-game/Mesh.hpp
new file mode 100644
index 0000000..f86fd55
--- /dev/null
+++ b/source/30-game/Mesh.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "Color.hpp"
+#include "VertexIndex.hpp"
+#include "PodVector.hpp"
+#include "RcPtr.hpp"
+
+#include <cstddef>
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <memory>
+
+struct StandardVertexExtra {
+ float u, v;
+ uint8_t r, g, b, a;
+};
+
+class StandardCpuMeshData {
+public:
+ PodVector<glm::vec3> vertPositions;
+ PodVector<StandardVertexExtra> vertExtra;
+ PodVector<uint32_t> index;
+ size_t vertexCount;
+ size_t triangleCount;
+};
+
+class StandardCpuMesh {
+// private:
+// StandardCpuMeshData* mData = nullptr;
+// RcPtr<GpuMesh> mGpuMesh;
+
+// public:
+// StandardCpuMesh();
+// ~StandardCpuMesh();
+
+// GpuVertexBuffer* GetPosBuffer() const;
+// GpuVertexBuffer* GetExtraBuffer() const;
+// GpuMesh* GetGpuMesh() const { return mGpuMesh.Get(); }
+
+// void CreateCpuData();
+// bool UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex);
+// bool UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex);
+// bool UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex);
+// bool UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex);
+};
diff --git a/source/30-game/Player.cpp b/source/30-game/Player.cpp
new file mode 100644
index 0000000..34c4549
--- /dev/null
+++ b/source/30-game/Player.cpp
@@ -0,0 +1,139 @@
+#include "Player.hpp"
+
+#include "AppConfig.hpp"
+#include "CommonVertexIndex.hpp"
+#include "ScopeGuard.hpp"
+#include "Utils.hpp"
+
+#include <cstdio>
+#include <cstdlib>
+
+// Keep the same number as # of fields in `struct {}` in PlayerKeyBinds
+constexpr int kPlayerKeyBindCount = 4;
+
+// Here be dragons: this treats consecutive fiels as an array, technically UB
+std::span<int> PlayerKeyBinds::GetKeyArray() {
+ return { &keyLeft, kPlayerKeyBindCount };
+}
+std::span<bool> PlayerKeyBinds::GetKeyStatusArray() {
+ return { &pressedLeft, kPlayerKeyBindCount };
+}
+
+Player::Player(GameWorld* world, int id)
+ : GameObject(KD_Player, world)
+ , mId{ id } {
+ renderObject.SetMaterial(gDefaultMaterial.Get());
+ renderObject.SetFormat(gVformatStandard.Get(), Tags::IT_16Bit);
+ renderObject.RebuildIfNecessary();
+}
+
+void Player::Awaken() {
+ LoadFromFile();
+}
+
+void Player::Resleep() {
+ SaveToFile();
+}
+
+void Player::Update() {
+ using namespace Tags;
+
+ if (keybinds.pressedLeft) {
+ }
+ if (keybinds.pressedRight) {
+ }
+
+ // TODO jump controller
+
+ // TODO attack controller
+
+ // TODO set default sprite to get rid of this check
+ if (sprite.GetDefinition()) {
+ int prevFrame = sprite.GetFrame();
+ sprite.PlayFrame();
+ int currFrame = sprite.GetFrame();
+ if (prevFrame != currFrame) {
+ uint16_t indices[6];
+ Index_U16::Assign(indices, 0);
+ renderObject.GetIndexBuffer()->Upload((const std::byte*)indices, IT_16Bit, std::size(indices));
+
+ Vertex_PTC vertices[4];
+ Vertex_PTC::Assign(vertices, Rect<float>{ GetPos(), sprite.GetDefinition()->GetBoundingBox() });
+ Vertex_PTC::Assign(vertices, 0.0f);
+ Vertex_PTC::Assign(vertices, RgbaColor(255, 255, 255));
+ Vertex_PTC::Assign(vertices, sprite.GetFrameSubregion());
+ renderObject.GetVertexBufferBindings().bindings[0]->Upload((const std::byte*)vertices, sizeof(vertices));
+ }
+ }
+}
+
+Material* Player::GetMaterial() const {
+ return renderObject.GetMaterial();
+}
+
+void Player::SetMaterial(Material* material) {
+ renderObject.SetMaterial(material);
+ renderObject.RebuildIfNecessary();
+}
+
+std::span<const RenderObject> Player::GetRenderObjects() const {
+ return { &renderObject, 1 };
+}
+
+void Player::HandleKeyInput(int key, int action) {
+ bool pressed;
+ if (action == GLFW_PRESS) {
+ pressed = true;
+ } else if (action == GLFW_REPEAT) {
+ return;
+ } else /* action == GLFW_RELEASE */ {
+ pressed = false;
+ }
+
+ for (int i = 0; i < kPlayerKeyBindCount; ++i) {
+ int kbKey = keybinds.GetKeyArray()[i];
+ bool& kbStatus = keybinds.GetKeyStatusArray()[i];
+
+ if (kbKey == key) {
+ kbStatus = pressed;
+ break;
+ }
+ }
+}
+
+#pragma push_macro("PLAYERKEYBINDS_DO_IO")
+#undef PLAYERKEYBINDS_DO_IO
+#define PLAYERKEYBINDS_DO_IO(function, fieldPrefix) \
+ function(file, "left=%d\n", fieldPrefix keybinds.keyLeft); \
+ function(file, "right=%d\n", fieldPrefix keybinds.keyRight); \
+ function(file, "jump=%d\n", fieldPrefix keybinds.keyJump); \
+ function(file, "attack=%d\n", fieldPrefix keybinds.keyAttack);
+
+static FILE* OpenPlayerConfigFile(Player* player, Utils::IoMode mode) {
+ char path[2048];
+ snprintf(path, sizeof(path), "%s/player%d.txt", AppConfig::dataDir.c_str(), player->GetId());
+
+ return Utils::OpenCstdioFile(path, mode);
+}
+
+bool Player::LoadFromFile() {
+ auto file = OpenPlayerConfigFile(this, Utils::Read);
+ if (!file) return false;
+ DEFER { fclose(file); };
+
+ // TODO input validation
+ PLAYERKEYBINDS_DO_IO(fscanf, &);
+
+ return true;
+}
+
+bool Player::SaveToFile() {
+ auto file = OpenPlayerConfigFile(this, Utils::WriteTruncate);
+ if (!file) return false;
+ DEFER { fclose(file); };
+
+ PLAYERKEYBINDS_DO_IO(fprintf, );
+
+ return true;
+}
+#pragma pop_macro("PLAYERKEYBINDS_DO_IO")
diff --git a/source/30-game/Player.hpp b/source/30-game/Player.hpp
new file mode 100644
index 0000000..5a6bab7
--- /dev/null
+++ b/source/30-game/Player.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "GameObject.hpp"
+#include "Material.hpp"
+#include "Sprite.hpp"
+
+#include <MacrosCodegen.hpp>
+#include <RcPtr.hpp>
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <span>
+#include <vector>
+
+struct PlayerKeyBinds {
+ int keyLeft = GLFW_KEY_A;
+ int keyRight = GLFW_KEY_D;
+ int keyJump = GLFW_KEY_SPACE;
+ int keyAttack = GLFW_KEY_J;
+
+ bool pressedLeft = 0;
+ bool pressedRight = 0;
+ bool pressedJump = 0;
+ bool pressedAttack = 0;
+
+ std::span<int> GetKeyArray();
+ std::span<bool> GetKeyStatusArray();
+};
+
+class Player : public GameObject {
+ BRUSSEL_CLASS()
+
+public:
+ std::vector<GLFWkeyboard*> boundKeyboards;
+ PlayerKeyBinds keybinds;
+ Sprite sprite;
+ RenderObject renderObject;
+ int mId;
+
+public:
+ Player(GameWorld* world, int id);
+
+ virtual void Awaken() override;
+ virtual void Resleep() override;
+ virtual void Update() override;
+
+ Material* GetMaterial() const;
+ void SetMaterial(Material* material);
+ virtual std::span<const RenderObject> GetRenderObjects() const override;
+
+ int GetId() const { return mId; }
+
+ void HandleKeyInput(int key, int action);
+
+ // File is designated by player ID
+ bool LoadFromFile();
+ bool SaveToFile();
+};
diff --git a/source/30-game/Renderer.cpp b/source/30-game/Renderer.cpp
new file mode 100644
index 0000000..0454efe
--- /dev/null
+++ b/source/30-game/Renderer.cpp
@@ -0,0 +1,256 @@
+#include "Renderer.hpp"
+
+#include "GameObject.hpp"
+
+#include <RapidJsonHelper.hpp>
+
+#include <rapidjson/document.h>
+#include <cassert>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/quaternion.hpp>
+#include <glm/gtx/quaternion.hpp>
+#include <string_view>
+
+using namespace std::literals;
+
+RenderObject::RenderObject()
+ : mVao{ 0 } {
+}
+
+RenderObject::~RenderObject() {
+ DeleteGLObjects();
+}
+
+GLuint RenderObject::GetGLVao() const {
+ return mVao;
+}
+
+void RenderObject::RebuildIfNecessary() {
+ if (mVao != 0) {
+ return;
+ }
+
+ assert(mIndexBuf != nullptr);
+ assert(mVertexFormat != nullptr);
+
+ glGenVertexArrays(1, &mVao);
+ glBindVertexArray(mVao);
+
+ auto& vBindings = mVertexBufBinding.bindings;
+ auto& shaderInfo = mMaterial->GetShader()->GetInfo();
+
+ // Setup vertex buffers
+ for (auto& elm : mVertexFormat->elements) {
+ assert(elm.bindingIndex < vBindings.size());
+ auto& buffer = vBindings[elm.bindingIndex];
+
+ int index = shaderInfo.FindInputLocation(elm.semantic);
+ if (index == -1) {
+ continue;
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, buffer->handle);
+ glEnableVertexAttribArray(index);
+ glVertexAttribPointer(
+ index,
+ Tags::VectorLenOf(elm.type),
+ Tags::FindGLType(elm.type),
+ Tags::IsNormalized(elm.type),
+ mVertexFormat->vertexSize,
+ (void*)(uintptr_t)elm.offset);
+ }
+
+ // Setup index buffer
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuf->handle);
+
+ glBindVertexArray(0);
+}
+
+void RenderObject::SetMaterial(Material* material) {
+ mMaterial.Attach(material);
+ DeleteGLObjects();
+}
+
+void RenderObject::UpdateIndexBuffer(GpuIndexBuffer* indexBuffer) {
+ mIndexBuf.Attach(indexBuffer);
+ DeleteGLObjects();
+}
+
+void RenderObject::UpdateVertexFormat(VertexFormat* vertexFormat) {
+ mVertexFormat.Attach(vertexFormat);
+ DeleteGLObjects();
+}
+
+void RenderObject::UpdateVertexBufferBindings(BufferBindings** bindingsOut) {
+ *bindingsOut = &mVertexBufBinding;
+ DeleteGLObjects();
+}
+
+void RenderObject::SetFormat(VertexFormat* vertexFormat, Tags::IndexType indexFormat) {
+ mIndexBuf.Attach(new GpuIndexBuffer());
+ mIndexBuf->indexType = indexFormat;
+ mIndexBuf->count = 0;
+
+ mVertexFormat.Attach(vertexFormat);
+ mVertexBufBinding.Clear();
+ for (auto& element : vertexFormat->elements) {
+ if (mVertexBufBinding.GetBinding(element.bindingIndex) == nullptr) {
+ mVertexBufBinding.SetBinding(element.bindingIndex, new GpuVertexBuffer());
+ }
+ }
+}
+
+void RenderObject::DeleteGLObjects() {
+ if (mVao != 0) {
+ glDeleteVertexArrays(1, &mVao);
+ mVao = 0;
+ }
+}
+
+Renderer::Renderer()
+ : binding_WireframeMaterial{ gDefaultMaterial } //
+{
+ mRenderOptions[RO_Shading] = true;
+ mRenderOptions[RO_Wireframe] = false;
+}
+
+void Renderer::LoadBindings(const rapidjson::Value& bindings) {
+ if (auto rvWireframe = rapidjson::GetProperty(bindings, "WireframeMaterial"sv)) {
+ Uid uidWireframe;
+ uidWireframe.Read(*rvWireframe);
+ // TODO don't assume
+ binding_WireframeMaterial.Attach(((IresMaterial*)IresManager::instance->FindIres(uidWireframe))->GetInstance());
+ }
+}
+
+void Renderer::SaveBindings(rapidjson::Value& into, rapidjson::Document& root) const {
+ if (auto ires = binding_WireframeMaterial->GetIres()) {
+ into.AddMember("WireframeMaterial", ires->GetUid().Write(root), root.GetAllocator());
+ }
+}
+
+void Renderer::BeginFrame(Camera& camera, float currentTime, float deltaTime) {
+ assert(mInsideFrame == false);
+ mInsideFrame = true;
+ mFrame.camera = &camera;
+ mFrame.matrixView = camera.CalcViewMatrix();
+ mFrame.matrixProj = camera.CalcProjectionMatrix();
+ mFrame.time = currentTime;
+ mFrame.deltaTime = deltaTime;
+}
+
+void Renderer::EndFrame() {
+ assert(mInsideFrame == true);
+ mInsideFrame = false;
+}
+
+void Renderer::Draw(const RenderObject* objects, const GameObject* gameObject, size_t count) {
+ using namespace Tags;
+
+ assert(mInsideFrame);
+
+ // Desired order: proj * view * (translate * rotate * scale) * vec
+ // <----- order of application <----- ^^^ input
+ glm::mat4 objectMatrix(1.0f);
+ objectMatrix = glm::translate(objectMatrix, gameObject->GetPos());
+ objectMatrix *= glm::toMat4(gameObject->GetRotation());
+ objectMatrix = glm::scale(objectMatrix, gameObject->GetScale());
+ auto mvpMatrix = mFrame.matrixProj * mFrame.matrixView * objectMatrix;
+
+ if (GetRenderOption(RO_Shading)) {
+ // TODO shader grouping
+ // TODO material grouping
+ for (size_t i = 0; i < count; ++i) {
+ auto& object = objects[i];
+ auto indexBuffer = object.GetIndexBuffer();
+ auto mat = object.GetMaterial();
+ auto shader = mat->GetShader();
+
+ glUseProgram(shader->GetProgram());
+
+ // Material uniforms
+ mat->UseUniforms();
+
+ // Next available texture unit ID after all material textures
+ int texIdx = mat->GetTextures().size();
+
+ // Autofill uniforms
+ if (shader->autofill_Transform != kInvalidLocation) {
+ glUniformMatrix4fv(shader->autofill_Transform, 1, GL_FALSE, &mvpMatrix[0][0]);
+ }
+ if (shader->autofill_Time != kInvalidLocation) {
+ glUniform1f(shader->autofill_Time, mFrame.time);
+ }
+ if (shader->autofill_DeltaTime != kInvalidLocation) {
+ glUniform1f(shader->autofill_DeltaTime, mFrame.deltaTime);
+ }
+ if (shader->autofill_TextureAtlas != kInvalidLocation &&
+ object.autofill_TextureAtlas != nullptr)
+ {
+ glActiveTexture(GL_TEXTURE0 + texIdx);
+ glBindTexture(GL_TEXTURE_2D, object.autofill_TextureAtlas->GetHandle());
+ glUniform1i(shader->autofill_TextureAtlas, texIdx);
+ ++texIdx;
+ }
+
+ glBindVertexArray(object.GetGLVao());
+ glDrawElements(GL_TRIANGLES, indexBuffer->count, indexBuffer->GetIndexTypeGL(), 0);
+ }
+ }
+
+ if (GetRenderOption(RO_Wireframe)) {
+ auto& mat = *binding_WireframeMaterial;
+ auto& shader = *mat.GetShader();
+ auto& shaderInfo = shader.GetInfo();
+
+ glUseProgram(shader.GetProgram());
+ mat.UseUniforms();
+
+ // TODO reduce calls with consecutive wireframe setting
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ for (size_t i = 0; i < count; ++i) {
+ auto& object = objects[i];
+
+ auto& vBindings = object.GetVertexBufferBindings().bindings;
+ auto vf = object.GetVertexFormat();
+
+ // Setup vertex buffers
+ for (auto& elm : vf->elements) {
+ assert(elm.bindingIndex < vBindings.size());
+ auto& buffer = vBindings[elm.bindingIndex];
+
+ int index = shaderInfo.FindInputLocation(elm.semantic);
+ if (index == -1) {
+ continue;
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, buffer->handle);
+ glEnableVertexAttribArray(index);
+ glVertexAttribPointer(
+ index,
+ Tags::VectorLenOf(elm.type),
+ Tags::FindGLType(elm.type),
+ Tags::IsNormalized(elm.type),
+ vf->vertexSize,
+ (void*)(uintptr_t)elm.offset);
+ }
+
+ // Setup index buffer
+ auto indexBuffer = object.GetIndexBuffer();
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer->handle);
+
+ glDrawElements(GL_TRIANGLES, indexBuffer->count, indexBuffer->GetIndexTypeGL(), 0);
+ }
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ return;
+ }
+}
+
+bool Renderer::GetRenderOption(RenderOption option) const {
+ return mRenderOptions[option];
+}
+
+void Renderer::SetRenderOption(RenderOption option, bool flag) {
+ mRenderOptions[option] = flag;
+}
diff --git a/source/30-game/Renderer.hpp b/source/30-game/Renderer.hpp
new file mode 100644
index 0000000..856dc31
--- /dev/null
+++ b/source/30-game/Renderer.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include "Camera.hpp"
+#include "Material.hpp"
+#include "VertexIndex.hpp"
+
+#include <RcPtr.hpp>
+
+#include <glad/glad.h>
+#include <rapidjson/fwd.h>
+#include <cstddef>
+#include <glm/glm.hpp>
+
+// TODO add optional support for OpenGL separate attrib binding & only depend on vertex format
+
+class GameObject;
+
+class RenderObject {
+public:
+ RcPtr<Texture> autofill_TextureAtlas;
+
+private:
+ RcPtr<Material> mMaterial;
+ RcPtr<GpuIndexBuffer> mIndexBuf;
+ RcPtr<VertexFormat> mVertexFormat;
+ BufferBindings mVertexBufBinding;
+ GLuint mVao;
+
+public:
+ RenderObject();
+ ~RenderObject();
+
+ GLuint GetGLVao() const;
+ void RebuildIfNecessary();
+
+ Material* GetMaterial() const { return mMaterial.Get(); }
+ void SetMaterial(Material* material);
+
+ GpuIndexBuffer* GetIndexBuffer() const { return mIndexBuf.Get(); }
+ const VertexFormat* GetVertexFormat() const { return mVertexFormat.Get(); }
+ const BufferBindings& GetVertexBufferBindings() const { return mVertexBufBinding; }
+ void UpdateIndexBuffer(GpuIndexBuffer* indexBuffer);
+ void UpdateVertexFormat(VertexFormat* vertexFormat);
+ // Assumes the fetched BufferBinding object is modified
+ void UpdateVertexBufferBindings(BufferBindings** bindingsOut);
+ void SetFormat(VertexFormat* vertexFormat, Tags::IndexType indexFormat);
+
+private:
+ void DeleteGLObjects();
+};
+
+struct RendererFrameInfo {
+ Camera* camera;
+ glm::mat4 matrixView;
+ glm::mat4 matrixProj;
+ float time;
+ float deltaTime;
+};
+
+class Renderer {
+public:
+ // NOTE: see Renderer constructor for default values
+ enum RenderOption {
+ /// Render everything directly using objects' provided material and vertex/index data.
+ RO_Shading,
+ /// Render everything as wireframes using provided position data.
+ RO_Wireframe,
+ RO_COUNT,
+ };
+
+public:
+ RcPtr<Material> binding_WireframeMaterial;
+
+private:
+ RendererFrameInfo mFrame;
+ bool mInsideFrame = false;
+ bool mRenderOptions[RO_COUNT] = {};
+
+public:
+ Renderer();
+
+ void LoadBindings(const rapidjson::Value& bindings);
+ void SaveBindings(rapidjson::Value& into, rapidjson::Document& root) const;
+
+ void BeginFrame(Camera& camera, float currentTime, float deltaTime);
+ const RendererFrameInfo& GetLastFrameInfo() const { return mFrame; }
+ void Draw(const RenderObject* objects, const GameObject* gameObject, size_t count);
+ void EndFrame();
+
+ bool GetRenderOption(RenderOption option) const;
+ void SetRenderOption(RenderOption option, bool flag);
+};
diff --git a/source/30-game/SceneThings.cpp b/source/30-game/SceneThings.cpp
new file mode 100644
index 0000000..3fa0436
--- /dev/null
+++ b/source/30-game/SceneThings.cpp
@@ -0,0 +1,142 @@
+#include "SceneThings.hpp"
+
+#include "CommonVertexIndex.hpp"
+#include "Rect.hpp"
+
+#include <utility>
+
+SimpleGeometryObject::SimpleGeometryObject(GameWorld* world)
+ : GameObject(KD_SimpleGeometry, world)
+ , mRenderObject()
+ , mSize{ 1.0f, 1.0f, 1.0f }
+ , mXFaceColor(kXAxisColor)
+ , mYFaceColor(kYAxisColor)
+ , mZFaceColor(kZAxisColor)
+ , mNeedsRebuildMesh{ true } {
+ mRenderObject.SetMaterial(gDefaultMaterial.Get());
+ mRenderObject.SetFormat(gVformatStandard.Get(), Tags::IT_16Bit);
+ mRenderObject.RebuildIfNecessary();
+}
+
+void SimpleGeometryObject::SetSize(glm::vec3 size) {
+ mSize = size;
+ mNeedsRebuildMesh = true;
+}
+
+void SimpleGeometryObject::SetXFaceColor(RgbaColor color) {
+ mXFaceColor = color;
+ mNeedsRebuildMesh = true;
+}
+
+void SimpleGeometryObject::SetYFaceColor(RgbaColor color) {
+ mYFaceColor = color;
+ mNeedsRebuildMesh = true;
+}
+
+void SimpleGeometryObject::SetZFaceColor(RgbaColor color) {
+ mZFaceColor = color;
+ mNeedsRebuildMesh = true;
+}
+
+std::span<const RenderObject> SimpleGeometryObject::GetRenderObjects() const {
+ using namespace Tags;
+
+ if (mNeedsRebuildMesh) {
+ mNeedsRebuildMesh = false;
+
+ Vertex_PTC vertices[4 /*vertices per face*/ * 6 /*faces*/];
+ uint16_t indices[3 /*indices per triangle*/ * 2 /*triangles per face*/ * 6 /*faces*/];
+
+ auto extents = mSize / 2.0f;
+
+ int faceGenVerticesIdx = 0;
+ int faceGenIndicesIdx = 0;
+ auto GenerateFace = [&](glm::vec3 faceCenter, glm::vec3 firstExtentVec, glm::vec3 secondExtentVec, RgbaColor color) {
+ // Generates (if viewing top down on the face): bottom left, top left, bottom right, top right
+ // (-1, -1) , (-1, 1) , (1, -1) , (1, 1)
+ // idx=0 , idx=1 , idx=2 , idx=3
+
+ // These are index offsets, see above comment
+ constexpr int kBottomLeft = 0;
+ constexpr int kTopLeft = 1;
+ constexpr int kBottomRight = 2;
+ constexpr int kTopRight = 3;
+
+ int startVertIdx = faceGenVerticesIdx;
+ for (float firstDir : { -1, 1 }) {
+ for (float secondDir : { -1, 1 }) {
+ auto vertPos = faceCenter + firstExtentVec * firstDir + secondExtentVec * secondDir;
+ auto& vert = vertices[faceGenVerticesIdx];
+ vert.x = vertPos.x;
+ vert.y = vertPos.y;
+ vert.z = vertPos.z;
+ vert.r = color.r;
+ vert.g = color.g;
+ vert.b = color.b;
+ vert.a = color.a;
+ faceGenVerticesIdx += 1;
+ }
+ }
+
+ // Triangle #1
+ indices[faceGenIndicesIdx++] = startVertIdx + kTopRight;
+ indices[faceGenIndicesIdx++] = startVertIdx + kTopLeft;
+ indices[faceGenIndicesIdx++] = startVertIdx + kBottomLeft;
+ // Triangle #2
+ indices[faceGenIndicesIdx++] = startVertIdx + kTopRight;
+ indices[faceGenIndicesIdx++] = startVertIdx + kBottomLeft;
+ indices[faceGenIndicesIdx++] = startVertIdx + kBottomRight;
+ };
+ for (int xDir : { -1, 1 }) {
+ float x = xDir * extents.x;
+ GenerateFace(glm::vec3(x, 0, 0), glm::vec3(0, 0, extents.z), glm::vec3(0, extents.y, 0), mXFaceColor);
+ }
+ for (int yDir : { -1, 1 }) {
+ float y = yDir * extents.y;
+ GenerateFace(glm::vec3(0, y, 0), glm::vec3(extents.x, 0, 0), glm::vec3(0, 0, extents.z), mYFaceColor);
+ }
+ for (int zDir : { -1, 1 }) {
+ float z = zDir * extents.z;
+ GenerateFace(glm::vec3(0, 0, z), glm::vec3(extents.x, 0, 0), glm::vec3(0, extents.y, 0), mZFaceColor);
+ }
+
+ for (auto& vert : vertices) {
+ vert.u = 0.0f;
+ vert.v = 0.0f;
+ }
+
+ mRenderObject.GetVertexBufferBindings().bindings[0]->Upload((const std::byte*)vertices, sizeof(vertices));
+ mRenderObject.GetIndexBuffer()->Upload((const std::byte*)indices, IT_16Bit, std::size(indices));
+ }
+
+ return { &mRenderObject, 1 };
+}
+
+BuildingObject::BuildingObject(GameWorld* world)
+ : GameObject(KD_Building, world) {
+ mRenderObject.SetMaterial(gDefaultMaterial.Get());
+ mRenderObject.SetFormat(gVformatStandard.Get(), Tags::IT_32Bit);
+ mRenderObject.RebuildIfNecessary();
+}
+
+// void BuildingObject::SetMeshMaterial(Material* material) {
+// mMaterial.Attach(material);
+// // TODO update render
+// }
+
+// const Material* BuildingObject::GetMeshMaterial() const {
+// return mMaterial.Get();
+// }
+
+// void BuildingObject::SetMesh(GpuMesh* mesh) {
+// mMesh.Attach(mesh);
+// // TODO update render
+// }
+
+// const GpuMesh* BuildingObject::GetMesh() const {
+// return mMesh.Get();
+// }
+
+std::span<const RenderObject> BuildingObject::GetRenderObjects() const {
+ return { &mRenderObject, 1 };
+}
diff --git a/source/30-game/SceneThings.hpp b/source/30-game/SceneThings.hpp
new file mode 100644
index 0000000..761eb59
--- /dev/null
+++ b/source/30-game/SceneThings.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "Color.hpp"
+#include "GameObject.hpp"
+#include "Renderer.hpp"
+
+#include <MacrosCodegen.hpp>
+
+#include <glm/glm.hpp>
+#include <vector>
+
+class SimpleGeometryObject : public GameObject {
+ BRUSSEL_CLASS()
+
+private:
+ RenderObject mRenderObject;
+ glm::vec3 mSize;
+ RgbaColor mXFaceColor;
+ RgbaColor mYFaceColor;
+ RgbaColor mZFaceColor;
+ mutable bool mNeedsRebuildMesh;
+
+public:
+ SimpleGeometryObject(GameWorld* world);
+
+ glm::vec3 GetSize() const { return mSize; }
+ void SetSize(glm::vec3 size);
+ RgbaColor GetXFaceColor() const { return mXFaceColor; }
+ void SetXFaceColor(RgbaColor color);
+ RgbaColor GetYFaceColor() const { return mYFaceColor; }
+ void SetYFaceColor(RgbaColor color);
+ RgbaColor GetZFaceColor() const { return mZFaceColor; }
+ void SetZFaceColor(RgbaColor color);
+ virtual std::span<const RenderObject> GetRenderObjects() const override;
+};
+
+class BuildingObject : public GameObject {
+ BRUSSEL_CLASS()
+
+private:
+ RenderObject mRenderObject;
+
+public:
+ BuildingObject(GameWorld* world);
+
+ // TODO
+ // void SetMeshMaterial(Material* material);
+ // virtual const Material* GetMeshMaterial() const override;
+ // void SetMesh(GpuMesh* mesh);
+ // virtual const GpuMesh* GetMesh() const override;
+ virtual std::span<const RenderObject> GetRenderObjects() const override;
+};
diff --git a/source/30-game/Shader.cpp b/source/30-game/Shader.cpp
new file mode 100644
index 0000000..9bf2e0e
--- /dev/null
+++ b/source/30-game/Shader.cpp
@@ -0,0 +1,711 @@
+#include "Shader.hpp"
+
+#include "AppConfig.hpp"
+
+#include <Metadata.hpp>
+#include <RapidJsonHelper.hpp>
+#include <ScopeGuard.hpp>
+#include <Utils.hpp>
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <rapidjson/document.h>
+#include <cassert>
+#include <cstddef>
+#include <cstdlib>
+#include <utility>
+
+using namespace std::literals;
+
+void ShaderMathVariable::ShowInfo() const {
+ ImGui::BulletText("Location: %d\nName: %s\nSemantic: %.*s\nType: %.*s %dx%d",
+ location,
+ name.c_str(),
+ PRINTF_STRING_VIEW(Metadata::EnumToString(semantic)),
+ PRINTF_STRING_VIEW(Tags::GLTypeToString(scalarType)),
+ width,
+ height);
+}
+
+void ShaderSamplerVariable::ShowInfo() const {
+ ImGui::BulletText("Location: %d\nName: %s\nSemantic: %.*s\nType: Sampler",
+ location,
+ name.c_str(),
+ PRINTF_STRING_VIEW(Metadata::EnumToString(semantic)));
+}
+
+namespace ProjectBrussel_UNITY_ID {
+GLuint FindLocation(const std::vector<ShaderMathVariable>& vars, Tags::VertexElementSemantic semantic) {
+ for (auto& var : vars) {
+ if (var.semantic == semantic) {
+ return var.location;
+ }
+ }
+ return Tags::kInvalidLocation;
+}
+
+constexpr auto kAfnTransform = "transform";
+constexpr auto kAfnTime = "time";
+constexpr auto kAfnDeltaTime = "deltaTime";
+constexpr auto kAfnTextureAtlas = "textureAtlas";
+
+void InitAutoFill(const char* name, GLuint program, GLuint& location) {
+ GLint result = glGetUniformLocation(program, name);
+ if (result != -1) {
+ location = result;
+ }
+}
+
+void InitAutoFills(Shader& shader) {
+ GLuint pg = shader.GetProgram();
+ InitAutoFill(kAfnTransform, pg, shader.autofill_Transform);
+ InitAutoFill(kAfnTime, pg, shader.autofill_Time);
+ InitAutoFill(kAfnDeltaTime, pg, shader.autofill_DeltaTime);
+ InitAutoFill(kAfnTextureAtlas, pg, shader.autofill_TextureAtlas);
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+GLuint ShaderInfo::FindInputLocation(Tags::VertexElementSemantic semantic) {
+ using namespace ProjectBrussel_UNITY_ID;
+ return FindLocation(inputs, semantic);
+}
+
+GLuint ShaderInfo::FindOutputLocation(Tags::VertexElementSemantic semantic) {
+ using namespace ProjectBrussel_UNITY_ID;
+ return FindLocation(outputs, semantic);
+}
+
+Shader::Shader() {
+}
+
+Shader::~Shader() {
+ glDeleteProgram(mProgram);
+}
+
+namespace ProjectBrussel_UNITY_ID {
+// Grabs section [begin, end)
+Shader::ErrorCode CreateShader(GLuint& out, const char* src, int beginIdx, int endIdx, GLenum type) {
+ out = glCreateShader(type);
+
+ const GLchar* begin = &src[beginIdx];
+ const GLint len = endIdx - beginIdx;
+ glShaderSource(out, 1, &begin, &len);
+
+ glCompileShader(out);
+ GLint compileStatus;
+ glGetShaderiv(out, GL_COMPILE_STATUS, &compileStatus);
+ if (compileStatus == GL_FALSE) {
+ GLint len;
+ glGetShaderiv(out, GL_INFO_LOG_LENGTH, &len);
+
+ std::string log(len, '\0');
+ glGetShaderInfoLog(out, len, nullptr, log.data());
+
+ return Shader ::EC_CompilationFailed;
+ }
+
+ return Shader::EC_Success;
+}
+
+Shader::ErrorCode CreateShader(GLuint& out, std::string_view str, GLenum type) {
+ return CreateShader(out, str.data(), 0, str.size(), type);
+}
+
+Shader::ErrorCode LinkShaderProgram(GLuint program) {
+ glLinkProgram(program);
+ GLint linkStatus;
+ glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+ if (linkStatus == GL_FALSE) {
+ GLint len;
+ glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
+
+ std::string log(len, '\0');
+ glGetProgramInfoLog(program, len, nullptr, log.data());
+
+ return Shader::EC_LinkingFailed;
+ }
+
+ return Shader::EC_Success;
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+#define CATCH_ERROR_IMPL(x, name) \
+ auto name = x; \
+ if (name != Shader::EC_Success) { \
+ return name; \
+ }
+#define CATCH_ERROR(x) CATCH_ERROR_IMPL(x, UNIQUE_NAME(result))
+
+Shader::ErrorCode Shader::InitFromSources(const ShaderSources& sources) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (IsValid()) {
+ return EC_AlreadyInitialized;
+ }
+
+ GLuint program = glCreateProgram();
+ ScopeGuard sg = [&]() { glDeleteProgram(program); };
+
+ GLuint vertex = 0;
+ DEFER {
+ glDeleteShader(vertex);
+ };
+ if (!sources.vertex.empty()) {
+ CATCH_ERROR(CreateShader(vertex, sources.vertex, GL_VERTEX_SHADER));
+ glAttachShader(program, vertex);
+ }
+
+ GLuint geometry = 0;
+ DEFER {
+ glDeleteShader(geometry);
+ };
+ if (!sources.geometry.empty()) {
+ CATCH_ERROR(CreateShader(geometry, sources.geometry, GL_GEOMETRY_SHADER));
+ glAttachShader(program, geometry);
+ }
+
+ GLuint tessControl = 0;
+ DEFER {
+ glDeleteShader(tessControl);
+ };
+ if (!sources.tessControl.empty()) {
+ CATCH_ERROR(CreateShader(tessControl, sources.tessControl, GL_TESS_CONTROL_SHADER));
+ glAttachShader(program, tessControl);
+ }
+
+ GLuint tessEval = 0;
+ DEFER {
+ glDeleteShader(tessEval);
+ };
+ if (!sources.tessEval.empty()) {
+ CATCH_ERROR(CreateShader(tessEval, sources.tessEval, GL_TESS_EVALUATION_SHADER));
+ glAttachShader(program, tessEval);
+ }
+
+ GLuint fragment = 0;
+ DEFER {
+ glDeleteShader(fragment);
+ };
+ if (!sources.fragment.empty()) {
+ CATCH_ERROR(CreateShader(fragment, sources.fragment, GL_FRAGMENT_SHADER));
+ glAttachShader(program, fragment);
+ }
+
+ CATCH_ERROR(LinkShaderProgram(program));
+
+ sg.Dismiss();
+ mProgram = program;
+
+ InitAutoFills(*this);
+
+ return EC_Success;
+}
+
+Shader::ErrorCode Shader::InitFromSource(std::string_view source) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ GLuint vertex = 0;
+ DEFER {
+ glDeleteShader(vertex);
+ };
+
+ GLuint geometry = 0;
+ DEFER {
+ glDeleteShader(geometry);
+ };
+
+ GLuint tessControl = 0;
+ DEFER {
+ glDeleteShader(tessControl);
+ };
+
+ GLuint tessEval = 0;
+ DEFER {
+ glDeleteShader(tessEval);
+ };
+
+ GLuint fragment = 0;
+ DEFER {
+ glDeleteShader(fragment);
+ };
+
+ int prevBegin = -1; // Excluding #type marker
+ int prevEnd = -1; // [begin, end)
+ std::string prevShaderVariant;
+
+ auto CommitSection = [&]() -> ErrorCode {
+ if (prevBegin == -1 || prevEnd == -1) {
+ // Not actually "succeeding" here, but we just want to skip this call and continue
+ return EC_Success;
+ }
+
+ if (prevShaderVariant == "vertex" && !vertex) {
+ CATCH_ERROR(CreateShader(vertex, source.data(), prevBegin, prevEnd, GL_VERTEX_SHADER));
+ } else if (prevShaderVariant == "geometry" && !geometry) {
+ CATCH_ERROR(CreateShader(geometry, source.data(), prevBegin, prevEnd, GL_GEOMETRY_SHADER));
+ } else if (prevShaderVariant == "tessellation_control" && !tessControl) {
+ CATCH_ERROR(CreateShader(tessControl, source.data(), prevBegin, prevEnd, GL_TESS_CONTROL_SHADER));
+ } else if (prevShaderVariant == "tessellation_evaluation" && !tessEval) {
+ CATCH_ERROR(CreateShader(tessEval, source.data(), prevBegin, prevEnd, GL_TESS_EVALUATION_SHADER));
+ } else if (prevShaderVariant == "fragment" && !fragment) {
+ CATCH_ERROR(CreateShader(fragment, source.data(), prevBegin, prevEnd, GL_FRAGMENT_SHADER));
+ } else {
+ return EC_InvalidShaderVariant;
+ }
+
+ prevBegin = -1;
+ prevEnd = -1;
+ prevShaderVariant.clear();
+
+ return EC_Success;
+ };
+
+ constexpr const char* kMarker = "#type ";
+ bool matchingDirective = true; // If true, we are matching marker pattern; if false, we are accumulating shader variant identifier
+ int matchIndex = 0; // Current index of the pattern trying to match
+ std::string shaderVariant;
+
+ // Don't use utf8 iterator, shader sources are expected to be ASCII only
+ for (size_t i = 0; i < source.size(); ++i) {
+ char c = source[i];
+
+ if (matchingDirective) {
+ if (c == kMarker[matchIndex]) {
+ // Matched the expected character, go to next char in pattern
+ matchIndex++;
+
+ // If we are at the end of the marker pattern...
+ if (kMarker[matchIndex] == '\0') {
+ matchingDirective = false;
+ matchIndex = 0;
+ continue;
+ }
+
+ // This might be a shader variant directive -> might be end of a section
+ if (c == '#') {
+ prevEnd = i;
+ continue;
+ }
+ } else {
+ // Unexpected character, rollback to beginning
+ matchIndex = 0;
+ }
+ } else {
+ if (c == '\n') {
+ // Found complete shader variant directive
+
+ CATCH_ERROR(CommitSection()); // Try commit section, for the first apparent of #type this should do nothing, as `prevEnd` will still be -1
+ prevBegin = i + 1; // +1 to skip new line (technically not needed)
+ prevShaderVariant = std::move(shaderVariant);
+
+ matchingDirective = true;
+ shaderVariant.clear();
+ } else {
+ // Simply accumulate to shader variant buffer
+ shaderVariant += c;
+ }
+ }
+ }
+
+ // Commit the last section
+ prevEnd = static_cast<int>(source.size());
+ CATCH_ERROR(CommitSection());
+
+ GLuint program = glCreateProgram();
+ ScopeGuard sg = [&]() { glDeleteProgram(program); };
+
+ if (vertex) glAttachShader(program, vertex);
+ if (geometry) glAttachShader(program, geometry);
+ if (tessControl) glAttachShader(program, tessControl);
+ if (tessEval) glAttachShader(program, tessEval);
+ if (fragment) glAttachShader(program, fragment);
+
+ CATCH_ERROR(LinkShaderProgram(program));
+
+ sg.Dismiss();
+ mProgram = program;
+
+ InitAutoFills(*this);
+
+ return EC_Success;
+}
+
+#undef CATCH_ERROR
+
+namespace ProjectBrussel_UNITY_ID {
+bool QueryMathInfo(GLenum type, GLenum& scalarType, int& width, int& height) {
+ auto DoOutput = [&](GLenum scalarTypeIn, int widthIn, int heightIn) {
+ width = widthIn;
+ height = heightIn;
+ scalarType = scalarTypeIn;
+ };
+
+ switch (type) {
+ case GL_FLOAT:
+ case GL_DOUBLE:
+ case GL_INT:
+ case GL_UNSIGNED_INT:
+ case GL_BOOL: {
+ DoOutput(type, 1, 1);
+ return true;
+ }
+
+ case GL_FLOAT_VEC2: DoOutput(GL_FLOAT, 1, 2); return true;
+ case GL_FLOAT_VEC3: DoOutput(GL_FLOAT, 1, 3); return true;
+ case GL_FLOAT_VEC4: DoOutput(GL_FLOAT, 1, 4); return true;
+ case GL_DOUBLE_VEC2: DoOutput(GL_DOUBLE, 1, 2); return true;
+ case GL_DOUBLE_VEC3: DoOutput(GL_DOUBLE, 1, 3); return true;
+ case GL_DOUBLE_VEC4: DoOutput(GL_DOUBLE, 1, 4); return true;
+ case GL_INT_VEC2: DoOutput(GL_INT, 1, 2); return true;
+ case GL_INT_VEC3: DoOutput(GL_INT, 1, 3); return true;
+ case GL_INT_VEC4: DoOutput(GL_INT, 1, 4); return true;
+ case GL_UNSIGNED_INT_VEC2: DoOutput(GL_UNSIGNED_INT, 1, 2); return true;
+ case GL_UNSIGNED_INT_VEC3: DoOutput(GL_UNSIGNED_INT, 1, 3); return true;
+ case GL_UNSIGNED_INT_VEC4: DoOutput(GL_UNSIGNED_INT, 1, 4); return true;
+ case GL_BOOL_VEC2: DoOutput(GL_BOOL, 1, 2); return true;
+ case GL_BOOL_VEC3: DoOutput(GL_BOOL, 1, 3); return true;
+ case GL_BOOL_VEC4: DoOutput(GL_BOOL, 1, 4); return true;
+
+ case GL_FLOAT_MAT2: DoOutput(GL_FLOAT, 2, 2); return true;
+ case GL_FLOAT_MAT3: DoOutput(GL_FLOAT, 3, 3); return true;
+ case GL_FLOAT_MAT4: DoOutput(GL_FLOAT, 4, 4); return true;
+ case GL_FLOAT_MAT2x3: DoOutput(GL_FLOAT, 2, 3); return true;
+ case GL_FLOAT_MAT2x4: DoOutput(GL_FLOAT, 2, 4); return true;
+ case GL_FLOAT_MAT3x2: DoOutput(GL_FLOAT, 3, 2); return true;
+ case GL_FLOAT_MAT3x4: DoOutput(GL_FLOAT, 3, 4); return true;
+ case GL_FLOAT_MAT4x2: DoOutput(GL_FLOAT, 4, 2); return true;
+ case GL_FLOAT_MAT4x3: DoOutput(GL_FLOAT, 4, 3); return true;
+
+ case GL_DOUBLE_MAT2: DoOutput(GL_DOUBLE, 2, 2); return true;
+ case GL_DOUBLE_MAT3: DoOutput(GL_DOUBLE, 3, 3); return true;
+ case GL_DOUBLE_MAT4: DoOutput(GL_DOUBLE, 4, 4); return true;
+ case GL_DOUBLE_MAT2x3: DoOutput(GL_DOUBLE, 2, 3); return true;
+ case GL_DOUBLE_MAT2x4: DoOutput(GL_DOUBLE, 2, 4); return true;
+ case GL_DOUBLE_MAT3x2: DoOutput(GL_DOUBLE, 3, 2); return true;
+ case GL_DOUBLE_MAT3x4: DoOutput(GL_DOUBLE, 3, 4); return true;
+ case GL_DOUBLE_MAT4x2: DoOutput(GL_DOUBLE, 4, 2); return true;
+ case GL_DOUBLE_MAT4x3: DoOutput(GL_DOUBLE, 4, 3); return true;
+
+ default: break;
+ }
+
+ return false;
+}
+
+bool QuerySamplerInfo(GLenum type) {
+ switch (type) {
+ case GL_SAMPLER_1D:
+ case GL_SAMPLER_2D:
+ case GL_SAMPLER_3D:
+ case GL_SAMPLER_CUBE:
+ case GL_SAMPLER_1D_SHADOW:
+ case GL_SAMPLER_2D_SHADOW:
+ case GL_SAMPLER_1D_ARRAY:
+ case GL_SAMPLER_2D_ARRAY:
+ case GL_SAMPLER_1D_ARRAY_SHADOW:
+ case GL_SAMPLER_2D_ARRAY_SHADOW:
+ case GL_SAMPLER_2D_MULTISAMPLE:
+ case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
+ case GL_SAMPLER_CUBE_SHADOW:
+ case GL_SAMPLER_BUFFER:
+ case GL_SAMPLER_2D_RECT:
+ case GL_SAMPLER_2D_RECT_SHADOW:
+
+ case GL_INT_SAMPLER_1D:
+ case GL_INT_SAMPLER_2D:
+ case GL_INT_SAMPLER_3D:
+ case GL_INT_SAMPLER_CUBE:
+ case GL_INT_SAMPLER_1D_ARRAY:
+ case GL_INT_SAMPLER_2D_ARRAY:
+ case GL_INT_SAMPLER_2D_MULTISAMPLE:
+ case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
+ case GL_INT_SAMPLER_BUFFER:
+ case GL_INT_SAMPLER_2D_RECT:
+
+ case GL_UNSIGNED_INT_SAMPLER_1D:
+ case GL_UNSIGNED_INT_SAMPLER_2D:
+ case GL_UNSIGNED_INT_SAMPLER_3D:
+ case GL_UNSIGNED_INT_SAMPLER_CUBE:
+ case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
+ case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
+ case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
+ case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
+ case GL_UNSIGNED_INT_SAMPLER_BUFFER:
+ case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
+ return true;
+
+ default: break;
+ }
+
+ return false;
+}
+
+std::variant<ShaderMathVariable, ShaderSamplerVariable> CreateVariable(GLenum type, GLuint loc) {
+ GLenum scalarType;
+ int width;
+ int height;
+ if (QueryMathInfo(type, scalarType, width, height)) {
+ ShaderMathVariable res;
+ res.location = loc;
+ res.scalarType = type;
+ res.width = width;
+ res.height = height;
+ return res;
+ }
+
+ if (QuerySamplerInfo(type)) {
+ ShaderSamplerVariable res;
+ res.location = loc;
+ res.samplerType = type;
+ return res;
+ }
+
+ throw std::runtime_error(fmt::format("Unknown OpenGL shader uniform type {}", type));
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+bool Shader::GatherInfoShaderIntrospection() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ mInfo = {};
+
+ // TODO handle differnt types of variables with the same name
+
+ // TODO work with OpenGL < 4.3, possibly with glslang
+ return true;
+
+ int inputCount;
+ glGetProgramInterfaceiv(mProgram, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &inputCount);
+ int outputCount;
+ glGetProgramInterfaceiv(mProgram, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputCount);
+ int uniformBlockCount;
+ glGetProgramInterfaceiv(mProgram, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES, &uniformBlockCount);
+ int uniformCount;
+ glGetProgramInterfaceiv(mProgram, GL_UNIFORM, GL_ACTIVE_RESOURCES, &uniformCount);
+
+ // Gather inputs
+ auto GatherMathVars = [&](int count, GLenum resourceType, ShaderThingId::Kind resourceKind, std::vector<ShaderMathVariable>& list) {
+ for (int i = 0; i < count; ++i) {
+ const GLenum query[] = { GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE };
+ GLint props[std::size(query)];
+ glGetProgramResourceiv(mProgram, resourceType, i, std::size(query), query, std::size(props), nullptr, props);
+ auto& nameLength = props[0];
+ auto& type = props[1];
+ auto& loc = props[2];
+ auto& arrayLength = props[3];
+
+ std::string fieldName(nameLength - 1, '\0');
+ glGetProgramResourceName(mProgram, GL_UNIFORM, i, nameLength, nullptr, fieldName.data());
+
+ mInfo.things.try_emplace(fieldName, ShaderThingId{ resourceKind, i });
+
+ auto& thing = list.emplace_back();
+ thing.name = std::move(fieldName);
+ thing.arrayLength = arrayLength;
+ QueryMathInfo(type, thing.scalarType, thing.width, thing.height);
+ }
+ };
+ GatherMathVars(inputCount, GL_PROGRAM_INPUT, ShaderThingId::KD_Input, mInfo.inputs);
+ GatherMathVars(outputCount, GL_PROGRAM_OUTPUT, ShaderThingId::KD_Output, mInfo.outputs);
+
+ // Gather uniform variables
+ for (int i = 0; i < uniformCount; ++i) {
+ const GLenum query[] = { GL_BLOCK_INDEX, GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE };
+ GLint props[std::size(query)];
+ glGetProgramResourceiv(mProgram, GL_UNIFORM, i, std::size(query), query, std::size(props), nullptr, props);
+ auto& blockIndex = props[0]; // Index in interface block
+ if (blockIndex != -1) { // If this is an interface block uniform, skip because it will be handled by our uniform blocks inspector
+ continue;
+ }
+ auto& nameLength = props[1];
+ auto& type = props[2];
+ auto& loc = props[3];
+ auto& arrayLength = props[4];
+
+ std::string fieldName(nameLength - 1, '\0');
+ glGetProgramResourceName(mProgram, GL_UNIFORM, i, nameLength, nullptr, fieldName.data());
+
+ mInfo.things.try_emplace(fieldName, ShaderThingId{ ShaderThingId::KD_Uniform, i });
+ mInfo.uniforms.push_back(CreateVariable(type, loc));
+ }
+
+ return true;
+}
+
+bool Shader::IsValid() const {
+ return mProgram != 0;
+}
+
+IresShader::IresShader()
+ : IresObject(KD_Shader) {
+ InvalidateInstance();
+}
+
+Shader* IresShader::GetInstance() const {
+ return mInstance.Get();
+}
+
+void IresShader::InvalidateInstance() {
+ if (mInstance != nullptr) {
+ mInstance->mIres = nullptr;
+ }
+ mInstance.Attach(new Shader());
+ mInstance->mIres = this;
+}
+
+void IresShader::ShowEditor(IEditor& editor) {
+ using namespace Tags;
+ using namespace ProjectBrussel_UNITY_ID;
+
+ IresObject::ShowEditor(editor);
+
+ if (ImGui::Button("Gather info")) {
+ mInstance->GatherInfoShaderIntrospection();
+ }
+
+ if (ImGui::InputText("Source file", &mSourceFile, ImGuiInputTextFlags_EnterReturnsTrue)) {
+ InvalidateInstance();
+ }
+ // In other cases, mSourceFile will be reverted to before edit
+
+ // TODO macros
+
+ ImGui::Separator();
+
+ auto& info = mInstance->GetInfo();
+ if (ImGui::CollapsingHeader("General")) {
+ ImGui::Text("OpenGL program ID: %u", mInstance->GetProgram());
+ }
+ if (ImGui::CollapsingHeader("Inputs")) {
+ for (auto& input : info.inputs) {
+ input.ShowInfo();
+ }
+ }
+ if (ImGui::CollapsingHeader("Outputs")) {
+ for (auto& output : info.outputs) {
+ output.ShowInfo();
+ }
+ }
+ if (ImGui::CollapsingHeader("Uniforms")) {
+ for (auto& uniform : info.uniforms) {
+ std::visit([](auto&& v) { v.ShowInfo(); }, uniform);
+ }
+ if (auto loc = mInstance->autofill_Transform; loc != kInvalidLocation) {
+ ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTransform);
+ }
+ if (auto loc = mInstance->autofill_Time; loc != kInvalidLocation) {
+ ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTime);
+ }
+ if (auto loc = mInstance->autofill_DeltaTime; loc != kInvalidLocation) {
+ ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnDeltaTime);
+ }
+ if (auto loc = mInstance->autofill_TextureAtlas; loc != kInvalidLocation) {
+ ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTextureAtlas);
+ }
+ }
+}
+
+void IresShader::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ IresObject::Write(ctx, value, root);
+ json_dto::json_output_t out( value, root.GetAllocator() );
+ out << mInstance->mInfo;
+}
+
+void IresShader::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ IresObject::Read(ctx, value);
+
+ auto rvSourceFile = rapidjson::GetProperty(value, rapidjson::kStringType, "SourceFile"sv);
+ if (!rvSourceFile) {
+ return;
+ } else {
+ this->mSourceFile = rapidjson::AsString(*rvSourceFile);
+
+ char shaderFilePath[256];
+ snprintf(shaderFilePath, sizeof(shaderFilePath), "%s/%s", AppConfig::assetDir.c_str(), rvSourceFile->GetString());
+
+ auto shaderFile = Utils::OpenCstdioFile(shaderFilePath, Utils::Read);
+ if (!shaderFile) return;
+ DEFER {
+ fclose(shaderFile);
+ };
+
+ fseek(shaderFile, 0, SEEK_END);
+ auto shaderFileSize = ftell(shaderFile);
+ rewind(shaderFile);
+
+ // Also add \0 ourselves
+ auto buffer = std::make_unique<char[]>(shaderFileSize + 1);
+ fread(buffer.get(), shaderFileSize, 1, shaderFile);
+ buffer[shaderFileSize] = '\0';
+ std::string_view source(buffer.get(), shaderFileSize);
+
+ if (mInstance->InitFromSource(source) != Shader::EC_Success) {
+ return;
+ }
+ }
+
+ auto& shaderInfo = mInstance->mInfo;
+ auto shaderProgram = mInstance->GetProgram();
+
+ auto LoadMathVars = [&](std::string_view name, ShaderThingId::Kind kind, std::vector<ShaderMathVariable>& vars) {
+ auto rvThings = rapidjson::GetProperty(value, rapidjson::kArrayType, name);
+ if (!rvThings) return;
+
+ for (auto& rv : rvThings->GetArray()) {
+ if (!rv.IsObject()) continue;
+ ShaderMathVariable thing;
+ ReadShaderMathVariable(rv, thing);
+
+ shaderInfo.things.try_emplace(thing.name, ShaderThingId{ kind, (int)vars.size() });
+ vars.push_back(std::move(thing));
+ }
+ };
+ LoadMathVars("Inputs"sv, ShaderThingId::KD_Input, shaderInfo.inputs);
+ LoadMathVars("Outputs"sv, ShaderThingId::KD_Output, shaderInfo.outputs);
+
+ auto rvUniforms = rapidjson::GetProperty(value, rapidjson::kArrayType, "Uniforms"sv);
+ if (!rvUniforms) return;
+ for (auto& rvUniform : rvUniforms->GetArray()) {
+ if (!rvUniform.IsObject()) continue;
+
+ auto rvType = rapidjson::GetProperty(rvUniform, rapidjson::kStringType, "Type"sv);
+ if (!rvType) continue;
+ auto type = rapidjson::AsStringView(*rvType);
+
+ auto rvValue = rapidjson::GetProperty(rvUniform, rapidjson::kObjectType, "Value"sv);
+ if (!rvValue) continue;
+
+ bool autoFill; // TODO store autofill uniforms somewhere else
+ BRUSSEL_JSON_GET_DEFAULT(rvUniform, "AutoFill", bool, autoFill, false);
+ if (autoFill) continue;
+
+ auto uniform = [&]() -> std::unique_ptr<ShaderVariable> {
+ if (type == "Math"sv) {
+ auto uniform = std::make_unique<ShaderMathVariable>();
+ ReadShaderMathVariable(*rvValue, *uniform);
+
+ return uniform;
+ } else if (type == "Sampler"sv) {
+ auto uniform = std::make_unique<ShaderSamplerVariable>();
+ ReadShaderSamplerVariable(*rvValue, *uniform);
+
+ return uniform;
+ }
+
+ return nullptr;
+ }();
+ if (uniform) {
+ shaderInfo.things.try_emplace(uniform->name, ShaderThingId{ ShaderThingId::KD_Uniform, (int)shaderInfo.uniforms.size() });
+ shaderInfo.uniforms.emplace_back(std::move(uniform));
+ }
+ }
+
+ for (auto& uniform : shaderInfo.uniforms) {
+ uniform->location = glGetUniformLocation(shaderProgram, uniform->name.c_str());
+ }
+}
diff --git a/source/30-game/Shader.hpp b/source/30-game/Shader.hpp
new file mode 100644
index 0000000..cb980cd
--- /dev/null
+++ b/source/30-game/Shader.hpp
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "GraphicsTags.hpp"
+#include "Ires.hpp"
+#include "RcPtr.hpp"
+#include "Utils.hpp"
+
+#include <glad/glad.h>
+#include <robin_hood.h>
+#include <json_dto/pub.hpp>
+#include <memory>
+#include <string_view>
+#include <variant>
+#include <vector>
+
+// TODO move to variable after pattern matching is in the language
+
+// Forward declarations
+class Shader;
+class IresShader;
+
+struct ShaderMathVariable {
+ std::string name;
+ GLuint location;
+ Tags::VertexElementSemantic semantic = Tags::VES_Generic;
+ GLenum scalarType;
+ int width;
+ int height;
+ int arrayLength = 1;
+
+ void ShowInfo() const;
+
+ template <typename TJsonIo>
+ void json_io(TJsonIo& io) {
+ io& json_dto::mandatory("Name", name);
+ io& json_dto::mandatory("Semantic", static_cast<int>(semantic));
+ io& json_dto::mandatory("ScalarType", scalarType);
+ io& json_dto::mandatory("Width", width);
+ io& json_dto::mandatory("Height", height);
+ io& json_dto::optional("ArrayLength", arrayLength, 1);
+ }
+};
+
+struct ShaderSamplerVariable {
+ std::string name;
+ GLuint location;
+ Tags::VertexElementSemantic semantic = Tags::VES_Generic;
+ GLenum samplerType;
+ int arrayLength = 1;
+
+ void ShowInfo() const;
+
+ template <typename TJsonIo>
+ void json_io(TJsonIo& io) {
+ io& json_dto::mandatory("Name", name);
+ io& json_dto::mandatory("Semantic", static_cast<int>(semantic));
+ io& json_dto::mandatory("SamplerType", samplerType);
+ io& json_dto::optional("ArrayLength", arrayLength, 1);
+ }
+};
+
+struct ShaderThingId {
+ enum Kind {
+ KD_Input,
+ KD_Output,
+ KD_Uniform,
+ };
+
+ Kind kind;
+ int index;
+};
+
+struct ShaderInfo {
+ robin_hood::unordered_map<std::string, ShaderThingId, StringHash, StringEqual> things;
+ std::vector<ShaderMathVariable> inputs;
+ std::vector<ShaderMathVariable> outputs;
+ std::vector<std::variant<ShaderMathVariable, ShaderSamplerVariable>> uniforms;
+
+ // Find the first variable with the matching semantic
+ GLuint FindInputLocation(Tags::VertexElementSemantic semantic);
+ GLuint FindOutputLocation(Tags::VertexElementSemantic semantic);
+
+ template <typename TJsonIo>
+ void json_io(TJsonIo& io) {
+ io& json_dto::mandatory("Inputs", inputs);
+ io& json_dto::mandatory("Outputs", outputs);
+ // TODO make json_dto support std::variant
+// io& json_dto::mandatory("Uniforms", uniforms);
+ }
+};
+
+class Shader : public RefCounted {
+ friend class IresShader;
+
+private:
+ IresShader* mIres = nullptr;
+ ShaderInfo mInfo;
+ GLuint mProgram = 0;
+
+public:
+ GLuint autofill_Transform = Tags::kInvalidLocation;
+ GLuint autofill_Time = Tags::kInvalidLocation;
+ GLuint autofill_DeltaTime = Tags::kInvalidLocation;
+ GLuint autofill_TextureAtlas = Tags::kInvalidLocation;
+
+public:
+ Shader();
+ ~Shader();
+ Shader(const Shader&) = delete;
+ Shader& operator=(const Shader&) = delete;
+ Shader(Shader&&) = default;
+ Shader& operator=(Shader&&) = default;
+
+ enum ErrorCode {
+ EC_Success,
+ /// Generated when Init*() functions are called on an already initialized Shader object.
+ EC_AlreadyInitialized,
+ /// Generated when the one-source-file text contains invalid or duplicate shader variants.
+ EC_InvalidShaderVariant,
+ EC_FileIoFailed,
+ EC_CompilationFailed,
+ EC_LinkingFailed,
+ };
+
+ struct ShaderSources {
+ std::string_view vertex;
+ std::string_view geometry;
+ std::string_view tessControl;
+ std::string_view tessEval;
+ std::string_view fragment;
+ };
+
+ /// Create shader by compiling each shader source file separately and then combining them together
+ /// into a Shader object.
+ ErrorCode InitFromSources(const ShaderSources& sources);
+
+ /// The given text will be split into different shader sections according to #type directives,
+ /// and combined to form a Shader object.
+ /// For OpenGL, this process involves compililng each section separately and then linking them
+ /// together.
+ ///
+ /// There are a directive for each shader type
+ /// - `#type vertex`: Vertex shader
+ /// - `#type geometry`: Geometry shader
+ /// - `#type tessellation_control`: Tessellation control shader
+ /// - `#type tessellation_evaluation`: Tessellation evaluation shader
+ /// - `#type fragment`: Fragment shader
+ ErrorCode InitFromSource(std::string_view source);
+
+ /// Rebuild info object using OpenGL shader introspection API. Requires OpenGL 4.3 or above. Overrides existing info object.
+ bool GatherInfoShaderIntrospection();
+ const ShaderInfo& GetInfo() const { return mInfo; }
+ ShaderInfo& GetInfo() { return mInfo; }
+ /// If not empty, this name must not duplicate with any other shader object in the process.
+ GLuint GetProgram() const { return mProgram; }
+
+ IresShader* GetIres() const { return mIres; }
+
+ bool IsValid() const;
+};
+
+// Initialized in main()
+inline RcPtr<Shader> gDefaultShader;
+
+class IresShader : public IresObject {
+private:
+ RcPtr<Shader> mInstance;
+ std::string mSourceFile;
+
+public:
+ IresShader();
+
+ Shader* GetInstance() const;
+ void InvalidateInstance();
+
+ void ShowEditor(IEditor& editor) override;
+
+ void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override;
+ void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override;
+};
diff --git a/source/30-game/Sprite.cpp b/source/30-game/Sprite.cpp
new file mode 100644
index 0000000..2b4923c
--- /dev/null
+++ b/source/30-game/Sprite.cpp
@@ -0,0 +1,328 @@
+#include "Sprite.hpp"
+
+#include "AppConfig.hpp"
+#include "CommonVertexIndex.hpp"
+#include "EditorCore.hpp"
+#include "EditorUtils.hpp"
+#include "Image.hpp"
+#include "RapidJsonHelper.hpp"
+#include "Rect.hpp"
+
+#include <imgui.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <rapidjson/document.h>
+#include <memory>
+
+using namespace std::literals;
+
+bool SpriteDefinition::IsValid() const {
+ return mAtlas != nullptr;
+}
+
+bool IresSpriteFiles::IsValid() const {
+ return !spriteFiles.empty();
+}
+
+SpriteDefinition* IresSpriteFiles::CreateInstance() const {
+ if (IsValid()) {
+ return nullptr;
+ }
+
+ std::vector<Texture::AtlasSource> sources;
+ sources.resize(spriteFiles.size());
+ for (auto& file : spriteFiles) {
+ }
+
+ Texture::AtlasOutput atlasOut;
+ Texture::AtlasInput atlasIn{
+ .sources = sources,
+ .packingMode = Texture::PM_KeepSquare,
+ };
+ atlasIn.sources = sources;
+ auto atlas = std::make_unique<Texture>();
+ if (atlas->InitAtlas(atlasIn, &atlasOut) != Texture::EC_Success) {
+ return nullptr;
+ }
+
+ auto sprite = std::make_unique<SpriteDefinition>();
+ sprite->mAtlas.Attach(atlas.release());
+ sprite->mBoundingBox = atlasOut.elements[0].subregionSize;
+ sprite->mFrames.reserve(atlasOut.elements.size());
+ for (auto& elm : atlasOut.elements) {
+ // Validate bounding box
+ if (sprite->mBoundingBox != elm.subregionSize) {
+ return nullptr;
+ }
+
+ // Copy frame subregion
+ sprite->mFrames.push_back(elm.subregion);
+ }
+ return sprite.release();
+}
+
+SpriteDefinition* IresSpriteFiles::GetInstance() {
+ if (mInstance == nullptr) {
+ mInstance.Attach(CreateInstance());
+ }
+ return mInstance.Get();
+}
+
+void IresSpriteFiles::InvalidateInstance() {
+ mInstance.Attach(nullptr);
+}
+
+void IresSpriteFiles::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+ IresObject::Write(ctx, value, root);
+ value.AddMember("Sprites", rapidjson::WriteVectorPrimitives(root, spriteFiles.begin(), spriteFiles.end()), root.GetAllocator());
+}
+
+void IresSpriteFiles::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ InvalidateInstance();
+
+ IresObject::Read(ctx, value);
+
+ auto rvFileList = rapidjson::GetProperty(value, rapidjson::kArrayType, "Sprites"sv);
+ if (!rvFileList) return;
+ spriteFiles.clear();
+ rapidjson::ReadVectorPrimitives<decltype(spriteFiles)>(*rvFileList, spriteFiles);
+}
+
+bool IresSpritesheet::IsValid() const {
+ return !spritesheetFile.empty() &&
+ sheetWSplit != 0 &&
+ sheetHSplit != 0;
+}
+
+void IresSpritesheet::ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf) {
+ ResplitSpritesheet(sprite, conf->sheetWSplit, conf->sheetHSplit, conf->frameCountOverride);
+}
+
+void IresSpritesheet::ResplitSpritesheet(SpriteDefinition* sprite, int wSplit, int hSplit, int frameCount) {
+ auto atlas = sprite->GetAtlas();
+ auto size = atlas->GetInfo().size;
+ int frameWidth = size.x / wSplit;
+ int frameHeight = size.y / hSplit;
+
+ sprite->mBoundingBox = { frameWidth, frameHeight };
+ sprite->mFrames.clear();
+ sprite->mFrames.reserve(wSplit * hSplit);
+
+ // Width and height in UV coordinates for each frame
+ float deltaU = 1.0f / wSplit;
+ float deltaV = 1.0f / hSplit;
+ int i = 0;
+ if (frameCount < 0) {
+ frameCount = std::numeric_limits<int>::max();
+ }
+ for (int y = 0; y < hSplit; ++y) {
+ for (int x = 0; x < wSplit; ++x) {
+ auto& subregion = sprite->mFrames.emplace_back();
+ // Top left
+ subregion.u0 = deltaU * x;
+ subregion.v0 = deltaV * y;
+ // Bottom right
+ subregion.u1 = subregion.u0 + deltaU;
+ subregion.v1 = subregion.v0 + deltaV;
+
+ if ((i + 1) >= frameCount) {
+ return;
+ }
+
+ ++i;
+ }
+ }
+}
+
+SpriteDefinition* IresSpritesheet::CreateInstance() const {
+ if (!IsValid()) {
+ return nullptr;
+ }
+
+ char path[2048];
+ snprintf(path, sizeof(path), "%s/%s", AppConfig::assetDir.c_str(), spritesheetFile.c_str());
+
+ auto atlas = std::make_unique<Texture>();
+ if (atlas->InitFromFile(path) != Texture::EC_Success) {
+ return nullptr;
+ }
+
+ auto sprite = std::make_unique<SpriteDefinition>();
+ sprite->mAtlas.Attach(atlas.release());
+ ResplitSpritesheet(sprite.get(), this);
+ return sprite.release();
+}
+
+SpriteDefinition* IresSpritesheet::GetInstance() {
+ if (mInstance == nullptr) {
+ mInstance.Attach(CreateInstance());
+ }
+ return mInstance.Get();
+}
+
+void IresSpritesheet::InvalidateInstance() {
+ mInstance.Attach(nullptr);
+}
+
+bool IresSpritesheet::IsFrameCountOverriden() const {
+ return frameCountOverride > 0;
+}
+
+int IresSpritesheet::GetFrameCount() const {
+ if (IsFrameCountOverriden()) {
+ return frameCountOverride;
+ } else {
+ return sheetWSplit * sheetHSplit;
+ }
+}
+
+void IresSpritesheet::ShowEditor(IEditor& editor) {
+ IresObject::ShowEditor(editor);
+
+ bool doInvalidateInstance = false;
+ auto instance = GetInstance(); // NOTE: may be null
+
+ if (ImGui::Button("View Sprite", instance == nullptr)) {
+ editor.OpenSpriteViewer(instance);
+ }
+
+ if (ImGui::InputText("Spritesheet", &spritesheetFile)) {
+ doInvalidateInstance = true;
+ }
+
+ if (ImGui::InputInt("Horizontal split", &sheetWSplit)) {
+ sheetWSplit = std::max(sheetWSplit, 1);
+ if (instance) IresSpritesheet::ResplitSpritesheet(instance, this);
+ }
+
+ if (ImGui::InputInt("Vertical split", &sheetHSplit)) {
+ sheetHSplit = std::max(sheetHSplit, 1);
+ if (instance) IresSpritesheet::ResplitSpritesheet(instance, this);
+ }
+
+ bool frameCountOverriden = frameCountOverride > 0;
+ if (ImGui::Checkbox("##", &frameCountOverriden)) {
+ if (frameCountOverriden) {
+ frameCountOverride = sheetWSplit * sheetHSplit;
+ } else {
+ frameCountOverride = 0;
+ }
+ }
+ ImGui::SameLine();
+ if (frameCountOverriden) {
+ if (ImGui::InputInt("Frame count", &frameCountOverride)) {
+ frameCountOverride = std::max(frameCountOverride, 1);
+ if (instance) IresSpritesheet::ResplitSpritesheet(instance, this);
+ }
+ } else {
+ int dummy = sheetWSplit * sheetHSplit;
+ ImGui::PushDisabled();
+ ImGui::InputInt("Frame count", &dummy, ImGuiInputTextFlags_ReadOnly);
+ ImGui::PopDisabled();
+ }
+
+ if (instance) {
+ auto atlas = instance->GetAtlas();
+ auto imageSize = Utils::FitImage(atlas->GetInfo().size);
+ auto imagePos = ImGui::GetCursorScreenPos();
+ ImGui::Image((ImTextureID)(uintptr_t)atlas->GetHandle(), imageSize);
+
+ auto drawlist = ImGui::GetWindowDrawList();
+ float deltaX = imageSize.x / sheetWSplit;
+ for (int ix = 0; ix < sheetWSplit + 1; ++ix) {
+ float x = ix * deltaX;
+ ImVec2 start{ imagePos.x + x, imagePos.y };
+ ImVec2 end{ imagePos.x + x, imagePos.y + imageSize.y };
+ drawlist->AddLine(start, end, IM_COL32(255, 255, 0, 255));
+ }
+ float deltaY = imageSize.y / sheetHSplit;
+ for (int iy = 0; iy < sheetHSplit + 1; ++iy) {
+ float y = iy * deltaY;
+ ImVec2 start{ imagePos.x, imagePos.y + y };
+ ImVec2 end{ imagePos.x + imageSize.x, imagePos.y + y };
+ drawlist->AddLine(start, end, IM_COL32(255, 255, 0, 255));
+ }
+
+ int i = sheetWSplit * sheetHSplit;
+ int frameCount = GetFrameCount();
+ for (int y = sheetHSplit - 1; y >= 0; --y) {
+ for (int x = sheetWSplit - 1; x >= 0; --x) {
+ if (i > frameCount) {
+ ImVec2 tl{ imagePos.x + x * deltaX + 1.0f, imagePos.y + y * deltaY + 1.0f };
+ ImVec2 br{ imagePos.x + (x + 1) * deltaX, imagePos.y + (y + 1) * deltaY };
+ drawlist->AddRectFilled(tl, br, IM_COL32(255, 0, 0, 100));
+ }
+ --i;
+ }
+ }
+ } else {
+ ImGui::TextUnformatted("Sprite configuration invalid");
+ }
+
+ if (doInvalidateInstance) {
+ InvalidateInstance();
+ }
+}
+
+void IresSpritesheet::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+ IresObject::Write(ctx, value, root);
+ value.AddMember("SpriteSheet", spritesheetFile, root.GetAllocator());
+ value.AddMember("WSplit", sheetWSplit, root.GetAllocator());
+ value.AddMember("HSplit", sheetHSplit, root.GetAllocator());
+ if (frameCountOverride > 0) {
+ value.AddMember("FrameCount", frameCountOverride, root.GetAllocator());
+ }
+}
+
+void IresSpritesheet::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ InvalidateInstance();
+
+ IresObject::Read(ctx, value);
+ BRUSSEL_JSON_GET(value, "SpriteSheet", std::string, spritesheetFile, return );
+ BRUSSEL_JSON_GET(value, "WSplit", int, sheetWSplit, return );
+ BRUSSEL_JSON_GET(value, "HSplit", int, sheetHSplit, return );
+ BRUSSEL_JSON_GET_DEFAULT(value, "FrameCount", int, frameCountOverride, 0);
+}
+
+Sprite::Sprite()
+ : mDefinition(nullptr) {
+}
+
+bool Sprite::IsValid() const {
+ return mDefinition != nullptr;
+}
+
+void Sprite::SetDefinition(SpriteDefinition* definition) {
+ mDefinition.Attach(definition);
+ mCurrentFrame = 0;
+}
+
+int Sprite::GetFrame() const {
+ return mCurrentFrame;
+}
+
+const Subregion& Sprite::GetFrameSubregion() const {
+ return mDefinition->GetFrames()[mCurrentFrame];
+}
+
+void Sprite::SetFrame(int frame) {
+ mCurrentFrame = frame;
+}
+
+void Sprite::PlayFrame() {
+ ++mTimeElapsed;
+ if (mTimeElapsed >= mPlaybackSpeed) {
+ mTimeElapsed -= mPlaybackSpeed;
+
+ int frameCount = mDefinition->GetFrames().size();
+ int nextFrame = (mCurrentFrame + 1) % frameCount;
+ SetFrame(nextFrame);
+ }
+}
+
+int Sprite::GetPlaybackSpeed() const {
+ return mPlaybackSpeed;
+}
+
+void Sprite::SetPlaybackSpeed(int speed) {
+ // TODO
+}
diff --git a/source/30-game/Sprite.hpp b/source/30-game/Sprite.hpp
new file mode 100644
index 0000000..e163a01
--- /dev/null
+++ b/source/30-game/Sprite.hpp
@@ -0,0 +1,111 @@
+#pragma once
+
+#include "Ires.hpp"
+#include "PodVector.hpp"
+#include "RcPtr.hpp"
+#include "Renderer.hpp"
+#include "Texture.hpp"
+
+#include <rapidjson/fwd.h>
+#include <glm/glm.hpp>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class SpriteDefinition : public RefCounted {
+ friend class IresSpriteFiles;
+ friend class IresSpritesheet;
+
+private:
+ RcPtr<Texture> mAtlas;
+ glm::ivec2 mBoundingBox;
+ std::vector<Subregion> mFrames;
+
+public:
+ bool IsValid() const;
+ Texture* GetAtlas() const { return mAtlas.Get(); }
+ glm::ivec2 GetBoundingBox() const { return mBoundingBox; }
+ const decltype(mFrames)& GetFrames() const { return mFrames; }
+};
+
+class IresSpriteFiles : public IresObject {
+public:
+ RcPtr<SpriteDefinition> mInstance;
+ std::vector<std::string> spriteFiles;
+
+public:
+ IresSpriteFiles()
+ : IresObject(KD_SpriteFiles) {}
+
+ // NOTE: does not check whether all specified files have the same dimensions
+ bool IsValid() const;
+
+ SpriteDefinition* CreateInstance() const;
+ SpriteDefinition* GetInstance();
+ void InvalidateInstance();
+
+ void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override;
+ void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override;
+};
+
+class IresSpritesheet : public IresObject {
+public:
+ RcPtr<SpriteDefinition> mInstance;
+ std::string spritesheetFile;
+ int sheetWSplit = 1;
+ int sheetHSplit = 1;
+ int frameCountOverride = 0;
+
+public:
+ IresSpritesheet()
+ : IresObject(KD_Spritesheet) {}
+
+ bool IsValid() const;
+
+ static void ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf);
+ static void ResplitSpritesheet(SpriteDefinition* sprite, int wSplit, int hSplit, int frameCountOverride = -1);
+
+ SpriteDefinition* CreateInstance() const;
+ SpriteDefinition* GetInstance();
+ void InvalidateInstance();
+
+ bool IsFrameCountOverriden() const;
+ int GetFrameCount() const;
+
+ void ShowEditor(IEditor& editor) override;
+
+ void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override;
+ void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override;
+};
+
+// TODO
+class SpriteCollection {
+private:
+ std::vector<SpriteDefinition> mSprites;
+};
+
+class Sprite {
+private:
+ RcPtr<SpriteDefinition> mDefinition;
+ int mCurrentFrame = 0;
+ int mTimeElapsed = 0;
+ // # of frames per second
+ int mPlaybackSpeed = 5;
+
+public:
+ Sprite();
+
+ bool IsValid() const;
+
+ SpriteDefinition* GetDefinition() const { return mDefinition.Get(); }
+ void SetDefinition(SpriteDefinition* definition);
+
+ int GetFrame() const;
+ const Subregion& GetFrameSubregion() const;
+ void SetFrame(int frame);
+ // Update as if a render frame has passed
+ void PlayFrame();
+
+ int GetPlaybackSpeed() const;
+ void SetPlaybackSpeed(int speed);
+};
diff --git a/source/30-game/Texture.cpp b/source/30-game/Texture.cpp
new file mode 100644
index 0000000..6fa7c8a
--- /dev/null
+++ b/source/30-game/Texture.cpp
@@ -0,0 +1,250 @@
+#include "Texture.hpp"
+
+#include "Macros.hpp"
+#include "PodVector.hpp"
+#include "ScopeGuard.hpp"
+
+#include <stb_image.h>
+#include <stb_rect_pack.h>
+#include <bit>
+#include <cstring>
+#include <utility>
+
+Texture::~Texture() {
+ glDeleteTextures(1, &mHandle);
+}
+
+static GLenum MapTextureFilteringToGL(Tags::TexFilter option) {
+ using namespace Tags;
+ switch (option) {
+ case TF_Linear: return GL_LINEAR;
+ case TF_Nearest: return GL_NEAREST;
+ }
+ return 0;
+}
+
+Texture::ErrorCode Texture::InitFromFile(const char* filePath) {
+ if (IsValid()) {
+ return EC_AlreadyInitialized;
+ }
+
+ int width, height;
+ int channels;
+
+ auto result = (uint8_t*)stbi_load(filePath, &width, &height, &channels, 4);
+ if (!result) {
+ return EC_FileIoFailed;
+ }
+ DEFER { stbi_image_free(result); };
+
+ glGenTextures(1, &mHandle);
+ glBindTexture(GL_TEXTURE_2D, mHandle);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result);
+
+ mInfo.size = { width, height };
+
+ return EC_Success;
+}
+
+Texture::ErrorCode Texture::InitFromImage(const Image& image) {
+ if (IsValid()) {
+ return EC_AlreadyInitialized;
+ }
+
+ GLenum sourceFormat;
+ switch (image.GetChannels()) {
+ case 1: sourceFormat = GL_RED; break;
+ case 2: sourceFormat = GL_RG; break;
+ case 3: sourceFormat = GL_RGB; break;
+ case 4: sourceFormat = GL_RGBA; break;
+ default: return EC_InvalidImage;
+ }
+
+ auto size = image.GetSize();
+ uint8_t* dataPtr = image.GetDataPtr();
+
+ glGenTextures(1, &mHandle);
+ glBindTexture(GL_TEXTURE_2D, mHandle);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexImage2D(GL_TEXTURE_2D, 0, sourceFormat, size.x, size.y, 0, sourceFormat, GL_UNSIGNED_BYTE, dataPtr);
+
+ mInfo.size = size;
+
+ return EC_Success;
+}
+
+Texture::ErrorCode Texture::InitAtlas(const AtlasInput& in, AtlasOutput* out) {
+ // Force RGBA for easier time uploading to GL texture
+ constexpr int kDesiredChannels = 4;
+
+ PodVector<stbrp_rect> rects;
+ rects.resize(in.sources.size());
+
+ for (size_t i = 0; i < in.sources.size(); ++i) {
+ auto size = in.sources[i].image.GetSize();
+ auto& rect = rects[i];
+ rect.w = static_cast<stbrp_coord>(size.x);
+ rect.h = static_cast<stbrp_coord>(size.y);
+ }
+
+ int atlasWidth;
+ int atlasHeight;
+
+ // 1. Pack the candidate rectanges onto the (not yet allocated) atlas
+ // Note that the coordinates here are top-left origin
+ switch (in.packingMode) {
+ case PM_KeepSquare: {
+ atlasWidth = 512;
+ atlasHeight = 512;
+
+ PodVector<stbrp_node> nodes;
+ while (true) {
+ // No need to zero initialize stbrp_node, library will take care of that
+ nodes.resize(atlasWidth);
+
+ stbrp_context ctx;
+ stbrp_init_target(&ctx, atlasWidth, atlasHeight, &nodes[0], (int)nodes.size());
+ int result = stbrp_pack_rects(&ctx, rects.data(), (int)rects.size());
+
+ if (result != 1) {
+ atlasWidth *= 2;
+ atlasHeight *= 2;
+ } else {
+ // Break out of the while loop
+ break;
+ }
+ }
+ } break;
+
+ case PM_VerticalExtension:
+ case PM_HorizontalExtension: {
+ constexpr int kMaxHeight = 1024 * 32;
+ atlasWidth = 0;
+ atlasHeight = 0;
+
+ PodVector<stbrp_node> nodes;
+ stbrp_context ctx;
+ stbrp_init_target(&ctx, atlasWidth, atlasHeight, &nodes[0], nodes.size());
+ stbrp_pack_rects(&ctx, rects.data(), rects.size());
+
+ // Calculate width/height needed for atlas
+ auto& limiter = in.packingMode == PM_VerticalExtension ? atlasHeight : atlasWidth;
+ for (auto& rect : rects) {
+ int bottom = rect.y + rect.h;
+ limiter = std::max(limiter, bottom);
+ }
+ limiter = std::bit_ceil<uint32_t>(limiter);
+ } break;
+ }
+
+ // 2. Allocate atlas bitmap
+
+ // Number of bytes in *bitmap*
+ auto bytes = atlasWidth * atlasHeight * kDesiredChannels * sizeof(uint8_t);
+ // Note that the origin (first pixel) is the bottom-left corner, to be consistent with OpenGL
+ auto bitmap = std::make_unique<uint8_t[]>(bytes);
+ std::memset(bitmap.get(), 0, bytes * sizeof(uint8_t));
+
+ // 3. Put all candidate images to the atlas bitmap
+ // TODO don't flip
+ // We essentially flip the candidate images vertically when putting into the atlas bitmap, so that when OpenGL reads
+ // these bytes, it sees the "bottom row" (if talking in top-left origin) first
+ // (empty spots are set with 0, "flipping" doesn't apply to them)
+ //
+ // Conceptually, we flip the atlas bitmap vertically so that the origin is at bottom-left
+ // i.e. all the coordinates we talk (e.g. rect.x/y) are still in top-left origin
+
+ // Unit: bytes
+ size_t bitmapRowStride = atlasWidth * kDesiredChannels * sizeof(uint8_t);
+ for (size_t i = 0; i < in.sources.size(); ++i) {
+ auto& rect = rects[i];
+ // Data is assumed to be stored in top-left origin
+ auto data = in.sources[i].image.GetDataPtr();
+
+ // We need to copy row by row, because the candidate image bytes won't land in a continuous chunk in our atlas bitmap
+ // Unit: bytes
+ size_t incomingRowStride = rect.w * kDesiredChannels * sizeof(uint8_t);
+ // Unit: bytes
+ size_t bitmapX = rect.x * kDesiredChannels * sizeof(uint8_t);
+ for (int y = 0; y < rect.h; ++y) {
+ auto src = data + y * incomingRowStride;
+
+ int bitmapY = y;
+ auto dst = bitmap.get() + bitmapY * bitmapRowStride + bitmapX;
+
+ std::memcpy(dst, src, incomingRowStride);
+ }
+ }
+
+ // 4. Upload to VRAM
+ GLuint atlasTexture;
+ glGenTextures(1, &atlasTexture);
+ glBindTexture(GL_TEXTURE_2D, atlasTexture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, atlasWidth, atlasHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap.get());
+
+ // 5. Generate atlas texture info
+ mHandle = atlasTexture;
+ mInfo.size = { atlasWidth, atlasHeight };
+
+ // 6. Generate output information
+ if (out) {
+ out->elements.reserve(in.sources.size());
+ for (size_t i = 0; i < in.sources.size(); ++i) {
+ auto& rect = rects[i];
+ auto& source = in.sources[i];
+ out->elements.push_back(AltasElement{
+ .name = source.name,
+ .subregion = Subregion{
+ .u0 = (float)(rect.x) / atlasWidth,
+ .v0 = (float)(rect.y + rect.h) / atlasHeight,
+ .u1 = (float)(rect.x + rect.w) / atlasWidth,
+ .v1 = (float)(rect.y) / atlasHeight,
+ },
+ .subregionSize = glm::ivec2(rect.w, rect.h),
+ });
+ }
+ }
+
+ return EC_Success;
+}
+
+const TextureInfo& Texture::GetInfo() const {
+ return mInfo;
+}
+
+GLuint Texture::GetHandle() const {
+ return mHandle;
+}
+
+bool Texture::IsValid() const {
+ return mHandle != 0;
+}
+
+Texture* IresTexture::CreateInstance() const {
+ return new Texture();
+}
+
+Texture* IresTexture::GetInstance() {
+ if (mInstance == nullptr) {
+ mInstance.Attach(CreateInstance());
+ }
+ return mInstance.Get();
+}
+
+void IresTexture::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const {
+ IresObject::Write(ctx, value, root);
+ // TODO
+}
+
+void IresTexture::Read(IresLoadingContext& ctx, const rapidjson::Value& value) {
+ IresObject::Read(ctx, value);
+ // TODO
+}
diff --git a/source/30-game/Texture.hpp b/source/30-game/Texture.hpp
new file mode 100644
index 0000000..108dfa7
--- /dev/null
+++ b/source/30-game/Texture.hpp
@@ -0,0 +1,99 @@
+#pragma once
+
+#include "GraphicsTags.hpp"
+#include "Image.hpp"
+#include "Ires.hpp"
+#include "RcPtr.hpp"
+
+#include <glad/glad.h>
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <span>
+
+// TODO abstract texture traits such as component sizes from OpenGL
+
+struct Subregion {
+ float u0 = 0.0f;
+ float v0 = 0.0f;
+ float u1 = 0.0f;
+ float v1 = 0.0f;
+};
+
+struct TextureInfo {
+ glm::ivec2 size;
+ Tags::TexFilter minifyingFilter = Tags::TF_Linear;
+ Tags::TexFilter magnifyingFilter = Tags::TF_Linear;
+};
+
+class Texture : public RefCounted {
+ friend class TextureStitcher;
+
+private:
+ TextureInfo mInfo;
+ GLuint mHandle = 0;
+
+public:
+ Texture() = default;
+ ~Texture();
+
+ Texture(const Texture&) = delete;
+ Texture& operator=(const Texture&) = delete;
+ Texture(Texture&&) = default;
+ Texture& operator=(Texture&&) = default;
+
+ enum ErrorCode {
+ EC_Success,
+ EC_AlreadyInitialized,
+ EC_FileIoFailed,
+ EC_InvalidImage,
+ };
+
+ ErrorCode InitFromFile(const char* filePath);
+ ErrorCode InitFromImage(const Image& image);
+
+ struct AtlasSource {
+ std::string name;
+ Image image;
+ };
+
+ struct AltasElement {
+ std::string name;
+ Subregion subregion;
+ glm::ivec2 subregionSize;
+ };
+
+ enum PackingMode {
+ PM_KeepSquare,
+ PM_VerticalExtension,
+ PM_HorizontalExtension,
+ };
+
+ struct AtlasInput {
+ std::span<AtlasSource> sources;
+ PackingMode packingMode;
+ };
+ struct AtlasOutput {
+ std::vector<AltasElement> elements;
+ };
+ ErrorCode InitAtlas(const AtlasInput& in, AtlasOutput* out = nullptr);
+
+ const TextureInfo& GetInfo() const;
+ GLuint GetHandle() const;
+
+ bool IsValid() const;
+};
+
+class IresTexture : public IresObject {
+public:
+ RcPtr<Texture> mInstance;
+
+public:
+ IresTexture()
+ : IresObject(KD_Texture) {}
+
+ Texture* CreateInstance() const;
+ Texture* GetInstance();
+
+ void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override;
+ void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override;
+};
diff --git a/source/30-game/VertexIndex.cpp b/source/30-game/VertexIndex.cpp
new file mode 100644
index 0000000..ac68289
--- /dev/null
+++ b/source/30-game/VertexIndex.cpp
@@ -0,0 +1,84 @@
+#include "VertexIndex.hpp"
+
+#include <algorithm>
+
+GpuVertexBuffer::GpuVertexBuffer() {
+ glGenBuffers(1, &handle);
+}
+
+GpuVertexBuffer::~GpuVertexBuffer() {
+ glDeleteBuffers(1, &handle);
+}
+
+void GpuVertexBuffer::Upload(const std::byte* data, size_t sizeInBytes) {
+ glBindBuffer(GL_ARRAY_BUFFER, handle);
+ glBufferData(GL_ARRAY_BUFFER, sizeInBytes, data, GL_DYNAMIC_DRAW);
+}
+
+GpuIndexBuffer::GpuIndexBuffer() {
+ glGenBuffers(1, &handle);
+}
+
+GpuIndexBuffer::~GpuIndexBuffer() {
+ glDeleteBuffers(1, &handle);
+}
+
+void GpuIndexBuffer::Upload(const std::byte* data, Tags::IndexType type, size_t count) {
+ this->indexType = type;
+ this->count = count;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * Tags::SizeOf(type), data, GL_DYNAMIC_DRAW);
+}
+
+int BufferBindings::GetMaxBindingIndex() const {
+ return bindings.size() - 1;
+}
+
+GpuVertexBuffer* BufferBindings::GetBinding(int index) const {
+ if (index >= 0 && index < bindings.size()) {
+ return bindings[index].Get();
+ } else {
+ return nullptr;
+ }
+}
+
+void BufferBindings::SetBinding(int index, GpuVertexBuffer* buffer) {
+ int maxBindingIndex = GetMaxBindingIndex();
+ if (index > maxBindingIndex) {
+ int countDelta = index - maxBindingIndex;
+ bindings.resize(bindings.size() + countDelta);
+ }
+
+ bindings[index].Attach(buffer);
+ if (index == maxBindingIndex && buffer == nullptr) {
+ bindings.pop_back();
+ }
+}
+
+void BufferBindings::Clear() {
+ bindings.clear();
+}
+
+int VertexElementFormat::GetStride() const {
+ return Tags::SizeOf(type);
+}
+
+void VertexFormat::AddElement(VertexElementFormat element) {
+ vertexSize += element.GetStride();
+
+ int lastIdx = (int)elements.size() - 1;
+ if (lastIdx >= 0) {
+ auto& last = elements[lastIdx];
+ element.offset = last.offset + last.GetStride();
+ } else {
+ element.offset = 0;
+ }
+
+ elements.push_back(std::move(element));
+}
+
+void VertexFormat::RemoveElement(int index) {
+ auto& element = elements[index];
+ vertexSize -= element.GetStride();
+ elements.erase(elements.begin() + index);
+}
diff --git a/source/30-game/VertexIndex.hpp b/source/30-game/VertexIndex.hpp
new file mode 100644
index 0000000..2d65617
--- /dev/null
+++ b/source/30-game/VertexIndex.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "GraphicsTags.hpp"
+#include "RcPtr.hpp"
+#include "SmallVector.hpp"
+
+#include <glad/glad.h>
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+struct GpuVertexBuffer : public RefCounted {
+ GLuint handle;
+ int sizeInBytes;
+
+ GpuVertexBuffer();
+ ~GpuVertexBuffer();
+
+ void Upload(const std::byte* data, size_t sizeInBytes);
+};
+
+struct GpuIndexBuffer : public RefCounted {
+ GLuint handle;
+ Tags::IndexType indexType;
+ int count;
+
+ GpuIndexBuffer();
+ ~GpuIndexBuffer();
+
+ Tags::IndexType GetIndexType() const { return indexType; }
+ GLenum GetIndexTypeGL() const { return Tags::FindGLType(indexType); }
+
+ void Upload(const std::byte* data, Tags::IndexType type, size_t count);
+};
+
+struct BufferBindings {
+ SmallVector<RcPtr<GpuVertexBuffer>, 4> bindings;
+
+ int GetMaxBindingIndex() const;
+
+ /// Safe. Returns nullptr if the index is not bound to any buffers.
+ GpuVertexBuffer* GetBinding(int index) const;
+ /// Adds or updates a buffer binding. Setting a binding to nullptr effectively removes the binding.
+ void SetBinding(int index, GpuVertexBuffer* buffer);
+ void Clear();
+};
+
+struct VertexElementFormat {
+ /// NOTE:
+ /// "Automatic" means it will be set inside VertexFormat::AddElement()
+ /// "Parameter" means it must be set by the user
+ /* Automatic */ int offset;
+ /* Parameter */ int bindingIndex;
+ /* Parameter */ Tags::VertexElementType type;
+ /* Parameter */ Tags::VertexElementSemantic semantic;
+
+ int GetStride() const;
+};
+
+struct VertexFormat : public RefCounted {
+ SmallVector<VertexElementFormat, 4> elements;
+ int vertexSize = 0;
+
+ const decltype(elements)& GetElements() const { return elements; }
+ void AddElement(VertexElementFormat element);
+ void RemoveElement(int index);
+};
diff --git a/source/30-game/World.cpp b/source/30-game/World.cpp
new file mode 100644
index 0000000..83b9a10
--- /dev/null
+++ b/source/30-game/World.cpp
@@ -0,0 +1,79 @@
+#include "World.hpp"
+
+#include "GameObject.hpp"
+#include "PodVector.hpp"
+
+#include <glad/glad.h>
+
+namespace ProjectBrussel_UNITY_ID {
+template <typename TFunction>
+void CallGameObjectRecursive(GameObject* start, TFunction&& func) {
+ PodVector<GameObject*> stack;
+ stack.push_back(start);
+
+ while (!stack.empty()) {
+ auto obj = stack.back();
+ stack.pop_back();
+
+ for (auto child : obj->GetChildren()) {
+ stack.push_back(child);
+ }
+
+ func(obj);
+ }
+}
+} // namespace ProjectBrussel_UNITY_ID
+
+GameWorld::GameWorld()
+ : mRoot{ new GameObject(this) } {
+}
+
+GameWorld::~GameWorld() {
+ if (mAwakened) {
+ Resleep();
+ }
+
+ delete mRoot;
+}
+
+const GameObject& GameWorld::GetRoot() const {
+ return *mRoot;
+};
+
+void GameWorld::Awaken() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (mAwakened) {
+ return;
+ }
+
+ CallGameObjectRecursive(mRoot, [](GameObject* obj) { obj->Awaken(); });
+ mAwakened = true;
+}
+
+void GameWorld::Resleep() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ if (!mAwakened) {
+ return;
+ }
+
+ CallGameObjectRecursive(mRoot, [](GameObject* obj) { obj->Resleep(); });
+ mAwakened = false;
+}
+
+void GameWorld::Update() {
+ using namespace ProjectBrussel_UNITY_ID;
+
+ CallGameObjectRecursive(mRoot, [this](GameObject* obj) {
+ obj->Update();
+ });
+}
+
+GameObject& GameWorld::GetRoot() {
+ return *mRoot;
+}
+
+bool GameWorld::IsAwake() const {
+ return mAwakened;
+}
diff --git a/source/30-game/World.hpp b/source/30-game/World.hpp
new file mode 100644
index 0000000..288142e
--- /dev/null
+++ b/source/30-game/World.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+class GameObject;
+class GameWorld {
+private:
+ GameObject* mRoot;
+ bool mAwakened = false;
+
+public:
+ GameWorld();
+ ~GameWorld();
+
+ GameWorld(const GameWorld&) = delete;
+ GameWorld& operator=(const GameWorld&) = delete;
+ GameWorld(GameWorld&&) = default;
+ GameWorld& operator=(GameWorld&&) = default;
+
+ bool IsAwake() const;
+ void Awaken();
+ void Resleep();
+ void Update();
+
+ const GameObject& GetRoot() const;
+ GameObject& GetRoot();
+};
diff --git a/source/30-game/main.cpp b/source/30-game/main.cpp
new file mode 100644
index 0000000..30ba9a6
--- /dev/null
+++ b/source/30-game/main.cpp
@@ -0,0 +1,545 @@
+#include "App.hpp"
+
+#include "AppConfig.hpp"
+#include "CommonVertexIndex.hpp"
+#include "ImGuiGuizmo.hpp"
+#include "Input.hpp"
+#include "Ires.hpp"
+#include "Level.hpp"
+#include "Log.hpp"
+#include "Material.hpp"
+#include "Shader.hpp"
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+
+#include <backends/imgui_impl_glfw.h>
+#include <backends/imgui_impl_opengl2.h>
+#include <backends/imgui_impl_opengl3.h>
+#include <glad/glad.h>
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <cstdlib>
+#include <cxxopts.hpp>
+#include <filesystem>
+#include <string>
+
+#include <tracy/Tracy.hpp>
+#include <tracy/TracyClient.cpp>
+
+namespace fs = std::filesystem;
+using namespace std::literals;
+
+struct GlfwUserData {
+ App* app = nullptr;
+};
+
+void GlfwErrorCallback(int error, const char* description) {
+ fprintf(stderr, "[GLFW] Error %d: %s\n", error, description);
+}
+
+void GlfwKeyboardCallback(GLFWkeyboard* keyboard, int event) {
+ if (InputState::instance == nullptr) {
+ // Called before initialization, skipping because we'll do a collect pass anyways when initializing
+ return;
+ }
+
+ switch (event) {
+ case GLFW_CONNECTED: {
+ InputState::instance->ConnectKeyboard(keyboard);
+ } break;
+
+ case GLFW_DISCONNECTED: {
+ InputState::instance->DisconnectKeyboard(keyboard);
+ } break;
+ }
+}
+
+void OpenGLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) {
+ fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), type, severity, message);
+}
+
+void GlfwFramebufferResizeCallback(GLFWwindow* window, int width, int height) {
+ AppConfig::mainWindowWidth = width;
+ AppConfig::mainWindowHeight = height;
+ AppConfig::mainWindowAspectRatio = (float)width / height;
+}
+
+void GlfwMouseCallback(GLFWwindow* window, int button, int action, int mods) {
+ if (ImGui::GetIO().WantCaptureMouse) {
+ return;
+ }
+
+ auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window));
+ auto app = userData->app;
+ app->HandleMouse(button, action);
+}
+
+void GlfwMouseMotionCallback(GLFWwindow* window, double xOff, double yOff) {
+ if (ImGui::GetIO().WantCaptureMouse) {
+ return;
+ }
+
+ auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window));
+ auto app = userData->app;
+ app->HandleMouseMotion(xOff, yOff);
+}
+
+void GlfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
+ if (ImGui::GetIO().WantCaptureKeyboard) {
+ return;
+ }
+
+ GLFWkeyboard* keyboard = glfwGetLastActiveKeyboard();
+ if (keyboard) {
+ auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window));
+ auto app = userData->app;
+ app->HandleKey(keyboard, key, action);
+ }
+}
+
+// For platform data path selection below
+// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c
+#if defined(_WIN32)
+# if defined(__MINGW32__)
+# include <ShlObj.h>
+# else
+# include <ShlObj_core.h>
+# endif
+# include <objbase.h>
+# pragma comment(lib, "shell32.lib")
+# pragma comment(lib, "ole32.lib")
+#elif defined(__linux__)
+fs::path GetEnvVar(const char* name, const char* backup) {
+ if (const char* path = std::getenv(name)) {
+ fs::path dataDir(path);
+ fs::create_directories(dataDir);
+ return dataDir;
+ } else {
+ fs::path dataDir(backup);
+ fs::create_directories(dataDir);
+ return dataDir;
+ }
+}
+#endif
+
+int main(int argc, char* argv[]) {
+ using namespace Tags;
+
+#if BRUSSEL_DEV_ENV
+ Log::gDefaultBuffer.messages.resize(1024);
+ Log::gDefaultBufferId = Log::RegisterBuffer(Log::gDefaultBuffer);
+#endif
+
+ constexpr auto kOpenGLDebug = "opengl-debug";
+ constexpr auto kImGuiBackend = "imgui-backend";
+ constexpr auto kGameDataDir = "game-data-dir";
+ constexpr auto kGameAssetDir = "game-asset-dir";
+
+ cxxopts::Options options(std::string(AppConfig::kAppName), "");
+ // clang-format off
+ options.add_options()
+ (kOpenGLDebug, "Enable OpenGL debugging messages.")
+ (kImGuiBackend, "ImGui backend. Options: opengl2, opengl3. Leave empty to default.", cxxopts::value<std::string>())
+ (kGameAssetDir, "Directory in which assets are looked up from. Can be relative paths to the executable.", cxxopts::value<std::string>()->default_value("."))
+ (kGameDataDir, "Directory in which game data (such as saves and options) are saved to. Leave empty to use the default directory on each platform.", cxxopts::value<std::string>())
+ ;
+ // clang-format on
+ auto args = options.parse(argc, argv);
+
+ bool imguiUseOpenGL3;
+ if (args.count(kImGuiBackend) > 0) {
+ auto imguiBackend = args[kImGuiBackend].as<std::string>();
+ if (imguiBackend == "opengl2") {
+ imguiUseOpenGL3 = false;
+ } else if (imguiBackend == "opengl3") {
+ imguiUseOpenGL3 = true;
+ } else {
+ // TODO support more backends?
+ imguiUseOpenGL3 = true;
+ }
+ } else {
+ imguiUseOpenGL3 = true;
+ }
+
+ if (args.count(kGameAssetDir) > 0) {
+ auto assetDir = args[kGameAssetDir].as<std::string>();
+
+ fs::path assetDirPath(assetDir);
+ if (!fs::exists(assetDirPath)) {
+ fprintf(stderr, "Invalid asset directory.\n");
+ return -4;
+ }
+
+ AppConfig::assetDir = std::move(assetDir);
+ AppConfig::assetDirPath = std::move(assetDirPath);
+ } else {
+ AppConfig::assetDir = ".";
+ AppConfig::assetDirPath = fs::path(".");
+ }
+
+ if (args.count(kGameDataDir) > 0) {
+ auto dataDir = args[kGameDataDir].as<std::string>();
+
+ fs::path dataDirPath(dataDir);
+ fs::create_directories(dataDir);
+
+ AppConfig::dataDir = std::move(dataDir);
+ AppConfig::dataDirPath = std::move(dataDirPath);
+ } else {
+#if BRUSSEL_DEV_ENV
+ AppConfig::dataDir = ".";
+ AppConfig::dataDirPath = fs::path(".");
+#else
+// In a regular build, use default platform data paths
+# if defined(_WIN32)
+ fs::path dataDirPath;
+
+ PWSTR path = nullptr;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path);
+ if (SUCCEEDED(hr)) {
+ dataDirPath = fs::path(path) / AppConfig::kAppName;
+ CoTaskMemFree(path);
+
+ fs::create_directories(dataDirPath);
+ } else {
+ std::string msg;
+ msg += "Failed to find/create the default user data directory at %APPDATA%. Error code: ";
+ msg += hr;
+ throw std::runtime_error(msg);
+ }
+# elif defined(__APPLE__)
+ // MacOS programming guide recommends apps to hardcode the path - user customization of "where data are stored" is done in Finder
+ auto dataDirPath = fs::path("~/Library/Application Support/") / AppConfig::kAppName;
+ fs::create_directories(dataDirPath);
+# elif defined(__linux__)
+ auto dataDirPath = GetEnvVar("XDG_DATA_HOME", "~/.local/share") / AppConfig::kAppName;
+ fs::create_directories(dataDirPath);
+# endif
+ AppConfig::dataDir = dataDirPath.string();
+ AppConfig::dataDirPath = dataDirPath;
+#endif
+ }
+
+ if (!glfwInit()) {
+ return -1;
+ }
+
+ glfwSetErrorCallback(&GlfwErrorCallback);
+ glfwSetKeyboardCallback(&GlfwKeyboardCallback);
+
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+ glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
+
+#if defined(__APPLE__)
+ const char* imguiGlslVersion = "#version 150";
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac
+#else
+ const char* imguiGlslVersion = "#version 130";
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
+#endif
+
+ GlfwUserData glfwUserData;
+
+ GLFWwindow* window = glfwCreateWindow(1280, 720, AppConfig::kAppNameC, nullptr, nullptr);
+ if (window == nullptr) {
+ return -2;
+ }
+
+ glfwSetWindowUserPointer(window, &glfwUserData);
+
+ // Window callbacks are retained by ImGui GLFW backend
+ glfwSetFramebufferSizeCallback(window, &GlfwFramebufferResizeCallback);
+ glfwSetKeyCallback(window, &GlfwKeyCallback);
+ glfwSetMouseButtonCallback(window, &GlfwMouseCallback);
+ glfwSetCursorPosCallback(window, &GlfwMouseMotionCallback);
+
+ {
+ int width, height;
+ glfwGetFramebufferSize(window, &width, &height);
+ GlfwFramebufferResizeCallback(window, width, height);
+ }
+
+ glfwMakeContextCurrent(window);
+ glfwSwapInterval(1);
+
+ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
+ return -3;
+ }
+
+#if defined(BRUSSEL_DEV_ENV)
+ auto glVersionString = glGetString(GL_VERSION);
+
+ int glMajorVersion;
+ glGetIntegerv(GL_MAJOR_VERSION, &glMajorVersion);
+ int glMinorVersion;
+ glGetIntegerv(GL_MINOR_VERSION, &glMinorVersion);
+
+ printf("OpenGL version (via glGetString(GL_VERSION)): %s\n", glVersionString);
+ printf("OpenGL version (via glGetIntegerv() with GL_MAJOR_VERSION and GL_MINOR_VERSION): %d.%d\n", glMajorVersion, glMinorVersion);
+#endif
+
+ bool useOpenGLDebug = args[kOpenGLDebug].as<bool>();
+ if (useOpenGLDebug) {
+ printf("Using OpenGL debugging\n --%s", kOpenGLDebug);
+
+ // TODO check extension KHR_debug availability
+ // TODO conan glad is not including any extensions
+ // NOTE: KHR_debug is a core extension, which means it may be available in lower version even though the feature is added in 4.3
+
+ glEnable(GL_DEBUG_OUTPUT);
+ glDebugMessageCallback(&OpenGLDebugCallback, 0);
+ }
+
+ IMGUI_CHECKVERSION();
+ auto ctx = ImGui::CreateContext();
+ auto& io = ImGui::GetIO();
+ ImGuizmo::SetImGuiContext(ctx);
+
+ ImGui_ImplGlfw_InitForOpenGL(window, true);
+ if (imguiUseOpenGL3) {
+ ImGui_ImplOpenGL3_Init(imguiGlslVersion);
+ } else {
+ ImGui_ImplOpenGL2_Init();
+ }
+
+ InputState::instance = new InputState();
+ {
+ int count;
+ GLFWkeyboard** list = glfwGetKeyboards(&count);
+ for (int i = 0; i < count; ++i) {
+ GLFWkeyboard* keyboard = list[i];
+ InputState::instance->ConnectKeyboard(keyboard);
+ }
+ }
+
+ IresManager::instance = new IresManager();
+ IresManager::instance->DiscoverFilesDesignatedLocation();
+
+ LevelManager::instance = new LevelManager();
+ LevelManager::instance->DiscoverFilesDesignatedLocation();
+
+ gVformatStandard.Attach(new VertexFormat());
+ gVformatStandard->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Float3,
+ .semantic = VES_Position,
+ });
+ gVformatStandard->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Float2,
+ .semantic = VES_TexCoords1,
+ });
+ gVformatStandard->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Ubyte4Norm,
+ .semantic = VES_Color1,
+ });
+
+ gVformatStandardSplit.Attach(new VertexFormat());
+ gVformatStandardSplit->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Float3,
+ .semantic = VES_Position,
+ });
+ gVformatStandardSplit->AddElement(VertexElementFormat{
+ .bindingIndex = 1,
+ .type = VET_Float2,
+ .semantic = VES_TexCoords1,
+ });
+ gVformatStandardSplit->AddElement(VertexElementFormat{
+ .bindingIndex = 1,
+ .type = VET_Ubyte4Norm,
+ .semantic = VES_Color1,
+ });
+
+ gVformatLines.Attach(new VertexFormat());
+ gVformatLines->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Float3,
+ .semantic = VES_Position,
+ });
+ gVformatLines->AddElement(VertexElementFormat{
+ .bindingIndex = 0,
+ .type = VET_Ubyte4Norm,
+ .semantic = VES_Color1,
+ });
+
+ // Matches gVformatStandard
+ gDefaultShader.Attach(new Shader());
+ gDefaultShader->InitFromSources(Shader::ShaderSources{
+ .vertex = R"""(
+#version 330 core
+layout(location = 0) in vec3 pos;
+layout(location = 1) in vec4 color;
+out vec4 v2fColor;
+uniform mat4 transform;
+void main() {
+ gl_Position = transform * vec4(pos, 1.0);
+ v2fColor = color;
+}
+)"""sv,
+ .fragment = R"""(
+#version 330 core
+in vec4 v2fColor;
+out vec4 fragColor;
+void main() {
+ fragColor = v2fColor;
+}
+)"""sv,
+ });
+ { // in vec3 pos;
+ ShaderMathVariable var;
+ var.scalarType = GL_FLOAT;
+ var.width = 1;
+ var.height = 3;
+ var.arrayLength = 1;
+ var.semantic = VES_Position;
+ var.location = 0;
+ gDefaultShader->GetInfo().inputs.push_back(std::move(var));
+ gDefaultShader->GetInfo().things.try_emplace(
+ "pos"s,
+ ShaderThingId{
+ .kind = ShaderThingId::KD_Input,
+ .index = (int)gDefaultShader->GetInfo().inputs.size() - 1,
+ });
+ }
+ { // in vec4 color;
+ ShaderMathVariable var;
+ var.scalarType = GL_FLOAT;
+ var.width = 1;
+ var.height = 4;
+ var.arrayLength = 1;
+ var.semantic = VES_Color1;
+ var.location = 1;
+ gDefaultShader->GetInfo().inputs.push_back(std::move(var));
+ gDefaultShader->GetInfo().things.try_emplace(
+ "color"s,
+ ShaderThingId{
+ .kind = ShaderThingId::KD_Input,
+ .index = (int)gDefaultShader->GetInfo().inputs.size() - 1,
+ });
+ }
+ { // out vec4 fragColor;
+ ShaderMathVariable var;
+ var.scalarType = GL_FLOAT;
+ var.width = 1;
+ var.height = 4;
+ var.arrayLength = 1;
+ gDefaultShader->GetInfo().outputs.push_back(std::move(var));
+ gDefaultShader->GetInfo().things.try_emplace(
+ "fragColor"s,
+ ShaderThingId{
+ .kind = ShaderThingId::KD_Output,
+ .index = (int)gDefaultShader->GetInfo().outputs.size() - 1,
+ });
+ }
+ // NOTE: autofill uniforms not recorded here
+
+ gDefaultMaterial.Attach(new Material());
+ gDefaultMaterial->SetShader(gDefaultShader.Get());
+
+ { // Main loop
+ App app;
+ glfwUserData.app = &app;
+
+ // NOTE: don't enable backface culling, because the game mainly runs in 2D and sometimes we'd like to flip sprites around
+ // it also helps with debugging layers in 3D view
+ glEnable(GL_DEPTH_TEST);
+
+ // 60 updates per second
+ constexpr double kMsPerUpdate = 1000.0 / 60;
+ constexpr double kSecondsPerUpdate = kMsPerUpdate / 1000;
+ double prevTime = glfwGetTime();
+ double accumulatedTime = 0.0;
+ while (!glfwWindowShouldClose(window)) {
+ {
+ ZoneScopedN("GameInput");
+ glfwPollEvents();
+ }
+
+ double currTime = glfwGetTime();
+ double deltaTime = prevTime - currTime;
+
+ // In seconds
+ accumulatedTime += currTime - prevTime;
+
+ // Update
+ // Play "catch up" to ensure a deterministic number of Update()'s per second
+ while (accumulatedTime >= kSecondsPerUpdate) {
+ double beg = glfwGetTime();
+ {
+ ZoneScopedN("GameUpdate");
+ app.Update();
+ }
+ double end = glfwGetTime();
+
+ // Update is taking longer than it should be, start skipping updates
+ auto diff = end - beg;
+ if (diff >= kSecondsPerUpdate) {
+ auto skippedUpdates = (int)(accumulatedTime / kSecondsPerUpdate);
+ accumulatedTime = 0.0;
+ fprintf(stderr, "Elapsed time %f, skipped %d updates.", diff, skippedUpdates);
+ } else {
+ accumulatedTime -= kSecondsPerUpdate;
+ }
+ }
+
+ int fbWidth = AppConfig::mainWindowWidth;
+ int fbHeight = AppConfig::mainWindowHeight;
+ glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
+ glViewport(0, 0, fbWidth, fbHeight);
+ auto clearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
+ glClearColor(clearColor.x * clearColor.w, clearColor.y * clearColor.w, clearColor.z * clearColor.w, clearColor.w);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ { // Regular draw
+ ZoneScopedN("Render");
+ app.Draw(currTime, deltaTime);
+ }
+
+ { // ImGui draw
+ ZoneScopedN("ImGui");
+ if (imguiUseOpenGL3) {
+ ImGui_ImplOpenGL3_NewFrame();
+ } else {
+ ImGui_ImplOpenGL2_NewFrame();
+ }
+ ImGui_ImplGlfw_NewFrame();
+ ImGui::NewFrame();
+
+ app.Show();
+
+ ImGui::Render();
+ if (imguiUseOpenGL3) {
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+ } else {
+ ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData());
+ }
+ }
+
+ glfwSwapBuffers(window);
+ FrameMark;
+
+ prevTime = currTime;
+ }
+ }
+
+ if (imguiUseOpenGL3) {
+ ImGui_ImplOpenGL3_Shutdown();
+ } else {
+ ImGui_ImplOpenGL2_Shutdown();
+ }
+ ImGui_ImplGlfw_Shutdown();
+
+ ImGui::DestroyContext();
+
+ glfwDestroyWindow(window);
+ glfwTerminate();
+
+ return 0;
+}