imgui implementation

This commit is contained in:
Connor
2026-02-22 15:19:54 +09:00
parent 7b80eda561
commit 3d4453b9ea
7 changed files with 1708 additions and 7 deletions

View File

@@ -53,10 +53,16 @@ FetchContent_Declare(
# ---- Frontend dependencies (used by tools/) ----------------------------------
# SDL2 install via your system package manager, e.g.:
# sudo apt install libsdl2-dev (Debian/Ubuntu)
# brew install sdl2 (macOS)
find_package(SDL2 QUIET)
# GLFW windowing for the 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(
@@ -74,9 +80,15 @@ FetchContent_Declare(
GIT_SHALLOW TRUE
)
# Make available what we need right now.
# imgui / imnodes / SDL2 / FastNoise2 are deferred until tools/ is built.
FetchContent_MakeAvailable(flecs doctest glm nlohmann_json)
# ImGuiFileDialog file browser widget for the node-editor tool
FetchContent_Declare(
ImGuiFileDialog
GIT_REPOSITORY https://github.com/aiekick/ImGuiFileDialog.git
GIT_TAG v0.6.7
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(flecs doctest glm nlohmann_json glfw imgui imnodes)
# ---- Core library ------------------------------------------------------------
@@ -125,3 +137,7 @@ target_link_libraries(factory-hole-tests PRIVATE
)
add_test(NAME factory-hole-tests COMMAND factory-hole-tests)
# ---- Tools -------------------------------------------------------------------
add_subdirectory(tools/node-editor)

43
graph.json Normal file
View File

@@ -0,0 +1,43 @@
{
"editor": {
"nodePositions": {
"1": [
-170.0,
-162.0
],
"__worldOutput": [
400.0,
0.0
]
},
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 1.0,
"seed": 0,
"tileRegistry": [
{
"color": [
0.7348794341087341,
0.730800986289978,
0.7554347515106201
],
"id": 1,
"name": "Stone"
}
],
"worldOutputPasses": [
1
]
},
"graph": {
"connections": [],
"nextId": 2,
"nodes": [
{
"id": 1,
"tileId": 1,
"type": "TileID"
}
]
}
}

36
imgui.ini Normal file
View File

@@ -0,0 +1,36 @@
[Window][Node Canvas]
Pos=200,18
Size=1080,702
[Window][Settings]
Pos=0,18
Size=200,702
[Window][Debug##Default]
Pos=60,60
Size=400,400
[Window][Save Graph##SaveFileDlg]
Pos=323,94
Size=600,400
[Window][Open Graph##OpenFileDlg]
Pos=60,60
Size=600,400
[Table][0x4ED31530,4]
RefScale=13
Column 0 Sort=0v
[Table][0x837FA078,4]
RefScale=13
Column 0 Sort=0v
[Table][0x80A59430,4]
RefScale=13
Column 0 Sort=0v
[Table][0xF2EF1CF5,4]
RefScale=13
Column 0 Sort=0v

View File

@@ -0,0 +1,213 @@
# WorldGraph
A node-based procedural tile generation system. Graphs are composed of typed nodes wired together; evaluating the graph at a given world cell produces a tile ID.
---
## Overview
World generation is expressed as a **directed acyclic graph (DAG)** of computation nodes. Each node takes zero or more typed inputs, performs some computation, and produces a single typed output. The graph is evaluated independently **per cell** — there is no shared mutable state between cells.
The final result is an integer **tile ID**, obtained by calling `AsInt()` on whatever value the output node produces.
---
## Core Types (`WorldGraphTypes.h`)
### `Value`
A tagged union that can hold an `Int`, `Float`, or `Bool`. Values are coerced automatically when a node requests a different type (e.g. `AsFloat()` on an Int just casts it). This means connections between different types are permissive at runtime, though the editor can flag them.
### `EvalContext`
The per-cell context forwarded to every node during evaluation:
| Field | Description |
|---|---|
| `worldX`, `worldY` | World-space tile coordinates of the cell being generated |
| `seed` | World seed for deterministic noise |
| `prevTiles` | Flat row-major array of tile IDs from the previous generation pass |
| `prevOriginX/Y`, `prevWidth/Height` | Position and size of the previous pass buffer |
`GetPrevTile(x, y)` safely reads the previous pass at any absolute world coordinate, returning `0` (AIR) for out-of-bounds or when no previous pass exists.
---
## Graph (`WorldGraph.h`)
`Graph` owns a set of nodes and the connections between them.
```
NodeID AddNode(unique_ptr<Node>) — add a node, returns its ID
bool Connect(from, to, inputSlot) — wire an output to an input
Value Evaluate(outputNode, ctx) — evaluate for one cell
bool IsValid(outputNode) — check all inputs are wired
```
**Cycle detection** runs at `Connect()` time. Any connection that would form a cycle is rejected and returns `false`.
**Evaluation** is recursive and on-demand. Starting from the output node, the graph walks upstream, evaluating each dependency before the node that depends on it. Unconnected inputs receive a zero value of their declared type.
---
## Nodes (`WorldGraphNode.h`)
All nodes derive from the abstract `Node` base class and implement:
- `GetOutputType()` — the type this node produces
- `GetInputTypes()` — the types of each input slot
- `Evaluate(ctx, inputs)` — computes and returns the output value
### Source Nodes (no inputs)
| Node | Output | Description |
|---|---|---|
| `ConstantNode` | Float | A fixed float value |
| `IDNode` | Int | A fixed tile ID integer |
| `PositionXNode` | Int | World X coordinate of the current cell |
| `PositionYNode` | Int | World Y coordinate of the current cell |
### Math Nodes
| Node | Inputs | Output | Description |
|---|---|---|---|
| `AddNode` | Float, Float | Float | a + b |
| `SubtractNode` | Float, Float | Float | a b |
| `MultiplyNode` | Float, Float | Float | a × b |
| `DivideNode` | Float, Float | Float | a / b (0 on divide-by-zero) |
| `ModuloNode` | Float, Float | Float | fmod(a, b) |
| `SinNode` | Float | Float | sin(a) in radians |
| `CosNode` | Float | Float | cos(a) in radians |
### Comparison Nodes
All produce `Bool`. Inputs are Float.
`LessNode`, `GreaterNode`, `LessEqualNode`, `GreaterEqualNode`, `EqualNode`
> **Note:** `EqualNode` compares floats directly — use only with integer-valued inputs.
### Boolean Logic Nodes
| Node | Inputs | Output |
|---|---|---|
| `AndNode` | Bool, Bool | Bool |
| `OrNode` | Bool, Bool | Bool |
| `NotNode` | Bool | Bool |
### Control Flow
`BranchNode` — selects between two values based on a condition:
```
inputs[0] Bool — condition
inputs[1] Float — value if true
inputs[2] Float — value if false
```
The concrete type of the chosen branch is passed through, so an `IDNode` connected to the true/false slot will produce an `Int` even though `GetOutputType()` reports `Float`.
### Previous-Pass Query Nodes
These nodes read from the previous generation pass via `EvalContext`. They have **no inputs** — their parameters are baked into the node at construction time. The chunk generator uses these baked offsets to pre-compute how much border padding the previous pass must produce.
| Node | Output | Description |
|---|---|---|
| `QueryTileNode(offsetX, offsetY, expectedID)` | Bool | `true` if the tile at `(worldX+offsetX, worldY+offsetY)` equals `expectedID` |
| `QueryRangeNode(minX, minY, maxX, maxY, tileID)` | Int | Count of tiles matching `tileID` within the relative rectangle |
| `QueryDistanceNode(tileID, maxDistance)` | Int | Chebyshev distance to the nearest tile matching `tileID`; returns `maxDistance + 1` if none found. The current cell is excluded. |
---
## Multi-Pass Chunk Generation (`WorldGraphChunk.h`)
Complex terrain is built in **ordered passes**, where each pass can read the output of the previous one. For example: pass 1 places stone, pass 2 adds ores based on nearby stone.
### `GenerationPass`
A thin struct pairing a `Graph` with the `NodeID` of its output node.
### Zero = "No Change"
When a pass outputs `0` for a cell, it means **keep the previous pass's value** (or AIR if there was no previous pass). This lets later passes act as selective overrides rather than full rewrites.
### `GenerateRegion`
Generates a single rectangular region using one graph. Walks every cell, builds an `EvalContext` pointing at `prevPassData`, evaluates the graph, and writes the result.
### `GenerateChunk`
Runs a full sequence of passes over a chunk, automatically computing the padding each pass needs.
```
Algorithm:
1. The last pass generates exactly [chunkOriginX, chunkOriginY, W × H].
2. Working backwards from the last pass, ComputeRequiredPadding inspects
every QueryTile / QueryRange / QueryDistance node in each pass's graph
to determine how much the previous pass's output region must expand.
This expansion accumulates from last pass to first.
3. Passes execute in forward order. Each feeds its TileGrid output to the
next pass as prevPassData.
4. The final TileGrid covers the original chunk region only.
```
### `PaddingBounds`
Records how many extra tiles are needed beyond the output region in each direction (`negX`, `posX`, `negY`, `posY`). Computed automatically by `ComputeRequiredPadding`, which walks the graph and unions the baked offsets from all query nodes.
---
## Serialization (`WorldGraphSerializer.h`)
`GraphSerializer` converts a `Graph` to/from JSON (nlohmann/json).
```cpp
nlohmann::json j = GraphSerializer::ToJson(graph);
optional<Graph> g = GraphSerializer::FromJson(j);
bool ok = GraphSerializer::Save(graph, "path/to/file.json");
optional<Graph> g2 = GraphSerializer::Load("path/to/file.json");
```
JSON structure:
```json
{
"nextId": 5,
"nodes": [
{ "id": 1, "type": "Constant", "value": 0.5 },
{ "id": 2, "type": "TileID", "tileId": 3 },
{ "id": 3, "type": "Branch" }
],
"connections": [
{ "from": 1, "to": 3, "slot": 0 },
{ "from": 2, "to": 3, "slot": 1 }
]
}
```
Only `Constant` and `TileID` nodes carry extra fields. All query nodes (`QueryTile`, `QueryRange`, `QueryDistance`) serialize their baked offset/ID parameters.
---
## Minimal Usage Example
```cpp
using namespace WorldGraph;
// Build a graph that places tile 2 when Y < -10, otherwise tile 1
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto thresh = g.AddNode(std::make_unique<ConstantNode>(-10.0f));
auto cond = g.AddNode(std::make_unique<LessNode>());
auto tileA = g.AddNode(std::make_unique<IDNode>(2)); // underground tile
auto tileB = g.AddNode(std::make_unique<IDNode>(1)); // surface tile
auto branch = g.AddNode(std::make_unique<BranchNode>());
g.Connect(posY, cond, 0); // posY → Less.a
g.Connect(thresh, cond, 1); // -10 → Less.b
g.Connect(cond, branch, 0); // bool → Branch.condition
g.Connect(tileA, branch, 1); // tile2 → Branch.true
g.Connect(tileB, branch, 2); // tile1 → Branch.false
// Generate a 64×64 chunk at world position (0, -32)
GenerationPass pass { g, branch };
TileGrid chunk = GenerateChunk({ pass }, 0, -32, 64, 64, /*seed=*/12345);
```

View File

@@ -0,0 +1,65 @@
find_package(OpenGL REQUIRED)
include(FetchContent)
# ── 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)
# ── ImGuiFileDialog static library ───────────────────────────────────────────
# Declared in root CMakeLists.txt; populated here so we build only the sources
# we need without running ImGuiFileDialog's own CMakeLists.txt.
FetchContent_GetProperties(ImGuiFileDialog)
if(NOT imguifiledialog_POPULATED)
FetchContent_Populate(ImGuiFileDialog)
endif()
add_library(imgui_file_dialog_lib STATIC
${imguifiledialog_SOURCE_DIR}/ImGuiFileDialog.cpp
)
target_include_directories(imgui_file_dialog_lib PUBLIC
${imguifiledialog_SOURCE_DIR}
)
target_link_libraries(imgui_file_dialog_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
imgui_file_dialog_lib
)

143
tools/node-editor/README.md Normal file
View File

@@ -0,0 +1,143 @@
# WorldGraph Node Editor
A standalone visual node editor for the WorldGraph procedural generation system, built with Dear ImGui and imnodes. Every node displays a live 64×64 preview of its output, ShaderGraph-style.
## Building & Running
```bash
make world-editor
```
This configures, builds, and launches the editor in one step.
---
## Interface
### Layout
| Area | Description |
|---|---|
| Left sidebar | Preview settings, World Output status, help |
| Main canvas | Node graph — pan with middle-mouse, zoom with scroll |
### Adding Nodes
Right-click anywhere on the empty canvas to open the node menu, grouped by category. The node spawns at the click position.
### Connecting Nodes
Drag from an **output pin** (right side of a node) to an **input pin** (left side). Dragging in either direction works. Connecting to an already-wired input replaces the existing connection. Cycles are rejected automatically.
### Disconnecting
Click an existing link to select it, then press **Delete**. Or drag a new connection onto the same input slot to replace it.
### Deleting Nodes
Select one or more nodes and press **Delete**. The **World Output** node cannot be deleted.
### Keyboard Shortcuts
| Key | Action |
|---|---|
| `Ctrl+S` | Save |
| `Ctrl+O` | Open |
| `Delete` | Delete selected nodes or links |
---
## World Output Node
The **World Output** node (blue title bar) is the fixed endpoint of the graph. It is always present and cannot be moved off-canvas or deleted.
Each input slot represents one **generation pass**. Passes execute in order — pass 1 can read the tile output of pass 0 via `QueryTile`, `QueryRange`, and `QueryDistance` nodes.
- **Connect a pass:** drag any node's output pin into a `Pass N` slot.
- **Add a pass slot:** click `+ Pass` inside the node.
- **Remove the last pass slot:** click `- Pass`.
The World Output preview shows the result of running `GenerateChunk()` with all connected passes over a 64×64 tile region starting at the preview origin. Each pixel equals one world tile.
---
## Per-Node Previews
Every node in the graph renders its own 64×64 preview by evaluating the subgraph rooted at that node across a grid of world coordinates.
| Output type | Preview color |
|---|---|
| `Float` | Grayscale, clamped to `[0, 1]` |
| `Bool` | White (true) / black (false) |
| `Int` (tile ID) | Hashed to a distinct hue; tile 0 (AIR) = near-black |
**Scale** in the sidebar controls how many world units one pixel represents in per-node previews. This lets you zoom in on high-frequency noise or zoom out to see large-scale structure.
> Query nodes (`QueryTile`, `QueryRange`, `QueryDistance`) always preview as blank in the per-node view because no previous-pass data is available at that stage. They work correctly in the World Output preview when used in a later pass.
---
## Preview Settings (Sidebar)
| Setting | Effect |
|---|---|
| **Origin X / Y** | World-space top-left corner of all previews |
| **Scale** | World units per pixel (per-node previews only; World Output is always 1 tile/pixel) |
| **Seed** | World seed passed to every `EvalContext` |
---
## Node Types
### Source
| Node | Output | Description |
|---|---|---|
| `Constant` | Float | Fixed float value (drag to edit) |
| `TileID` | Int | Fixed tile ID integer (drag to edit) |
| `PositionX` | Int | World X coordinate of the current cell |
| `PositionY` | Int | World Y coordinate of the current cell |
### Math
`Add`, `Subtract`, `Multiply`, `Divide`, `Modulo`, `Sin`, `Cos`
### Compare
`Less`, `Greater`, `LessEqual`, `GreaterEqual`, `Equal` — all output `Bool`
### Logic
`And`, `Or`, `Not`
### Control
| Node | Inputs | Description |
|---|---|---|
| `Branch` | condition (Bool), true, false | Passes through whichever branch the condition selects |
### Query (previous pass)
These nodes read from the previous generation pass. They have no input pins — their parameters are edited inline by dragging.
| Node | Output | Parameters |
|---|---|---|
| `QueryTile` | Bool | `offsetX`, `offsetY`, `expectedID` — true if prev-pass tile at offset equals ID |
| `QueryRange` | Int | `minX..maxX`, `minY..maxY`, `tileID` — count of matching tiles in rectangle |
| `QueryDistance` | Int | `tileID`, `maxDistance` — Chebyshev distance to nearest matching tile |
---
## File Format
Graphs are saved as `.wge` JSON files containing both the graph data and editor layout:
```json
{
"graph": { ... },
"editor": {
"nodePositions": { "1": [x, y], "__worldOutput": [x, y] },
"worldOutputPasses": [3, 7],
"seed": 0,
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 1.0
}
}
```
`worldOutputPasses` is an array of graph node IDs, one per pass slot (`0` = empty slot).

1185
tools/node-editor/main.cpp Normal file

File diff suppressed because it is too large Load Diff