multiple files can be open at the same time

This commit is contained in:
Connor
2026-02-22 20:39:58 +09:00
parent 376442e95a
commit 1e6a8d4f60
8 changed files with 980 additions and 229 deletions

205
data/WorldGraphs/dirt.json Normal file
View File

@@ -0,0 +1,205 @@
{
"editor": {
"nodePositions": {
"1": [
-1011.0,
-214.0
],
"10": [
-601.0,
-250.0
],
"11": [
-337.0,
-265.0
],
"12": [
-351.2725830078125,
240.48638916015625
],
"2": [
-740.0,
-231.0
],
"3": [
-885.0,
-153.0
],
"4": [
-472.0,
-118.0
],
"5": [
-144.0,
-283.0
],
"6": [
-341.0,
-61.0
],
"7": [
-23.0,
-218.0
],
"9": [
136.0,
-300.0
],
"__worldOutput": [
400.0,
0.0
]
},
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 0.4000000059604645,
"seed": 0,
"tileRegistry": [
{
"color": [
0.11764705926179886,
0.11764705926179886,
0.11764705926179886
],
"id": 0,
"name": "Empty"
},
{
"color": [
0.6958129405975342,
0.6926098465919495,
0.7119565010070801
],
"id": 1,
"name": "Stone"
},
{
"color": [
0.05567699670791626,
0.7880434989929199,
0.09547946602106094
],
"id": 2,
"name": "Dirt"
},
{
"color": [
0.2771739363670349,
0.06326796859502792,
0.0
],
"id": 3,
"name": "Tree"
}
],
"worldOutputPasses": [
9,
0
]
},
"graph": {
"connections": [
{
"from": 1,
"slot": 0,
"to": 2
},
{
"from": 3,
"slot": 1,
"to": 2
},
{
"from": 11,
"slot": 0,
"to": 5
},
{
"from": 6,
"slot": 1,
"to": 5
},
{
"from": 5,
"slot": 0,
"to": 9
},
{
"from": 7,
"slot": 1,
"to": 9
},
{
"from": 2,
"slot": 0,
"to": 10
},
{
"from": 10,
"slot": 0,
"to": 11
},
{
"from": 4,
"slot": 1,
"to": 11
}
],
"nextId": 13,
"nodes": [
{
"id": 1,
"type": "PositionY"
},
{
"id": 2,
"type": "Multiply"
},
{
"id": 3,
"type": "Constant",
"value": 0.019999999552965164
},
{
"frequency": 0.07880000025033951,
"id": 4,
"type": "SimplexNoise"
},
{
"id": 5,
"type": "Greater"
},
{
"id": 6,
"type": "Constant",
"value": 0.1599999964237213
},
{
"id": 7,
"tileId": 1,
"type": "TileID"
},
{
"id": 9,
"type": "IntBranch"
},
{
"id": 10,
"type": "Max"
},
{
"id": 11,
"type": "Multiply"
},
{
"id": 12,
"maxX": 1,
"maxY": 1,
"minX": -1,
"minY": -1,
"tileId": 1,
"type": "QueryRange"
}
]
}
}

120
data/WorldGraphs/stone.json Normal file
View File

@@ -0,0 +1,120 @@
{
"editor": {
"nodePositions": {
"1": [
-70.0,
-259.0
],
"13": [
-220.0,
24.0
],
"15": [
-77.0,
8.0
],
"16": [
-223.0,
206.0
],
"2": [
112.0,
-289.0
],
"4": [
-360.0,
41.0
],
"__worldOutput": [
400.0,
0.0
]
},
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 0.4000000059604645,
"seed": 0,
"tileRegistry": [
{
"color": [
0.11764705926179886,
0.11764705926179886,
0.11764705926179886
],
"id": 0,
"name": "Empty"
},
{
"color": [
0.7348794341087341,
0.730800986289978,
0.7554347515106201
],
"id": 1,
"name": "Stone"
}
],
"worldOutputPasses": [
2
]
},
"graph": {
"connections": [
{
"from": 15,
"slot": 0,
"to": 2
},
{
"from": 1,
"slot": 1,
"to": 2
},
{
"from": 4,
"slot": 0,
"to": 13
},
{
"from": 13,
"slot": 0,
"to": 15
},
{
"from": 16,
"slot": 1,
"to": 15
}
],
"nextId": 17,
"nodes": [
{
"id": 1,
"tileId": 1,
"type": "TileID"
},
{
"id": 2,
"type": "IntBranch"
},
{
"frequency": 0.10000000149011612,
"id": 4,
"type": "SimplexNoise"
},
{
"id": 13,
"type": "Abs"
},
{
"id": 15,
"type": "Less"
},
{
"id": 16,
"type": "Constant",
"value": 0.3199999928474426
}
]
}
}

View File

@@ -1,43 +0,0 @@
{
"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"
}
]
}
}

View File

