docs(issues): marcar 0025 y 0026 como completados + WIP master

Wave 1 de parallel-fix-issues integrada a master:
- 0025: text_editor_cpp_core + file_watcher_cpp_core
- 0026: gl_texture_load_cpp_gfx (vendor: stb_image v2.30)

Ademas se commitea WIP previo de master que estaba sin commitear (cambios
en shaders_lab, dag_*, framework, tokens, kpi_card, gl_loader.md, etc.)
para dejar HEAD buildable.

Notas:
- Algunos deps del gallery (button.cpp, toolbar.cpp, modal_dialog.cpp...)
  siguen UNTRACKED — gating con FN_BUILD_GALLERY=ON (default OFF) para
  que master build (sin flag) no los necesite.
- Build OK con y sin flag. fn index registra 904 functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:11:26 +02:00
parent d3d5af51f2
commit b093c898a8
37 changed files with 1819 additions and 342 deletions
+307 -34
View File
@@ -4,6 +4,7 @@
#include "imgui.h"
#include "imgui_node_editor.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <queue>
@@ -21,6 +22,10 @@ static constexpr int MAX_NODES = 16;
static ed::EditorContext* s_ctx = nullptr;
static uint32_t s_next_uid = 1;
static std::unordered_set<uint32_t> s_positioned;
// Real pin positions in canvas space, captured during node draw and consulted
// by the splice hit-test. Without this, a cable's hit zone is offset from the
// visible cable whenever node height ≠ pin row height (e.g. preview open).
static std::unordered_map<uintptr_t, ImVec2> s_pin_canvas_pos;
// ── ID encoding ──────────────────────────────────────────────────────────────
// node id = editor_uid
@@ -48,6 +53,45 @@ static bool is_output_pin(uintptr_t id) { return (id & 0xFF) == 0; }
static uint32_t uid_from_pin(uintptr_t id) { return static_cast<uint32_t>(id >> 8); }
static int slot_from_input_pin(uintptr_t id) { return static_cast<int>(id & 0xFF) - 1; }
// Closest distance from point p to the segment [a, b] (canvas space).
static float dist_point_to_segment(ImVec2 p, ImVec2 a, ImVec2 b) {
float abx = b.x - a.x, aby = b.y - a.y;
float apx = p.x - a.x, apy = p.y - a.y;
float ab2 = abx * abx + aby * aby;
if (ab2 <= 1e-6f) return std::sqrt(apx * apx + apy * apy);
float t = (apx * abx + apy * aby) / ab2;
t = std::max(0.0f, std::min(1.0f, t));
float qx = a.x + t * abx, qy = a.y + t * aby;
float dx = p.x - qx, dy = p.y - qy;
return std::sqrt(dx * dx + dy * dy);
}
// Same horizontal-bias control offset imgui-node-editor uses for its links.
static inline float bezier_ctrl(float dx) {
return std::max(40.0f, std::abs(dx) * 0.5f);
}
// Closest distance from point p to a cubic bezier curve, sampled as 24 chords.
static float dist_point_to_bezier(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) {
constexpr int N = 24;
float best = 1e30f;
ImVec2 prev = p0;
for (int i = 1; i <= N; ++i) {
float t = static_cast<float>(i) / static_cast<float>(N);
float u = 1.0f - t;
float b0 = u * u * u;
float b1 = 3.0f * u * u * t;
float b2 = 3.0f * u * t * t;
float b3 = t * t * t;
ImVec2 pt(b0 * p0.x + b1 * p1.x + b2 * p2.x + b3 * p3.x,
b0 * p0.y + b1 * p1.y + b2 * p2.y + b3 * p3.y);
float d = dist_point_to_segment(p, prev, pt);
if (d < best) best = d;
prev = pt;
}
return best;
}
static int find_by_uid(const std::vector<DagStep>& p, uint32_t uid) {
for (int i = 0; i < static_cast<int>(p.size()); ++i) {
if (p[static_cast<size_t>(i)].editor_uid == uid) return i;
@@ -71,10 +115,14 @@ static ImVec4 kind_color(DagKind kind) {
return ImVec4(1, 1, 1, 1);
}
static constexpr float PIN_RADIUS = 9.0f;
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
static constexpr float PIN_RADIUS = 14.0f; // big grabbable target
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
static constexpr float CONTROL_WIDTH = 220.0f;
static constexpr float COL_GAP = 14.0f; // input ↔ controls ↔ output gap
static constexpr float CABLE_THICK = 3.5f;
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
static const ImVec4 SPLICE_COLOR = ImVec4(1.00f, 0.82f, 0.18f, 1.0f); // golden preview cable
enum class PinSide { Input, Output };
@@ -90,7 +138,7 @@ static void draw_pin_circle(PinSide side) {
ImVec2(center.x + PIN_RADIUS, center.y + PIN_RADIUS));
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddCircleFilled(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_COLOR));
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 1.5f);
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 2.0f);
ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
}
@@ -197,30 +245,238 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
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;
// Insert before the Output node so the Output stays at the back;
// otherwise new nodes can never be wired into it (compiler and
// cycle check only search indices strictly before the target).
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
if (def) {
ImVec2 drop = ed::ScreenToCanvas(s_pending_add_pos);
// ── Priority 1: drop on an existing cable → splice (src → new → dst).
// Only valid if the new def actually has an input pin (Op / Blend).
int splice_src_idx = -1, splice_dst_idx = -1, splice_slot = -1;
if (def->num_inputs >= 1 && def->kind != DagKind::Output) {
constexpr float HIT_THRESH = 14.0f; // canvas px
float best_d = HIT_THRESH;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagStep& dst = pipeline[i];
const DagNodeDef* dd = dag_find(dst.name);
if (!dd) continue;
for (int k = 0; k < dd->num_inputs; ++k) {
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
if (out_it == s_pin_canvas_pos.end() ||
in_it == s_pin_canvas_pos.end()) continue;
ImVec2 A = out_it->second;
ImVec2 B = in_it->second;
float ctrl = bezier_ctrl(B.x - A.x);
ImVec2 P1(A.x + ctrl, A.y);
ImVec2 P2(B.x - ctrl, B.y);
float d = dist_point_to_bezier(drop, A, P1, P2, B);
if (d < best_d) {
best_d = d;
splice_src_idx = src_idx;
splice_dst_idx = static_cast<int>(i);
splice_slot = k;
}
}
}
}
// ── Priority 2: drop on an existing node of the same kind → replace.
int hit_idx = -1;
if (splice_dst_idx < 0) {
for (size_t i = 0; i < pipeline.size(); ++i) {
auto npos = ed::GetNodePosition(ed::NodeId(node_id(pipeline[i].editor_uid)));
auto nsz = ed::GetNodeSize (ed::NodeId(node_id(pipeline[i].editor_uid)));
if (drop.x >= npos.x && drop.x <= npos.x + nsz.x &&
drop.y >= npos.y && drop.y <= npos.y + nsz.y) {
hit_idx = static_cast<int>(i);
break;
}
}
}
const DagNodeDef* hit_def = (hit_idx >= 0)
? dag_find(pipeline[static_cast<size_t>(hit_idx)].name) : nullptr;
if (splice_dst_idx >= 0 && static_cast<int>(pipeline.size()) < MAX_NODES) {
// Splice: build the new node wired to the existing source, then
// rewire the existing destination's input to point to it.
const std::string src_id = pipeline[static_cast<size_t>(splice_src_idx)].id;
const std::string dst_id = pipeline[static_cast<size_t>(splice_dst_idx)].id;
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;
step.editor_pos_x = drop.x;
step.editor_pos_y = drop.y;
step.source_ids[0] = src_id; // wire src → new
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
}
pipeline.insert(insert_it, step);
// Re-find dst by id (insertion may have shifted indices) and
// rewire its slot to the new node.
int dst_now = find_by_id(pipeline, dst_id);
if (dst_now >= 0) {
pipeline[static_cast<size_t>(dst_now)].source_ids[static_cast<size_t>(splice_slot)] = step.id;
}
changed = true;
} else if (hit_def && hit_def->kind == def->kind && def->kind != DagKind::Output) {
// Replace path: same-kind node hit. Keep id, editor_uid, pos,
// source_ids, preview_open. Reset params + clear stale input
// slots beyond the new def's input count.
DagStep& tgt = pipeline[static_cast<size_t>(hit_idx)];
tgt.name = def->name;
tgt.params = def->param_defaults;
for (int k = def->num_inputs; k < 4; ++k) {
tgt.source_ids[static_cast<size_t>(k)].clear();
}
changed = true;
} else if (static_cast<int>(pipeline.size()) < MAX_NODES) {
// Add path: brand-new node, inserted before Output so the sink stays last.
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;
step.editor_pos_x = drop.x;
step.editor_pos_y = drop.y;
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
}
pipeline.insert(insert_it, step);
changed = true;
}
pipeline.insert(insert_it, step);
changed = true;
}
s_pending_add = false;
}
// ── Live splice candidate detection ─────────────────────────────────────
// The user can splice into a cable in two ways:
// (a) drag a node from the palette → ImGui drag-drop payload.
// (b) drag an existing node by its body → tracked via mouse-down on a node.
// In both cases, while the drag is active we hit-test against existing
// cables and remember the candidate so:
// 1. the link-drawing pass below paints it in SPLICE_COLOR (preview).
// 2. the release handler farther down rewires the graph.
static uint32_t s_drag_existing_uid = 0;
// Start tracking an existing-node drag when the user mouse-down on a node
// body (not on a pin, not on the Output sink, must have at least one input).
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ed::PinId hp = ed::GetHoveredPin();
ed::NodeId hn = ed::GetHoveredNode();
if (hp.Get() == 0 && hn.Get() != 0) {
uint32_t uid = static_cast<uint32_t>(hn.Get());
int idx = find_by_uid(pipeline, uid);
if (idx >= 0) {
const DagNodeDef* d = dag_find(pipeline[static_cast<size_t>(idx)].name);
if (d && d->num_inputs >= 1 && d->kind != DagKind::Output) {
s_drag_existing_uid = uid;
}
}
}
}
// Resolve which definition (if any) is the active splice candidate.
const DagNodeDef* candidate_def = nullptr;
const std::string* exclude_node_id = nullptr;
int exclude_node_idx = -1;
if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) {
if (p->IsDataType("DAG_NODE_TYPE")) {
std::string drag_name(static_cast<const char*>(p->Data),
static_cast<size_t>(p->DataSize));
candidate_def = dag_find(drag_name);
}
}
if (!candidate_def && s_drag_existing_uid != 0) {
exclude_node_idx = find_by_uid(pipeline, s_drag_existing_uid);
if (exclude_node_idx >= 0) {
candidate_def = dag_find(pipeline[static_cast<size_t>(exclude_node_idx)].name);
exclude_node_id = &pipeline[static_cast<size_t>(exclude_node_idx)].id;
}
}
// Hit-test cables against current mouse position (canvas space).
uint32_t splice_hl_from_uid = 0;
uint32_t splice_hl_to_uid = 0;
int splice_hl_slot = -1;
if (candidate_def && candidate_def->num_inputs >= 1
&& candidate_def->kind != DagKind::Output) {
ImVec2 cur = ed::ScreenToCanvas(ImGui::GetMousePos());
constexpr float HIT_THRESH = 16.0f;
float best_d = HIT_THRESH;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagStep& dst = pipeline[i];
// Skip cables that touch the moving node (its own in/out edges).
if (exclude_node_id && dst.id == *exclude_node_id) continue;
const DagNodeDef* dd = dag_find(dst.name);
if (!dd) continue;
for (int k = 0; k < dd->num_inputs; ++k) {
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
if (exclude_node_id && sid == *exclude_node_id) continue;
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
if (out_it == s_pin_canvas_pos.end() ||
in_it == s_pin_canvas_pos.end()) continue;
ImVec2 A = out_it->second;
ImVec2 B = in_it->second;
float ctrl = bezier_ctrl(B.x - A.x);
ImVec2 P1(A.x + ctrl, A.y);
ImVec2 P2(B.x - ctrl, B.y);
float d = dist_point_to_bezier(cur, A, P1, P2, B);
if (d < best_d) {
best_d = d;
splice_hl_from_uid = src.editor_uid;
splice_hl_to_uid = dst.editor_uid;
splice_hl_slot = k;
}
}
}
}
// Release handler for the existing-node-drag splice. Palette splice goes
// through s_pending_add above; this branch handles "drag node body onto cable".
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && s_drag_existing_uid != 0) {
uint32_t moving_uid = s_drag_existing_uid;
s_drag_existing_uid = 0;
if (splice_hl_to_uid != 0) {
int mv_idx = find_by_uid(pipeline, moving_uid);
int src_idx = find_by_uid(pipeline, splice_hl_from_uid);
int dst_idx = find_by_uid(pipeline, splice_hl_to_uid);
if (mv_idx >= 0 && src_idx >= 0 && dst_idx >= 0) {
const std::string moving_id = pipeline[static_cast<size_t>(mv_idx)].id;
const std::string src_id = pipeline[static_cast<size_t>(src_idx)].id;
// Detach moving node from any existing consumer.
for (auto& s : pipeline) {
for (auto& sid : s.source_ids) {
if (sid == moving_id) sid.clear();
}
}
pipeline[static_cast<size_t>(mv_idx)].source_ids[0] = src_id;
pipeline[static_cast<size_t>(dst_idx)].source_ids[static_cast<size_t>(splice_hl_slot)] = moving_id;
changed = true;
}
}
}
// ── Draw nodes ───────────────────────────────────────────────────────────
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
DagStep& step = pipeline[static_cast<size_t>(i)];
@@ -233,7 +489,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
// and user drags must not be overwritten.
if (s_positioned.find(step.editor_uid) == s_positioned.end()) {
if (step.editor_pos_x == 0.0f && step.editor_pos_y == 0.0f) {
step.editor_pos_x = 50.0f + static_cast<float>(i) * 220.0f;
step.editor_pos_x = 50.0f + static_cast<float>(i) * 320.0f;
step.editor_pos_y = 100.0f;
}
ed::SetNodePosition(ed::NodeId(node_id(step.editor_uid)),
@@ -243,7 +499,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
// Zero lateral padding so the input/output pin circles sit flush
// with the node's left and right edges.
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 8, 0, 8));
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 12, 0, 12));
ed::BeginNode(ed::NodeId(node_id(step.editor_uid)));
// Header (with horizontal padding so the title doesn't touch the edge)
@@ -266,36 +522,43 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
ed::BeginPin(ed::PinId(input_pin_id(step.editor_uid, k)), ed::PinKind::Input);
ed::PinPivotAlignment(ImVec2(0.0f, 0.5f));
ed::PinPivotSize(ImVec2(0, 0));
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
ImVec2 center_screen(cur_screen.x, cur_screen.y + PIN_RADIUS);
s_pin_canvas_pos[input_pin_id(step.editor_uid, k)] =
ed::ScreenToCanvas(center_screen);
draw_pin_circle(PinSide::Input);
ed::EndPin();
}
ImGui::EndGroup();
ImGui::SameLine(0, 8); // gap between pin column and controls
ImGui::SameLine(0, COL_GAP); // gap between pin column and controls
ImGui::BeginGroup(); // controls column (centre, with internal padding)
ImGui::PushID(static_cast<int>(step.editor_uid));
if (def->controls.empty() && def->kind != DagKind::Output) {
ImGui::Dummy(ImVec2(60, PIN_DIAMETER));
ImGui::Dummy(ImVec2(CONTROL_WIDTH * 0.5f, PIN_DIAMETER));
}
for (size_t ci = 0; ci < def->controls.size(); ++ci) {
const DagControl& ctrl = def->controls[ci];
ImGui::SetNextItemWidth(150.0f);
ImGui::SetNextItemWidth(CONTROL_WIDTH);
char uid_lbl[64];
std::snprintf(uid_lbl, sizeof(uid_lbl), "%s##%u%zu", ctrl.label.c_str(), step.editor_uid, ci);
int pcount = static_cast<int>(step.params.size());
if (ctrl.kind == DagControl::Kind::Slider) {
int pidx = ctrl.param_idx[0];
if (pidx >= 0 && pidx < 4) {
if (pidx >= 0 && pidx < pcount) {
ImGui::SliderFloat(uid_lbl, &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::XY) {
int px = ctrl.param_idx[0], py = ctrl.param_idx[1];
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
ImGui::SliderFloat2(uid_lbl, &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::Color) {
int pr = ctrl.param_idx[0];
if (pr >= 0 && pr + 2 < 4) {
if (pr >= 0 && pr + 2 < pcount) {
ImGui::TextUnformatted(ctrl.label.c_str());
ImGui::SameLine();
ed::Suspend();
ImGui::ColorEdit3(uid_lbl, &step.params[static_cast<size_t>(pr)],
ImGuiColorEditFlags_NoInputs |
@@ -327,13 +590,17 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
ImGui::PopID();
ImGui::EndGroup();
ImGui::SameLine(0, 8); // gap between controls and output pin
ImGui::SameLine(0, COL_GAP); // gap between controls and output pin
ImGui::BeginGroup(); // output column (right edge)
if (has_output_pin) {
ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
ed::PinPivotAlignment(ImVec2(1.0f, 0.5f));
ed::PinPivotSize(ImVec2(0, 0));
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
ImVec2 center_screen(cur_screen.x + PIN_RADIUS, cur_screen.y + PIN_RADIUS);
s_pin_canvas_pos[output_pin_id(step.editor_uid)] =
ed::ScreenToCanvas(center_screen);
draw_pin_circle(PinSide::Output);
ed::EndPin();
} else {
@@ -356,10 +623,16 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src_step = pipeline[static_cast<size_t>(src_idx)];
const bool is_splice_preview =
(src_step.editor_uid == splice_hl_from_uid &&
step.editor_uid == splice_hl_to_uid &&
k == splice_hl_slot);
ImVec4 link_col = is_splice_preview ? SPLICE_COLOR : PIN_COLOR;
float link_thick = is_splice_preview ? CABLE_THICK + 2.0f : CABLE_THICK;
ed::Link(ed::LinkId(link_id(src_step.editor_uid, step.editor_uid, k)),
ed::PinId(output_pin_id(src_step.editor_uid)),
ed::PinId(input_pin_id(step.editor_uid, k)),
PIN_COLOR, 2.5f);
link_col, link_thick);
}
}