initial commit

This commit is contained in:
Connor
2026-02-20 22:50:05 +09:00
parent cd745f0eda
commit 88e33be91b
16 changed files with 1284 additions and 722 deletions

View File

@@ -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)

1
graph.json Normal file

File diff suppressed because one or more lines are too long

8
imgui.ini Normal file
View File

@@ -0,0 +1,8 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
[Window][##canvas]
Pos=0,19
Size=1280,701

View File

@@ -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;

View File

@@ -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<VisualNodeKind>()
.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<TileType>()
.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<VisualNodeType>()
.member<VisualNodeKind>("kind");
world.component<VisualNodePos>()
.member<float>("x")
.member<float>("y");
world.component<VisualNodeOutput>();
// ── Connection relationship tags ────────────────────────────────────────
world.component<InputPin0>();
world.component<InputPin1>();
world.component<InputPin2>();
// ── Leaf parameter components ───────────────────────────────────────────
world.component<NodeParam_Constant>()
.member<float>("value");
world.component<NodeParam_Noise>()
.member<float>("frequency");
world.component<NodeParam_IsTile>()
.member<int8_t>("relativeX")
.member<int8_t>("relativeY")
.member<TileType>("tileType");
world.component<NodeParam_TileDistance>()
.member<int8_t>("range")
.member<TileType>("tileType");
}

View File

@@ -84,7 +84,7 @@ public:
uint32_t hash() const { return static_cast<uint32_t>(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> Chunk{};
std::unique_ptr<Chunk> TileData{};
std::vector<EntityTile> Entities{};
std::vector<EntityTile> PersistantEntities{};
};

View File

@@ -1,30 +1,40 @@
#pragma once
#include "WorldGraphVisualNode.h"
#include "WorldGraphNode.h"
#include "flecs.h"
#include <memory>
#include <unordered_map>
// 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<Ref<WorldGraphVisualNodeBase>>& 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<WorldGraphVisualNodeBase> node, const WorldNodeParameters& params) const;
WorldNodeBase* GetNode(Ref<WorldGraphVisualNodeBase> 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<Ref<WorldGraphVisualNodeBase>>& nodes);
std::unique_ptr<WorldNodeBase*[]> CopyMemory(HashMap<Ref<WorldGraphVisualNodeBase>, WorldNodeBase*>& nodeMap) const;
private:
int MemorySize{};
std::unique_ptr<WorldNodeBase*[]> CompiledData{};
HashMap<Ref<WorldGraphVisualNodeBase>, WorldNodeBase*> NodeMap{};
uint32_t MemorySize{};
std::unique_ptr<uint8_t[]> CompiledData{};
WorldNodeBase* RootNode{};
};

View File

