From 08069d65cea269aec6d19c4d99f9f38138e6c962 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 16 Feb 2026 21:51:05 +0100 Subject: refactor: remove C++ seq_compiler and Gantt chart references - Remove tools/seq_compiler.cc (replaced by seq_compiler.py) - Remove C++ seq_compiler build target from cmake/DemoTools.cmake - Update documentation to remove Gantt chart mentions - Keep seq_compiler.py (active Python compiler) - All tests passing (34/34) --- cmake/DemoTools.cmake | 14 +- doc/COMPLETED.md | 2 +- tools/seq_compiler.cc | 1146 --------------------------------------- tools/timeline_editor/README.md | 2 +- 4 files changed, 4 insertions(+), 1160 deletions(-) delete mode 100644 tools/seq_compiler.cc diff --git a/cmake/DemoTools.cmake b/cmake/DemoTools.cmake index 43c4716..ae39ec9 100644 --- a/cmake/DemoTools.cmake +++ b/cmake/DemoTools.cmake @@ -1,5 +1,5 @@ # Build Tools Setup -# Configures asset_packer, seq_compiler, and tracker_compiler +# Configures asset_packer and tracker_compiler # Asset packer tool if(DEFINED ASSET_PACKER_PATH) @@ -13,17 +13,7 @@ else() set(ASSET_PACKER_DEPENDS asset_packer) endif() -# Sequence compiler tool (v1) -if(DEFINED SEQ_COMPILER_PATH) - set(SEQ_COMPILER_CMD ${SEQ_COMPILER_PATH}) - set(SEQ_COMPILER_DEPENDS ${SEQ_COMPILER_PATH}) -else() - add_executable(seq_compiler tools/seq_compiler.cc) - set(SEQ_COMPILER_CMD $) - set(SEQ_COMPILER_DEPENDS seq_compiler) -endif() - -# Sequence compiler tool (v2 - Python) +# Sequence compiler tool (Python) set(SEQ_COMPILER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/seq_compiler.py) set(SEQ_COMPILER_CMD ${CMAKE_COMMAND} -E env python3 ${SEQ_COMPILER_SCRIPT}) set(SEQ_COMPILER_DEPENDS ${SEQ_COMPILER_SCRIPT}) diff --git a/doc/COMPLETED.md b/doc/COMPLETED.md index 2a22845..4f0a889 100644 --- a/doc/COMPLETED.md +++ b/doc/COMPLETED.md @@ -399,7 +399,7 @@ Use `read @doc/archive/FILENAME.md` to access archived documents. - [x] **Scene Integrity:** Restored proper object indexing and removed redundant geometry, ensuring the floor grid and objects render correctly. - [x] **Task #57: Interactive Timeline Editor (Phase 1 Complete)** ๐ŸŽ‰ - - [x] **Core Parser & Renderer**: Implemented demo.seq parser with BPM, beat notation, priority modifiers. Gantt-style timeline rendering with dynamic sequence/effect positioning. + - [x] **Core Parser & Renderer**: Implemented demo.seq parser with BPM, beat notation, priority modifiers. Visual timeline rendering with dynamic sequence/effect positioning. - [x] **Drag & Drop**: Sequences and effects draggable along timeline with proper offset calculation. Fixed critical e.target vs e.currentTarget bug preventing erratic jumping. - [x] **Resize Handles**: Left/right handles on selected effects. Allow negative relative times (effects extend before sequence start). - [x] **Snap-to-Beat**: Checkbox toggle with beat markers. Automatic snapping when dragging in beat mode. diff --git a/tools/seq_compiler.cc b/tools/seq_compiler.cc deleted file mode 100644 index 462bdba..0000000 --- a/tools/seq_compiler.cc +++ /dev/null @@ -1,1146 +0,0 @@ -// This file is part of the 64k demo project. -// It implements the sequence compiler tool. -// Converts a text-based timeline description into C++ code. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct EffectEntry { - std::string class_name; - std::string start; - std::string end; - std::string priority; - std::string extra_args; - std::vector> params; // key=value pairs -}; - -struct SequenceEntry { - std::string start_time; - std::string priority; - std::string end_time; // Optional: -1.0f means "no explicit end" - std::string name; // Optional: human-readable name for Gantt charts - std::vector effects; -}; - -std::string trim(const std::string& str) { - size_t first = str.find_first_not_of(" \t"); - if (std::string::npos == first) - return ""; // String is all whitespace, return empty string - size_t last = str.find_last_not_of(" \t"); - return str.substr(first, (last - first + 1)); -} - -// Parse key=value parameters from extra_args string -// Example: "color=1.0,0.0,0.0 decay=0.95" -> {{"color", "1.0,0.0,0.0"}, -// {"decay", "0.95"}} -std::vector> -parse_parameters(const std::string& args) { - std::vector> params; - std::istringstream ss(args); - std::string token; - - while (ss >> token) { - size_t eq_pos = token.find('='); - if (eq_pos != std::string::npos) { - std::string key = token.substr(0, eq_pos); - std::string value = token.substr(eq_pos + 1); - params.push_back({key, value}); - } - } - - return params; -} - -// Check if an effect is a post-process effect -bool is_post_process_effect(const std::string& class_name) { - // List of known post-process effects - static const std::vector post_process_effects = { - "FadeEffect", "FlashEffect", "GaussianBlurEffect", - "SolarizeEffect", "VignetteEffect", "ChromaAberrationEffect", - "DistortEffect", "ThemeModulationEffect", "CNNEffect", - "PassthroughEffect", "CircleMaskEffect"}; - return std::find(post_process_effects.begin(), post_process_effects.end(), - class_name) != post_process_effects.end(); -} - -// Calculate adaptive tick interval based on timeline duration -int calculate_tick_interval(float max_time) { - if (max_time <= 5) - return 1; - if (max_time <= 40) - return 2; - if (max_time <= 100) - return 5; - if (max_time <= 200) - return 10; - return 20; -} - -// Timeline analysis result: max time and sequences sorted by start time -struct TimelineMetrics { - float max_time; - std::vector sorted_sequences; -}; - -// Calculate sequence end time (explicit or derived from latest effect) -float get_sequence_end(const SequenceEntry& seq) { - float seq_start = std::stof(seq.start_time); - if (seq.end_time != "-1.0") { - return seq_start + std::stof(seq.end_time); - } - float seq_end = seq_start; - for (const auto& eff : seq.effects) { - seq_end = std::max(seq_end, seq_start + std::stof(eff.end)); - } - return seq_end; -} - -// Analyze timeline: find max time and sort sequences by start time -TimelineMetrics analyze_timeline(const std::vector& sequences, - const std::string& demo_end_time) { - float max_time = demo_end_time.empty() ? 0.0f : std::stof(demo_end_time); - for (const auto& seq : sequences) { - float seq_start = std::stof(seq.start_time); - for (const auto& eff : seq.effects) { - max_time = std::max(max_time, seq_start + std::stof(eff.end)); - } - if (seq.end_time != "-1.0") { - max_time = std::max(max_time, seq_start + std::stof(seq.end_time)); - } - } - - std::vector sorted = sequences; - std::sort(sorted.begin(), sorted.end(), - [](const SequenceEntry& a, const SequenceEntry& b) { - return std::stof(a.start_time) < std::stof(b.start_time); - }); - - return {max_time, sorted}; -} - -// Analyze effect stacking depth across the timeline -void analyze_effect_depth(const std::vector& sequences, - const std::string& demo_end_time, - float sample_rate = 10.0f) { - TimelineMetrics metrics = analyze_timeline(sequences, demo_end_time); - float max_time = metrics.max_time; - - if (max_time <= 0.0f) { - std::cout << "\n=== Effect Depth Analysis ===\n"; - std::cout << "No effects found in timeline.\n"; - return; - } - - // Build list of all effects with absolute times - struct ActiveEffect { - std::string name; - float start_time; - float end_time; - int priority; - }; - std::vector all_effects; - - for (const auto& seq : sequences) { - float seq_start = std::stof(seq.start_time); - float seq_end = get_sequence_end(seq); - - for (const auto& eff : seq.effects) { - float eff_start = seq_start + std::stof(eff.start); - float eff_end = seq_start + std::stof(eff.end); - - // Clamp effect end to sequence end if specified - if (seq.end_time != "-1.0") { - eff_end = std::min(eff_end, seq_end); - } - - all_effects.push_back( - {eff.class_name, eff_start, eff_end, std::stoi(eff.priority)}); - } - } - - // Sample timeline at regular intervals - const float dt = 1.0f / sample_rate; - int max_depth = 0; - float max_depth_time = 0.0f; - std::vector depth_histogram(21, 0); // Track depths 0-20+ - - struct PeakInfo { - float time; - int depth; - std::vector effects; - }; - std::vector peaks; - - for (float t = 0.0f; t <= max_time; t += dt) { - int depth = 0; - std::vector active_effects; - - for (const auto& eff : all_effects) { - if (t >= eff.start_time && t < eff.end_time) { - depth++; - active_effects.push_back(eff.name); - } - } - - // Update max depth - if (depth > max_depth) { - max_depth = depth; - max_depth_time = t; - } - - // Record histogram - int hist_idx = std::min(depth, 20); - depth_histogram[hist_idx]++; - - // Record peaks (>5 effects) - if (depth > 5 && (peaks.empty() || t - peaks.back().time > 0.5f)) { - peaks.push_back({t, depth, active_effects}); - } - } - - // Print analysis report - std::cout << "\n=== Effect Depth Analysis ===\n"; - std::cout << "Timeline duration: " << max_time << "s\n"; - std::cout << "Total effects: " << all_effects.size() << "\n"; - std::cout << "Sample rate: " << sample_rate << " Hz (every " << dt << "s)\n"; - std::cout << "\n"; - - std::cout << "Max concurrent effects: " << max_depth - << " at t=" << max_depth_time << "s\n"; - std::cout << "\n"; - - // Print histogram - std::cout << "Effect Depth Distribution:\n"; - std::cout << "Depth | Count | Percentage | Bar\n"; - std::cout << "------|---------|------------|" - "-------------------------------------\n"; - - int total_samples = - std::accumulate(depth_histogram.begin(), depth_histogram.end(), 0); - - for (size_t i = 0; i < depth_histogram.size(); ++i) { - if (depth_histogram[i] == 0 && i > max_depth) - continue; - - float percentage = 100.0f * depth_histogram[i] / total_samples; - int bar_length = (int)(percentage / 2.0f); // Scale to ~50 chars max - - std::cout << std::setw(5) << (i < 20 ? std::to_string(i) : "20+") << " | " - << std::setw(7) << depth_histogram[i] << " | " << std::setw(9) - << std::fixed << std::setprecision(1) << percentage << "% | "; - - for (int j = 0; j < bar_length; ++j) { - std::cout << "โ–ˆ"; - } - std::cout << "\n"; - } - - // Print bottleneck warnings - if (max_depth > 5) { - std::cout << "\nโš  WARNING: Performance bottlenecks detected!\n"; - std::cout << "Found " << peaks.size() - << " time periods with >5 effects:\n\n"; - - int peak_count = 0; - for (const auto& peak : peaks) { - if (peak_count >= 10) - break; // Limit output - - std::cout << " t=" << std::fixed << std::setprecision(2) << peak.time - << "s: " << peak.depth << " effects [ "; - - // Show first 5 effects - for (size_t i = 0; i < std::min(size_t(5), peak.effects.size()); ++i) { - std::cout << peak.effects[i]; - if (i < peak.effects.size() - 1) - std::cout << ", "; - } - if (peak.effects.size() > 5) { - std::cout << " +" << (peak.effects.size() - 5) << " more"; - } - std::cout << " ]\n"; - - peak_count++; - } - - if (peaks.size() > 10) { - std::cout << " ... and " << (peaks.size() - 10) << " more peaks\n"; - } - } else { - std::cout << "\nโœ“ No significant bottlenecks detected (max depth: " - << max_depth << " <= 5)\n"; - } - - std::cout << "\n"; -} - -// Generate ASCII Gantt chart for timeline visualization -void generate_gantt_chart(const std::string& output_file, - const std::vector& sequences, - float bpm, const std::string& demo_end_time) { - std::ofstream out(output_file); - if (!out.is_open()) { - std::cerr << "Warning: Could not open Gantt chart output file: " - << output_file << "\n"; - return; - } - - TimelineMetrics metrics = analyze_timeline(sequences, demo_end_time); - float max_time = metrics.max_time; - - // Chart configuration - const int chart_width = 100; - const float time_scale = chart_width / max_time; - - out << "Demo Timeline Gantt Chart\n"; - out << "=====================================================================" - "=========\n"; - out << "BPM: " << bpm << ", Duration: " << max_time << "s"; - if (!demo_end_time.empty()) { - out << " (explicit end)"; - } - out << "\n\n"; - - // Time axis header with adaptive tick interval - const int tick_interval = calculate_tick_interval(max_time); - out << "Time (s): "; - for (int i = 0; i <= (int)max_time; i += tick_interval) { - out << i; - int spacing = (i < 10) ? 4 : (i < 100) ? 3 : 2; - if (i + tick_interval <= max_time) { - for (int j = 0; j < spacing; ++j) - out << " "; - } - } - out << "\n"; - out << " "; - for (int i = 0; i < chart_width; ++i) { - // Check if this column aligns with any tick mark - bool is_tick = false; - for (int t = 0; t <= (int)max_time; t += tick_interval) { - if (std::abs(i - (int)(t * time_scale)) < 1) { - is_tick = true; - break; - } - } - out << (is_tick ? "|" : "-"); - } - out << "\n\n"; - - // Draw sequences and effects - for (size_t seq_idx = 0; seq_idx < metrics.sorted_sequences.size(); - ++seq_idx) { - const auto& seq = metrics.sorted_sequences[seq_idx]; - float seq_start = std::stof(seq.start_time); - float seq_end = get_sequence_end(seq); - - // Draw sequence bar - out << "SEQ@" << seq_start << "s"; - if (!seq.name.empty()) { - out << " \"" << seq.name << "\""; - } - out << " [pri=" << seq.priority << "]"; - if (seq.end_time != "-1.0") { - out << " [END=" << seq_end << "s]"; - } - out << "\n"; - - int start_col = (int)(seq_start * time_scale); - int end_col = (int)(seq_end * time_scale); - out << " "; - for (int i = 0; i < chart_width; ++i) { - if (i >= start_col && i < end_col) - out << "โ–ˆ"; - else - out << " "; - } - out << " (" << seq_start << "-" << seq_end << "s)\n"; - - // Draw effects within sequence - for (const auto& eff : seq.effects) { - float eff_start = seq_start + std::stof(eff.start); - float eff_end = seq_start + std::stof(eff.end); - - // Truncate if sequence has explicit end time - if (seq.end_time != "-1.0") { - eff_end = std::min(eff_end, seq_end); - } - - out << " " << eff.class_name << " [pri=" << eff.priority << "]"; - if (eff_end < eff_start) { - out << " *** INVALID TIME RANGE ***"; - } - out << "\n"; - out << " "; - - int eff_start_col = (int)(eff_start * time_scale); - int eff_end_col = (int)(eff_end * time_scale); - - for (int i = 0; i < chart_width; ++i) { - if (i >= eff_start_col && i < eff_end_col) { - out << "โ–“"; - } else if (i >= start_col && i < end_col) { - out << "ยท"; // Show sequence background - } else { - out << " "; - } - } - out << " (" << eff_start << "-" << eff_end << "s)\n"; - } - - // Add separator between sequences - if (seq_idx < metrics.sorted_sequences.size() - 1) { - out << " "; - for (int i = 0; i < chart_width; ++i) { - out << "โ”€"; - } - out << "\n\n"; - } else { - out << "\n"; - } - } - - out << "=====================================================================" - "=========\n"; - out << "Legend: โ–ˆ Sequence โ–“ Effect ยท Sequence background\n"; - out << "Priority: Higher numbers render later (on top)\n"; - - out.close(); - std::cout << "Gantt chart written to: " << output_file << "\n"; -} - -// Generate HTML/SVG Gantt chart for timeline visualization -void generate_gantt_html(const std::string& output_file, - const std::vector& sequences, float bpm, - const std::string& demo_end_time) { - std::ofstream out(output_file); - if (!out.is_open()) { - std::cerr << "Warning: Could not open HTML Gantt output file: " - << output_file << "\n"; - return; - } - - TimelineMetrics metrics = analyze_timeline(sequences, demo_end_time); - float max_time = metrics.max_time; - - const int svg_width = 1400; - const int row_height = 30; - const int effect_height = 20; - const int margin_left = 250; - const int margin_top = 60; - const float time_scale = (svg_width - margin_left - 50) / max_time; - - // Count total rows needed - int total_rows = 0; - for (const auto& seq : sequences) { - total_rows += 1 + seq.effects.size(); // 1 for sequence + N for effects - } - - const int svg_height = margin_top + total_rows * row_height + 40; - - out << "\n\n\n"; - out << "\n"; - out << "Demo Timeline - BPM " << bpm << "\n"; - out << "\n\n\n"; - - out << "

