fix(shaders_lab): drop zone no longer eats node/slider input + inline tests
The previous InvisibleButton captured mouse events, so you could drag from the Functions palette into the canvas, but node dragging and slider interaction inside the canvas stopped working. Fix: watch the global drag-drop payload without an explicit target. When the mouse releases LMB over the DAG window with a "DAG_NODE_TYPE" payload active, queue an add at that canvas position. No button, no capture. Tests (compiled standalone with preprocessor defines): - dag_compile: 6/6 asserts (empty, single gen, op chain, multi-source blend, Output-driven fragColor, unconnected-Output fallback). - dag_catalog: 8/8 asserts (uniqueness, per-kind input invariants, exactly one Output, body_glsl present & returns, control param indices valid). Build with: g++ -std=c++17 -Icpp/functions -DDAG_COMPILE_TEST cpp/functions/gfx/dag_compile.cpp cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_compile_test g++ -std=c++17 -Icpp/functions -DDAG_CATALOG_TEST cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_catalog_test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -257,3 +257,78 @@ const DagNodeDef* dag_find(const std::string& name) {
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
#ifdef DAG_CATALOG_TEST
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <set>
|
||||
|
||||
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<std::string> 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
|
||||
|
||||
@@ -106,3 +106,83 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
#ifdef DAG_COMPILE_TEST
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
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<DagStep> p;
|
||||
auto s = compile_dag_to_glsl(p);
|
||||
assert(contains(s, "fragColor = vec4(0.04"));
|
||||
}
|
||||
|
||||
// 2. Single Gen → fragColor = out_0
|
||||
{
|
||||
std::vector<DagStep> 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<DagStep> 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<DagStep> 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<DagStep> 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<DagStep> 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
|
||||
|
||||
@@ -138,25 +138,26 @@ bool dag_node_editor(std::vector<DagStep>& 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<const char*>(p->Data), static_cast<size_t>(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<const char*>(p->Data),
|
||||
static_cast<size_t>(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));
|
||||
|
||||
Reference in New Issue
Block a user