@@ -1,10 +1,10 @@
[Window][Node Canvas] [Window][Node Canvas]
Pos=200,18 Pos=200,42
Size=1080,702 Size=1720,1039
[Window][Settings] [Window][Settings]
Pos=0,18 Pos=0,42
Size=200,702 Size=200,1039
[Window][Debug##Default] [Window][Debug##Default]
Pos=60,60 Pos=60,60
@@ -34,3 +34,59 @@ Column 0 Sort=0v
RefScale=13 RefScale=13
Column 0 Sort=0v Column 0 Sort=0v
[Table][0x3E490810,4]
RefScale=13
Column 0 Sort=0v
[Table][0x4546A666,4]
RefScale=13
Column 0 Sort=0v
[Table][0xFE9B7E06,4]
RefScale=13
Column 0 Sort=0v
[Table][0xD2055CEE,4]
RefScale=13
Column 0 Sort=0v
[Table][0xCDBCCF58,4]
RefScale=13
Column 0 Sort=0v
[Table][0x9E317450,4]
RefScale=13
Column 0 Sort=0v
[Table][0x26C1F0A6,4]
RefScale=13
Column 0 Sort=0v
[Table][0x5DDF70E5,4]
RefScale=13
Column 0 Sort=0v
[Table][0x4AC5837A,4]
RefScale=13
Column 0 Sort=0v
[Table][0xFB4E8B6C,4]
RefScale=13
Column 0 Sort=0v
[Table][0x8F6E2C61,4]
RefScale=13
Column 0 Sort=0v
[Table][0x42C29929,4]
RefScale=13
Column 0 Sort=0v
[Table][0x8FD0E34F,4]
RefScale=13
Column 0 Sort=0v
[NodeEditor][Session]
LastFiles=/home/connor/repos/factory-hole-core/data/WorldGraphs/stone.json;/home/connor/repos/factory-hole-core/data/WorldGraphs/dirt.json
ActiveTab=1

View File

@@ -366,19 +366,45 @@ public:
/// Outputs the world-space X coordinate of the cell being evaluated. /// Outputs the world-space X coordinate of the cell being evaluated.
class PositionXNode : public Node { class PositionXNode : public Node {
public: public:
Type GetOutputType() const override { return Type::Int; } Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; } std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PositionX"; } std::string GetName() const override { return "PositionX"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeInt(ctx.worldX); }; Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeFloat(ctx.worldX); };
}; };
/// Outputs the world-space Y coordinate of the cell being evaluated. /// Outputs the world-space Y coordinate of the cell being evaluated.
class PositionYNode : public Node { class PositionYNode : public Node {
public: public:
Type GetOutputType() const override { return Type::Int; } Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; } std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PositionY"; } std::string GetName() const override { return "PositionY"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeInt(ctx.worldY); }; Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeFloat(ctx.worldY); };
};
// ─────────────────────────────── Abs / Negate ────────────────────────────────
/// |a| (Float)
class AbsNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Abs"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::abs(in[0].AsFloat()));
}
};
/// a (Float)
class NegateNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Negate"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(-in[0].AsFloat());
}
}; };
// ─────────────────────────────── Min / Max / Clamp ─────────────────────────── // ─────────────────────────────── Min / Max / Clamp ───────────────────────────
@@ -419,6 +445,37 @@ public:
} }
}; };
// ─────────────────────────────── Map (range remap) ───────────────────────────
/// Remaps a value from [min0, max0] to [min1, max1].
///
/// inputs[0] value to remap (Float)
///
/// The four range endpoints are baked into the node at construction time.
/// When min0 == max0 the output is min1 (avoids divide-by-zero).
class MapNode : public Node {
public:
float min0 { 0.0f };
float max0 { 1.0f };
float min1 { 0.0f };
float max1 { 1.0f };
MapNode() = default;
MapNode(float mn0, float mx0, float mn1, float mx1)
: min0(mn0), max0(mx0), min1(mn1), max1(mx1) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Map"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
float range0 = max0 - min0;
if (range0 == 0.0f) return Value::MakeFloat(min1);
float t = (in[0].AsFloat() - min0) / range0;
return Value::MakeFloat(min1 + t * (max1 - min1));
}
};
// ─────────────────────────────── Int control flow ──────────────────────────── // ─────────────────────────────── Int control flow ────────────────────────────
/// Selects between two integer inputs based on a boolean condition. /// Selects between two integer inputs based on a boolean condition.

View File

