From 88e33be91b43ae7d8e35c582a00ba22d9474d82f Mon Sep 17 00:00:00 2001 From: Connor Date: Fri, 20 Feb 2026 22:50:05 +0900 Subject: [PATCH] initial commit --- CMakeLists.txt | 49 +- graph.json | 1 + imgui.ini | 8 + include/Components/Misc.hpp | 13 + include/Components/WorldGraph.hpp | 91 +++ include/Core/Chunk.h | 6 +- include/Types/WorldGraph/WorldGraph.h | 42 +- .../Types/WorldGraph/WorldGraphAllocator.h | 3 +- include/Types/WorldGraph/WorldGraphNode.h | 743 +++++------------- .../Types/WorldGraph/WorldGraphVisualNode.h | 204 +---- include/Util/FastNoiseLite.h | 4 +- src/Components/WorldGraph.cpp | 208 +++++ src/Core/WorldInstance.cpp | 2 + tools/node-editor/CMakeLists.txt | 44 ++ tools/node-editor/README.md | 96 +++ tools/node-editor/main.cpp | 492 ++++++++++++ 16 files changed, 1284 insertions(+), 722 deletions(-) create mode 100644 graph.json create mode 100644 imgui.ini create mode 100644 include/Components/WorldGraph.hpp create mode 100644 src/Components/WorldGraph.cpp create mode 100644 tools/node-editor/CMakeLists.txt create mode 100644 tools/node-editor/README.md create mode 100644 tools/node-editor/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3459336..4d9b079 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,21 +23,46 @@ FetchContent_Declare( GIT_SHALLOW TRUE ) -FetchContent_MakeAvailable(flecs doctest) +# GLFW (windowing for node-editor tool) +FetchContent_Declare( + glfw + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.4 + GIT_SHALLOW TRUE +) +set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + +# Dear ImGui +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.91.6 + GIT_SHALLOW TRUE +) + +# imnodes +FetchContent_Declare( + imnodes + GIT_REPOSITORY https://github.com/Nelarius/imnodes.git + GIT_TAG v0.5 + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(flecs doctest glfw imgui imnodes) + +# ── Core library ────────────────────────────────────────────────────────────── -# Only compile sources needed for the core library set(SOURCES src/Components/Config/WorldConfig.cpp src/Components/Support.cpp + src/Components/WorldGraph.cpp src/Core/WorldInstance.cpp ) add_library(factory-hole-core ${SOURCES}) -# Main executable -add_executable(factory-hole-app src/main.cpp) -target_link_libraries(factory-hole-app PRIVATE factory-hole-core) - target_include_directories(factory-hole-core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) @@ -47,7 +72,13 @@ target_link_libraries(factory-hole-core PUBLIC doctest::doctest ) -# Tests +# ── Main executable ─────────────────────────────────────────────────────────── + +add_executable(factory-hole-app src/main.cpp) +target_link_libraries(factory-hole-app PRIVATE factory-hole-core) + +# ── Tests ───────────────────────────────────────────────────────────────────── + enable_testing() file(GLOB_RECURSE TEST_SOURCES tests/*.cpp) @@ -64,3 +95,7 @@ target_link_libraries(factory-hole-tests PRIVATE ) add_test(NAME factory-hole-tests COMMAND factory-hole-tests) + +# ── Node editor tool ────────────────────────────────────────────────────────── + +add_subdirectory(tools/node-editor) diff --git a/graph.json b/graph.json new file mode 100644 index 0000000..be4ac1f --- /dev/null +++ b/graph.json @@ -0,0 +1 @@ +{"results":[{"name":"Graph", "id":634}, {"name":"VisualNodeOutput", "id":25, "components":{"flecs.core.Component":{"size":0, "alignment":0}, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"VisualNodeOutput"}}}, {"name":"InputPin0", "id":26, "components":{"flecs.core.Component":{"size":0, "alignment":0}, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"InputPin0"}}}, {"name":"InputPin1", "id":27, "components":{"flecs.core.Component":{"size":0, "alignment":0}, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"InputPin1"}}}, {"name":"InputPin2", "id":28, "components":{"flecs.core.Component":{"size":0, "alignment":0}, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"InputPin2"}}}, {"name":"VisualNodeKind", "id":21, "tags":["flecs.core.PairIsTag", "flecs.core.Exclusive", "flecs.core.OneOf"], "components":{"flecs.core.Component":{"size":4, "alignment":4}, "flecs.meta.type":{"kind":"EnumType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.enum":{"underlying_type":"flecs.meta.u32"}, "flecs.meta.constants":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"VisualNodeKind"}}}, {"name":"TileType", "id":22, "tags":["flecs.core.PairIsTag", "flecs.core.Exclusive", "flecs.core.OneOf"], "components":{"flecs.core.Component":{"size":1, "alignment":1}, "flecs.meta.type":{"kind":"EnumType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.enum":{"underlying_type":"flecs.meta.u8"}, "flecs.meta.constants":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"TileType"}}}, {"parent":"VisualNodeKind", "name":"Add", "id":578, "components":{"(flecs.core.constant,flecs.meta.u32)":0}}, {"parent":"VisualNodeKind", "name":"Subtract", "id":579, "components":{"(flecs.core.constant,flecs.meta.u32)":1}}, {"parent":"VisualNodeKind", "name":"Multiply", "id":580, "components":{"(flecs.core.constant,flecs.meta.u32)":2}}, {"parent":"VisualNodeKind", "name":"Divide", "id":581, "components":{"(flecs.core.constant,flecs.meta.u32)":3}}, {"parent":"VisualNodeKind", "name":"Modulo", "id":582, "components":{"(flecs.core.constant,flecs.meta.u32)":4}}, {"parent":"VisualNodeKind", "name":"Pow", "id":583, "components":{"(flecs.core.constant,flecs.meta.u32)":5}}, {"parent":"VisualNodeKind", "name":"Max", "id":584, "components":{"(flecs.core.constant,flecs.meta.u32)":6}}, {"parent":"VisualNodeKind", "name":"Min", "id":585, "components":{"(flecs.core.constant,flecs.meta.u32)":7}}, {"parent":"VisualNodeKind", "name":"Negate", "id":586, "components":{"(flecs.core.constant,flecs.meta.u32)":8}}, {"parent":"VisualNodeKind", "name":"Abs", "id":587, "components":{"(flecs.core.constant,flecs.meta.u32)":9}}, {"parent":"VisualNodeKind", "name":"Ceil", "id":588, "components":{"(flecs.core.constant,flecs.meta.u32)":10}}, {"parent":"VisualNodeKind", "name":"Floor", "id":589, "components":{"(flecs.core.constant,flecs.meta.u32)":11}}, {"parent":"VisualNodeKind", "name":"Sin", "id":590, "components":{"(flecs.core.constant,flecs.meta.u32)":12}}, {"parent":"VisualNodeKind", "name":"Cos", "id":591, "components":{"(flecs.core.constant,flecs.meta.u32)":13}}, {"parent":"VisualNodeKind", "name":"Tan", "id":592, "components":{"(flecs.core.constant,flecs.meta.u32)":14}}, {"parent":"VisualNodeKind", "name":"Exp", "id":593, "components":{"(flecs.core.constant,flecs.meta.u32)":15}}, {"parent":"VisualNodeKind", "name":"Log", "id":594, "components":{"(flecs.core.constant,flecs.meta.u32)":16}}, {"parent":"VisualNodeKind", "name":"Square", "id":595, "components":{"(flecs.core.constant,flecs.meta.u32)":17}}, {"parent":"VisualNodeKind", "name":"Round", "id":596, "components":{"(flecs.core.constant,flecs.meta.u32)":18}}, {"parent":"VisualNodeKind", "name":"OneMinus", "id":597, "components":{"(flecs.core.constant,flecs.meta.u32)":19}}, {"parent":"VisualNodeKind", "name":"Lerp", "id":598, "components":{"(flecs.core.constant,flecs.meta.u32)":20}}, {"parent":"VisualNodeKind", "name":"Clamp", "id":599, "components":{"(flecs.core.constant,flecs.meta.u32)":21}}, {"parent":"VisualNodeKind", "name":"Equal", "id":600, "components":{"(flecs.core.constant,flecs.meta.u32)":22}}, {"parent":"VisualNodeKind", "name":"Smaller", "id":601, "components":{"(flecs.core.constant,flecs.meta.u32)":23}}, {"parent":"VisualNodeKind", "name":"Greater", "id":602, "components":{"(flecs.core.constant,flecs.meta.u32)":24}}, {"parent":"VisualNodeKind", "name":"SmallerEqual", "id":603, "components":{"(flecs.core.constant,flecs.meta.u32)":25}}, {"parent":"VisualNodeKind", "name":"GreaterEqual", "id":604, "components":{"(flecs.core.constant,flecs.meta.u32)":26}}, {"parent":"VisualNodeKind", "name":"And", "id":605, "components":{"(flecs.core.constant,flecs.meta.u32)":27}}, {"parent":"VisualNodeKind", "name":"Or", "id":606, "components":{"(flecs.core.constant,flecs.meta.u32)":28}}, {"parent":"VisualNodeKind", "name":"Branch", "id":607, "components":{"(flecs.core.constant,flecs.meta.u32)":29}}, {"parent":"VisualNodeKind", "name":"Simplex", "id":608, "components":{"(flecs.core.constant,flecs.meta.u32)":30}}, {"parent":"VisualNodeKind", "name":"OpenSimplex", "id":609, "components":{"(flecs.core.constant,flecs.meta.u32)":31}}, {"parent":"VisualNodeKind", "name":"Perlin", "id":610, "components":{"(flecs.core.constant,flecs.meta.u32)":32}}, {"parent":"VisualNodeKind", "name":"Value", "id":611, "components":{"(flecs.core.constant,flecs.meta.u32)":33}}, {"parent":"VisualNodeKind", "name":"ValueCubic", "id":612, "components":{"(flecs.core.constant,flecs.meta.u32)":34}}, {"parent":"VisualNodeKind", "name":"Constant", "id":613, "components":{"(flecs.core.constant,flecs.meta.u32)":35}}, {"parent":"VisualNodeKind", "name":"IsTile", "id":614, "components":{"(flecs.core.constant,flecs.meta.u32)":36}}, {"parent":"VisualNodeKind", "name":"TileDistance", "id":615, "components":{"(flecs.core.constant,flecs.meta.u32)":37}}, {"parent":"TileType", "name":"Air", "id":616, "components":{"(flecs.core.constant,flecs.meta.u8)":0}}, {"parent":"TileType", "name":"Filler", "id":617, "components":{"(flecs.core.constant,flecs.meta.u8)":1}}, {"parent":"TileType", "name":"Liquid", "id":618, "components":{"(flecs.core.constant,flecs.meta.u8)":2}}, {"parent":"TileType", "name":"Ore", "id":619, "components":{"(flecs.core.constant,flecs.meta.u8)":3}}, {"parent":"TileType", "name":"NPC", "id":620, "components":{"(flecs.core.constant,flecs.meta.u8)":4}}, {"parent":"TileType", "name":"Plant", "id":621, "components":{"(flecs.core.constant,flecs.meta.u8)":5}}, {"parent":"TileType", "name":"MAX", "id":622, "components":{"(flecs.core.constant,flecs.meta.u8)":6}}, {"parent":"TileType", "name":"NONE", "id":623, "components":{"(flecs.core.constant,flecs.meta.u8)":7}}, {"parent":"VisualNodeType", "name":"kind", "id":624, "components":{"flecs.meta.member":{"type":"VisualNodeKind", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"name":"VisualNodeType", "id":23, "components":{"flecs.core.Component":{"size":4, "alignment":4}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"VisualNodeType"}}}, {"name":"VisualNodePos", "id":24, "components":{"flecs.core.Component":{"size":8, "alignment":4}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"VisualNodePos"}}}, {"name":"NodeParam_Constant", "id":29, "components":{"flecs.core.Component":{"size":4, "alignment":4}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"NodeParam_Constant"}}}, {"name":"NodeParam_Noise", "id":30, "components":{"flecs.core.Component":{"size":4, "alignment":4}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"NodeParam_Noise"}}}, {"name":"NodeParam_IsTile", "id":31, "components":{"flecs.core.Component":{"size":3, "alignment":1}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"NodeParam_IsTile"}}}, {"name":"NodeParam_TileDistance", "id":32, "components":{"flecs.core.Component":{"size":2, "alignment":1}, "flecs.meta.type":{"kind":"StructType"}, "flecs.meta.TypeSerializer":null, "flecs.meta.struct":null, "(flecs.core.Identifier,flecs.core.Symbol)":{"value":"NodeParam_TileDistance"}}}, {"parent":"VisualNodePos", "name":"x", "id":625, "components":{"flecs.meta.member":{"type":"flecs.meta.f32", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"parent":"VisualNodePos", "name":"y", "id":626, "components":{"flecs.meta.member":{"type":"flecs.meta.f32", "count":0, "unit":"#0", "offset":4, "use_offset":false}}}, {"parent":"NodeParam_Constant", "name":"value", "id":627, "components":{"flecs.meta.member":{"type":"flecs.meta.f32", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"parent":"NodeParam_Noise", "name":"frequency", "id":628, "components":{"flecs.meta.member":{"type":"flecs.meta.f32", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"parent":"NodeParam_IsTile", "name":"relativeX", "id":629, "components":{"flecs.meta.member":{"type":"flecs.meta.i8", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"parent":"NodeParam_IsTile", "name":"relativeY", "id":630, "components":{"flecs.meta.member":{"type":"flecs.meta.i8", "count":0, "unit":"#0", "offset":1, "use_offset":false}}}, {"parent":"NodeParam_IsTile", "name":"tileType", "id":631, "components":{"flecs.meta.member":{"type":"TileType", "count":0, "unit":"#0", "offset":2, "use_offset":false}}}, {"parent":"NodeParam_TileDistance", "name":"range", "id":632, "components":{"flecs.meta.member":{"type":"flecs.meta.i8", "count":0, "unit":"#0", "offset":0, "use_offset":false}}}, {"parent":"NodeParam_TileDistance", "name":"tileType", "id":633, "components":{"flecs.meta.member":{"type":"TileType", "count":0, "unit":"#0", "offset":1, "use_offset":false}}}, {"parent":"Graph", "name":"Clamp_1", "id":636, "components":{"VisualNodeType":{"kind":"Clamp"}, "VisualNodePos":{"x":100, "y":100}}}, {"parent":"Graph", "name":"Add_0", "id":635, "pairs":{"InputPin0":"Graph.Clamp_1"}, "components":{"VisualNodeType":{"kind":"Add"}, "VisualNodePos":{"x":511, "y":108}}}]} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 0000000..f0b2340 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,8 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 + +[Window][##canvas] +Pos=0,19 +Size=1280,701 + diff --git a/include/Components/Misc.hpp b/include/Components/Misc.hpp index e8cccdc..e950940 100644 --- a/include/Components/Misc.hpp +++ b/include/Components/Misc.hpp @@ -26,6 +26,19 @@ struct Bounds Vector2 Max; }; +inline bool BoundsHasPoint(const Bounds& b, int x, int y) +{ + return x >= b.Min.X && x < b.Max.X && y >= b.Min.Y && y < b.Max.Y; +} + +inline Bounds BoundsGrow(const Bounds& b, int32_t amount) +{ + return Bounds{ + Vector2{b.Min.X - amount, b.Min.Y - amount}, + Vector2{b.Max.X + amount, b.Max.Y + amount} + }; +} + struct Level { Level() = default; diff --git a/include/Components/WorldGraph.hpp b/include/Components/WorldGraph.hpp new file mode 100644 index 0000000..276963e --- /dev/null +++ b/include/Components/WorldGraph.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include "flecs.h" +#include "Types/WorldGraph/WorldGraphVisualNode.h" + +// Register all WorldGraph components with Flecs reflection so they can be +// serialized to/from JSON via world.to_json() / world.from_json(). + +inline void Flecs_WorldGraph(flecs::world& world) +{ + // ── Node kind enum ────────────────────────────────────────────────────── + world.component() + .constant("Add", VisualNodeKind::Add) + .constant("Subtract", VisualNodeKind::Subtract) + .constant("Multiply", VisualNodeKind::Multiply) + .constant("Divide", VisualNodeKind::Divide) + .constant("Modulo", VisualNodeKind::Modulo) + .constant("Pow", VisualNodeKind::Pow) + .constant("Max", VisualNodeKind::Max) + .constant("Min", VisualNodeKind::Min) + .constant("Negate", VisualNodeKind::Negate) + .constant("Abs", VisualNodeKind::Abs) + .constant("Ceil", VisualNodeKind::Ceil) + .constant("Floor", VisualNodeKind::Floor) + .constant("Sin", VisualNodeKind::Sin) + .constant("Cos", VisualNodeKind::Cos) + .constant("Tan", VisualNodeKind::Tan) + .constant("Exp", VisualNodeKind::Exp) + .constant("Log", VisualNodeKind::Log) + .constant("Square", VisualNodeKind::Square) + .constant("Round", VisualNodeKind::Round) + .constant("OneMinus", VisualNodeKind::OneMinus) + .constant("Lerp", VisualNodeKind::Lerp) + .constant("Clamp", VisualNodeKind::Clamp) + .constant("Equal", VisualNodeKind::Equal) + .constant("Smaller", VisualNodeKind::Smaller) + .constant("Greater", VisualNodeKind::Greater) + .constant("SmallerEqual", VisualNodeKind::SmallerEqual) + .constant("GreaterEqual", VisualNodeKind::GreaterEqual) + .constant("And", VisualNodeKind::And) + .constant("Or", VisualNodeKind::Or) + .constant("Branch", VisualNodeKind::Branch) + .constant("Simplex", VisualNodeKind::Simplex) + .constant("OpenSimplex", VisualNodeKind::OpenSimplex) + .constant("Perlin", VisualNodeKind::Perlin) + .constant("Value", VisualNodeKind::Value) + .constant("ValueCubic", VisualNodeKind::ValueCubic) + .constant("Constant", VisualNodeKind::Constant) + .constant("IsTile", VisualNodeKind::IsTile) + .constant("TileDistance", VisualNodeKind::TileDistance); + + // ── TileType enum (needed by IsTile / TileDistance params) ───────────── + world.component() + .constant("Air", TileType::Air) + .constant("Filler", TileType::Filler) + .constant("Liquid", TileType::Liquid) + .constant("Ore", TileType::Ore) + .constant("NPC", TileType::NPC) + .constant("Plant", TileType::Plant); + + // ── Core node components ──────────────────────────────────────────────── + world.component() + .member("kind"); + + world.component() + .member("x") + .member("y"); + + world.component(); + + // ── Connection relationship tags ──────────────────────────────────────── + world.component(); + world.component(); + world.component(); + + // ── Leaf parameter components ─────────────────────────────────────────── + world.component() + .member("value"); + + world.component() + .member("frequency"); + + world.component() + .member("relativeX") + .member("relativeY") + .member("tileType"); + + world.component() + .member("range") + .member("tileType"); +} diff --git a/include/Core/Chunk.h b/include/Core/Chunk.h index 2e2ee54..9c8c7da 100644 --- a/include/Core/Chunk.h +++ b/include/Core/Chunk.h @@ -84,7 +84,7 @@ public: uint32_t hash() const { return static_cast(hash64()); } static uint32_t hash(ChunkKey key) { return key.hash(); } - Bounds SetBounds() const { return Bounds{Vector2{ChunkToWorld(X), ChunkToWorld(Y)}, Vector2{ChunkToWorld(X + 1), ChunkToWorld(Y + 1)}}; } + Bounds GetBounds() const { return Bounds{Vector2{ChunkToWorld(X), ChunkToWorld(Y)}, Vector2{ChunkToWorld(X + 1), ChunkToWorld(Y + 1)}}; } public: bool operator==(const ChunkKey& rhs) const { return hash() == rhs.hash(); } @@ -99,12 +99,12 @@ public: void RemovePersistance(flecs::entity entity); void Clear() { - Chunk = {}; + TileData = {}; Entities.resize(0); } public: - std::unique_ptr Chunk{}; + std::unique_ptr TileData{}; std::vector Entities{}; std::vector PersistantEntities{}; }; diff --git a/include/Types/WorldGraph/WorldGraph.h b/include/Types/WorldGraph/WorldGraph.h index 465642c..925602e 100644 --- a/include/Types/WorldGraph/WorldGraph.h +++ b/include/Types/WorldGraph/WorldGraph.h @@ -1,30 +1,40 @@ #pragma once #include "WorldGraphVisualNode.h" +#include "WorldGraphNode.h" +#include "flecs.h" +#include +#include + +// WorldGraph compiles a Flecs entity graph into a flat memory-pooled runtime +// graph of WorldNodeBase* objects that can be evaluated per-tile. +// +// Usage: +// auto graph = WorldGraph::Compile(world, world.lookup("MyGraph")); +// NodeValue result = graph.Execute(params); class WorldGraph final { public: WorldGraph() = default; - WorldGraph(const Vector>& nodes); - WorldGraph(const WorldGraph& other); - WorldGraph(WorldGraph&& other) noexcept = default; + // Non-copyable (raw pointers into owned buffer); movable. + WorldGraph(const WorldGraph&) = delete; + WorldGraph& operator=(const WorldGraph&) = delete; + WorldGraph(WorldGraph&&) noexcept = default; + WorldGraph& operator=(WorldGraph&&) noexcept = default; - WorldGraph& operator=(const WorldGraph& other); - WorldGraph& operator=(WorldGraph&& other) noexcept = default; + // Compile from Flecs world. graphRoot is the parent entity whose children + // are the node entities. Returns an invalid WorldGraph if compilation fails. + static WorldGraph Compile(flecs::world& world, flecs::entity graphRoot); -public: - Variant Execute(Ref node, const WorldNodeParameters& params) const; - WorldNodeBase* GetNode(Ref node) const; + // Evaluate from the designated output node (VisualNodeOutput tag). + NodeValue Execute(const WorldNodeParameters& params) const; + + bool IsValid() const { return RootNode != nullptr; } private: - void Compile(const Vector>& nodes); - std::unique_ptr CopyMemory(HashMap, WorldNodeBase*>& nodeMap) const; - -private: - int MemorySize{}; - std::unique_ptr CompiledData{}; - HashMap, WorldNodeBase*> NodeMap{}; + uint32_t MemorySize{}; + std::unique_ptr CompiledData{}; + WorldNodeBase* RootNode{}; }; - diff --git a/include/Types/WorldGraph/WorldGraphAllocator.h b/include/Types/WorldGraph/WorldGraphAllocator.h index 0d95c1f..4d597e3 100644 --- a/include/Types/WorldGraph/WorldGraphAllocator.h +++ b/include/Types/WorldGraph/WorldGraphAllocator.h @@ -1,7 +1,8 @@ #pragma once #include +#include #include -#include "modules/factory/include/Util/Span.h" +#include "Util/Span.h" struct WorldGraphAllocatorBase { diff --git a/include/Types/WorldGraph/WorldGraphNode.h b/include/Types/WorldGraph/WorldGraphNode.h index fb4ff4b..88c391f 100644 --- a/include/Types/WorldGraph/WorldGraphNode.h +++ b/include/Types/WorldGraph/WorldGraphNode.h @@ -5,77 +5,58 @@ #include #include #include +#include +#include +#include #include "WorldGraphAllocator.h" #include "Util/FastNoiseLite.h" -#include "core/variant/variant.h" -#include "modules/factory/include/Core/Chunk.h" +#include "Core/Chunk.h" +#include "Components/Misc.hpp" -// // last bit determines if it is a boolean or a float -// // if it is a float, last bit of precision will be lost (not that bad) -// // if it is a bool, the bool value will be stored on the second bit -// // for floats the last bit is set -// // for bools the last bit is unset -// class BoolFloat -// { -// public: -// BoolFloat() = default; -// BoolFloat(bool val) {} -// BoolFloat(float val) {} +// ─── Node value type ───────────────────────────────────────────────────────── -// public: -// void SetFloat(float val) { Data = reinterpret_cast(val) | 0b1u; } -// void SetBool(bool val) { Data = (val << 1) & (~0b1u); } +enum class NodeValueType : uint8_t { Float, Bool }; -// constexpr float IsFloat() const { return Data & 0b1u; } -// constexpr float IsBool() const { return Data & (~0b1u); } +struct NodeValue +{ + union { float f; bool b; } data{}; + NodeValueType type = NodeValueType::Float; -// float GetFloat() const { _ASSERT(IsFloat()); return reinterpret_cast(Data); } -// float GetBool() const { _ASSERT(IsBool()); return Data; } + NodeValue() = default; + NodeValue(float v) : type(NodeValueType::Float) { data.f = v; } + NodeValue(bool v) : type(NodeValueType::Bool) { data.b = v; } -// public: -// operator float() const { return GetFloat(); } - -// private: -// uint32_t Data{}; -// }; - -struct WorldNodeParameters; - -struct alignas(void*) WorldNodeBase -{ - virtual Variant Evaluate(const WorldNodeParameters& params) const = 0; - virtual Variant::Type GetReturnType() const = 0; - virtual Vector GetInputTypes() const = 0; - virtual bool IsValid() const = 0; - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const = 0; - virtual void SetInput(int index, WorldNodeBase* input) = 0; + template T get() const; }; +template<> inline float NodeValue::get() const { return data.f; } +template<> inline bool NodeValue::get() const { return data.b; } + +// ─── Parameters passed per-tile during evaluation ──────────────────────────── + struct WorldNodeParameters { - static constexpr int MaxQueryOffset = 4; + static constexpr int MaxQueryOffset = 4; static constexpr int PaddedChunkSide = Chunk::ChunkSize + MaxQueryOffset * 2; static constexpr int PaddedChunkSize = PaddedChunkSide * PaddedChunkSide; - typedef std::array TileArray; + using TileArray = std::array; + + int X{}; + int Y{}; + int Seed{}; + float FinalValueSubstract{}; + + ChunkKey ChunkInfo{}; + TileArray* GeneratedTiles{}; - int X{ }; - int Y{ }; - int Seed{ }; - float FinalValueSubstract{ }; - - ChunkKey ChunkInfo{ }; - TileArray* GeneratedTiles{ }; - Tile GetTile(int x, int y) const { auto bounds = GetGenerationBounds(); - if (unlikely(!bounds.has_point(Vector2i{x, y}))) - return {}; - // DEV_ASSERT(bounds.has_point(Vector2i{x, y})); - - return (*GeneratedTiles)[(y - bounds.position.y) * PaddedChunkSide + (x - bounds.position.x)]; + if (!BoundsHasPoint(bounds, x, y)) + return {}; + return (*GeneratedTiles)[(y - bounds.Min.Y) * PaddedChunkSide + (x - bounds.Min.X)]; } static int GetArrayIndex(int x, int y) @@ -83,583 +64,283 @@ struct WorldNodeParameters return (y + MaxQueryOffset) * PaddedChunkSide + (x + MaxQueryOffset); } - Rect2i GetGenerationBounds() const + Bounds GetGenerationBounds() const { - return ChunkInfo.GetBounds().grow(MaxQueryOffset); + return BoundsGrow(ChunkInfo.GetBounds(), MaxQueryOffset); } }; -template -struct TtoVariant +// ─── Base interface ─────────────────────────────────────────────────────────── + +struct alignas(void*) WorldNodeBase { - typedef Variant TVariant; + virtual NodeValue Evaluate(const WorldNodeParameters& params) const = 0; + virtual NodeValueType GetReturnType() const = 0; + virtual std::vector GetInputTypes() const = 0; + virtual bool IsValid() const = 0; + virtual void Allocate(WorldGraphAllocatorBase* allocator) const = 0; + virtual void SetInput(int index, WorldNodeBase* input) = 0; }; -template +// ─── Typed templated base ───────────────────────────────────────────────────── + +template struct WorldNodeTemplated : public WorldNodeBase { std::array InputNodes{}; - virtual Variant::Type GetReturnType() const override { return Variant::get_type_t(); } - virtual Vector GetInputTypes() const override - { - return { Variant::get_type_t()... }; - }; - virtual bool IsValid() const override - { - bool valid{ true }; - for (auto input : InputNodes) - { - valid = valid && input; - } - return valid; - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override { Allocator->Allocate(*this); } - - virtual Variant Evaluate(const WorldNodeParameters& params) const override + virtual NodeValueType GetReturnType() const override { - std::array results{}; - for (int i{}; i < sizeof...(Inputs); ++i) - { - results[i] = InputNodes[i]->Evaluate(params); - } - - auto EvaluateFunctor = [this](typename TtoVariant::TVariant... args) -> Variant - { - return EvaluateT(args.get_unsafe_t()...); - }; - - return std::apply(EvaluateFunctor, results); + if constexpr (std::is_same_v) return NodeValueType::Float; + else return NodeValueType::Bool; } - + + virtual std::vector GetInputTypes() const override + { + return { (std::is_same_v ? NodeValueType::Float : NodeValueType::Bool)... }; + } + + virtual bool IsValid() const override + { + for (auto* input : InputNodes) + if (!input) return false; + return true; + } + + virtual void Allocate(WorldGraphAllocatorBase* allocator) const override + { + allocator->Allocate(*this); + } + + virtual NodeValue Evaluate(const WorldNodeParameters& params) const override + { + // Collect input results into an array + auto inputValues = [&](std::index_sequence) { + return std::array{ InputNodes[I]->Evaluate(params)... }; + }(std::make_index_sequence{}); + + // Unpack typed values and call EvaluateT + return [&](std::index_sequence) -> NodeValue { + return NodeValue(EvaluateT( + inputValues[I].template get>>()... + )); + }(std::make_index_sequence{}); + } + virtual void SetInput(int index, WorldNodeBase* input) override { InputNodes[index] = input; } - virtual Return EvaluateT(Inputs...) const = 0; }; -struct WorldNode_Add : public WorldNodeTemplated +// ─── Math nodes ────────────────────────────────────────────────────────────── + +struct WorldNode_Add : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return a+b;} }; +struct WorldNode_Subtract : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return a-b;} }; +struct WorldNode_Multiply : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return a*b;} }; +struct WorldNode_Divide : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return a/b;} }; +struct WorldNode_Modulo : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return std::fmod(a,b);} }; +struct WorldNode_Pow : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return std::pow(a,b);} }; +struct WorldNode_Max : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return std::max(a,b);} }; +struct WorldNode_Min : WorldNodeTemplated { float EvaluateT(float a,float b)const override{return std::min(a,b);} }; + +struct WorldNode_Negate : WorldNodeTemplated { float EvaluateT(float v)const override{return -v;} }; +struct WorldNode_Abs : WorldNodeTemplated { float EvaluateT(float v)const override{return std::abs(v);} }; +struct WorldNode_Ceil : WorldNodeTemplated { float EvaluateT(float v)const override{return std::ceil(v);} }; +struct WorldNode_Floor : WorldNodeTemplated { float EvaluateT(float v)const override{return std::floor(v);} }; +struct WorldNode_Sin : WorldNodeTemplated { float EvaluateT(float v)const override{return std::sin(v);} }; +struct WorldNode_Cos : WorldNodeTemplated { float EvaluateT(float v)const override{return std::cos(v);} }; +struct WorldNode_Tan : WorldNodeTemplated { float EvaluateT(float v)const override{return std::tan(v);} }; +struct WorldNode_Exp : WorldNodeTemplated { float EvaluateT(float v)const override{return std::exp(v);} }; +struct WorldNode_Log : WorldNodeTemplated { float EvaluateT(float v)const override{return std::log(v);} }; +struct WorldNode_Square : WorldNodeTemplated { float EvaluateT(float v)const override{return v*v;} }; +struct WorldNode_Round : WorldNodeTemplated { float EvaluateT(float v)const override{return std::round(v);} }; +struct WorldNode_OneMinus : WorldNodeTemplated { float EvaluateT(float v)const override{return 1.f-v;} }; + +struct WorldNode_Lerp : WorldNodeTemplated { - virtual float EvaluateT(float v0, float v1) const override - { - return v0 + v1; - } + float EvaluateT(float from, float to, float t) const override { return std::lerp(from, to, t); } +}; +struct WorldNode_Clamp : WorldNodeTemplated +{ + float EvaluateT(float v, float lo, float hi) const override { return std::clamp(v, lo, hi); } }; -struct WorldNode_Subtract : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return v0 - v1; - } -}; +// ─── Comparison nodes ───────────────────────────────────────────────────────── -struct WorldNode_Multiply : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return v0 * v1; - } -}; +struct WorldNode_Equal : WorldNodeTemplated { bool EvaluateT(float a,float b)const override{return a==b;} }; +struct WorldNode_Smaller : WorldNodeTemplated { bool EvaluateT(float a,float b)const override{return a { bool EvaluateT(float a,float b)const override{return a>b;} }; +struct WorldNode_SmallerEqual : WorldNodeTemplated { bool EvaluateT(float a,float b)const override{return a<=b;} }; +struct WorldNode_GreaterEqual : WorldNodeTemplated { bool EvaluateT(float a,float b)const override{return a>=b;} }; -struct WorldNode_Divide : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return v0 / v1; - } -}; +// ─── Logic nodes ────────────────────────────────────────────────────────────── -struct WorldNode_Modulo : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return fmod(v0, v1); - } -}; +struct WorldNode_And : WorldNodeTemplated { bool EvaluateT(bool a,bool b)const override{return a&&b;} }; +struct WorldNode_Or : WorldNodeTemplated { bool EvaluateT(bool a,bool b)const override{return a||b;} }; -struct WorldNode_Equal : public WorldNodeTemplated -{ - virtual bool EvaluateT(float v0, float v1) const override - { - return v0 == v1; - } -}; - -struct WorldNode_Smaller : public WorldNodeTemplated -{ - virtual bool EvaluateT(float v0, float v1) const override - { - return v0 < v1; - } -}; - -struct WorldNode_Greater : public WorldNodeTemplated -{ - virtual bool EvaluateT(float v0, float v1) const override - { - return v0 > v1; - } -}; - -struct WorldNode_SmallerEqual : public WorldNodeTemplated -{ - virtual bool EvaluateT(float v0, float v1) const override - { - return v0 <= v1; - } -}; - -struct WorldNode_GreaterEqual : public WorldNodeTemplated -{ - virtual bool EvaluateT(float v0, float v1) const override - { - return v0 >= v1; - } -}; - -struct WorldNode_Negate : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return -v; - } -}; - -struct WorldNode_Abs : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::abs(v); - } -}; - -struct WorldNode_Ceil : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::ceil(v); - } -}; - -struct WorldNode_Floor : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::floor(v); - } -}; - -struct WorldNode_Sin : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::sin(v); - } -}; - -struct WorldNode_Cos : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::cos(v); - } -}; - -struct WorldNode_Tan : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::tan(v); - } -}; - -struct WorldNode_Exp : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::exp(v); - } -}; - -struct WorldNode_Pow : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return std::pow(v0, v1); - } -}; - -struct WorldNode_Max : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return std::max(v0, v1); - } -}; - -struct WorldNode_Min : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1) const override - { - return std::min(v0, v1); - } -}; - -struct WorldNode_Clamp : public WorldNodeTemplated -{ - virtual float EvaluateT(float v0, float v1, float v2) const override - { - return std::clamp(v0, v1, v2); - } -}; - -struct WorldNode_Round : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::round(v); - } -}; - -struct WorldNode_Log : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return std::log(v); - } -}; - -struct WorldNode_Square : public WorldNodeTemplated -{ - virtual float EvaluateT(float v) const override - { - return v * v; - } -}; - -struct WorldNode_Lerp : public WorldNodeTemplated -{ - virtual float EvaluateT(float from, float to, float weight) const override - { - return Math::lerp(from, to, weight); - } -}; - -struct WorldNode_OneMinus : public WorldNodeTemplated -{ - virtual float EvaluateT(float val) const override - { - return 1 - val; - } -}; - -struct WorldNode_And : public WorldNodeTemplated -{ - virtual bool EvaluateT(bool val0, bool val1) const override - { - return val0 && val1; - } -}; - -struct WorldNode_Or : public WorldNodeTemplated -{ - virtual bool EvaluateT(bool val0, bool val1) const override - { - return val0 || val1; - } -}; - -struct WorldNode_Constant : public WorldNodeBase -{ - float Value{}; - virtual Variant Evaluate(const WorldNodeParameters& params) const override - { - return Value; - } - virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; } - virtual Vector GetInputTypes() const override - { - return { }; - }; - virtual bool IsValid() const override - { - return !std::_Is_nan(Value); - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override - { - Allocator->Allocate(*this); - } - virtual void SetInput(int index, WorldNodeBase* input) override - { - DEV_ASSERT(false); - } -}; +// ─── Branch (if/else) ───────────────────────────────────────────────────────── struct WorldNode_Branch : public WorldNodeBase { - WorldNodeBase* InputBool{}; - WorldNodeBase* InputTrue{}; - WorldNodeBase* InputFalse{}; + WorldNodeBase* InputBool {}; + WorldNodeBase* InputTrue {}; + WorldNodeBase* InputFalse {}; - virtual Variant Evaluate(const WorldNodeParameters& params) const override + virtual NodeValue Evaluate(const WorldNodeParameters& params) const override { - bool condition = InputBool->Evaluate(params).get_unsafe_bool(); + bool condition = InputBool->Evaluate(params).get(); return condition ? InputTrue->Evaluate(params) : InputFalse->Evaluate(params); } - virtual Variant::Type GetReturnType() const override { return Variant::Type::FLOAT; } - virtual Vector GetInputTypes() const override - { - return { Variant::Type::BOOL, Variant::Type::FLOAT, Variant::Type::FLOAT }; - }; - virtual bool IsValid() const override - { - return InputBool && InputTrue && InputFalse; - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override - { - Allocator->Allocate(*this); - } + virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; } + virtual std::vector GetInputTypes() const override { return { NodeValueType::Bool, NodeValueType::Float, NodeValueType::Float }; } + virtual bool IsValid() const override { return InputBool && InputTrue && InputFalse; } + virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); } virtual void SetInput(int index, WorldNodeBase* input) override { switch (index) { - case 0: InputBool = input; - case 1: InputTrue = input; - case 2: InputFalse = input; + case 0: InputBool = input; break; + case 1: InputTrue = input; break; + case 2: InputFalse = input; break; } } }; +// ─── Constant ───────────────────────────────────────────────────────────────── + +struct WorldNode_Constant : public WorldNodeBase +{ + float Value{}; + + virtual NodeValue Evaluate(const WorldNodeParameters&) const override { return NodeValue(Value); } + virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; } + virtual std::vector GetInputTypes() const override { return {}; } + virtual bool IsValid() const override { return !std::isnan(Value); } + virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); } + virtual void SetInput(int, WorldNodeBase*) override { assert(false); } +}; + +// ─── Noise base ─────────────────────────────────────────────────────────────── + struct WorldNode_NoiseBase : public WorldNodeBase { float Frequency{ 1.f }; - virtual Variant Evaluate(const WorldNodeParameters& params) const override + virtual NodeValue Evaluate(const WorldNodeParameters& params) const override { - float x = params.X * Frequency; - float y = params.Y * Frequency; - return EvaluateNoise(params.Seed, x, y); - } - virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; } - virtual Vector GetInputTypes() const override - { - return { }; - }; - virtual bool IsValid() const override - { - return Frequency != 0; - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override - { - Allocator->Allocate(*this); - } - - virtual void SetInput(int index, WorldNodeBase* input) override - { - DEV_ASSERT(false); + return NodeValue(EvaluateNoise(params.Seed, params.X * Frequency, params.Y * Frequency)); } + virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; } + virtual std::vector GetInputTypes() const override { return {}; } + virtual bool IsValid() const override { return Frequency != 0.f; } + virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); } + virtual void SetInput(int, WorldNodeBase*) override { assert(false); } virtual float EvaluateNoise(int seed, float x, float y) const = 0; }; struct WorldNode_Simplex : public WorldNode_NoiseBase { - virtual float EvaluateNoise(int seed, float x, float y) const override + float EvaluateNoise(int seed, float x, float y) const override { - const float SQRT3 = (float)1.7320508075688772935274463415059; - const float F2 = 0.5f * (SQRT3 - 1); + const float F2 = 0.5f * (1.7320508075688772935f - 1.f); float t = (x + y) * F2; - x += t; - y += t; - - return fastnoiselitestatic::SingleSimplex(seed, x, y); + return fastnoiselitestatic::SingleSimplex(seed, x + t, y + t); } }; - struct WorldNode_OpenSimplex : public WorldNode_NoiseBase { - virtual float EvaluateNoise(int seed, float x, float y) const override + float EvaluateNoise(int seed, float x, float y) const override { - const float SQRT3 = (float)1.7320508075688772935274463415059; - const float F2 = 0.5f * (SQRT3 - 1); + const float F2 = 0.5f * (1.7320508075688772935f - 1.f); float t = (x + y) * F2; - x += t; - y += t; - - return fastnoiselitestatic::SingleOpenSimplex2S(seed, x, y); + return fastnoiselitestatic::SingleOpenSimplex2S(seed, x + t, y + t); } }; +struct WorldNode_Perlin : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SinglePerlin(s,x,y);} }; +struct WorldNode_ValueCubic : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValueCubic(s,x,y);} }; +struct WorldNode_Value : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValue(s,x,y);} }; -struct WorldNode_Perlin : public WorldNode_NoiseBase -{ - virtual float EvaluateNoise(int seed, float x, float y) const override - { - return fastnoiselitestatic::SinglePerlin(seed, x, y); - } -}; - -struct WorldNode_ValueCubic : public WorldNode_NoiseBase -{ - virtual float EvaluateNoise(int seed, float x, float y) const override - { - return fastnoiselitestatic::SingleValueCubic(seed, x, y); - } -}; - -struct WorldNode_Value : public WorldNode_NoiseBase -{ - virtual float EvaluateNoise(int seed, float x, float y) const override - { - return fastnoiselitestatic::SingleValue(seed, x, y); - } -}; +// ─── Tile query nodes ───────────────────────────────────────────────────────── struct WorldNode_IsTile : public WorldNodeBase { - int8_t RelativeX{}; - int8_t RelativeY{}; - TILE_TYPE TileType{}; + int8_t RelativeX{}; + int8_t RelativeY{}; + TileType Type{}; - virtual Variant Evaluate(const WorldNodeParameters& params) const override + virtual NodeValue Evaluate(const WorldNodeParameters& params) const override { - return params.GetTile(params.X + RelativeX, params.Y + RelativeY).GetType() == TileType; - } - virtual Variant::Type GetReturnType() const override { return Variant::BOOL; } - virtual Vector GetInputTypes() const override - { - return { }; - }; - virtual bool IsValid() const override - { - return true; - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override - { - Allocator->Allocate(*this); - } - - virtual void SetInput(int index, WorldNodeBase* input) override - { - DEV_ASSERT(false); + return NodeValue(params.GetTile(params.X + RelativeX, params.Y + RelativeY).GetType() == Type); } + virtual NodeValueType GetReturnType() const override { return NodeValueType::Bool; } + virtual std::vector GetInputTypes() const override { return {}; } + virtual bool IsValid() const override { return true; } + virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); } + virtual void SetInput(int, WorldNodeBase*) override { assert(false); } }; struct WorldNode_TileDistance : public WorldNodeBase { - int8_t Range{}; - TILE_TYPE TileType{}; + int8_t Range{}; + TileType Type{}; private: - struct SmallVectorI2{ - int8_t X; int8_t Y; - SmallVectorI2(int8_t x, int8_t y) : X{ x }, Y{ y } {}; - SmallVectorI2() = default; - }; + struct SmallVec2I { int8_t X, Y; SmallVec2I(int8_t x,int8_t y):X{x},Y{y}{} SmallVec2I()=default; }; - static constexpr int ArraySize{(WorldNodeParameters::MaxQueryOffset * 2 + 1) * (WorldNodeParameters::MaxQueryOffset * 2 + 1) - 1}; - - static std::array CreateSortedOffsets() + static constexpr int ArraySize = + (WorldNodeParameters::MaxQueryOffset * 2 + 1) * + (WorldNodeParameters::MaxQueryOffset * 2 + 1) - 1; + + static std::array CreateSortedOffsets() { - std::array offsets{}; - int counter{}; - for (int y{-WorldNodeParameters::MaxQueryOffset}; y <= WorldNodeParameters::MaxQueryOffset; ++y) - for (int x{-WorldNodeParameters::MaxQueryOffset}; x <= WorldNodeParameters::MaxQueryOffset; ++x) - if (y != 0 && x != 0) - { - offsets[counter] = SmallVectorI2{ static_cast(x), static_cast(y) }; - ++counter; - } - std::sort(offsets.begin(), offsets.end(), [] (SmallVectorI2 lhs, SmallVectorI2 rhs) - { - return rhs.X * rhs.X + rhs.Y * rhs.Y > lhs.X * lhs.X + lhs.Y * lhs.Y; + std::array offsets{}; + int n{}; + for (int y = -WorldNodeParameters::MaxQueryOffset; y <= WorldNodeParameters::MaxQueryOffset; ++y) + for (int x = -WorldNodeParameters::MaxQueryOffset; x <= WorldNodeParameters::MaxQueryOffset; ++x) + if (x != 0 || y != 0) + offsets[n++] = SmallVec2I{ static_cast(x), static_cast(y) }; + std::sort(offsets.begin(), offsets.end(), [](SmallVec2I l, SmallVec2I r) { + return r.X*r.X + r.Y*r.Y > l.X*l.X + l.Y*l.Y; }); return offsets; } - - inline static const std::array SortedOffsets{ CreateSortedOffsets() }; static std::array CreateRangeOffsets() { std::array offsets{}; - for (int i{}; i < WorldNodeParameters::MaxQueryOffset; ++i) { - offsets[i] = ArraySize - (((WorldNodeParameters::MaxQueryOffset - i) * 2 + 1) * ((WorldNodeParameters::MaxQueryOffset - i) * 2 + 1) - 1); + int side = (WorldNodeParameters::MaxQueryOffset - i) * 2 + 1; + offsets[i] = ArraySize - (side * side - 1); } - return offsets; } - inline static const std::array RangeOffsets{ CreateRangeOffsets() }; - - // inline static const SmallVectorI2 SortedOffset[] = - // { - // {+3, +3}, {-3, -3}, {+3, -3}, {-3, +3}, // 3 - // {+2, +3}, {+3, +2}, {-2, -3}, {-3, -2}, {-2, +3}, {-3, +2}, {+2, -3}, {+3, -2}, - // {+1, +3}, {+3, +1}, {-1, -3}, {-3, -1}, {-1, +3}, {-3, +1}, {+1, -3}, {+3, -1}, - // {+3, +0}, {-3, -0}, {+0, -3}, {-0, +3}, - // {+2, +2}, {-2, -2}, {+2, -2}, {-2, +2}, // 2 - // {+1, +2}, {+2, +1}, {-1, -2}, {-2, -1}, {-1, +2}, {-2, +1}, {+1, -2}, {+2, -1}, - // {+2, +0}, {-2, -0}, {+0, -2}, {-0, +2}, - // {+1, +1}, {-1, -1}, {+1, -1}, {-1, +1}, // 1 - // {+1, +0}, {-1, -0}, {+0, -1}, {-0, +1}, - // }; - - // inline static const int8_t RangeOffsets[] = - // { - // 0, 24, 40 - // }; + inline static const std::array SortedOffsets{ CreateSortedOffsets() }; + inline static const std::array RangeOffsets{ CreateRangeOffsets() }; public: - virtual Variant Evaluate(const WorldNodeParameters& params) const override + virtual NodeValue Evaluate(const WorldNodeParameters& params) const override { - if (!params.ChunkInfo.GetBounds().has_point(Vector2i{params.X, params.Y})) return 16'384.f; + if (!BoundsHasPoint(params.ChunkInfo.GetBounds(), params.X, params.Y)) + return NodeValue(16384.f); - int maxRangeSQ = 16'384; - for (int i{RangeOffsets[Range]}; i < 48; ++i) + int maxRangeSQ = 16384; + for (int i = RangeOffsets[Range]; i < ArraySize; ++i) { - auto offset = SortedOffsets[i]; - if (params.GetTile(params.X + offset.X, params.Y + offset.Y).GetType() == TileType) - { - maxRangeSQ = offset.X * offset.X + offset.Y * offset.Y; - } + auto off = SortedOffsets[i]; + if (params.GetTile(params.X + off.X, params.Y + off.Y).GetType() == Type) + maxRangeSQ = off.X * off.X + off.Y * off.Y; } - return sqrtf(static_cast(maxRangeSQ)); - } - virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; } - virtual Vector GetInputTypes() const override - { - return { }; - }; - virtual bool IsValid() const override - { - return Range >= 1 && Range <= 3; - } - virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override - { - Allocator->Allocate(*this); - } - - virtual void SetInput(int index, WorldNodeBase* input) override - { - DEV_ASSERT(false); + return NodeValue(std::sqrt(static_cast(maxRangeSQ))); } + virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; } + virtual std::vector GetInputTypes() const override { return {}; } + virtual bool IsValid() const override { return Range >= 1 && Range <= 3; } + virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); } + virtual void SetInput(int, WorldNodeBase*) override { assert(false); } }; - -// struct WorldNode_Cellular : public WorldNode_NoiseBase -// { -// virtual float EvaluateNoise(int seed, float x, float y) const override -// { -// const float SQRT3 = (float)1.7320508075688772935274463415059; -// const float F2 = 0.5f * (SQRT3 - 1); -// float t = (x + y) * F2; -// x += t; -// y += t; - -// return fastnoiselite::FastNoiseLite::SingleCellular(seed, x, y); -// } -// }; \ No newline at end of file diff --git a/include/Types/WorldGraph/WorldGraphVisualNode.h b/include/Types/WorldGraph/WorldGraphVisualNode.h index b80028c..6a83c4f 100644 --- a/include/Types/WorldGraph/WorldGraphVisualNode.h +++ b/include/Types/WorldGraph/WorldGraphVisualNode.h @@ -1,174 +1,54 @@ #pragma once -#include "WorldGraphNode.h" -#include "core/io/resource.h" -#include "modules/factory/include/Util/Helpers.h" +#include "flecs.h" +#include "Types/Tile.h" +#include -class WorldGraphVisualNodeBase : public Resource +// ─── Node kind enum ──────────────────────────────────────────────────────────── +// Each value corresponds to one WorldNode_* runtime type. + +enum class VisualNodeKind : uint32_t { - GDCLASS(WorldGraphVisualNodeBase, Resource); -public: - static void _bind_methods(); - -public: - virtual ~WorldGraphVisualNodeBase() = default; - -public: - Vector2i GetPosition() const { return Position; } - TypedArray GetInputs() const - { - return VectorToTypedArray(InputNodes); - } - - void SetPosition(Vector2i pos) { Position = pos; } - void SetInputNodes(TypedArray inputNodes) - { - InputNodes = TypedArrayToVector(inputNodes); - RefreshInputs(); - } - - bool NodeIsValid() const { return IsValid(); } - TypedArray NodeGetInputTypes() const { return VectorToTypedArrayCast(GetInputTypes()); } - int NodeGetOutputType() const { return GetOutputType(); } - void NodeSetInput(int index, Ref input) { SetInput(index, input); } - bool HasInternalNode() const { return InternalNode.get(); }; - - bool CanExecuteNode(); - - void SetInternalNode(std::unique_ptr&& node); - WorldNodeBase* GetInternalNode() const { return InternalNode.get(); } - void RefreshInputs(); - -public: - virtual Vector GetInputTypes() const { return InternalNode ? InternalNode->GetInputTypes() : Vector{}; }; - virtual Variant::Type GetOutputType() const { return InternalNode ? InternalNode->GetReturnType() : Variant::Type{}; }; - virtual void SetInput(int index, Ref input) - { - InputNodes.set(index, input); - InternalNode->SetInput(index, input.is_valid() ? input->InternalNode.get() : nullptr); - } - virtual bool IsValid() const - { - if (!InternalNode) - print_error(String("No internal node for ") + get_class_name()); - if (!InternalNode->IsValid()) - print_error(String("node is invalid ") + get_class_name()); - return InternalNode && InternalNode->IsValid(); - } - virtual void RefreshValues() {}; - -public: - Vector2i Position{}; - Vector> InputNodes{}; -private: - std::unique_ptr InternalNode{}; + // Binary float→float + Add, Subtract, Multiply, Divide, Modulo, Pow, Max, Min, + // Unary float→float + Negate, Abs, Ceil, Floor, Sin, Cos, Tan, Exp, Log, Square, Round, OneMinus, + // Ternary float→float + Lerp, Clamp, + // Comparison float→bool + Equal, Smaller, Greater, SmallerEqual, GreaterEqual, + // Logic bool→bool + And, Or, + // Branch + Branch, + // Noise (leaf, float output) + Simplex, OpenSimplex, Perlin, Value, ValueCubic, + // Other leaf nodes + Constant, IsTile, TileDistance, }; -class WorldGraphVisualNode_Math : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_Math, WorldGraphVisualNodeBase); -public: - static void _bind_methods(); +// ─── Core components (every node entity has these) ──────────────────────────── -public: - WorldGraphVisualNode_Math(); - virtual ~WorldGraphVisualNode_Math() = default; +struct VisualNodeType { VisualNodeKind kind; }; +struct VisualNodePos { float x, y; }; // canvas position in editor -public: - TypedArray GetNodeNames() const; - void SetNode(String nodeName); - String GetNode() const { return NodeID; } +// Mark this node as the graph's output (execution root). Only one per graph. +struct VisualNodeOutput {}; -private: - String NodeID{}; +// ─── Input connection relationships ─────────────────────────────────────────── +// To connect node B's input slot 0 to node A's output: +// b.add(a); +// Flecs serializes entity relationship targets by name, so these round-trip +// correctly through to_json / from_json. -}; +struct InputPin0 {}; // first input slot +struct InputPin1 {}; // second input slot +struct InputPin2 {}; // third input slot (Branch: bool, true, false) -class WorldGraphVisualNode_Constant : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_Constant, WorldGraphVisualNodeBase); -public: - static void _bind_methods(); +// ─── Leaf node parameter components ────────────────────────────────────────── +// Only nodes that carry configurable parameters have these. -public: - WorldGraphVisualNode_Constant(); - virtual ~WorldGraphVisualNode_Constant() = default; - -private: - void SetValue(float val); - float GetValue() const; -}; - -class WorldGraphVisualNode_If : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_If, WorldGraphVisualNodeBase); -public: - static void _bind_methods() {}; - -public: - WorldGraphVisualNode_If(); - virtual ~WorldGraphVisualNode_If() = default; -}; - -class WorldGraphVisualNode_Noise : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_Noise, WorldGraphVisualNodeBase); -public: - static void _bind_methods(); - -public: - void SetNoiseType(String noiseType); - String GetNoiseType() const { return NoiseType; } - float GetFrequency() const { return Frequency; } - - TypedArray GetNoiseTypes() const; - void SetFrequency(float val); - - virtual void RefreshValues() override; - -public: - WorldGraphVisualNode_Noise(); - virtual ~WorldGraphVisualNode_Noise() = default; - -private: - String NoiseType{}; - float Frequency{}; -}; - -class WorldGraphVisualNode_Tile : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_Tile, WorldGraphVisualNodeBase); -public: - static void _bind_methods(); - -public: - WorldGraphVisualNode_Tile(); - virtual ~WorldGraphVisualNode_Tile() = default; - -public: - void SetType(int type); - void SetRelativeX(int offset); - void SetRelativeY(int offset); - - int GetType() const; - int GetRelativeX() const; - int GetRelativeY() const; -}; - -class WorldGraphVisualNode_TileDistance : public WorldGraphVisualNodeBase -{ - GDCLASS(WorldGraphVisualNode_TileDistance, WorldGraphVisualNodeBase); -public: - static void _bind_methods(); - -public: - WorldGraphVisualNode_TileDistance(); - virtual ~WorldGraphVisualNode_TileDistance() = default; - -public: - void SetType(int type); - void SetRange(int range); - - int GetType() const; - int GetRange() const; -}; \ No newline at end of file +struct NodeParam_Constant { float value; }; +struct NodeParam_Noise { float frequency; }; +struct NodeParam_IsTile { int8_t relativeX; int8_t relativeY; TileType tileType; }; +struct NodeParam_TileDistance{ int8_t range; TileType tileType; }; diff --git a/include/Util/FastNoiseLite.h b/include/Util/FastNoiseLite.h index 03ac939..a440a0f 100644 --- a/include/Util/FastNoiseLite.h +++ b/include/Util/FastNoiseLite.h @@ -752,8 +752,8 @@ namespace fastnoiselitestatic { float distance = 1e10f; constexpr float cellularJitter = 0.43701595f /** mCellularJitterModifier*/; - constexpr int xPrimed = (x - 1) * PrimeX; - constexpr int yPrimedBase = (y - 1) * PrimeY; + int xPrimed = (x - 1) * PrimeX; + int yPrimedBase = (y - 1) * PrimeY; for (int xi = x - 1; xi <= x + 1; xi++) { diff --git a/src/Components/WorldGraph.cpp b/src/Components/WorldGraph.cpp new file mode 100644 index 0000000..30ca58c --- /dev/null +++ b/src/Components/WorldGraph.cpp @@ -0,0 +1,208 @@ +#include "Types/WorldGraph/WorldGraph.h" +#include "Types/WorldGraph/WorldGraphAllocator.h" + +#include +#include + +// ─── Helper: allocate the right runtime node for an entity ──────────────────── + +static WorldNodeBase* AllocateNode(WorldGraphAllocatorBase& alloc, flecs::entity e) +{ + const auto* t = e.try_get(); + if (!t) return nullptr; + + switch (t->kind) + { + // ── Binary float→float ────────────────────────────────────────────── + case VisualNodeKind::Add: return alloc.Allocate(WorldNode_Add{}); + case VisualNodeKind::Subtract: return alloc.Allocate(WorldNode_Subtract{}); + case VisualNodeKind::Multiply: return alloc.Allocate(WorldNode_Multiply{}); + case VisualNodeKind::Divide: return alloc.Allocate(WorldNode_Divide{}); + case VisualNodeKind::Modulo: return alloc.Allocate(WorldNode_Modulo{}); + case VisualNodeKind::Pow: return alloc.Allocate(WorldNode_Pow{}); + case VisualNodeKind::Max: return alloc.Allocate(WorldNode_Max{}); + case VisualNodeKind::Min: return alloc.Allocate(WorldNode_Min{}); + + // ── Unary float→float ─────────────────────────────────────────────── + case VisualNodeKind::Negate: return alloc.Allocate(WorldNode_Negate{}); + case VisualNodeKind::Abs: return alloc.Allocate(WorldNode_Abs{}); + case VisualNodeKind::Ceil: return alloc.Allocate(WorldNode_Ceil{}); + case VisualNodeKind::Floor: return alloc.Allocate(WorldNode_Floor{}); + case VisualNodeKind::Sin: return alloc.Allocate(WorldNode_Sin{}); + case VisualNodeKind::Cos: return alloc.Allocate(WorldNode_Cos{}); + case VisualNodeKind::Tan: return alloc.Allocate(WorldNode_Tan{}); + case VisualNodeKind::Exp: return alloc.Allocate(WorldNode_Exp{}); + case VisualNodeKind::Log: return alloc.Allocate(WorldNode_Log{}); + case VisualNodeKind::Square: return alloc.Allocate(WorldNode_Square{}); + case VisualNodeKind::Round: return alloc.Allocate(WorldNode_Round{}); + case VisualNodeKind::OneMinus: return alloc.Allocate(WorldNode_OneMinus{}); + + // ── Ternary float→float ───────────────────────────────────────────── + case VisualNodeKind::Lerp: return alloc.Allocate(WorldNode_Lerp{}); + case VisualNodeKind::Clamp: return alloc.Allocate(WorldNode_Clamp{}); + + // ── Comparison float→bool ─────────────────────────────────────────── + case VisualNodeKind::Equal: return alloc.Allocate(WorldNode_Equal{}); + case VisualNodeKind::Smaller: return alloc.Allocate(WorldNode_Smaller{}); + case VisualNodeKind::Greater: return alloc.Allocate(WorldNode_Greater{}); + case VisualNodeKind::SmallerEqual: return alloc.Allocate(WorldNode_SmallerEqual{}); + case VisualNodeKind::GreaterEqual: return alloc.Allocate(WorldNode_GreaterEqual{}); + + // ── Logic ─────────────────────────────────────────────────────────── + case VisualNodeKind::And: return alloc.Allocate(WorldNode_And{}); + case VisualNodeKind::Or: return alloc.Allocate(WorldNode_Or{}); + + // ── Branch ────────────────────────────────────────────────────────── + case VisualNodeKind::Branch: return alloc.Allocate(WorldNode_Branch{}); + + // ── Noise ─────────────────────────────────────────────────────────── + case VisualNodeKind::Simplex: { + WorldNode_Simplex n; + if (const auto* p = e.try_get()) n.Frequency = p->frequency; + return alloc.Allocate(n); + } + case VisualNodeKind::OpenSimplex: { + WorldNode_OpenSimplex n; + if (const auto* p = e.try_get()) n.Frequency = p->frequency; + return alloc.Allocate(n); + } + case VisualNodeKind::Perlin: { + WorldNode_Perlin n; + if (const auto* p = e.try_get()) n.Frequency = p->frequency; + return alloc.Allocate(n); + } + case VisualNodeKind::Value: { + WorldNode_Value n; + if (const auto* p = e.try_get()) n.Frequency = p->frequency; + return alloc.Allocate(n); + } + case VisualNodeKind::ValueCubic: { + WorldNode_ValueCubic n; + if (const auto* p = e.try_get()) n.Frequency = p->frequency; + return alloc.Allocate(n); + } + + // ── Constant ──────────────────────────────────────────────────────── + case VisualNodeKind::Constant: { + WorldNode_Constant n; + if (const auto* p = e.try_get()) n.Value = p->value; + return alloc.Allocate(n); + } + + // ── IsTile ────────────────────────────────────────────────────────── + case VisualNodeKind::IsTile: { + WorldNode_IsTile n; + if (const auto* p = e.try_get()) + { + n.RelativeX = p->relativeX; + n.RelativeY = p->relativeY; + n.Type = p->tileType; + } + return alloc.Allocate(n); + } + + // ── TileDistance ──────────────────────────────────────────────────── + case VisualNodeKind::TileDistance: { + WorldNode_TileDistance n; + if (const auto* p = e.try_get()) + { + n.Range = p->range; + n.Type = p->tileType; + } + return alloc.Allocate(n); + } + } + return nullptr; +} + +// ─── Wire input connections for one node ────────────────────────────────────── + +static void WireInputs( + flecs::entity e, + WorldNodeBase* runtimeNode, + const std::unordered_map& nodeMap) +{ + auto wire = [&](int slot, flecs::entity src) + { + if (!src) return; + auto it = nodeMap.find(src.id()); + if (it == nodeMap.end()) return; + runtimeNode->SetInput(slot, it->second); + }; + + wire(0, e.target()); + wire(1, e.target()); + wire(2, e.target()); +} + +// ─── WorldGraph::Compile ────────────────────────────────────────────────────── + +WorldGraph WorldGraph::Compile(flecs::world& world, flecs::entity graphRoot) +{ + // 1. Collect all child node entities + std::vector nodeEntities; + world.query_builder() + .with(flecs::ChildOf, graphRoot) + .build() + .each([&](flecs::entity e, VisualNodeType&) + { + nodeEntities.push_back(e); + }); + + if (nodeEntities.empty()) + return {}; + + // 2. Measure total memory needed + WorldGraphSizeMeasurer measurer; + for (auto e : nodeEntities) + AllocateNode(measurer, e); + + // 3. Allocate buffer and fill it + WorldGraphAllocator alloc(measurer.TotalSize); + + std::unordered_map nodeMap; + nodeMap.reserve(nodeEntities.size()); + + for (auto e : nodeEntities) + { + WorldNodeBase* runtimeNode = AllocateNode(alloc, e); + if (runtimeNode) + nodeMap[e.id()] = runtimeNode; + } + + // 4. Wire connections + for (auto e : nodeEntities) + { + auto it = nodeMap.find(e.id()); + if (it != nodeMap.end()) + WireInputs(e, it->second, nodeMap); + } + + // 5. Find the output node + WorldNodeBase* rootNode = nullptr; + for (auto e : nodeEntities) + { + if (e.has()) + { + auto it = nodeMap.find(e.id()); + if (it != nodeMap.end()) + rootNode = it->second; + break; + } + } + + // 6. Build and return the compiled graph + WorldGraph graph; + graph.MemorySize = alloc.CurrentOffset; + graph.CompiledData = std::move(alloc.Data); + graph.RootNode = rootNode; + return graph; +} + +// ─── WorldGraph::Execute ───────────────────────────────────────────────────── + +NodeValue WorldGraph::Execute(const WorldNodeParameters& params) const +{ + assert(IsValid()); + return RootNode->Evaluate(params); +} diff --git a/src/Core/WorldInstance.cpp b/src/Core/WorldInstance.cpp index 222067b..b95cb17 100644 --- a/src/Core/WorldInstance.cpp +++ b/src/Core/WorldInstance.cpp @@ -7,6 +7,7 @@ #include "Components/Tick.hpp" #include "Components/Chute.hpp" #include "Components/Support.h" +#include "Components/WorldGraph.hpp" WorldInstance::WorldInstance(const WorldConfig& worldConfig) { @@ -28,6 +29,7 @@ void WorldInstance::RegisterTypes(flecs::world &world) Flecs_Resource(world); Flecs_Chute(world); Flecs_Support(world); + Flecs_WorldGraph(world); } void WorldInstance::ProcessFrame() diff --git a/tools/node-editor/CMakeLists.txt b/tools/node-editor/CMakeLists.txt new file mode 100644 index 0000000..5dcb1b2 --- /dev/null +++ b/tools/node-editor/CMakeLists.txt @@ -0,0 +1,44 @@ +find_package(OpenGL REQUIRED) + +# ── ImGui static library ────────────────────────────────────────────────────── + +add_library(imgui_lib STATIC + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp +) + +target_include_directories(imgui_lib PUBLIC + ${imgui_SOURCE_DIR} + ${imgui_SOURCE_DIR}/backends +) + +target_link_libraries(imgui_lib PUBLIC glfw OpenGL::GL) +target_compile_definitions(imgui_lib PUBLIC IMGUI_DEFINE_MATH_OPERATORS) + +# ── imnodes static library ──────────────────────────────────────────────────── + +add_library(imnodes_lib STATIC + ${imnodes_SOURCE_DIR}/imnodes.cpp +) + +target_include_directories(imnodes_lib PUBLIC + ${imnodes_SOURCE_DIR} +) + +target_link_libraries(imnodes_lib PUBLIC imgui_lib) + +# ── Node editor executable ──────────────────────────────────────────────────── + +add_executable(node-editor + main.cpp +) + +target_link_libraries(node-editor PRIVATE + factory-hole-core + imgui_lib + imnodes_lib +) diff --git a/tools/node-editor/README.md b/tools/node-editor/README.md new file mode 100644 index 0000000..6d870cb --- /dev/null +++ b/tools/node-editor/README.md @@ -0,0 +1,96 @@ +# World Graph Node Editor + +A standalone visual node editor for authoring world generation graphs. Built with Dear ImGui and imnodes, styled after Blender's geometry nodes. + +## What it does + +Graphs describe how tiles are generated for each chunk in the world. Each node is a mathematical or contextual operation — noise functions, arithmetic, comparisons, tile queries — and they connect together into a tree that evaluates to a float value per tile. The game uses this value to decide what tile type to place. + +Graphs are saved as Flecs JSON (`graph.json`) and loaded by the game at runtime, where they are compiled into an efficient memory-pooled runtime graph and evaluated per-tile during chunk generation. + +## Controls + +| Action | How | +|--------|-----| +| Add a node | **Add Node** menu in the menu bar | +| Connect nodes | Drag from an output pin to an input pin | +| Disconnect a link | Right-click the link | +| Pan the canvas | Middle-click drag, or Alt + left-click drag | +| Zoom | Scroll wheel | +| Mark output node | Right-click a node → **Set as Output** | +| Delete a node | Right-click a node → **Delete Node** | +| Save graph | **File → Save** or **Ctrl+S** (writes `graph.json`) | +| Load graph | **File → Load** or **Ctrl+O** (reads `graph.json`) | + +The **output node** (highlighted in orange) is the root of evaluation — it must be set before the graph can be compiled by the game. + +## Node types + +### Math (binary, float → float) +`Add` `Subtract` `Multiply` `Divide` `Modulo` `Pow` `Max` `Min` + +### Math (unary, float → float) +`Negate` `Abs` `Ceil` `Floor` `Sin` `Cos` `Tan` `Exp` `Log` `Square` `Round` `OneMinus` + +### Math (ternary, float → float) +`Lerp` `Clamp` + +### Comparison (float → bool) +`Equal` `Smaller` `Greater` `SmallerEqual` `GreaterEqual` + +### Logic (bool → bool) +`And` `Or` + +### Control flow +`Branch` — inputs: condition (bool), true value (float), false value (float) + +### Noise (no inputs, float output) +`Simplex` `OpenSimplex` `Perlin` `Value` `ValueCubic` + +Parameters: **Frequency** — controls the scale of the noise. + +### Leaf nodes +| Node | Parameters | Output | +|------|-----------|--------| +| `Constant` | Value (float) | The constant value | +| `IsTile` | RelativeX, RelativeY, TileType | bool — true if the tile at the offset matches the type | +| `TileDistance` | Range (1–3), TileType | float — distance to the nearest tile of that type | + +## Graph file format + +Graphs are stored as Flecs JSON. Example of a simple graph that outputs Perlin noise: + +```json +{ + "entities": [ + { + "name": "Graph", + "children": [ + { + "name": "Perlin_0", + "components": { + "VisualNodeType": {"kind": "Perlin"}, + "VisualNodePos": {"x": 100.0, "y": 100.0}, + "NodeParam_Noise": {"frequency": 0.01}, + "VisualNodeOutput": {} + } + } + ] + } + ] +} +``` + +Connections between nodes are stored as Flecs relationships (`InputPin0`, `InputPin1`, `InputPin2`) on the destination node, referencing the source node by name. This makes the JSON human-readable and easy to hand-edit. + +## Building + +Built automatically as part of the main CMake project: + +```sh +cmake -B build +cmake --build build --target node-editor +./build/tools/node-editor/node-editor +``` + +Dependencies (fetched automatically via FetchContent): GLFW, Dear ImGui, imnodes. diff --git a/tools/node-editor/main.cpp b/tools/node-editor/main.cpp new file mode 100644 index 0000000..ae3ef6a --- /dev/null +++ b/tools/node-editor/main.cpp @@ -0,0 +1,492 @@ +// World Graph Node Editor +// A standalone Dear ImGui + imnodes tool for editing WorldGraph node graphs. +// Graphs are saved/loaded as Flecs JSON (graph.json in the working directory). + +#include +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" +#include "imnodes.h" + +#include "flecs.h" +#include "Components/WorldGraph.hpp" +#include "Types/WorldGraph/WorldGraph.h" + +#include +#include +#include +#include +#include +#include +#include + +// ─── Editor state ───────────────────────────────────────────────────────────── + +struct EditorNode +{ + int nodeId; // imnodes node ID + flecs::entity entity; + VisualNodeKind kind; + bool isOutput; +}; + +struct EditorLink +{ + int linkId; + int srcAttrId; // output attribute of source node + int dstAttrId; // input attribute (pin slot) of destination node +}; + +// Attribute ID encoding: +// node's output pin = nodeId * 10 + 9 +// node's input pin N = nodeId * 10 + N (N = 0,1,2) + +static int OutputAttr(int nodeId) { return nodeId * 10 + 9; } +static int InputAttr (int nodeId, int slot) { return nodeId * 10 + slot; } +static int AttrToNode(int attr) { return attr / 10; } +static int AttrToSlot(int attr) { return attr % 10; } // 9 = output + +// ─── Kind metadata ──────────────────────────────────────────────────────────── + +struct KindInfo { const char* name; int inputCount; bool hasParamConstant; bool hasParamNoise; bool hasParamIsTile; bool hasParamTileDistance; }; + +static const KindInfo KindInfoTable[] = { + // name ins const noise isTile tileDist + {"Add", 2, 0, 0, 0, 0}, + {"Subtract", 2, 0, 0, 0, 0}, + {"Multiply", 2, 0, 0, 0, 0}, + {"Divide", 2, 0, 0, 0, 0}, + {"Modulo", 2, 0, 0, 0, 0}, + {"Pow", 2, 0, 0, 0, 0}, + {"Max", 2, 0, 0, 0, 0}, + {"Min", 2, 0, 0, 0, 0}, + {"Negate", 1, 0, 0, 0, 0}, + {"Abs", 1, 0, 0, 0, 0}, + {"Ceil", 1, 0, 0, 0, 0}, + {"Floor", 1, 0, 0, 0, 0}, + {"Sin", 1, 0, 0, 0, 0}, + {"Cos", 1, 0, 0, 0, 0}, + {"Tan", 1, 0, 0, 0, 0}, + {"Exp", 1, 0, 0, 0, 0}, + {"Log", 1, 0, 0, 0, 0}, + {"Square", 1, 0, 0, 0, 0}, + {"Round", 1, 0, 0, 0, 0}, + {"OneMinus", 1, 0, 0, 0, 0}, + {"Lerp", 3, 0, 0, 0, 0}, + {"Clamp", 3, 0, 0, 0, 0}, + {"Equal", 2, 0, 0, 0, 0}, + {"Smaller", 2, 0, 0, 0, 0}, + {"Greater", 2, 0, 0, 0, 0}, + {"SmallerEqual", 2, 0, 0, 0, 0}, + {"GreaterEqual", 2, 0, 0, 0, 0}, + {"And", 2, 0, 0, 0, 0}, + {"Or", 2, 0, 0, 0, 0}, + {"Branch", 3, 0, 0, 0, 0}, + {"Simplex", 0, 0, 1, 0, 0}, + {"OpenSimplex", 0, 0, 1, 0, 0}, + {"Perlin", 0, 0, 1, 0, 0}, + {"Value", 0, 0, 1, 0, 0}, + {"ValueCubic", 0, 0, 1, 0, 0}, + {"Constant", 0, 1, 0, 0, 0}, + {"IsTile", 0, 0, 0, 1, 0}, + {"TileDistance", 0, 0, 0, 0, 1}, +}; + +static const KindInfo& GetKindInfo(VisualNodeKind k) +{ + return KindInfoTable[static_cast(k)]; +} + +// ─── Application ───────────────────────────────────────────────────────────── + +class NodeEditorApp +{ +public: + NodeEditorApp() + { + Flecs_WorldGraph(World); + GraphRoot = World.entity("Graph"); + } + + // ── Graph I/O ───────────────────────────────────────────────────────────── + + void Save(const char* path) + { + // Sync editor positions back to components before saving + for (auto& n : Nodes) + { + ImVec2 pos = ImNodes::GetNodeGridSpacePos(n.nodeId); + n.entity.set({pos.x, pos.y}); + } + char* json = ecs_world_to_json(World.c_ptr(), nullptr); + std::ofstream f(path); + f << json; + ecs_os_free(json); + printf("Saved to %s\n", path); + } + + void Load(const char* path) + { + std::ifstream f(path); + if (!f) { printf("Could not open %s\n", path); return; } + std::ostringstream ss; ss << f.rdbuf(); + std::string json = ss.str(); + ecs_world_from_json(World.c_ptr(), json.c_str(), nullptr); + RebuildEditorState(); + printf("Loaded from %s\n", path); + } + + // ── Rebuild editor state from Flecs world ───────────────────────────────── + + void RebuildEditorState() + { + Nodes.clear(); + Links.clear(); + NextId = 1; + + // Map entity → nodeId + std::unordered_map entityToNode; + + World.query_builder() + .with(flecs::ChildOf, GraphRoot) + .build() + .each([&](flecs::entity e, VisualNodeType& t) + { + int nid = NextId++; + entityToNode[e.id()] = nid; + Nodes.push_back({ nid, e, t.kind, e.has() }); + + // Restore canvas position + const VisualNodePos* pos = e.try_get(); + if (pos) ImNodes::SetNodeGridSpacePos(nid, ImVec2{pos->x, pos->y}); + }); + + // Rebuild links + for (auto& n : Nodes) + { + auto addLink = [&](int slot, flecs::entity src) + { + if (!src) return; + auto it = entityToNode.find(src.id()); + if (it == entityToNode.end()) return; + int srcNodeId = it->second; + Links.push_back({ NextId++, + OutputAttr(srcNodeId), + InputAttr(n.nodeId, slot) }); + }; + addLink(0, n.entity.target()); + addLink(1, n.entity.target()); + addLink(2, n.entity.target()); + } + } + + // ── Add a new node ──────────────────────────────────────────────────────── + + void AddNode(VisualNodeKind kind) + { + static int nameCounter = 0; + char name[64]; + snprintf(name, sizeof(name), "%s_%d", GetKindInfo(kind).name, nameCounter++); + + flecs::entity e = World.entity(name) + .child_of(GraphRoot) + .set({ kind }) + .set({ 100.f, 100.f }); + + // Add default parameters + if (GetKindInfo(kind).hasParamConstant) e.set({ 0.f }); + if (GetKindInfo(kind).hasParamNoise) e.set({ 0.01f }); + if (GetKindInfo(kind).hasParamIsTile) e.set({ 0, 0, TileType::Air }); + if (GetKindInfo(kind).hasParamTileDistance)e.set({ 1, TileType::Air }); + + int nid = NextId++; + Nodes.push_back({ nid, e, kind, false }); + ImNodes::SetNodeGridSpacePos(nid, { 100.f, 100.f }); + } + + // ── Render one frame ────────────────────────────────────────────────────── + + void Render() + { + // ── Menu bar ───────────────────────────────────────────────────────── + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Save", "Ctrl+S")) Save("graph.json"); + if (ImGui::MenuItem("Load", "Ctrl+O")) Load("graph.json"); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Add Node")) + { + for (int i = 0; i <= (int)VisualNodeKind::TileDistance; ++i) + { + if (ImGui::MenuItem(KindInfoTable[i].name)) + AddNode(static_cast(i)); + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + + // ── Full-screen canvas ──────────────────────────────────────────────── + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowPos({0, ImGui::GetFrameHeight()}); + ImGui::SetNextWindowSize({io.DisplaySize.x, io.DisplaySize.y - ImGui::GetFrameHeight()}); + ImGui::Begin("##canvas", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove); + + ImNodes::BeginNodeEditor(); + + // ── Draw nodes ──────────────────────────────────────────────────────── + for (auto& n : Nodes) + { + const KindInfo& info = GetKindInfo(n.kind); + + if (n.isOutput) + ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(150, 80, 20, 255)); + + ImNodes::BeginNode(n.nodeId); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted(info.name); + if (n.isOutput) ImGui::SameLine(); if (n.isOutput) ImGui::TextUnformatted(" [OUT]"); + ImNodes::EndNodeTitleBar(); + + // Input pins + static const char* slotLabels[] = { "In 0", "In 1", "In 2" }; + for (int s = 0; s < info.inputCount; ++s) + { + ImNodes::BeginInputAttribute(InputAttr(n.nodeId, s)); + ImGui::TextUnformatted(slotLabels[s]); + ImNodes::EndInputAttribute(); + } + + // Inline parameters + ImGui::PushItemWidth(120.f); + if (info.hasParamConstant) + { + auto* p = n.entity.try_get_mut(); + ImGui::DragFloat("##val", &p->value, 0.01f); + } + if (info.hasParamNoise) + { + auto* p = n.entity.try_get_mut(); + ImGui::DragFloat("Freq##", &p->frequency, 0.001f, 0.0001f, 10.f); + } + if (info.hasParamIsTile) + { + auto* p = n.entity.try_get_mut(); + ImGui::DragInt("RelX##", reinterpret_cast(&p->relativeX), 1.f, -4, 4); + ImGui::DragInt("RelY##", reinterpret_cast(&p->relativeY), 1.f, -4, 4); + int t = static_cast(p->tileType); + if (ImGui::Combo("Type##", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0")) + p->tileType = static_cast(t); + } + if (info.hasParamTileDistance) + { + auto* p = n.entity.try_get_mut(); + int r = p->range; + if (ImGui::SliderInt("Range##", &r, 1, 3)) p->range = static_cast(r); + int t = static_cast(p->tileType); + if (ImGui::Combo("Type##td", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0")) + p->tileType = static_cast(t); + } + ImGui::PopItemWidth(); + + // Output pin + ImNodes::BeginOutputAttribute(OutputAttr(n.nodeId)); + ImGui::TextUnformatted("Out"); + ImNodes::EndOutputAttribute(); + + ImNodes::EndNode(); + + if (n.isOutput) + ImNodes::PopColorStyle(); + } + + // ── Draw links ──────────────────────────────────────────────────────── + for (auto& l : Links) + ImNodes::Link(l.linkId, l.srcAttrId, l.dstAttrId); + + ImNodes::EndNodeEditor(); + + // ── Handle new link ─────────────────────────────────────────────────── + { + int startAttr, endAttr; + if (ImNodes::IsLinkCreated(&startAttr, &endAttr)) + { + // Normalise: output attr should be startAttr + if (AttrToSlot(startAttr) != 9) std::swap(startAttr, endAttr); + if (AttrToSlot(startAttr) == 9 && AttrToSlot(endAttr) != 9) + { + int srcNodeId = AttrToNode(startAttr); + int dstNodeId = AttrToNode(endAttr); + int slot = AttrToSlot(endAttr); + + auto srcIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == srcNodeId; }); + auto dstIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == dstNodeId; }); + + if (srcIt != Nodes.end() && dstIt != Nodes.end()) + { + flecs::entity src = srcIt->entity; + flecs::entity dst = dstIt->entity; + + // Remove existing connection on this slot + if (slot == 0) dst.remove(flecs::Wildcard); + if (slot == 1) dst.remove(flecs::Wildcard); + if (slot == 2) dst.remove(flecs::Wildcard); + + // Add new connection + if (slot == 0) dst.add(src); + if (slot == 1) dst.add(src); + if (slot == 2) dst.add(src); + + Links.push_back({ NextId++, startAttr, endAttr }); + } + } + } + } + + // ── Handle link deletion ────────────────────────────────────────────── + { + int linkId; + if (ImNodes::IsLinkDestroyed(&linkId)) + { + auto it = std::find_if(Links.begin(), Links.end(), [&](auto& l){ return l.linkId == linkId; }); + if (it != Links.end()) + { + int dstNodeId = AttrToNode(it->dstAttrId); + int slot = AttrToSlot(it->dstAttrId); + auto dstIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == dstNodeId; }); + if (dstIt != Nodes.end()) + { + if (slot == 0) dstIt->entity.remove(flecs::Wildcard); + if (slot == 1) dstIt->entity.remove(flecs::Wildcard); + if (slot == 2) dstIt->entity.remove(flecs::Wildcard); + } + Links.erase(it); + } + } + } + + // ── Context menu: mark output / delete ─────────────────────────────── + { + int hoveredNode = -1; + if (ImNodes::IsNodeHovered(&hoveredNode) && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + ContextMenuNodeId = hoveredNode; + ImGui::OpenPopup("node_ctx"); + } + + if (ImGui::BeginPopup("node_ctx")) + { + int nodeId = ContextMenuNodeId; + auto it = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == nodeId; }); + if (it != Nodes.end()) + { + if (ImGui::MenuItem("Set as Output")) + { + for (auto& n : Nodes) { n.isOutput = false; n.entity.remove(); } + it->isOutput = true; + it->entity.add(); + } + if (ImGui::MenuItem("Delete Node")) + { + // Remove all links connected to this node + Links.erase(std::remove_if(Links.begin(), Links.end(), [&](auto& l) + { + return AttrToNode(l.srcAttrId) == nodeId || AttrToNode(l.dstAttrId) == nodeId; + }), Links.end()); + it->entity.destruct(); + Nodes.erase(it); + } + } + ImGui::EndPopup(); + } + } + + ImGui::End(); + } + + flecs::world World; + flecs::entity GraphRoot; + std::vector Nodes; + std::vector Links; + int NextId = 1; + int ContextMenuNodeId = -1; +}; + +// ─── GLFW error callback ────────────────────────────────────────────────────── + +static void GLFWErrorCallback(int error, const char* desc) +{ + fprintf(stderr, "GLFW error %d: %s\n", error, desc); +} + +// ─── main ───────────────────────────────────────────────────────────────────── + +int main() +{ + glfwSetErrorCallback(GLFWErrorCallback); + if (!glfwInit()) return 1; + + const char* glsl_version = "#version 330"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + GLFWwindow* window = glfwCreateWindow(1280, 720, "World Graph Editor", nullptr, nullptr); + if (!window) { glfwTerminate(); return 1; } + + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImNodes::CreateContext(); + + ImGui::StyleColorsDark(); + ImNodes::StyleColorsDark(); + + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init(glsl_version); + + NodeEditorApp app; + + while (!glfwWindowShouldClose(window)) + { + glfwPollEvents(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + // Ctrl+S / Ctrl+O shortcuts + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) + { + if (ImGui::IsKeyPressed(ImGuiKey_S)) app.Save("graph.json"); + if (ImGui::IsKeyPressed(ImGuiKey_O)) app.Load("graph.json"); + } + + app.Render(); + + ImGui::Render(); + int w, h; + glfwGetFramebufferSize(window, &w, &h); + glViewport(0, 0, w, h); + glClearColor(0.15f, 0.15f, 0.15f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(window); + } + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImNodes::DestroyContext(); + ImGui::DestroyContext(); + glfwDestroyWindow(window); + glfwTerminate(); + return 0; +}