Demo Timeline Gantt Chart

\n"; - out << "
\n"; - out << "BPM: " << bpm << " | "; - out << "Duration: " << max_time << "s"; - if (!demo_end_time.empty()) { - out << " (explicit end)"; - } - out << " | Sequences: " << sequences.size() << "\n"; - out << "
\n\n"; - - out << "\n"; - - // Draw time axis with adaptive tick interval - const int tick_interval = calculate_tick_interval(max_time); - out << " \n"; - out << " \n"; - - for (int t = 0; t <= (int)max_time; t += tick_interval) { - int x = margin_left + (int)(t * time_scale); - out << " \n"; - out << " " << t - << "s\n"; - // Draw vertical time markers - out << " \n"; - } - - // Draw sequences and effects - int y_offset = margin_top; - for (size_t seq_idx = 0; seq_idx < metrics.sorted_sequences.size(); - ++seq_idx) { - const auto& seq = metrics.sorted_sequences[seq_idx]; - float seq_start = std::stof(seq.start_time); - float seq_end = get_sequence_end(seq); - - int x1 = margin_left + (int)(seq_start * time_scale); - int x2 = margin_left + (int)(seq_end * time_scale); - - // Draw sequence bar - out << " \n"; - out << " \n"; - out << " SEQ@" << seq_start << "s"; - if (!seq.name.empty()) { - out << " \"" << seq.name << "\""; - } - out << " [pri=" << seq.priority << "] (" << seq_start << "-" << seq_end - << "s)\n"; - out << " \n"; - - // Draw sequence label - out << " SEQ@" << seq_start << "s"; - if (!seq.name.empty()) { - out << " \"" << seq.name << "\""; - } - out << " [pri=" << seq.priority << "]\n"; - - y_offset += row_height; - - // Draw effects - for (const auto& eff : seq.effects) { - float eff_start = seq_start + std::stof(eff.start); - float eff_end = seq_start + std::stof(eff.end); - - if (seq.end_time != "-1.0") { - eff_end = std::min(eff_end, seq_end); - } - - bool invalid = eff_end < eff_start; - int eff_x1 = margin_left + (int)(eff_start * time_scale); - int eff_x2 = margin_left + (int)(eff_end * time_scale); - int eff_width = std::max(2, eff_x2 - eff_x1); - - out << " \n"; - out << " " << eff.class_name << " [pri=" << eff.priority - << "] (" << eff_start << "-" << eff_end << "s)" - << (invalid ? " *** INVALID TIME RANGE ***" : "") << "\n"; - out << " \n"; - - out << " " << eff.class_name - << " [pri=" << eff.priority << "]" << (invalid ? " โš " : "") - << "\n"; - - y_offset += row_height; - } - - // Add separator between sequences - if (seq_idx < metrics.sorted_sequences.size() - 1) { - out << " \n"; - out << " \n"; - y_offset += 10; // Extra spacing after separator - } - } - - // Legend - out << " \n"; - out << " \n"; - out << " Sequence\n"; - out << " \n"; - out << " Effect\n"; - out << " \n"; - out << " Invalid Time Range\n"; - - out << "\n"; - out << "
\n"; - out << "Tip: Hover over bars to see details. "; - out << "Higher priority numbers render later (on top).\n"; - out << "
\n"; - out << "\n\n"; - - out.close(); - std::cout << "HTML Gantt chart written to: " << output_file << "\n"; -} - -// Convert beat notation to time in seconds -// Supports: "64b" or "64" (beats), "32.0s" or "32.0" with decimal point -// (seconds) -std::string convert_to_time(const std::string& value, float bpm) { - std::string val = value; - - // Check for explicit 's' suffix (seconds) - return as-is - if (!val.empty() && val.back() == 's') { - val.pop_back(); - return val; - } - - // Check for explicit 'b' suffix (beats) - strip and convert - if (!val.empty() && val.back() == 'b') { - val.pop_back(); - } - - // DEFAULT: All numbers (with or without 'b' suffix) are beats - float beat = std::stof(val); - float time = beat * 60.0f / bpm; - return std::to_string(time); -} - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] - << " [output.cc] [--gantt=] " - "[--gantt-html=] [--analyze]\n"; - std::cerr << "Examples:\n"; - std::cerr << " " << argv[0] - << " assets/demo.seq src/generated/timeline.cc\n"; - std::cerr << " " << argv[0] << " assets/demo.seq --gantt=timeline.txt\n"; - std::cerr << " " << argv[0] - << " assets/demo.seq --gantt-html=timeline.html\n"; - std::cerr << " " << argv[0] << " assets/demo.seq --analyze\n"; - std::cerr << " " << argv[0] - << " assets/demo.seq timeline.cc --gantt=timeline.txt " - "--gantt-html=timeline.html\n"; - std::cerr << "\nOptions:\n"; - std::cerr << " --analyze Analyze effect stacking depth and " - "identify bottlenecks\n"; - std::cerr << "\nIf output.cc is omitted, only validation and Gantt " - "generation are performed.\n"; - return 1; - } - - std::string output_cc = ""; - std::string gantt_output = ""; - std::string gantt_html_output = ""; - bool analyze_depth = false; - - // Parse command line arguments - for (int i = 2; i < argc; ++i) { - std::string arg = argv[i]; - if (arg.rfind("--gantt=", 0) == 0) { - gantt_output = arg.substr(8); - } else if (arg.rfind("--gantt-html=", 0) == 0) { - gantt_html_output = arg.substr(13); - } else if (arg == "--analyze") { - analyze_depth = true; - } else if (output_cc.empty() && arg[0] != '-') { - output_cc = arg; - } - } - - std::ifstream in_file(argv[1]); - if (!in_file.is_open()) { - std::cerr << "Error: Could not open input file " << argv[1] << "\n"; - return 1; - } - - std::vector sequences; - SequenceEntry* current_seq = nullptr; - float bpm = 120.0f; // Default BPM - std::string demo_end_time = ""; // Demo end time (optional) - - std::string line; - int line_num = 0; - while (std::getline(in_file, line)) { - ++line_num; - std::string trimmed = trim(line); - if (trimmed.empty()) - continue; - - // Parse BPM from comment - if (trimmed[0] == '#') { - std::stringstream ss(trimmed); - std::string hash, keyword; - ss >> hash >> keyword; - if (keyword == "BPM") { - ss >> bpm; - std::cout << "Using BPM: " << bpm << "\n"; - } - continue; - } - - std::stringstream ss(trimmed); - std::string command; - ss >> command; - - if (command == "SEQUENCE") { - std::string start, priority; - if (!(ss >> start >> priority)) { - std::cerr << "Error line " << line_num - << ": SEQUENCE requires \n"; - return 1; - } - // Convert beat notation to time - std::string start_time = convert_to_time(start, bpm); - - // Check for optional "name" and [end_time] - std::string end_time_str = "-1.0"; // Default: no explicit end - std::string seq_name = ""; // Default: no name - - // Read remaining tokens - std::string rest_of_line; - std::getline(ss, rest_of_line); - std::stringstream rest_ss(rest_of_line); - std::string token; - - while (rest_ss >> token) { - if (token.front() == '"') { - // Name in quotes: read until closing quote - std::string name_part = token.substr(1); // Remove opening quote - if (name_part.back() == '"') { - // Complete name in single token - name_part.pop_back(); // Remove closing quote - seq_name = name_part; - } else { - // Multi-word name: read until closing quote - seq_name = name_part; - while (rest_ss >> token) { - if (token.back() == '"') { - token.pop_back(); // Remove closing quote - seq_name += " " + token; - break; - } - seq_name += " " + token; - } - } - } else if (token.front() == '[' && token.back() == ']') { - // End time in brackets [time] - std::string time_value = token.substr(1, token.size() - 2); - end_time_str = convert_to_time(time_value, bpm); - } else { - std::cerr << "Error line " << line_num << ": Unexpected token '" - << token << "'. Expected \"name\" or [end_time]\n"; - return 1; - } - } - - sequences.push_back({start_time, priority, end_time_str, seq_name, {}}); - current_seq = &sequences.back(); - } else if (command == "EFFECT") { - if (!current_seq) { - std::cerr << "Error line " << line_num - << ": EFFECT found outside of SEQUENCE\n"; - return 1; - } - std::string priority_mod, class_name, start, end; - if (!(ss >> priority_mod >> class_name >> start >> end)) { - std::cerr << "Error line " << line_num - << ": EFFECT requires <+|=|-> \n"; - return 1; - } - - // Validate priority modifier - if (priority_mod != "+" && priority_mod != "=" && priority_mod != "-") { - std::cerr << "Error line " << line_num - << ": Priority modifier must be '+', '=', or '-', got: " - << priority_mod << "\n"; - return 1; - } - - // Calculate priority based on modifier and sequence state - static int current_priority = 0; - static bool first_in_sequence = true; - static const SequenceEntry* last_seq = nullptr; - - // Reset priority tracking for new sequence - if (current_seq != last_seq) { - current_priority = 0; - first_in_sequence = true; - last_seq = current_seq; - } - - // Handle first effect in sequence - if (first_in_sequence) { - if (priority_mod == "-") { - current_priority = -1; // Background layer - } else { - current_priority = 0; // Default start (+ or =) - } - first_in_sequence = false; - } else { - // Update priority based on modifier for subsequent effects - if (priority_mod == "+") { - current_priority++; - } else if (priority_mod == "-") { - current_priority--; - } - // '=' keeps current_priority unchanged - } - - std::string priority = std::to_string(current_priority); - - // Convert beat notation to time - std::string start_time = convert_to_time(start, bpm); - std::string end_time = convert_to_time(end, bpm); - - // Capture remaining args (but strip inline comments) - std::string rest_of_line; - std::getline(ss, rest_of_line); // Read rest of line - // Strip inline comments (everything from '#' onwards) - size_t comment_pos = rest_of_line.find('#'); - if (comment_pos != std::string::npos) { - rest_of_line = rest_of_line.substr(0, comment_pos); - } - // Remove leading/trailing whitespace - rest_of_line = trim(rest_of_line); - - // Parse parameters from rest of line - std::vector> params; - std::string extra_args = ""; - if (!rest_of_line.empty()) { - params = parse_parameters(rest_of_line); - // Keep extra_args for backward compatibility (if no key=value pairs - // found) - if (params.empty()) { - extra_args = ", " + rest_of_line; - } - } - - current_seq->effects.push_back( - {class_name, start_time, end_time, priority, extra_args, params}); - } else { - std::cerr << "Error line " << line_num << ": Unknown command '" << command - << "'\n"; - return 1; - } - } - - // Calculate demo end time from maximum effect end time - float max_end_time = 0.0f; - for (const auto& seq : sequences) { - float seq_start = std::stof(seq.start_time); - for (const auto& eff : seq.effects) { - max_end_time = std::max(max_end_time, seq_start + std::stof(eff.end)); - } - } - demo_end_time = std::to_string(max_end_time); - std::cout << "Demo end time (calculated): " << demo_end_time << "s\n"; - - // Sort sequences by start time (primary) then priority (secondary) - std::sort(sequences.begin(), sequences.end(), - [](const SequenceEntry& a, const SequenceEntry& b) { - float a_start = std::stof(a.start_time); - float b_start = std::stof(b.start_time); - if (a_start != b_start) return a_start < b_start; - return std::stoi(a.priority) < std::stoi(b.priority); - }); - - // Sort effects within each sequence by priority - for (auto& seq : sequences) { - std::sort(seq.effects.begin(), seq.effects.end(), - [](const EffectEntry& a, const EffectEntry& b) { - return std::stoi(a.priority) < std::stoi(b.priority); - }); - } - - // Validate: detect priority collisions among post-process effects - for (size_t seq_idx = 0; seq_idx < sequences.size(); ++seq_idx) { - const auto& seq = sequences[seq_idx]; - std::map> priority_map; - - // Group post-process effects by priority - for (const auto& eff : seq.effects) { - if (is_post_process_effect(eff.class_name)) { - int prio = std::stoi(eff.priority); - priority_map[prio].push_back(eff.class_name); - } - } - - // Check for collisions - for (const auto& [prio, effects] : priority_map) { - if (effects.size() > 1) { - std::cerr << "Warning: Priority collision detected in sequence at time " - << seq.start_time << "s\n"; - std::cerr << " Multiple post-process effects have priority " << prio - << ":\n"; - for (const auto& effect : effects) { - std::cerr << " - " << effect << "\n"; - } - std::cerr << " This may cause unexpected render order. Consider " - "adjusting priorities.\n"; - } - } - } - - // Validate: detect cross-sequence priority collisions for concurrent - // sequences - std::map> time_groups; - for (size_t i = 0; i < sequences.size(); ++i) { - time_groups[sequences[i].start_time].push_back(i); - } - - for (const auto& [start_time, seq_indices] : time_groups) { - if (seq_indices.size() > 1) { - // Multiple sequences start at the same time - std::map>> - cross_priority_map; - - for (size_t seq_idx : seq_indices) { - const auto& seq = sequences[seq_idx]; - for (const auto& eff : seq.effects) { - if (is_post_process_effect(eff.class_name)) { - int prio = std::stoi(eff.priority); - cross_priority_map[prio].push_back({eff.class_name, seq_idx}); - } - } - } - - // Check for cross-sequence collisions - for (const auto& [prio, effects] : cross_priority_map) { - if (effects.size() > 1) { - std::cerr << "Warning: Cross-sequence priority collision at time " - << start_time << "s\n"; - std::cerr << " Multiple post-process effects across sequences have " - "priority " - << prio << ":\n"; - for (const auto& [effect, seq_idx] : effects) { - std::cerr << " - " << effect << " (sequence #" << seq_idx - << ")\n"; - } - std::cerr << " Post-process effects from different sequences at the " - "same time will be\n"; - std::cerr - << " merged into a single render chain. Consider adjusting " - "priorities to clarify order.\n"; - } - } - } - } - - // Generate C++ code if output file is specified - if (!output_cc.empty()) { - std::ofstream out_file(output_cc); - if (!out_file.is_open()) { - std::cerr << "Error: Could not open output file " << output_cc << "\n"; - return 1; - } - - out_file << "// Auto-generated by seq_compiler. Do not edit.\n"; - out_file << "#include \"gpu/demo_effects.h\"\n"; - out_file << "#include \"gpu/effect.h\"\n\n"; - - // Generate demo duration function - if (!demo_end_time.empty()) { - out_file << "float GetDemoDuration() {\n"; - out_file << " return " << demo_end_time << "f;\n"; - out_file << "}\n\n"; - } else { - out_file << "float GetDemoDuration() {\n"; - out_file << " return -1.0f; // No end time specified\n"; - out_file << "}\n\n"; - } - - out_file << "void LoadTimeline(MainSequence& main_seq, const GpuContext& " - "ctx) {\n"; - - for (const SequenceEntry& seq : sequences) { - out_file << " {\n"; - out_file << " auto seq = std::make_shared();\n"; - // Set sequence end time if specified - if (seq.end_time != "-1.0") { - out_file << " seq->set_end_time(" << seq.end_time << "f);\n"; - } - for (const EffectEntry& eff : seq.effects) { - // Check if effect has parameters - if (!eff.params.empty() && eff.class_name == "FlashEffect") { - // Generate parameter struct initialization for FlashEffect - out_file << " {\n"; - out_file << " FlashEffectParams p;\n"; - - for (const auto& [key, value] : eff.params) { - if (key == "color") { - // Parse color as r,g,b - std::istringstream color_ss(value); - std::string r, g, b; - std::getline(color_ss, r, ','); - std::getline(color_ss, g, ','); - std::getline(color_ss, b, ','); - out_file << " p.color[0] = " << r << "f;\n"; - out_file << " p.color[1] = " << g << "f;\n"; - out_file << " p.color[2] = " << b << "f;\n"; - } else if (key == "decay") { - out_file << " p.decay_rate = " << value << "f;\n"; - } else if (key == "threshold") { - out_file << " p.trigger_threshold = " << value << "f;\n"; - } - } - - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - out_file << " }\n"; - } else if (!eff.params.empty() && - eff.class_name == "ChromaAberrationEffect") { - // Generate parameter struct initialization for ChromaAberrationEffect - out_file << " {\n"; - out_file << " ChromaAberrationParams p;\n"; - - for (const auto& [key, value] : eff.params) { - if (key == "offset") { - out_file << " p.offset_scale = " << value << "f;\n"; - } else if (key == "angle") { - out_file << " p.angle = " << value << "f;\n"; - } - } - - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - out_file << " }\n"; - } else if (!eff.params.empty() && - eff.class_name == "GaussianBlurEffect") { - // Generate parameter struct initialization for GaussianBlurEffect - out_file << " {\n"; - out_file << " GaussianBlurParams p;\n"; - - for (const auto& [key, value] : eff.params) { - if (key == "strength") { - out_file << " p.strength = " << value << "f;\n"; - } - } - - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - out_file << " }\n"; - } else if (!eff.params.empty() && eff.class_name == "VignetteEffect") { - // Generate parameter struct initialization for VignetteEffect - out_file << " {\n"; - out_file << " VignetteParams p;\n"; - - for (const auto& [key, value] : eff.params) { - if (key == "radius") { - out_file << " p.radius = " << value << "f;\n"; - } else if (key == "softness") { - out_file << " p.softness = " << value << "f;\n"; - } - } - - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - out_file << " }\n"; - } else if (!eff.params.empty() && eff.class_name == "CNNEffect") { - // Generate parameter struct initialization for CNNEffect - // If layers>1, expand into multiple chained effect instances - int num_layers = 1; - float blend_amount = 1.0f; - - for (const auto& [key, value] : eff.params) { - if (key == "layers") { - num_layers = std::stoi(value); - } else if (key == "blend") { - blend_amount = std::stof(value); - } - } - - // Generate one effect per layer - for (int layer = 0; layer < num_layers; ++layer) { - out_file << " {\n"; - out_file << " CNNEffectParams p;\n"; - out_file << " p.layer_index = " << layer << ";\n"; - out_file << " p.total_layers = " << num_layers << ";\n"; - // Only apply blend_amount on the last layer - if (layer == num_layers - 1) { - out_file << " p.blend_amount = " << blend_amount << "f;\n"; - } else { - out_file << " p.blend_amount = 1.0f;\n"; - } - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << (std::stoi(eff.priority) + layer) - << ");\n"; - out_file << " }\n"; - } - } else if (!eff.params.empty() && eff.class_name == "CNNv2Effect") { - // Generate parameter struct initialization for CNNv2Effect - out_file << " {\n"; - out_file << " CNNv2EffectParams p;\n"; - - for (const auto& [key, value] : eff.params) { - if (key == "blend") { - out_file << " p.blend_amount = " << value << "f;\n"; - } - } - - out_file << " seq->add_effect(std::make_shared<" - << eff.class_name << ">(ctx, p), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - out_file << " }\n"; - } else { - // No parameters or unsupported effect - use default constructor - out_file << " seq->add_effect(std::make_shared<" << eff.class_name - << ">(ctx" << eff.extra_args << "), " << eff.start << "f, " - << eff.end << "f, " << eff.priority << ");\n"; - } - } - out_file << " main_seq.add_sequence(seq, " << seq.start_time << "f, " - << seq.priority << ");\n"; - out_file << " }\n"; - } - - out_file << "}\n"; - out_file.close(); - - std::cout << "Successfully generated timeline with " << sequences.size() - << " sequences.\n"; - } else { - std::cout << "Validation successful: " << sequences.size() << " sequences, " - << (demo_end_time.empty() ? "no" : "explicit") << " end time.\n"; - } - - // Generate Gantt charts if requested - if (!gantt_output.empty()) { - generate_gantt_chart(gantt_output, sequences, bpm, demo_end_time); - } - if (!gantt_html_output.empty()) { - generate_gantt_html(gantt_html_output, sequences, bpm, demo_end_time); - } - - // Analyze effect stacking depth if requested - if (analyze_depth) { - analyze_effect_depth(sequences, demo_end_time); - } - - return 0; -} diff --git a/tools/timeline_editor/README.md b/tools/timeline_editor/README.md index 66e39bd..4d41cd7 100644 --- a/tools/timeline_editor/README.md +++ b/tools/timeline_editor/README.md @@ -5,7 +5,7 @@ Interactive web-based editor for `timeline.seq` files. ## Features - ๐Ÿ“‚ Load/save `timeline.seq` files -- ๐Ÿ“Š Visual Gantt-style timeline with sticky time markers (beat-based) +- ๐Ÿ“Š Visual timeline with sticky time markers (beat-based) - ๐ŸŽฏ Drag & drop sequences and effects - ๐ŸŽฏ Resize effects with handles - ๐Ÿ“ฆ Collapsible sequences (double-click to collapse) -- cgit v1.2.3