@@ -1,7 +1,8 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "modules/factory/include/Util/Span.h"
#include "Util/Span.h"
struct WorldGraphAllocatorBase
{

View File

@@ -5,60 +5,43 @@
#include <utility>
#include <functional>
#include <array>
#include <vector>
#include <cmath>
#include <cassert>
#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<uint32_t&>(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); }
// float GetFloat() const { _ASSERT(IsFloat()); return reinterpret_cast<const float&>(Data); }
// float GetBool() const { _ASSERT(IsBool()); return Data; }
// public:
// operator float() const { return GetFloat(); }
// private:
// uint32_t Data{};
// };
struct WorldNodeParameters;
struct alignas(void*) WorldNodeBase
struct NodeValue
{
virtual Variant Evaluate(const WorldNodeParameters& params) const = 0;
virtual Variant::Type GetReturnType() const = 0;
virtual Vector<Variant::Type> GetInputTypes() const = 0;
virtual bool IsValid() const = 0;
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const = 0;
virtual void SetInput(int index, WorldNodeBase* input) = 0;
union { float f; bool b; } data{};
NodeValueType type = NodeValueType::Float;
NodeValue() = default;
NodeValue(float v) : type(NodeValueType::Float) { data.f = v; }
NodeValue(bool v) : type(NodeValueType::Bool) { data.b = v; }
template<typename T> T get() const;
};
template<> inline float NodeValue::get<float>() const { return data.f; }
template<> inline bool NodeValue::get<bool>() const { return data.b; }
// ─── Parameters passed per-tile during evaluation ────────────────────────────
struct WorldNodeParameters
{
static constexpr int MaxQueryOffset = 4;
static constexpr int PaddedChunkSide = Chunk::ChunkSize + MaxQueryOffset * 2;
static constexpr int PaddedChunkSize = PaddedChunkSide * PaddedChunkSide;
typedef std::array<Tile, PaddedChunkSize> TileArray;
using TileArray = std::array<Tile, PaddedChunkSize>;
int X{};
int Y{};
@@ -71,11 +54,9 @@ struct WorldNodeParameters
Tile GetTile(int x, int y) const
{
auto bounds = GetGenerationBounds();
if (unlikely(!bounds.has_point(Vector2i{x, y})))
if (!BoundsHasPoint(bounds, x, y))
return {};
// DEV_ASSERT(bounds.has_point(Vector2i{x, y}));
return (*GeneratedTiles)[(y - bounds.position.y) * PaddedChunkSide + (x - bounds.position.x)];
return (*GeneratedTiles)[(y - bounds.Min.Y) * PaddedChunkSide + (x - bounds.Min.X)];
}
static int GetArrayIndex(int x, int y)
@@ -83,53 +64,67 @@ 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 <typename T>
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<NodeValueType> GetInputTypes() const = 0;
virtual bool IsValid() const = 0;
virtual void Allocate(WorldGraphAllocatorBase* allocator) const = 0;
virtual void SetInput(int index, WorldNodeBase* input) = 0;
};
// ─── Typed templated base ─────────────────────────────────────────────────────
template <typename Return, typename... Inputs>
struct WorldNodeTemplated : public WorldNodeBase
{
std::array<WorldNodeBase*, sizeof...(Inputs)> InputNodes{};
virtual Variant::Type GetReturnType() const override { return Variant::get_type_t<Return>(); }
virtual Vector<Variant::Type> GetInputTypes() const override
virtual NodeValueType GetReturnType() const override
{
return { Variant::get_type_t<Inputs>()... };
};
if constexpr (std::is_same_v<Return, float>) return NodeValueType::Float;
else return NodeValueType::Bool;
}
virtual std::vector<NodeValueType> GetInputTypes() const override
{
return { (std::is_same_v<Inputs, float> ? NodeValueType::Float : NodeValueType::Bool)... };
}
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
{
std::array<Variant, sizeof...(Inputs)> results{};
for (int i{}; i < sizeof...(Inputs); ++i)
{
results[i] = InputNodes[i]->Evaluate(params);
for (auto* input : InputNodes)
if (!input) return false;
return true;
}
auto EvaluateFunctor = [this](typename TtoVariant<Inputs>::TVariant... args) -> Variant
virtual void Allocate(WorldGraphAllocatorBase* allocator) const override
{
return EvaluateT(args.get_unsafe_t<Inputs>()...);
};
allocator->Allocate(*this);
}
return std::apply(EvaluateFunctor, results);
virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{
// Collect input results into an array
auto inputValues = [&]<size_t... I>(std::index_sequence<I...>) {
return std::array<NodeValue, sizeof...(Inputs)>{ InputNodes[I]->Evaluate(params)... };
}(std::make_index_sequence<sizeof...(Inputs)>{});
// Unpack typed values and call EvaluateT
return [&]<size_t... I>(std::index_sequence<I...>) -> NodeValue {
return NodeValue(EvaluateT(
inputValues[I].template get<std::tuple_element_t<I, std::tuple<Inputs...>>>()...
));
}(std::make_index_sequence<sizeof...(Inputs)>{});
}
virtual void SetInput(int index, WorldNodeBase* input) override
@@ -137,267 +132,56 @@ struct WorldNodeTemplated : public WorldNodeBase
InputNodes[index] = input;
}
virtual Return EvaluateT(Inputs...) const = 0;
};
struct WorldNode_Add : public WorldNodeTemplated<float, float, float>
// ─── Math nodes ──────────────────────────────────────────────────────────────
struct WorldNode_Add : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a+b;} };
struct WorldNode_Subtract : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a-b;} };
struct WorldNode_Multiply : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a*b;} };
struct WorldNode_Divide : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a/b;} };
struct WorldNode_Modulo : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::fmod(a,b);} };
struct WorldNode_Pow : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::pow(a,b);} };
struct WorldNode_Max : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::max(a,b);} };
struct WorldNode_Min : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::min(a,b);} };
struct WorldNode_Negate : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return -v;} };
struct WorldNode_Abs : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::abs(v);} };
struct WorldNode_Ceil : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::ceil(v);} };
struct WorldNode_Floor : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::floor(v);} };
struct WorldNode_Sin : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::sin(v);} };
struct WorldNode_Cos : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::cos(v);} };
struct WorldNode_Tan : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::tan(v);} };
struct WorldNode_Exp : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::exp(v);} };
struct WorldNode_Log : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::log(v);} };
struct WorldNode_Square : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return v*v;} };
struct WorldNode_Round : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::round(v);} };
struct WorldNode_OneMinus : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return 1.f-v;} };
struct WorldNode_Lerp : WorldNodeTemplated<float,float,float,float>
{
virtual float EvaluateT(float v0, float v1) const override
float EvaluateT(float from, float to, float t) const override { return std::lerp(from, to, t); }
};
struct WorldNode_Clamp : WorldNodeTemplated<float,float,float,float>
{
return v0 + v1;
}
float EvaluateT(float v, float lo, float hi) const override { return std::clamp(v, lo, hi); }
};
struct WorldNode_Subtract : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return v0 - v1;
}
};
// ─── Comparison nodes ─────────────────────────────────────────────────────────
struct WorldNode_Multiply : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return v0 * v1;
}
};
struct WorldNode_Equal : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a==b;} };
struct WorldNode_Smaller : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a<b;} };
struct WorldNode_Greater : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a>b;} };
struct WorldNode_SmallerEqual : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a<=b;} };
struct WorldNode_GreaterEqual : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a>=b;} };
struct WorldNode_Divide : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return v0 / v1;
}
};
// ─── Logic nodes ──────────────────────────────────────────────────────────────
struct WorldNode_Modulo : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return fmod(v0, v1);
}
};
struct WorldNode_And : WorldNodeTemplated<bool,bool,bool> { bool EvaluateT(bool a,bool b)const override{return a&&b;} };
struct WorldNode_Or : WorldNodeTemplated<bool,bool,bool> { bool EvaluateT(bool a,bool b)const override{return a||b;} };
struct WorldNode_Equal : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 == v1;
}
};
struct WorldNode_Smaller : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 < v1;
}
};
struct WorldNode_Greater : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 > v1;
}
};
struct WorldNode_SmallerEqual : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 <= v1;
}
};
struct WorldNode_GreaterEqual : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 >= v1;
}
};
struct WorldNode_Negate : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return -v;
}
};
struct WorldNode_Abs : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::abs(v);
}
};
struct WorldNode_Ceil : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::ceil(v);
}
};
struct WorldNode_Floor : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::floor(v);
}
};
struct WorldNode_Sin : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::sin(v);
}
};
struct WorldNode_Cos : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::cos(v);
}
};
struct WorldNode_Tan : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::tan(v);
}
};
struct WorldNode_Exp : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::exp(v);
}
};
struct WorldNode_Pow : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::pow(v0, v1);
}
};
struct WorldNode_Max : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::max(v0, v1);
}
};
struct WorldNode_Min : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::min(v0, v1);
}
};
struct WorldNode_Clamp : public WorldNodeTemplated<float, float, float, float>
{
virtual float EvaluateT(float v0, float v1, float v2) const override
{
return std::clamp(v0, v1, v2);
}
};
struct WorldNode_Round : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::round(v);
}
};
struct WorldNode_Log : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::log(v);
}
};
struct WorldNode_Square : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return v * v;
}
};
struct WorldNode_Lerp : public WorldNodeTemplated<float, float, float, float>
{
virtual float EvaluateT(float from, float to, float weight) const override
{
return Math::lerp(from, to, weight);
}
};
struct WorldNode_OneMinus : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float val) const override
{
return 1 - val;
}
};
struct WorldNode_And : public WorldNodeTemplated<bool, bool, bool>
{
virtual bool EvaluateT(bool val0, bool val1) const override
{
return val0 && val1;
}
};
struct WorldNode_Or : public WorldNodeTemplated<bool, bool, bool>
{
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<Variant::Type> 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
{
@@ -405,261 +189,158 @@ struct WorldNode_Branch : public WorldNodeBase
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<bool>();
return condition ? InputTrue->Evaluate(params) : InputFalse->Evaluate(params);
}
virtual Variant::Type GetReturnType() const override { return Variant::Type::FLOAT; }
virtual Vector<Variant::Type> 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<NodeValueType> 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<NodeValueType> 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<Variant::Type> 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<NodeValueType> 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<float>(seed, x, y);
return fastnoiselitestatic::SingleSimplex<float>(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<float>(seed, x, y);
return fastnoiselitestatic::SingleOpenSimplex2S<float>(seed, x + t, y + t);
}
};
struct WorldNode_Perlin : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SinglePerlin<float>(s,x,y);} };
struct WorldNode_ValueCubic : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValueCubic<float>(s,x,y);} };
struct WorldNode_Value : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValue<float>(s,x,y);} };
struct WorldNode_Perlin : public WorldNode_NoiseBase
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SinglePerlin<float>(seed, x, y);
}
};
struct WorldNode_ValueCubic : public WorldNode_NoiseBase
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SingleValueCubic<float>(seed, x, y);
}
};
struct WorldNode_Value : public WorldNode_NoiseBase
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SingleValue<float>(seed, x, y);
}
};
// ─── Tile query nodes ─────────────────────────────────────────────────────────
struct WorldNode_IsTile : public WorldNodeBase
{
int8_t RelativeX{};
int8_t RelativeY{};
TILE_TYPE TileType{};
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<Variant::Type> 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<NodeValueType> 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{};
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 constexpr int ArraySize =
(WorldNodeParameters::MaxQueryOffset * 2 + 1) *
(WorldNodeParameters::MaxQueryOffset * 2 + 1) - 1;
static std::array<SmallVectorI2, ArraySize> CreateSortedOffsets()
static std::array<SmallVec2I, ArraySize> CreateSortedOffsets()
{
std::array<SmallVectorI2, ArraySize> 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<int8_t>(x), static_cast<int8_t>(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<SmallVec2I, ArraySize> 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<int8_t>(x), static_cast<int8_t>(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<SmallVectorI2, ArraySize> SortedOffsets{ CreateSortedOffsets() };
static std::array<int, WorldNodeParameters::MaxQueryOffset> CreateRangeOffsets()
{
std::array<int, WorldNodeParameters::MaxQueryOffset> 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<SmallVec2I, ArraySize> SortedOffsets{ CreateSortedOffsets() };
inline static const std::array<int, WorldNodeParameters::MaxQueryOffset> 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
// };
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 NodeValue(std::sqrt(static_cast<float>(maxRangeSQ)));
}
return sqrtf(static_cast<float>(maxRangeSQ));
}
virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; }
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { };
virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; }
virtual std::vector<NodeValueType> 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); }
};
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);
}
};
// 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<float>(seed, x, y);
// }
// };

