layer strength

This commit is contained in:
Connor
2026-02-23 23:07:10 +09:00
parent 1e6a8d4f60
commit 1b7fd1c7f8
10 changed files with 725 additions and 230 deletions

View File

@@ -162,6 +162,7 @@ static ImU32 PinColorHovered(Type t)
static constexpr int PREVIEW_SIZE = 64;
static constexpr float PREVIEW_SCALE = 1.5f; // world units per pixel
static constexpr float FINAL_PREVIEW_SCALE = 4.0f; // for World Output node
static GLuint MakePreviewTexture()
{
@@ -203,8 +204,9 @@ static const std::vector<NodeMenuItem> NODE_MENU = {
// ── Source ──────────────────────────────────────────────────────────────
{ "Constant", "Source", [] { return std::make_unique<ConstantNode>(0.0f); } },
{ "TileID", "Source", [] { return std::make_unique<IDNode>(1); } },
{ "PositionX", "Source", [] { return std::make_unique<PositionXNode>(); } },
{ "PositionY", "Source", [] { return std::make_unique<PositionYNode>(); } },
{ "PositionX", "Source", [] { return std::make_unique<PositionXNode>(); } },
{ "PositionY", "Source", [] { return std::make_unique<PositionYNode>(); } },
{ "LayerStrength", "Source", [] { return std::make_unique<LayerStrengthNode>(); } },
// ── Math ────────────────────────────────────────────────────────────────
{ "Add", "Math", [] { return std::make_unique<AddNode>(); } },
{ "Subtract", "Math", [] { return std::make_unique<SubtractNode>(); } },
@@ -213,6 +215,8 @@ static const std::vector<NodeMenuItem> NODE_MENU = {
{ "Modulo", "Math", [] { return std::make_unique<ModuloNode>(); } },
{ "Sin", "Math", [] { return std::make_unique<SinNode>(); } },
{ "Cos", "Math", [] { return std::make_unique<CosNode>(); } },
{ "Floor", "Math", [] { return std::make_unique<FloorNode>(); } },
{ "Ceil", "Math", [] { return std::make_unique<CeilNode>(); } },
{ "Sqrt", "Math", [] { return std::make_unique<SqrtNode>(); } },
{ "Pow", "Math", [] { return std::make_unique<PowNode>(); } },
{ "Square", "Math", [] { return std::make_unique<SquareNode>(); } },
@@ -245,6 +249,7 @@ static const std::vector<NodeMenuItem> NODE_MENU = {
{ "QueryRange", "Query", [] { return std::make_unique<QueryRangeNode>(-1, -1, 1, 1, 1); } },
{ "QueryDistance", "Query", [] { return std::make_unique<QueryDistanceNode>(1, 4); } },
// ── Noise ───────────────────────────────────────────────────────────────
{ "Random", "Noise", [] { return std::make_unique<RandomNode>(); } },
{ "PerlinNoise", "Noise", [] { return std::make_unique<PerlinNoiseNode>(0.01f); } },
{ "SimplexNoise", "Noise", [] { return std::make_unique<SimplexNoiseNode>(0.01f); } },
{ "CellularNoise", "Noise", [] { return std::make_unique<CellularNoiseNode>(0.01f); } },
@@ -260,7 +265,6 @@ struct GraphTab {
Graph graph;
std::unordered_map<Graph::NodeID, VisualNode> visualNodes;
std::vector<TileEntry> tileRegistry;
std::vector<Graph::NodeID> worldOutputPasses;
GLuint worldOutputTex { 0 };
@@ -276,7 +280,6 @@ struct GraphTab {
void Init()
{
tileRegistry = { TileEntry{0, "Empty", {30.0f/255.0f, 30.0f/255.0f, 30.0f/255.0f}} };
worldOutputPasses = { Graph::INVALID_ID };
worldOutputTex = MakePreviewTexture();
imnodesCtx = ImNodes::EditorContextCreate();
@@ -317,6 +320,10 @@ public:
void Render()
{
// Restore last session written by the ini settings handler.
if (!pendingLoadRegistry.empty()) {
LoadSharedRegistry(pendingLoadRegistry);
pendingLoadRegistry.clear();
}
if (!pendingLoadFiles.empty()) {
for (const auto& f : pendingLoadFiles) {
auto& cur = Active();
@@ -378,9 +385,16 @@ private:
int activeTab { 0 };
int requestedTab { -1 }; // set to trigger programmatic tab switch (one frame)
// Shared tile registry — one across all tabs / graph files
std::vector<TileEntry> sharedRegistry {
TileEntry{0, "Empty", {30.0f/255.0f, 30.0f/255.0f, 30.0f/255.0f}}
};
std::string sharedRegistryPath;
// Pending data from ini settings handler
std::vector<std::string> pendingLoadFiles;
int pendingActiveTab { -1 };
int pendingActiveTab { -1 };
std::string pendingLoadRegistry;
GraphTab& Active() { return tabs[activeTab]; }
@@ -449,12 +463,12 @@ private:
for (int px = 0; px < PREVIEW_SIZE; ++px) {
EvalContext ctx;
ctx.worldX = tab.previewOriginX + static_cast<int>(px * tab.previewScale);
ctx.worldY = tab.previewOriginY + static_cast<int>(py * tab.previewScale);
ctx.worldY = tab.previewOriginY + static_cast<int>((PREVIEW_SIZE - 1 - py) * tab.previewScale);
ctx.seed = tab.previewSeed;
Value v = tab.graph.Evaluate(id, ctx);
int base = (py * PREVIEW_SIZE + px) * 4;
ValueToRGBA(v, &tab.tileRegistry,
ValueToRGBA(v, &sharedRegistry,
pixels[base], pixels[base+1], pixels[base+2], pixels[base+3]);
}
}
@@ -485,10 +499,10 @@ private:
for (int py = 0; py < PREVIEW_SIZE; ++py) {
for (int px = 0; px < PREVIEW_SIZE; ++px) {
int32_t tileID = chunk.Get(tab.previewOriginX + px, tab.previewOriginY + py);
int32_t tileID = chunk.Get(tab.previewOriginX + px, tab.previewOriginY + (PREVIEW_SIZE - 1 - py));
int base = (py * PREVIEW_SIZE + px) * 4;
Value v = Value::MakeInt(tileID);
ValueToRGBA(v, &tab.tileRegistry,
ValueToRGBA(v, &sharedRegistry,
pixels[base], pixels[base+1], pixels[base+2], pixels[base+3]);
}
}
@@ -546,10 +560,27 @@ private:
ImGui::Spacing();
ImGui::SeparatorText("Tile IDs");
// Registry file path (read-only display)
{
const char* rpath = sharedRegistryPath.empty() ? "(unsaved)" : sharedRegistryPath.c_str();
ImGui::TextDisabled("%s", rpath);
}
if (ImGui::SmallButton("Open##reg")) OpenRegistryDialog();
ImGui::SameLine();
if (ImGui::SmallButton("Save##reg"))
{
if (sharedRegistryPath.empty()) SaveRegistryDialog();
else SaveSharedRegistry(sharedRegistryPath);
}
ImGui::SameLine();
if (ImGui::SmallButton("Save As##reg")) SaveRegistryDialog();
ImGui::Spacing();
int toRemove = -1;
for (int i = 0; i < static_cast<int>(tab.tileRegistry.size()); ++i) {
for (int i = 0; i < static_cast<int>(sharedRegistry.size()); ++i) {
ImGui::PushID(i);
TileEntry& entry = tab.tileRegistry[i];
TileEntry& entry = sharedRegistry[i];
// Color button — opens a popup picker.
char cpopup[32];
@@ -593,11 +624,11 @@ private:
ImGui::PopID();
}
if (toRemove >= 0)
tab.tileRegistry.erase(tab.tileRegistry.begin() + toRemove);
sharedRegistry.erase(sharedRegistry.begin() + toRemove);
if (ImGui::SmallButton("+ Add Tile")) {
int32_t nextId = tab.tileRegistry.empty() ? 1
: tab.tileRegistry.back().id + 1;
int32_t nextId = sharedRegistry.empty() ? 1
: sharedRegistry.back().id + 1;
TileEntry entry;
entry.id = nextId;
entry.name = "Tile";
@@ -608,7 +639,7 @@ private:
entry.color[0] = hr / 255.0f;
entry.color[1] = hg / 255.0f;
entry.color[2] = hb / 255.0f;
tab.tileRegistry.push_back(entry);
sharedRegistry.push_back(entry);
}
ImGui::Spacing();
@@ -913,7 +944,7 @@ private:
// Generated chunk preview — always 1 tile per pixel.
ImGui::Image(
static_cast<ImTextureID>(static_cast<uint64_t>(tab.worldOutputTex)),
ImVec2(PREVIEW_SIZE * PREVIEW_SCALE, PREVIEW_SIZE * PREVIEW_SCALE));
ImVec2(PREVIEW_SIZE * FINAL_PREVIEW_SCALE, PREVIEW_SIZE * FINAL_PREVIEW_SCALE));
ImNodes::EndNode();
@@ -986,38 +1017,46 @@ private:
return ImGui::DragFloat("##val", &n->value, 0.01f);
}
static bool DrawIDParams(NodeEditorApp& app, Node* node)
static bool DrawIDDropdown(const char* label, NodeEditorApp& app, int& currentID)
{
auto* n = static_cast<IDNode*>(node);
auto& reg = app.Active().tileRegistry;
auto& reg = app.sharedRegistry;
int selIdx = -1;
bool changed = false;
if (reg.empty()) {
changed = ImGui::DragInt("##id", &n->tileID, 1, 0, 9999);
} else {
int selIdx = -1;
for (int i = 0; i < static_cast<int>(reg.size()); ++i)
if (reg[i].id == n->tileID) { selIdx = i; break; }
const char* preview = selIdx >= 0 ? reg[selIdx].name.c_str() : "???";
ImGui::SetNextItemWidth(80);
if (ImGui::BeginCombo("##tile", preview)) {
for (int i = 0; i < static_cast<int>(reg.size()); ++i) {
bool sel = (i == selIdx);
char label[80];
snprintf(label, sizeof(label), "%s (%d)",
reg[i].name.c_str(), reg[i].id);
if (ImGui::Selectable(label, sel)) {
n->tileID = reg[i].id;
changed = true;
}
if (sel) ImGui::SetItemDefaultFocus();
for (int i = 0; i < static_cast<int>(reg.size()); ++i)
if (reg[i].id == currentID) { selIdx = i; break; }
const char* preview = selIdx >= 0 ? reg[selIdx].name.c_str() : "???";
ImGui::SetNextItemWidth(80);
if (ImGui::BeginCombo(label, preview)) {
for (int i = 0; i < static_cast<int>(reg.size()); ++i) {
bool sel = (i == selIdx);
char label[80];
snprintf(label, sizeof(label), "%s (%d)",
reg[i].name.c_str(), reg[i].id);
if (ImGui::Selectable(label, sel)) {
currentID = reg[i].id;
changed = true;
}
ImGui::EndCombo();
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
return changed;
}
static bool DrawQueryTileParams(NodeEditorApp& /*app*/, Node* node)
static bool DrawIDParams(NodeEditorApp& app, Node* node)
{
auto* n = static_cast<IDNode*>(node);
auto& reg = app.sharedRegistry;
bool changed = false;
if (reg.empty()) {
changed = ImGui::DragInt("##id", &n->tileID, 1, 0, 9999);
} else {
changed |= DrawIDDropdown("##id", app, n->tileID);
}
return changed;
}
static bool DrawQueryTileParams(NodeEditorApp& app, Node* node)
{
auto* n = static_cast<QueryTileNode*>(node);
bool changed = false;
@@ -1028,33 +1067,35 @@ private:
changed |= ImGui::DragInt("##oy", &n->offsetY, 1);
// TODO: it would be best if this is a dropdown of tile ids that the user can choose from
ImGui::Text("Tile ID");
changed |= ImGui::DragInt("##eid", &n->expectedID, 1, 0, 1024);
changed |= DrawIDDropdown("##eid", app, n->expectedID);
ImGui::PopItemWidth();
return changed;
}
static bool DrawQueryRangeParams(NodeEditorApp& /*app*/, Node* node)
static bool DrawQueryRangeParams(NodeEditorApp& app, Node* node)
{
auto* n = static_cast<QueryRangeNode*>(node);
bool changed = false;
ImGui::PushItemWidth(70);
ImGui::Text("min x -> max x");
changed |= ImGui::DragInt("##mnx", &n->minX, 1);
ImGui::SameLine();
changed |= ImGui::DragInt("##mxx", &n->maxX, 1);
ImGui::Text("min y -> max y");
changed |= ImGui::DragInt("##mny", &n->minY, 1);
ImGui::SameLine();
changed |= ImGui::DragInt("##mxy", &n->maxY, 1);
changed |= ImGui::DragInt("##rtid", &n->tileID, 1, 0, 1024);
changed |= DrawIDDropdown("##rtid", app, n->tileID);
ImGui::PopItemWidth();
return changed;
}
static bool DrawQueryDistanceParams(NodeEditorApp& /*app*/, Node* node)
static bool DrawQueryDistanceParams(NodeEditorApp& app, Node* node)
{
auto* n = static_cast<QueryDistanceNode*>(node);
bool changed = false;
ImGui::PushItemWidth(70);
changed |= ImGui::DragInt("##dtid", &n->tileID, 1, 0, 1024);
changed |= DrawIDDropdown("##dtid", app, n->tileID);
changed |= ImGui::DragInt("##md", &n->maxDistance, 1, 1, 32);
ImGui::PopItemWidth();
return changed;
@@ -1126,6 +1167,75 @@ private:
// ── Serialization ─────────────────────────────────────────────────────────
// ── Shared tile registry I/O ──────────────────────────────────────────────
void SaveSharedRegistry(const std::string& path)
{
nlohmann::json j;
auto& tiles = j["tiles"] = nlohmann::json::array();
for (auto& t : sharedRegistry)
tiles.push_back({ {"id", t.id}, {"name", t.name},
{"color", { t.color[0], t.color[1], t.color[2] }} });
std::ofstream f(path);
if (f) { f << j.dump(2); sharedRegistryPath = path; }
else fprintf(stderr, "Failed to write registry %s\n", path.c_str());
}
void LoadSharedRegistry(const std::string& path)
{
std::ifstream f(path);
if (!f) { fprintf(stderr, "Cannot open registry %s\n", path.c_str()); return; }
nlohmann::json j;
try { j = nlohmann::json::parse(f); }
catch (...) { fprintf(stderr, "JSON parse error in registry %s\n", path.c_str()); return; }
if (!j.contains("tiles") || !j["tiles"].is_array()) return;
sharedRegistry.clear();
for (auto& t : j["tiles"]) {
TileEntry entry;
entry.id = t.value("id", 0);
entry.name = t.value("name", std::string("Tile"));
if (t.contains("color") && t["color"].size() == 3) {
entry.color[0] = t["color"][0].get<float>();
entry.color[1] = t["color"][1].get<float>();
entry.color[2] = t["color"][2].get<float>();
}
sharedRegistry.push_back(entry);
}
// Always ensure ID 0 sentinel is present.
bool hasEmpty = std::any_of(sharedRegistry.begin(), sharedRegistry.end(),
[](const TileEntry& e) { return e.id == 0; });
if (!hasEmpty)
sharedRegistry.insert(sharedRegistry.begin(),
TileEntry{0, "Empty", {30.0f/255.0f, 30.0f/255.0f, 30.0f/255.0f}});
sharedRegistryPath = path;
MarkAllDirty();
}
void OpenRegistryDialog()
{
IGFD::FileDialogConfig cfg;
cfg.path = sharedRegistryPath.empty() ? "."
: std::filesystem::path(sharedRegistryPath).parent_path().string();
ImGuiFileDialog::Instance()->OpenDialog("OpenRegistryDlg", "Open Tile Registry", ".json", cfg);
}
void SaveRegistryDialog()
{
IGFD::FileDialogConfig cfg;
if (!sharedRegistryPath.empty()) {
std::filesystem::path p(sharedRegistryPath);
cfg.path = p.parent_path().string();
cfg.fileName = p.filename().string();
} else {
cfg.path = ".";
cfg.fileName = "tiles.json";
}
ImGuiFileDialog::Instance()->OpenDialog("SaveRegistryDlg", "Save Tile Registry", ".json", cfg);
}
void Save(const std::string& path)
{
auto& tab = Active();
@@ -1156,12 +1266,6 @@ private:
passes.push_back(id);
root["editor"]["worldOutputPasses"] = passes;
nlohmann::json tiles = nlohmann::json::array();
for (auto& t : tab.tileRegistry)
tiles.push_back({ {"id", t.id}, {"name", t.name},
{"color", { t.color[0], t.color[1], t.color[2] }} });
root["editor"]["tileRegistry"] = tiles;
std::ofstream f(path);
if (f) { f << root.dump(2); tab.currentFile = path; }
else fprintf(stderr, "Failed to write %s\n", path.c_str());
@@ -1188,7 +1292,6 @@ private:
tab.worldOutputDirty = true;
tab.pendingWorldOutputPos = true;
tab.currentFile = "";
tab.tileRegistry = { TileEntry{0, "Empty", {30.0f/255.0f, 30.0f/255.0f, 30.0f/255.0f}} };
tab.graph = std::move(*newGraph);
@@ -1210,26 +1313,8 @@ private:
tab.previewOriginY = ed.value("previewOriginY", 0);
tab.previewScale = ed.value("previewScale", 1.0f);
if (ed.contains("tileRegistry")) {
tab.tileRegistry.clear();
for (auto& t : ed["tileRegistry"]) {
TileEntry entry;
entry.id = t.value("id", 0);
entry.name = t.value("name", std::string("Tile"));
if (t.contains("color") && t["color"].size() == 3) {
entry.color[0] = t["color"][0].get<float>();
entry.color[1] = t["color"][1].get<float>();
entry.color[2] = t["color"][2].get<float>();
}
tab.tileRegistry.push_back(entry);
}
// Always guarantee the Empty (ID 0) sentinel is present.
bool hasEmpty = std::any_of(tab.tileRegistry.begin(), tab.tileRegistry.end(),
[](const TileEntry& e) { return e.id == 0; });
if (!hasEmpty)
tab.tileRegistry.insert(tab.tileRegistry.begin(),
TileEntry{0, "Empty", {30.0f/255.0f, 30.0f/255.0f, 30.0f/255.0f}});
}
// Note: "tileRegistry" embedded in old .wge files is intentionally
// ignored — use the shared tiles.json registry instead.
if (ed.contains("worldOutputPasses")) {
tab.worldOutputPasses.clear();
@@ -1320,6 +1405,20 @@ public:
Save(ImGuiFileDialog::Instance()->GetFilePathName());
ImGuiFileDialog::Instance()->Close();
}
if (ImGuiFileDialog::Instance()->Display("OpenRegistryDlg",
ImGuiWindowFlags_NoCollapse, dlgSize)) {
if (ImGuiFileDialog::Instance()->IsOk())
LoadSharedRegistry(ImGuiFileDialog::Instance()->GetFilePathName());
ImGuiFileDialog::Instance()->Close();
}
if (ImGuiFileDialog::Instance()->Display("SaveRegistryDlg",
ImGuiWindowFlags_NoCollapse, dlgSize)) {
if (ImGuiFileDialog::Instance()->IsOk())
SaveSharedRegistry(ImGuiFileDialog::Instance()->GetFilePathName());
ImGuiFileDialog::Instance()->Close();
}
}
void HandleKeyboardShortcuts()
@@ -1358,6 +1457,8 @@ public:
}
if (strncmp(line, "ActiveTab=", 10) == 0)
app->pendingActiveTab = std::atoi(line + 10);
if (strncmp(line, "SharedRegistry=", 15) == 0)
app->pendingLoadRegistry = line + 15;
};
// Called when ImGui writes imgui.ini (periodically and on shutdown).
@@ -1370,12 +1471,14 @@ public:
files += tab.currentFile;
}
}
out_buf->appendf("[NodeEditor][Session]\n");
if (!files.empty()) {
out_buf->appendf("[NodeEditor][Session]\n");
out_buf->appendf("LastFiles=%s\n", files.c_str());
out_buf->appendf("ActiveTab=%d\n", app->activeTab);
out_buf->appendf("\n");
}
if (!app->sharedRegistryPath.empty())
out_buf->appendf("SharedRegistry=%s\n", app->sharedRegistryPath.c_str());
out_buf->appendf("\n");
};
ImGui::AddSettingsHandler(&h);