feat(shaders_lab): Output node + Functions palette with drag-drop

- DagKind::Output (new enum): terminal sink; compiler wires fragColor to its source_ids[0]
- dag_catalog: "output" node (1 input, red)
- dag_compile: skips Output in node_<i> emission; final fragColor resolves from Output's connection
- dag_node_editor: no more Add button; drops "DAG_NODE_TYPE" payloads at mouse canvas position; Output cannot be deleted; Output has no output pin
- dag_palette (new fn): Functions window with grouped, draggable node cards
- main.cpp: "Functions" window added; ensure_dag_default seeds plasma + connected Output
This commit is contained in:
2026-04-24 22:16:47 +02:00
parent e91e80bfcf
commit 0be4b29a4b
10 changed files with 253 additions and 106 deletions
+56 -77
View File
@@ -61,9 +61,10 @@ static int find_by_id(const std::vector<DagStep>& p, const std::string& id) {
static ImVec4 kind_color(DagKind kind) {
switch (kind) {
case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f);
case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f);
case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f);
case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f);
case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f);
case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f);
case DagKind::Output: return ImVec4(0.85f, 0.25f, 0.25f, 1.0f);
}
return ImVec4(1, 1, 1, 1);
}
@@ -120,70 +121,6 @@ static bool topo_sort(std::vector<DagStep>& pipeline) {
return true;
}
// Add node popup toolbar — rendered OUTSIDE ed::Begin
static bool draw_add_toolbar(std::vector<DagStep>& pipeline) {
bool changed = false;
int sz = static_cast<int>(pipeline.size());
if (ImGui::Button("+ Add Node")) {
ImGui::OpenPopup("ne_add_popup");
}
ImGui::SameLine();
ImGui::Text("%d/%d", sz, MAX_NODES);
ImGui::SameLine();
if (ImGui::Button("Clear") && !pipeline.empty()) {
ImGui::OpenPopup("ne_clear_confirm");
}
ImGui::SameLine();
if (ImGui::Button("Fit")) {
if (s_ctx) {
ed::SetCurrentEditor(s_ctx);
ed::NavigateToContent(0.0f);
ed::SetCurrentEditor(nullptr);
}
}
if (ImGui::BeginPopupModal("ne_clear_confirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("Vaciar el pipeline?");
if (ImGui::Button("Si")) {
pipeline.clear();
changed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No")) { ImGui::CloseCurrentPopup(); }
ImGui::EndPopup();
}
if (ImGui::BeginPopup("ne_add_popup")) {
const char* kind_names[] = { "Gen", "Op", "Blend" };
DagKind kinds[] = { DagKind::Gen, DagKind::Op, DagKind::Blend };
for (int k = 0; k < 3; ++k) {
if (ImGui::BeginMenu(kind_names[k])) {
for (const auto& def : dag_catalog()) {
if (def.kind != kinds[k]) continue;
if (ImGui::MenuItem(def.label.c_str())) {
if (sz < MAX_NODES) {
DagStep step;
step.id = "n" + std::to_string(s_next_uid);
step.name = def.name;
step.params = def.param_defaults;
step.editor_uid = s_next_uid++;
// stagger position so nodes don't stack
step.editor_pos_x = 50.0f + static_cast<float>(sz) * 220.0f;
step.editor_pos_y = 100.0f;
pipeline.push_back(step);
changed = true;
}
}
}
ImGui::EndMenu();
}
}
ImGui::EndPopup();
}
return changed;
}
bool dag_node_editor(std::vector<DagStep>& pipeline) {
bool changed = false;
@@ -201,12 +138,47 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
s_ctx = ed::CreateEditor(&cfg);
}
changed |= draw_add_toolbar(pipeline);
ImGui::Separator();
// Palette drop zone: accept dropped node types (from the Functions panel)
// and queue an add at the mouse canvas position (resolved after ed::Begin).
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;
}
ImGui::EndDragDropTarget();
}
ImGui::SetCursorScreenPos(origin);
ed::SetCurrentEditor(s_ctx);
ed::Begin("dag_editor", ImVec2(0.0f, 0.0f));
if (s_pending_add) {
const DagNodeDef* def = dag_find(s_pending_add_name);
if (def && static_cast<int>(pipeline.size()) < MAX_NODES) {
uint32_t uid = s_next_uid++;
DagStep step;
step.id = "n" + std::to_string(uid);
step.name = def->name;
step.params = def->param_defaults;
step.editor_uid = uid;
ImVec2 canvas_pos = ed::ScreenToCanvas(s_pending_add_pos);
step.editor_pos_x = canvas_pos.x;
step.editor_pos_y = canvas_pos.y;
pipeline.push_back(step);
changed = true;
}
s_pending_add = false;
}
// ── Draw nodes ───────────────────────────────────────────────────────────
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
DagStep& step = pipeline[static_cast<size_t>(i)];
@@ -279,11 +251,13 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
}
ImGui::PopID();
// Output pin
ImGui::Dummy(ImVec2(0, 2));
ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
ImGui::Text("out ->");
ed::EndPin();
// Output pin (skip for the terminal Output node — it has no output)
if (def->kind != DagKind::Output) {
ImGui::Dummy(ImVec2(0, 2));
ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
ImGui::Text("out ->");
ed::EndPin();
}
ed::EndNode();
}
@@ -369,12 +343,17 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
ed::NodeId del_nid;
while (ed::QueryDeletedNode(&del_nid)) {
uint32_t uid = static_cast<uint32_t>(del_nid.Get());
int idx = find_by_uid(pipeline, uid);
// Refuse to delete the Output node (it's the sink)
const DagNodeDef* ddef = (idx >= 0) ? dag_find(pipeline[static_cast<size_t>(idx)].name) : nullptr;
if (ddef && ddef->kind == DagKind::Output) {
ed::RejectDeletedItem();
continue;
}
if (ed::AcceptDeletedItem()) {
uint32_t uid = static_cast<uint32_t>(del_nid.Get());
int idx = find_by_uid(pipeline, uid);
if (idx >= 0) {
const std::string& del_step_id = pipeline[static_cast<size_t>(idx)].id;
// Clear any source_ids pointing to the deleted node
for (auto& step : pipeline) {
for (auto& sid : step.source_ids) {
if (sid == del_step_id) sid.clear();