summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/DemoTools.cmake14
-rw-r--r--doc/COMPLETED.md2
-rw-r--r--tools/seq_compiler.cc1146
-rw-r--r--tools/timeline_editor/README.md2
4 files changed, 4 insertions, 1160 deletions
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 $<TARGET_FILE:seq_compiler>)
- 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 <algorithm>
-#include <cmath>
-#include <fstream>
-#include <iomanip>
-#include <iostream>
-#include <map>
-#include <numeric>
-#include <sstream>
-#include <string>
-#include <vector>
-
-struct EffectEntry {
- std::string class_name;
- std::string start;
- std::string end;
- std::string priority;
- std::string extra_args;
- std::vector<std::pair<std::string, std::string>> 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<EffectEntry> 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<std::pair<std::string, std::string>>
-parse_parameters(const std::string& args) {
- std::vector<std::pair<std::string, std::string>> 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<std::string> 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<SequenceEntry> 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<SequenceEntry>& 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<SequenceEntry> 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<SequenceEntry>& 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<ActiveEffect> 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<int> depth_histogram(21, 0); // Track depths 0-20+
-
- struct PeakInfo {
- float time;
- int depth;
- std::vector<std::string> effects;
- };
- std::vector<PeakInfo> peaks;
-
- for (float t = 0.0f; t <= max_time; t += dt) {
- int depth = 0;
- std::vector<std::string> 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<SequenceEntry>& 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<SequenceEntry>& 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 << "<!DOCTYPE html>\n<html>\n<head>\n";
- out << "<meta charset=\"UTF-8\">\n";
- out << "<title>Demo Timeline - BPM " << bpm << "</title>\n";
- out << "<style>\n";
- out << "body { font-family: 'Courier New', monospace; margin: 20px; "
- "background: #1e1e1e; color: #d4d4d4; }\n";
- out << "h1 { color: #569cd6; }\n";
- out << ".info { background: #252526; padding: 10px; border-radius: 4px; "
- "margin: 10px 0; }\n";
- out << "svg { background: #252526; border-radius: 4px; }\n";
- out << ".sequence-bar { fill: #3a3a3a; stroke: #569cd6; stroke-width: 2; }\n";
- out << ".effect-bar { fill: #4ec9b0; opacity: 0.8; stroke: #2a7a6a; "
- "stroke-width: 1; }\n";
- out << ".effect-bar.invalid { fill: #f48771; stroke: #d16969; }\n";
- out << ".label { fill: #d4d4d4; font-size: 12px; }\n";
- out << ".label.effect { fill: #cccccc; font-size: 11px; }\n";
- out << ".axis-line { stroke: #6a6a6a; stroke-width: 1; }\n";
- out << ".axis-label { fill: #858585; font-size: 10px; }\n";
- out << ".time-marker { stroke: #444444; stroke-width: 1; stroke-dasharray: "
- "2,2; }\n";
- out << "rect:hover { opacity: 1; }\n";
- out << "title { font-size: 11px; }\n";
- out << "</style>\n</head>\n<body>\n";
-
- out << "<h1>Demo Timeline Gantt Chart</h1>\n";
- out << "<div class=\"info\">\n";
- out << "<strong>BPM:</strong> " << bpm << " | ";
- out << "<strong>Duration:</strong> " << max_time << "s";
- if (!demo_end_time.empty()) {
- out << " (explicit end)";
- }
- out << " | <strong>Sequences:</strong> " << sequences.size() << "\n";
- out << "</div>\n\n";
-
- out << "<svg width=\"" << svg_width << "\" height=\"" << svg_height
- << "\" xmlns=\"http://www.w3.org/2000/svg\">\n";
-
- // Draw time axis with adaptive tick interval
- const int tick_interval = calculate_tick_interval(max_time);
- out << " <!-- Time axis -->\n";
- out << " <line x1=\"" << margin_left << "\" y1=\"" << margin_top - 10
- << "\" x2=\"" << (svg_width - 50) << "\" y2=\"" << margin_top - 10
- << "\" class=\"axis-line\"/>\n";
-
- for (int t = 0; t <= (int)max_time; t += tick_interval) {
- int x = margin_left + (int)(t * time_scale);
- out << " <line x1=\"" << x << "\" y1=\"" << margin_top - 15 << "\" x2=\""
- << x << "\" y2=\"" << margin_top - 5 << "\" class=\"axis-line\"/>\n";
- out << " <text x=\"" << x << "\" y=\"" << margin_top - 20
- << "\" class=\"axis-label\" text-anchor=\"middle\">" << t
- << "s</text>\n";
- // Draw vertical time markers
- out << " <line x1=\"" << x << "\" y1=\"" << margin_top << "\" x2=\"" << x
- << "\" y2=\"" << svg_height - 20 << "\" class=\"time-marker\"/>\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 << " <!-- Sequence -->\n";
- out << " <rect x=\"" << x1 << "\" y=\"" << y_offset << "\" width=\""
- << (x2 - x1) << "\" height=\"" << row_height
- << "\" class=\"sequence-bar\">\n";
- out << " <title>SEQ@" << seq_start << "s";
- if (!seq.name.empty()) {
- out << " \"" << seq.name << "\"";
- }
- out << " [pri=" << seq.priority << "] (" << seq_start << "-" << seq_end
- << "s)</title>\n";
- out << " </rect>\n";
-
- // Draw sequence label
- out << " <text x=\"10\" y=\"" << (y_offset + row_height / 2 + 4)
- << "\" class=\"label\">SEQ@" << seq_start << "s";
- if (!seq.name.empty()) {
- out << " \"" << seq.name << "\"";
- }
- out << " [pri=" << seq.priority << "]</text>\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 << " <rect x=\"" << eff_x1 << "\" y=\"" << (y_offset + 5)
- << "\" width=\"" << eff_width << "\" height=\"" << effect_height
- << "\" class=\"effect-bar" << (invalid ? " invalid" : "") << "\">\n";
- out << " <title>" << eff.class_name << " [pri=" << eff.priority
- << "] (" << eff_start << "-" << eff_end << "s)"
- << (invalid ? " *** INVALID TIME RANGE ***" : "") << "</title>\n";
- out << " </rect>\n";
-
- out << " <text x=\"20\" y=\"" << (y_offset + effect_height)
- << "\" class=\"label effect\">" << eff.class_name
- << " [pri=" << eff.priority << "]" << (invalid ? " โš " : "")
- << "</text>\n";
-
- y_offset += row_height;
- }
-
- // Add separator between sequences
- if (seq_idx < metrics.sorted_sequences.size() - 1) {
- out << " <!-- Separator -->\n";
- out << " <line x1=\"" << margin_left << "\" y1=\"" << (y_offset + 5)
- << "\" x2=\"" << (svg_width - 50) << "\" y2=\"" << (y_offset + 5)
- << "\" style=\"stroke:#444444; stroke-width:1; "
- "stroke-dasharray:4,2;\"/>\n";
- y_offset += 10; // Extra spacing after separator
- }
- }
-
- // Legend
- out << " <!-- Legend -->\n";
- out << " <rect x=\"10\" y=\"" << (svg_height - 15)
- << "\" width=\"20\" height=\"10\" class=\"sequence-bar\"/>\n";
- out << " <text x=\"35\" y=\"" << (svg_height - 7)
- << "\" class=\"axis-label\">Sequence</text>\n";
- out << " <rect x=\"120\" y=\"" << (svg_height - 15)
- << "\" width=\"20\" height=\"10\" class=\"effect-bar\"/>\n";
- out << " <text x=\"145\" y=\"" << (svg_height - 7)
- << "\" class=\"axis-label\">Effect</text>\n";
- out << " <rect x=\"220\" y=\"" << (svg_height - 15)
- << "\" width=\"20\" height=\"10\" class=\"effect-bar invalid\"/>\n";
- out << " <text x=\"245\" y=\"" << (svg_height - 7)
- << "\" class=\"axis-label\">Invalid Time Range</text>\n";
-
- out << "</svg>\n";
- out << "<div class=\"info\">\n";
- out << "<strong>Tip:</strong> Hover over bars to see details. ";
- out << "Higher priority numbers render later (on top).\n";
- out << "</div>\n";
- out << "</body>\n</html>\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]
- << " <input.seq> [output.cc] [--gantt=<file.txt>] "
- "[--gantt-html=<file.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<SequenceEntry> 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 <start> <priority>\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 <+|=|-> <Class> <start> <end>\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<std::pair<std::string, std::string>> 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<int, std::vector<std::string>> 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<std::string, std::vector<size_t>> 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<int, std::vector<std::pair<std::string, size_t>>>
- 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<Sequence>();\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)