View File

@@ -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 <cstdint>
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<WorldGraphVisualNodeBase> GetInputs() const
{
return VectorToTypedArray(InputNodes);
}
void SetPosition(Vector2i pos) { Position = pos; }
void SetInputNodes(TypedArray<WorldGraphVisualNodeBase> inputNodes)
{
InputNodes = TypedArrayToVector(inputNodes);
RefreshInputs();
}
bool NodeIsValid() const { return IsValid(); }
TypedArray<int> NodeGetInputTypes() const { return VectorToTypedArrayCast<int>(GetInputTypes()); }
int NodeGetOutputType() const { return GetOutputType(); }
void NodeSetInput(int index, Ref<WorldGraphVisualNodeBase> input) { SetInput(index, input); }
bool HasInternalNode() const { return InternalNode.get(); };
bool CanExecuteNode();
void SetInternalNode(std::unique_ptr<WorldNodeBase>&& node);
WorldNodeBase* GetInternalNode() const { return InternalNode.get(); }
void RefreshInputs();
public:
virtual Vector<Variant::Type> GetInputTypes() const { return InternalNode ? InternalNode->GetInputTypes() : Vector<Variant::Type>{}; };
virtual Variant::Type GetOutputType() const { return InternalNode ? InternalNode->GetReturnType() : Variant::Type{}; };
virtual void SetInput(int index, Ref<WorldGraphVisualNodeBase> 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<Ref<WorldGraphVisualNodeBase>> InputNodes{};
private:
std::unique_ptr<WorldNodeBase> 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<String> 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<InputPin0>(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<String> 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;
};
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; };