@@ -38,9 +38,18 @@ std::unique_ptr<Node> GraphSerializer::CreateNode(const std::string& type,
if (type == "Pow") return std::make_unique<PowNode>(); if (type == "Pow") return std::make_unique<PowNode>();
if (type == "Square") return std::make_unique<SquareNode>(); if (type == "Square") return std::make_unique<SquareNode>();
if (type == "OneMinus") return std::make_unique<OneMinusNode>(); if (type == "OneMinus") return std::make_unique<OneMinusNode>();
if (type == "Abs") return std::make_unique<AbsNode>();
if (type == "Negate") return std::make_unique<NegateNode>();
if (type == "Min") return std::make_unique<MinNode>(); if (type == "Min") return std::make_unique<MinNode>();
if (type == "Max") return std::make_unique<MaxNode>(); if (type == "Max") return std::make_unique<MaxNode>();
if (type == "Clamp") return std::make_unique<ClampNode>(); if (type == "Clamp") return std::make_unique<ClampNode>();
if (type == "Map") {
return std::make_unique<MapNode>(
j.value("min0", 0.0f),
j.value("max0", 1.0f),
j.value("min1", 0.0f),
j.value("max1", 1.0f));
}
// Noise nodes (frequency baked in) // Noise nodes (frequency baked in)
if (type == "PerlinNoise") return std::make_unique<PerlinNoiseNode>(j.value("frequency", 0.01f)); if (type == "PerlinNoise") return std::make_unique<PerlinNoiseNode>(j.value("frequency", 0.01f));
@@ -117,6 +126,11 @@ nlohmann::json GraphSerializer::ToJson(const Graph& g)
} else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(node)) { } else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(node)) {
jNode["tileId"] = qd->tileID; jNode["tileId"] = qd->tileID;
jNode["maxDistance"] = qd->maxDistance; jNode["maxDistance"] = qd->maxDistance;
} else if (const auto* mn = dynamic_cast<const MapNode*>(node)) {
jNode["min0"] = mn->min0;
jNode["max0"] = mn->max0;
jNode["min1"] = mn->min1;
jNode["max1"] = mn->max1;
} else if (const auto* pn = dynamic_cast<const PerlinNoiseNode*>(node)) { } else if (const auto* pn = dynamic_cast<const PerlinNoiseNode*>(node)) {
jNode["frequency"] = pn->frequency; jNode["frequency"] = pn->frequency;
} else if (const auto* sn = dynamic_cast<const SimplexNoiseNode*>(node)) { } else if (const auto* sn = dynamic_cast<const SimplexNoiseNode*>(node)) {

View File

@@ -168,6 +168,38 @@ TEST_SUITE("WorldGraph::Nodes") {
CHECK(n.Evaluate(c, {}).AsInt() == -3); CHECK(n.Evaluate(c, {}).AsInt() == -3);
} }
// ── Abs / Negate ──────────────────────────────────────────────────────────
TEST_CASE("AbsNode: positive input unchanged") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f) }).AsFloat() == doctest::Approx(3.0f));
}
TEST_CASE("AbsNode: negative input becomes positive") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-7.5f) }).AsFloat() == doctest::Approx(7.5f));
}
TEST_CASE("AbsNode: zero") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("NegateNode: positive becomes negative") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(4.0f) }).AsFloat() == doctest::Approx(-4.0f));
}
TEST_CASE("NegateNode: negative becomes positive") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-2.5f) }).AsFloat() == doctest::Approx(2.5f));
}
TEST_CASE("NegateNode: zero") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
// ── Min / Max / Clamp ───────────────────────────────────────────────────── // ── Min / Max / Clamp ─────────────────────────────────────────────────────
TEST_CASE("MinNode: returns smaller value") { TEST_CASE("MinNode: returns smaller value") {
@@ -216,6 +248,42 @@ TEST_SUITE("WorldGraph::Nodes") {
== doctest::Approx(10.0f)); == doctest::Approx(10.0f));
} }
// ── Map (range remap) ─────────────────────────────────────────────────────
TEST_CASE("MapNode: maps midpoint correctly") {
MapNode n(0.0f, 1.0f, 0.0f, 100.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.5f) }).AsFloat() == doctest::Approx(50.0f));
}
TEST_CASE("MapNode: maps min boundary") {
MapNode n(0.0f, 1.0f, 10.0f, 20.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(10.0f));
}
TEST_CASE("MapNode: maps max boundary") {
MapNode n(0.0f, 1.0f, 10.0f, 20.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(20.0f));
}
TEST_CASE("MapNode: maps noise range [-1, 1] to [0, 1]") {
MapNode n(-1.0f, 1.0f, 0.0f, 1.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f) }).AsFloat() == doctest::Approx(0.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat( 0.0f) }).AsFloat() == doctest::Approx(0.5f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat( 1.0f) }).AsFloat() == doctest::Approx(1.0f));
}
TEST_CASE("MapNode: inverted output range") {
MapNode n(0.0f, 1.0f, 1.0f, 0.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(1.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(0.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.25f) }).AsFloat() == doctest::Approx(0.75f));
}
TEST_CASE("MapNode: degenerate input range returns min1") {
MapNode n(5.0f, 5.0f, 99.0f, 100.0f); // min0 == max0
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f) }).AsFloat() == doctest::Approx(99.0f));
}
// ── Int control flow ────────────────────────────────────────────────────── // ── Int control flow ──────────────────────────────────────────────────────
TEST_CASE("IntBranchNode: selects true branch") { TEST_CASE("IntBranchNode: selects true branch") {

File diff suppressed because it is too large Load Diff