From abd1d77d4718a680f17d8ca25d31294d72068acb Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Feb 2026 00:44:11 +0100 Subject: feat: Enhance Gantt charts with sequence names, adaptive ticks, and sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improvements to seq_compiler Gantt chart visualization: - Add optional sequence name support: SEQUENCE ["name"] [end] Names displayed in both ASCII and HTML Gantt charts for better readability - Implement adaptive tick intervals based on demo duration: * ≤5s: 1s intervals * ≤40s: 2s intervals (fixes 32.5s demo from 5s to 2s) * ≤100s: 5s intervals * >100s: 10s+ intervals - Sort sequences by start time in Gantt output for chronological visualization - Add horizontal visual separators between sequences in both ASCII and HTML - Update documentation (SEQUENCE.md) and quick reference (demo.seq) Co-Authored-By: Claude Sonnet 4.5 --- assets/demo.seq | 3 +- doc/SEQUENCE.md | 13 ++++- tools/seq_compiler.cc | 137 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/assets/demo.seq b/assets/demo.seq index 44df234..b30dd68 100644 --- a/assets/demo.seq +++ b/assets/demo.seq @@ -7,11 +7,12 @@ # DOCUMENTATION: See doc/SEQUENCE.md for complete syntax reference # # QUICK REFERENCE: -# SEQUENCE [optional_end] +# SEQUENCE ["optional_name"] [optional_end] # EFFECT <+|=|-> # # Priority modifiers: + (increment), = (same), - (decrement/background) # Time notation: 0b (beats), 0.0 (seconds) +# Optional name: Displayed in Gantt charts (e.g., "Opening Scene") # # VALIDATION & VISUALIZATION: # ./build/seq_compiler assets/demo.seq # Validate only diff --git a/doc/SEQUENCE.md b/doc/SEQUENCE.md index 614339e..7aa951d 100644 --- a/doc/SEQUENCE.md +++ b/doc/SEQUENCE.md @@ -63,7 +63,7 @@ Specifies when the demo should automatically exit (optional). ### SEQUENCE Declaration ``` -SEQUENCE [optional_end] +SEQUENCE ["optional_name"] [optional_end] EFFECT [constructor_args...] ``` @@ -72,11 +72,22 @@ SEQUENCE [optional_end] - `priority`: Render order between sequences (higher = rendered later/on top) - Use 0-9 for scene effects - Use 10+ for post-processing +- `"optional_name"`: Optional human-readable name in quotes (e.g., `"Opening Scene"`) + - If specified, the name will be displayed in Gantt charts for better readability + - Helps identify sequences when visualizing complex timelines - `[optional_end]`: Optional end time in brackets (e.g., `[30.0]`) - If specified, all effects in the sequence will be forcefully ended at this time - Time is relative to the sequence start - If omitted, effects run until their individual end times +**Examples:** +``` +SEQUENCE 0 0 # Basic sequence (no name, no explicit end) +SEQUENCE 0 0 "Intro" # Named sequence +SEQUENCE 0 0 [5.0] # Sequence with explicit end time +SEQUENCE 0 0 "Action Scene" [10.0] # Named sequence with explicit end time +``` + ### EFFECT Declaration ``` EFFECT <+|=|-> [constructor_args...] diff --git a/tools/seq_compiler.cc b/tools/seq_compiler.cc index b61e895..3931e32 100644 --- a/tools/seq_compiler.cc +++ b/tools/seq_compiler.cc @@ -21,6 +21,7 @@ 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; }; @@ -32,6 +33,15 @@ std::string trim(const std::string& str) { return str.substr(first, (last - first + 1)); } +// 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; +} + // Generate ASCII Gantt chart for timeline visualization void generate_gantt_chart(const std::string& output_file, const std::vector& sequences, @@ -67,25 +77,41 @@ void generate_gantt_chart(const std::string& output_file, } out << "\n\n"; - // Time axis header + // 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 += 5) { + for (int i = 0; i <= (int)max_time; i += tick_interval) { out << i; int spacing = (i < 10) ? 4 : (i < 100) ? 3 : 2; - if (i + 5 <= max_time) { + 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) { - if (i % 5 == 0) out << "|"; - else out << "-"; + // 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"; + // Sort sequences by start time for better readability + std::vector sorted_sequences = sequences; + std::sort(sorted_sequences.begin(), sorted_sequences.end(), + [](const SequenceEntry& a, const SequenceEntry& b) { + return std::stof(a.start_time) < std::stof(b.start_time); + }); + // Draw sequences and effects - for (const auto& seq : sequences) { + for (size_t seq_idx = 0; seq_idx < sorted_sequences.size(); ++seq_idx) { + const auto& seq = sorted_sequences[seq_idx]; float seq_start = std::stof(seq.start_time); float seq_end = seq_start; // Start at sequence start @@ -100,7 +126,11 @@ void generate_gantt_chart(const std::string& output_file, } // Draw sequence bar - out << "SEQ@" << seq_start << "s [pri=" << seq.priority << "]"; + 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]"; } @@ -146,7 +176,17 @@ void generate_gantt_chart(const std::string& output_file, } out << " (" << eff_start << "-" << eff_end << "s)\n"; } - out << "\n"; + + // Add separator between sequences + if (seq_idx < sorted_sequences.size() - 1) { + out << " "; + for (int i = 0; i < chart_width; ++i) { + out << "─"; + } + out << "\n\n"; + } else { + out << "\n"; + } } out << "==============================================================================\n"; @@ -227,13 +267,14 @@ void generate_gantt_html(const std::string& output_file, out << "\n"; - // Draw time axis + // 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 += 5) { + for (int t = 0; t <= (int)max_time; t += tick_interval) { int x = margin_left + (int)(t * time_scale); out << " \n"; } + // Sort sequences by start time for better readability + std::vector sorted_sequences = sequences; + std::sort(sorted_sequences.begin(), sorted_sequences.end(), + [](const SequenceEntry& a, const SequenceEntry& b) { + return std::stof(a.start_time) < std::stof(b.start_time); + }); + // Draw sequences and effects int y_offset = margin_top; - for (const auto& seq : sequences) { + for (size_t seq_idx = 0; seq_idx < sorted_sequences.size(); ++seq_idx) { + const auto& seq = sorted_sequences[seq_idx]; float seq_start = std::stof(seq.start_time); float seq_end = seq_start; // Start at sequence start @@ -268,13 +317,21 @@ void generate_gantt_html(const std::string& output_file, out << " \n"; - out << " SEQ@" << seq_start << "s [pri=" << seq.priority << "] (" + 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 [pri=" << seq.priority << "]\n"; + << "\" class=\"label\">SEQ@" << seq_start << "s"; + if (!seq.name.empty()) { + out << " \"" << seq.name << "\""; + } + out << " [pri=" << seq.priority << "]\n"; y_offset += row_height; @@ -306,6 +363,15 @@ void generate_gantt_html(const std::string& output_file, y_offset += row_height; } + + // Add separator between sequences + if (seq_idx < sorted_sequences.size() - 1) { + out << " \n"; + out << " \n"; + y_offset += 10; // Extra spacing after separator + } } // Legend @@ -441,25 +507,48 @@ int main(int argc, char* argv[]) { // Convert beat notation to time std::string start_time = convert_to_time(start, bpm); - // Check for optional [end_time] + // Check for optional "name" and [end_time] std::string end_time_str = "-1.0"; // Default: no explicit end - std::string optional_param; - if (ss >> optional_param) { - // Check if it's wrapped in brackets [time] - if (optional_param.size() >= 3 && - optional_param.front() == '[' && - optional_param.back() == ']') { - // Extract time from [time] - std::string time_value = optional_param.substr(1, optional_param.size() - 2); + 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 - << ": Optional sequence end time must be in brackets [time]\n"; + << ": Unexpected token '" << token << "'. Expected \"name\" or [end_time]\n"; return 1; } } - sequences.push_back({start_time, priority, end_time_str, {}}); + sequences.push_back({start_time, priority, end_time_str, seq_name, {}}); current_seq = &sequences.back(); } else if (command == "EFFECT") { if (!current_seq) { -- cgit v1.2.3