View File

@@ -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++)
{

View File

@@ -0,0 +1,208 @@
#include "Types/WorldGraph/WorldGraph.h"
#include "Types/WorldGraph/WorldGraphAllocator.h"
#include <unordered_map>
#include <cassert>
// ─── Helper: allocate the right runtime node for an entity ────────────────────
static WorldNodeBase* AllocateNode(WorldGraphAllocatorBase& alloc, flecs::entity e)
{
const auto* t = e.try_get<VisualNodeType>();
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<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::OpenSimplex: {
WorldNode_OpenSimplex n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::Perlin: {
WorldNode_Perlin n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::Value: {
WorldNode_Value n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::ValueCubic: {
WorldNode_ValueCubic n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
// ── Constant ────────────────────────────────────────────────────────
case VisualNodeKind::Constant: {
WorldNode_Constant n;
if (const auto* p = e.try_get<NodeParam_Constant>()) n.Value = p->value;
return alloc.Allocate(n);
}
// ── IsTile ──────────────────────────────────────────────────────────
case VisualNodeKind::IsTile: {
WorldNode_IsTile n;
if (const auto* p = e.try_get<NodeParam_IsTile>())
{
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<NodeParam_TileDistance>())
{
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<uint64_t, WorldNodeBase*>& 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<InputPin0>());
wire(1, e.target<InputPin1>());
wire(2, e.target<InputPin2>());
}
// ─── WorldGraph::Compile ──────────────────────────────────────────────────────
WorldGraph WorldGraph::Compile(flecs::world& world, flecs::entity graphRoot)
{
// 1. Collect all child node entities
std::vector<flecs::entity> nodeEntities;
world.query_builder<VisualNodeType>()
.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<uint64_t, WorldNodeBase*> 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<VisualNodeOutput>())
{
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);
}

View File

@@ -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()

View File

@@ -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
)

View File

@@ -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 (13), 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.

492
tools/node-editor/main.cpp Normal file
View File

@@ -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 <GLFW/glfw3.h>
#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 <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
// ─── 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<int>(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<VisualNodePos>({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<uint64_t, int> entityToNode;
World.query_builder<VisualNodeType>()
.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<VisualNodeOutput>() });
// Restore canvas position
const VisualNodePos* pos = e.try_get<VisualNodePos>();
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<InputPin0>());
addLink(1, n.entity.target<InputPin1>());
addLink(2, n.entity.target<InputPin2>());
}
}
// ── 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<VisualNodeType>({ kind })
.set<VisualNodePos>({ 100.f, 100.f });
// Add default parameters
if (GetKindInfo(kind).hasParamConstant) e.set<NodeParam_Constant>({ 0.f });
if (GetKindInfo(kind).hasParamNoise) e.set<NodeParam_Noise>({ 0.01f });
if (GetKindInfo(kind).hasParamIsTile) e.set<NodeParam_IsTile>({ 0, 0, TileType::Air });
if (GetKindInfo(kind).hasParamTileDistance)e.set<NodeParam_TileDistance>({ 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<VisualNodeKind>(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<NodeParam_Constant>();
ImGui::DragFloat("##val", &p->value, 0.01f);
}
if (info.hasParamNoise)
{
auto* p = n.entity.try_get_mut<NodeParam_Noise>();
ImGui::DragFloat("Freq##", &p->frequency, 0.001f, 0.0001f, 10.f);
}
if (info.hasParamIsTile)
{
auto* p = n.entity.try_get_mut<NodeParam_IsTile>();
ImGui::DragInt("RelX##", reinterpret_cast<int*>(&p->relativeX), 1.f, -4, 4);
ImGui::DragInt("RelY##", reinterpret_cast<int*>(&p->relativeY), 1.f, -4, 4);
int t = static_cast<int>(p->tileType);
if (ImGui::Combo("Type##", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0"))
p->tileType = static_cast<TileType>(t);
}
if (info.hasParamTileDistance)
{
auto* p = n.entity.try_get_mut<NodeParam_TileDistance>();
int r = p->range;
if (ImGui::SliderInt("Range##", &r, 1, 3)) p->range = static_cast<int8_t>(r);
int t = static_cast<int>(p->tileType);
if (ImGui::Combo("Type##td", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0"))
p->tileType = static_cast<TileType>(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<InputPin0>(flecs::Wildcard);
if (slot == 1) dst.remove<InputPin1>(flecs::Wildcard);
if (slot == 2) dst.remove<InputPin2>(flecs::Wildcard);
// Add new connection
if (slot == 0) dst.add<InputPin0>(src);
if (slot == 1) dst.add<InputPin1>(src);
if (slot == 2) dst.add<InputPin2>(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<InputPin0>(flecs::Wildcard);
if (slot == 1) dstIt->entity.remove<InputPin1>(flecs::Wildcard);
if (slot == 2) dstIt->entity.remove<InputPin2>(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<VisualNodeOutput>(); }
it->isOutput = true;
it->entity.add<VisualNodeOutput>();
}
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<EditorNode> Nodes;
std::vector<EditorLink> 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;
}