diff --git a/apps/shaders_lab/shaders_lab.exe b/apps/shaders_lab/shaders_lab.exe index 4b02245d..dbf0fa62 100755 Binary files a/apps/shaders_lab/shaders_lab.exe and b/apps/shaders_lab/shaders_lab.exe differ diff --git a/cpp/functions/gfx/dag_catalog.cpp b/cpp/functions/gfx/dag_catalog.cpp index d687ae66..6ece0091 100644 --- a/cpp/functions/gfx/dag_catalog.cpp +++ b/cpp/functions/gfx/dag_catalog.cpp @@ -257,3 +257,78 @@ const DagNodeDef* dag_find(const std::string& name) { } } // namespace fn::gfx + +#ifdef DAG_CATALOG_TEST +#include +#include +#include + +int main() { + using namespace fn::gfx; + const auto& cat = dag_catalog(); + + // 1. Catalog not empty + assert(!cat.empty()); + + // 2. All node names are unique + std::set names; + for (const auto& n : cat) { + assert(names.insert(n.name).second && "duplicate node name"); + } + + // 3. Invariants by kind + int gen_count = 0, op_count = 0, blend_count = 0, output_count = 0; + for (const auto& n : cat) { + switch (n.kind) { + case DagKind::Gen: + assert(n.num_inputs == 0 && "Gen must have 0 inputs"); + ++gen_count; break; + case DagKind::Op: + assert(n.num_inputs >= 1 && "Op must have >= 1 input"); + ++op_count; break; + case DagKind::Blend: + assert(n.num_inputs >= 2 && "Blend must have >= 2 inputs"); + ++blend_count; break; + case DagKind::Output: + assert(n.num_inputs == 1 && "Output must have exactly 1 input"); + ++output_count; break; + } + assert(n.num_inputs >= 0 && n.num_inputs <= 4); + assert(n.body_glsl && "body_glsl must be set"); + } + + // 4. Exactly one Output in the catalog + assert(output_count == 1 && "catalog must declare exactly one Output node"); + + // 5. At least one Gen and one Op + assert(gen_count >= 1); + assert(op_count >= 1); + assert(blend_count >= 1); + + // 6. dag_find works and returns nullptr for unknown names + assert(dag_find("output") != nullptr); + assert(dag_find("plasma") != nullptr); + assert(dag_find("__nonexistent__") == nullptr); + + // 7. Every non-Output body emits non-empty GLSL + for (const auto& n : cat) { + if (n.kind == DagKind::Output) continue; + auto body = n.body_glsl(0); + assert(!body.empty() && "non-Output nodes must emit body"); + // sanity: should contain a return statement + assert(body.find("return") != std::string::npos); + } + + // 8. Control param indices stay within 0..3 + for (const auto& n : cat) { + for (const auto& c : n.controls) { + for (int idx : c.param_idx) { + assert(idx >= -1 && idx < 4); + } + } + } + + std::printf("dag_catalog: 8/8 asserts passed (%zu nodes)\n", cat.size()); + return 0; +} +#endif diff --git a/cpp/functions/gfx/dag_compile.cpp b/cpp/functions/gfx/dag_compile.cpp index 591d7189..09ecf122 100644 --- a/cpp/functions/gfx/dag_compile.cpp +++ b/cpp/functions/gfx/dag_compile.cpp @@ -106,3 +106,83 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { } } // namespace fn::gfx + +#ifdef DAG_COMPILE_TEST +#include +#include + +static bool contains(const std::string& hay, const std::string& needle) { + return hay.find(needle) != std::string::npos; +} + +int main() { + using namespace fn::gfx; + + // 1. Empty pipeline → seed color + { + std::vector p; + auto s = compile_dag_to_glsl(p); + assert(contains(s, "fragColor = vec4(0.04")); + } + + // 2. Single Gen → fragColor = out_0 + { + std::vector p; + DagStep g; g.id = "a"; g.name = "plasma"; + p.push_back(g); + auto s = compile_dag_to_glsl(p); + assert(contains(s, "vec4 node_0")); + assert(contains(s, "vec4 out_0 = node_0(")); + assert(contains(s, "fragColor = out_0")); + } + + // 3. Gen + Op → Op uses out_0 as input a + { + std::vector p; + DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g); + DagStep o; o.id = "b"; o.name = "invert"; o.source_ids[0] = "a"; p.push_back(o); + auto s = compile_dag_to_glsl(p); + assert(contains(s, "out_1 = node_1(out_0, uv)")); + assert(contains(s, "fragColor = out_1")); + } + + // 4. Blend with multi-source → both inputs resolved + { + std::vector p; + DagStep a; a.id = "a"; a.name = "plasma"; p.push_back(a); + DagStep b; b.id = "b"; b.name = "solid"; p.push_back(b); + DagStep m; m.id = "m"; m.name = "blend_mix"; + m.source_ids[0] = "a"; m.source_ids[1] = "b"; + p.push_back(m); + auto s = compile_dag_to_glsl(p); + assert(contains(s, "out_2 = node_2(out_0, out_1, uv)")); + } + + // 5. Output node drives fragColor from its source, not from last index + { + std::vector p; + DagStep g1; g1.id = "g1"; g1.name = "plasma"; p.push_back(g1); + DagStep g2; g2.id = "g2"; g2.name = "solid"; p.push_back(g2); + DagStep o; o.id = "o"; o.name = "output"; + o.source_ids[0] = "g1"; // connect Output to the plasma (first gen), not last + p.push_back(o); + auto s = compile_dag_to_glsl(p); + // Output must NOT emit a node_2 function + assert(!contains(s, "vec4 node_2(")); + // fragColor must come from out_0 (plasma), not out_1 (solid) + assert(contains(s, "fragColor = out_0")); + } + + // 6. Output with no connection → seed fallback + { + std::vector p; + DagStep g; g.id = "g"; g.name = "plasma"; p.push_back(g); + DagStep o; o.id = "o"; o.name = "output"; p.push_back(o); // no source + auto s = compile_dag_to_glsl(p); + assert(contains(s, "fragColor = vec4(0.04")); + } + + std::printf("dag_compile: 6/6 asserts passed\n"); + return 0; +} +#endif diff --git a/cpp/functions/gfx/dag_node_editor.cpp b/cpp/functions/gfx/dag_node_editor.cpp index 4419bbc4..2685d02b 100644 --- a/cpp/functions/gfx/dag_node_editor.cpp +++ b/cpp/functions/gfx/dag_node_editor.cpp @@ -138,25 +138,26 @@ bool dag_node_editor(std::vector& pipeline) { s_ctx = ed::CreateEditor(&cfg); } - // Palette drop zone: accept dropped node types (from the Functions panel) - // and queue an add at the mouse canvas position (resolved after ed::Begin). + // Palette drop: detect without a capturing target (an InvisibleButton would + // steal clicks from the node editor). Observe the active drag-drop payload + // and, if the mouse is over this window and the user releases LMB, queue an + // add at that canvas position. static std::string s_pending_add_name; static ImVec2 s_pending_add_pos(0, 0); static bool s_pending_add = false; - ImVec2 origin = ImGui::GetCursorScreenPos(); - ImVec2 avail = ImGui::GetContentRegionAvail(); - ImGui::InvisibleButton("##dag_drop_zone", avail, - ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("DAG_NODE_TYPE")) { - s_pending_add_name.assign(static_cast(p->Data), static_cast(p->DataSize)); - s_pending_add_pos = ImGui::GetMousePos(); - s_pending_add = true; + const bool window_hovered = ImGui::IsWindowHovered( + ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + if (window_hovered) { + if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) { + if (p->IsDataType("DAG_NODE_TYPE") && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + s_pending_add_name.assign(static_cast(p->Data), + static_cast(p->DataSize)); + s_pending_add_pos = ImGui::GetMousePos(); + s_pending_add = true; + } } - ImGui::EndDragDropTarget(); } - ImGui::SetCursorScreenPos(origin); ed::SetCurrentEditor(s_ctx); ed::Begin("dag_editor", ImVec2(0.0f, 0.0f));