53402d84d5
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>
462 lines
16 KiB
C++
462 lines
16 KiB
C++
#include "app_base.h"
|
|
#include "imgui.h"
|
|
|
|
#include "gfx/shader_canvas.h"
|
|
#include "gfx/gl_shader.h"
|
|
#include "gfx/uniform_parser.h"
|
|
#include "gfx/uniform_panel.h"
|
|
#include "gfx/dag_catalog.h"
|
|
#include "gfx/dag_compile.h"
|
|
#include "gfx/dag_uniforms.h"
|
|
#include "gfx/dag_panel.h"
|
|
#include "gfx/dag_node_editor.h"
|
|
#include "gfx/dag_palette.h"
|
|
#include "gfx/dag_node_previews.h"
|
|
#include "gfx/code_to_generator.h"
|
|
#include "gfx/shaderlab_db.h"
|
|
#include "core/panel_menu.h"
|
|
#include "core/layouts_menu.h"
|
|
#include "core/app_menubar.h"
|
|
#include "core/layout_storage_sqlite.h"
|
|
|
|
#include <chrono>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
static fn::gfx::ShaderCanvas g_canvas_code;
|
|
static fn::gfx::ShaderCanvas g_canvas_dag;
|
|
|
|
// Default placeholder so the Code panel does something useful on first launch
|
|
// without committing to one specific look.
|
|
static const char* CODE_PLACEHOLDER = R"glsl(// Escribe tu fragment shader aqui.
|
|
// Declara uniforms con anotaciones (// @slider, @color, @xy)
|
|
// para que aparezcan como controles al guardar como generator.
|
|
|
|
uniform vec3 u_color; // @color default=0.5,0.2,0.8
|
|
uniform float u_speed; // @slider min=0.1 max=5 default=1
|
|
|
|
void main() {
|
|
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
float t = u_time * u_speed;
|
|
vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0)));
|
|
fragColor = vec4(c, 1.0);
|
|
}
|
|
)glsl";
|
|
|
|
static std::string g_source = CODE_PLACEHOLDER;
|
|
static std::string g_code_err;
|
|
static int g_code_err_line = -1;
|
|
static std::chrono::steady_clock::time_point g_code_last_edit;
|
|
static bool g_code_dirty = true;
|
|
static std::vector<fn::gfx::UniformDescriptor> g_descs;
|
|
static fn::gfx::UniformStore g_store;
|
|
|
|
static std::vector<fn::gfx::DagStep> g_pipeline;
|
|
static std::string g_dag_glsl;
|
|
static std::string g_dag_err;
|
|
static int g_dag_err_line = -1;
|
|
static bool g_dag_dirty = true;
|
|
|
|
// ── Panel visibility (toggled from View menu and panel close button) ──────
|
|
static bool g_show_code = true;
|
|
static bool g_show_dag = true;
|
|
static bool g_show_canvas_c = true;
|
|
static bool g_show_canvas_d = true;
|
|
static bool g_show_controls = true;
|
|
static bool g_show_functions = true;
|
|
static bool g_show_generated = true;
|
|
|
|
// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ───────
|
|
static fn_ui::LayoutCallbacks g_layout_cb;
|
|
static std::string g_pending_layout_blob; // applied at start of next frame
|
|
static std::string g_pending_layout_name; // becomes active_name after apply
|
|
|
|
// ── Save-as-generator modal state ─────────────────────────────────────────
|
|
static bool g_save_modal_open = false;
|
|
static char g_save_name[64] = "my_shader";
|
|
static char g_save_label[64] = "my shader";
|
|
static char g_save_desc[256] = "";
|
|
static char g_save_tags[128] = "shaders_lab,user";
|
|
static std::string g_save_err;
|
|
|
|
static void compile_code() {
|
|
auto r = fn::gfx::compile_fragment(g_source);
|
|
if (r.ok) {
|
|
g_descs = fn::gfx::parse_uniforms(g_source);
|
|
fn::gfx::uniforms_sync(g_store, g_descs);
|
|
fn::gfx::canvas_set_program(g_canvas_code, r.program);
|
|
g_code_err.clear();
|
|
g_code_err_line = -1;
|
|
} else {
|
|
g_code_err = r.err_msg;
|
|
g_code_err_line = r.err_line;
|
|
}
|
|
}
|
|
|
|
static void compile_dag() {
|
|
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
|
|
auto r = fn::gfx::compile_fragment(g_dag_glsl);
|
|
if (r.ok) {
|
|
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
|
|
g_dag_err.clear();
|
|
g_dag_err_line = -1;
|
|
} else {
|
|
g_dag_err = r.err_msg;
|
|
g_dag_err_line = r.err_line;
|
|
}
|
|
}
|
|
|
|
static void mark_code_dirty() {
|
|
g_code_last_edit = std::chrono::steady_clock::now();
|
|
g_code_dirty = true;
|
|
}
|
|
|
|
static void ensure_dag_default() {
|
|
if (g_pipeline.empty()) {
|
|
const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma");
|
|
if (plasma) {
|
|
fn::gfx::DagStep s;
|
|
s.id = "n_plasma";
|
|
s.name = plasma->name;
|
|
s.params = plasma->param_defaults;
|
|
g_pipeline.push_back(s);
|
|
}
|
|
}
|
|
bool has_output = false;
|
|
for (const auto& s : g_pipeline) {
|
|
const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name);
|
|
if (d && d->kind == fn::gfx::DagKind::Output) { has_output = true; break; }
|
|
}
|
|
if (!has_output) {
|
|
const fn::gfx::DagNodeDef* out = fn::gfx::dag_find("output");
|
|
if (out) {
|
|
fn::gfx::DagStep s;
|
|
s.id = "n_output";
|
|
s.name = out->name;
|
|
s.editor_pos_x = 500.0f;
|
|
if (!g_pipeline.empty()) s.source_ids[0] = g_pipeline.front().id;
|
|
g_pipeline.push_back(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void draw_err(const std::string& msg, int line) {
|
|
if (msg.empty()) return;
|
|
ImGui::Separator();
|
|
if (line > 0) {
|
|
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str());
|
|
} else {
|
|
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str());
|
|
}
|
|
}
|
|
|
|
// snake_case validation: lowercase letters, digits, underscores; first char a-z.
|
|
static bool valid_id(const char* s) {
|
|
if (!s || !*s) return false;
|
|
if (!(*s >= 'a' && *s <= 'z')) return false;
|
|
for (const char* p = s; *p; ++p) {
|
|
char c = *p;
|
|
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Build a DagNodeDef from current Code source + form fields, persist it, and
|
|
// register in the live catalog. Returns "" on success or an error message.
|
|
static std::string save_current_as_generator() {
|
|
if (!valid_id(g_save_name)) return "name must be snake_case (a-z, 0-9, _) and start with a letter";
|
|
|
|
if (fn::gfx::dag_find(g_save_name)) {
|
|
const fn::gfx::DagNodeDef* existing = fn::gfx::dag_find(g_save_name);
|
|
if (existing && existing->is_builtin) {
|
|
return std::string("name '") + g_save_name + "' collides with a built-in node";
|
|
}
|
|
// user node with same name → overwrite is allowed
|
|
}
|
|
|
|
auto tr = fn::gfx::code_to_generator(g_source);
|
|
if (!tr.ok) return tr.err;
|
|
|
|
fn::gfx::GeneratorRecord rec;
|
|
rec.id = g_save_name;
|
|
rec.label = g_save_label[0] ? g_save_label : g_save_name;
|
|
rec.description = g_save_desc;
|
|
rec.source_glsl = g_source;
|
|
rec.body_glsl = tr.body_template;
|
|
rec.param_count = tr.param_count;
|
|
rec.param_defaults = tr.param_defaults;
|
|
rec.param_names = tr.param_names;
|
|
rec.controls = tr.controls;
|
|
rec.tags = g_save_tags;
|
|
|
|
std::string err;
|
|
if (!fn::gfx::shaderlab_db_save_generator(rec, &err)) {
|
|
return std::string("db save failed: ") + err;
|
|
}
|
|
|
|
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
|
|
if (!fn::gfx::dag_register_node(def)) {
|
|
return std::string("could not register node '") + rec.id + "'";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Reconstitute every persisted generator and inject it into the live catalog.
|
|
static void load_user_generators_into_catalog() {
|
|
for (const auto& rec : fn::gfx::shaderlab_db_list_generators()) {
|
|
// Re-translate body_template from source to keep the lambda fresh.
|
|
// (We could trust rec.body_glsl, but re-running ensures forward-compat
|
|
// when we tweak the translator.)
|
|
auto tr = fn::gfx::code_to_generator(rec.source_glsl);
|
|
if (!tr.ok) continue; // skip broken records
|
|
|
|
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
|
|
fn::gfx::dag_register_node(def);
|
|
}
|
|
}
|
|
|
|
static void render() {
|
|
// Apply pending layout BEFORE any ImGui::Begin this frame.
|
|
// (LoadIniSettingsFromMemory must happen before windows are submitted.)
|
|
if (!g_pending_layout_blob.empty()) {
|
|
ImGui::LoadIniSettingsFromMemory(g_pending_layout_blob.c_str(),
|
|
g_pending_layout_blob.size());
|
|
g_layout_cb.active_name = g_pending_layout_name;
|
|
g_pending_layout_blob.clear();
|
|
g_pending_layout_name.clear();
|
|
}
|
|
|
|
if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code);
|
|
if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag);
|
|
|
|
if (g_code_dirty) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_code_last_edit).count();
|
|
if (elapsed > 250) {
|
|
compile_code();
|
|
g_code_dirty = false;
|
|
}
|
|
}
|
|
if (g_dag_dirty) {
|
|
compile_dag();
|
|
g_dag_dirty = false;
|
|
}
|
|
|
|
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
|
|
|
|
// --- Menubar (View + Layouts) ---
|
|
fn_ui::PanelToggle toggles[] = {
|
|
{"Code", "Ctrl+1", &g_show_code},
|
|
{"DAG Pipeline", "Ctrl+2", &g_show_dag},
|
|
{"Canvas Code", "Ctrl+3", &g_show_canvas_c},
|
|
{"Canvas DAG", "Ctrl+4", &g_show_canvas_d},
|
|
{"Controls", "Ctrl+5", &g_show_controls},
|
|
{"Functions", "Ctrl+6", &g_show_functions},
|
|
{"Generated GLSL","Ctrl+7", &g_show_generated},
|
|
};
|
|
fn_ui::app_menubar(toggles, std::size(toggles), &g_layout_cb);
|
|
|
|
// --- Code window ---
|
|
if (g_show_code) {
|
|
if (ImGui::Begin("Code", &g_show_code)) {
|
|
if (ImGui::Button("Save as generator...")) {
|
|
g_save_modal_open = true;
|
|
g_save_err.clear();
|
|
ImGui::OpenPopup("save_as_generator");
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("save_as_generator", &g_save_modal_open,
|
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Guardar shader actual como nodo Gen del DAG.");
|
|
ImGui::Spacing();
|
|
ImGui::InputText("name (snake_case)", g_save_name, sizeof(g_save_name));
|
|
ImGui::InputText("label", g_save_label, sizeof(g_save_label));
|
|
ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc),
|
|
ImVec2(380, 60));
|
|
ImGui::InputText("tags (CSV)", g_save_tags, sizeof(g_save_tags));
|
|
|
|
if (!g_save_err.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", g_save_err.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Save", ImVec2(120, 0))) {
|
|
g_save_err = save_current_as_generator();
|
|
if (g_save_err.empty()) {
|
|
g_save_modal_open = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
g_save_modal_open = false;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
|
|
ImVec2 editor_size(avail.x, avail.y - footer_h);
|
|
|
|
char buf[1 << 16];
|
|
size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1;
|
|
memcpy(buf, g_source.c_str(), copy_len);
|
|
buf[copy_len] = '\0';
|
|
|
|
if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size,
|
|
ImGuiInputTextFlags_AllowTabInput)) {
|
|
g_source = buf;
|
|
mark_code_dirty();
|
|
}
|
|
|
|
draw_err(g_code_err, g_code_err_line);
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// --- DAG Pipeline window ---
|
|
if (g_show_dag) {
|
|
if (ImGui::Begin("DAG Pipeline", &g_show_dag)) {
|
|
if (fn::gfx::dag_node_editor(g_pipeline)) {
|
|
g_dag_dirty = true;
|
|
}
|
|
draw_err(g_dag_err, g_dag_err_line);
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// --- Canvas Code ---
|
|
// Code is fully independent from the DAG: only the uniforms declared in
|
|
// the Code source itself (parsed via parse_uniforms) get fed. To reproduce
|
|
// a DAG render here, paste the *baked* "Generated GLSL" — its u_params live
|
|
// as a const array inside the source.
|
|
if (g_show_canvas_c) {
|
|
if (ImGui::Begin("Canvas Code", &g_show_canvas_c)) {
|
|
fn::gfx::canvas_render(g_canvas_code, static_cast<float>(ImGui::GetTime()),
|
|
[](unsigned int program) {
|
|
fn::gfx::uniforms_apply(g_store, g_descs, program);
|
|
});
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// --- Canvas DAG ---
|
|
if (g_show_canvas_d) {
|
|
if (ImGui::Begin("Canvas DAG", &g_show_canvas_d)) {
|
|
fn::gfx::canvas_render(g_canvas_dag, static_cast<float>(ImGui::GetTime()),
|
|
[](unsigned int program) {
|
|
fn::gfx::dag_uniforms_apply(g_pipeline, program);
|
|
});
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
if (g_canvas_dag.program) {
|
|
fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program);
|
|
}
|
|
|
|
// --- Controls window (Code uniforms) ---
|
|
if (g_show_controls) {
|
|
if (ImGui::Begin("Controls", &g_show_controls)) {
|
|
if (g_descs.empty()) {
|
|
ImGui::TextDisabled("No uniforms declared in Code.");
|
|
ImGui::TextDisabled("Use // @slider, @color, @toggle, @xy annotations.");
|
|
} else {
|
|
fn::gfx::uniforms_panel(g_store, g_descs);
|
|
}
|
|
// fps_overlay ahora se renderiza desde fn::run_app cuando el usuario
|
|
// lo activa en Settings → Show FPS overlay.
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// --- Functions palette (drag into DAG Pipeline) ---
|
|
if (g_show_functions) {
|
|
if (ImGui::Begin("Functions", &g_show_functions)) {
|
|
fn::gfx::dag_palette();
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// --- Generated GLSL window (self-contained DAG → paste-able into Code) ---
|
|
// We bake the live params into a `const vec4 u_params[]` so the displayed
|
|
// text is a complete shader: copy-pasting it into the Code editor yields
|
|
// the same render at the moment of the copy, and nothing in the DAG can
|
|
// change the Code canvas afterwards.
|
|
if (g_show_generated) {
|
|
if (ImGui::Begin("Generated GLSL", &g_show_generated)) {
|
|
if (g_pipeline.empty()) {
|
|
ImGui::TextDisabled("(DAG not compiled yet)");
|
|
} else {
|
|
static std::string s_baked;
|
|
s_baked = fn::gfx::compile_dag_to_glsl_baked(g_pipeline);
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
ImGui::InputTextMultiline("##dag_glsl",
|
|
const_cast<char*>(s_baked.c_str()),
|
|
s_baked.size() + 1,
|
|
avail,
|
|
ImGuiInputTextFlags_ReadOnly);
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
fn::gfx::shaderlab_db_open("shaders_lab.db");
|
|
load_user_generators_into_catalog();
|
|
ensure_dag_default();
|
|
|
|
// Layout persistence on the same shaders_lab.db connection.
|
|
sqlite3* db = fn::gfx::shaderlab_db_handle();
|
|
fn_ui::layout_storage_init(db);
|
|
|
|
g_layout_cb.list = [db]() {
|
|
return fn_ui::layout_storage_list(db);
|
|
};
|
|
g_layout_cb.on_apply = [db](const std::string& name) {
|
|
std::string blob = fn_ui::layout_storage_load_blob(db, name);
|
|
if (!blob.empty()) {
|
|
g_pending_layout_blob = std::move(blob);
|
|
g_pending_layout_name = name;
|
|
}
|
|
};
|
|
g_layout_cb.on_save = [db](const std::string& name) {
|
|
size_t size = 0;
|
|
const char* blob = ImGui::SaveIniSettingsToMemory(&size);
|
|
if (blob && size > 0) {
|
|
fn_ui::layout_storage_save(db, name, std::string(blob, size));
|
|
g_layout_cb.active_name = name;
|
|
}
|
|
};
|
|
g_layout_cb.on_delete = [db](const std::string& name) {
|
|
fn_ui::layout_storage_delete(db, name);
|
|
if (g_layout_cb.active_name == name) g_layout_cb.active_name.clear();
|
|
};
|
|
g_layout_cb.on_reset = []() {
|
|
// Default reset: open every panel and clear active layout marker.
|
|
// The actual dock layout is whatever ImGui rebuilt on first launch.
|
|
g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d =
|
|
g_show_controls = g_show_functions = g_show_generated = true;
|
|
g_layout_cb.active_name.clear();
|
|
};
|
|
|
|
fn::AppConfig cfg;
|
|
cfg.title = "shaders_lab";
|
|
cfg.width = 1600;
|
|
cfg.height = 900;
|
|
int rc = fn::run_app(cfg, render);
|
|
|
|
fn::gfx::canvas_destroy(g_canvas_code);
|
|
fn::gfx::canvas_destroy(g_canvas_dag);
|
|
fn::gfx::dag_node_editor_destroy();
|
|
fn::gfx::dag_previews_destroy();
|
|
fn::gfx::shaderlab_db_close();
|
|
return rc;
|
|
}
|