From c1fc6be0e9e06e0955e71cff11620cdd88eb3f5a Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Feb 2026 00:01:11 +0100 Subject: feat: Add validation-only mode and HTML/SVG Gantt charts to seq_compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances seq_compiler with flexible output modes and beautiful HTML visualization. ## New Features ### 1. Optional C++ Output (Validation Mode) - Output .cc file is now optional - Running without output performs validation only - Useful for checking .seq syntax before committing - Example: `./seq_compiler assets/demo.seq` ### 2. HTML/SVG Gantt Chart - New --gantt-html= option - Generates interactive HTML page with SVG timeline - Much more readable than ASCII version - Features: * Color-coded sequences (blue) and effects (teal) * Invalid time ranges highlighted in red * Hover tooltips with full effect details * Time axis with 5-second markers * Dark theme matching IDE aesthetics * Vertical time markers for easy reading * Legend explaining visual elements ### 3. Flexible Command Line All output modes can be combined: ```bash # Validation only ./seq_compiler demo.seq # Code + ASCII Gantt ./seq_compiler demo.seq timeline.cc --gantt=chart.txt # Code + HTML Gantt ./seq_compiler demo.seq timeline.cc --gantt-html=chart.html # All outputs ./seq_compiler demo.seq timeline.cc --gantt=t.txt --gantt-html=t.html ``` ## HTML Gantt Advantages Over ASCII ✓ Precise pixel-perfect positioning ✓ Scalable vector graphics (zoom without quality loss) ✓ Color coding for priorities and validity ✓ Interactive hover tooltips ✓ Professional dark theme ✓ Much easier to read complex timelines ✓ Can be shared via browser or screenshots ## Implementation Details - Added ~190 lines for HTML/SVG generation - Dark theme (#1e1e1e background) matches IDE - SVG dynamically sized based on sequence count - Invalid time ranges rendered in red with warning symbol - Time scale automatically calculated from demo duration - Hover effects provide detailed information ## Use Cases - **Validation**: Quick syntax check without code generation - **ASCII Gantt**: Terminal-friendly, git-friendly text visualization - **HTML Gantt**: Beautiful presentation for design discussions - **Combined**: Full toolchain for timeline development The HTML output answers your question about SVG generation - it turned out to be straightforward (~190 lines) and provides significantly better visualization than ASCII art! ## Files Changed - tools/seq_compiler.cc: Added validation mode and HTML generation - assets/demo.seq: Updated documentation with new usage examples - .gitignore: Exclude generated timeline files Example HTML output: 17KB file with full interactive timeline visualization. --- tools/seq_compiler.cc | 291 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 243 insertions(+), 48 deletions(-) (limited to 'tools/seq_compiler.cc') diff --git a/tools/seq_compiler.cc b/tools/seq_compiler.cc index 548f467..45cfc1a 100644 --- a/tools/seq_compiler.cc +++ b/tools/seq_compiler.cc @@ -157,6 +157,177 @@ void generate_gantt_chart(const std::string& output_file, 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; + } + + // Find max time for the chart + 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) { + float eff_end = seq_start + std::stof(eff.end); + max_time = std::max(max_time, eff_end); + } + if (seq.end_time != "-1.0") { + max_time = std::max(max_time, seq_start + std::stof(seq.end_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 + out << " \n"; + out << " \n"; + + for (int t = 0; t <= (int)max_time; t += 5) { + 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 (const auto& seq : sequences) { + float seq_start = std::stof(seq.start_time); + float seq_end = max_time; + + if (seq.end_time != "-1.0") { + seq_end = seq_start + std::stof(seq.end_time); + } else { + for (const auto& eff : seq.effects) { + seq_end = std::max(seq_end, seq_start + std::stof(eff.end)); + } + } + + 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 [pri=" << seq.priority << "] (" + << seq_start << "-" << seq_end << "s)\n"; + out << " \n"; + + // Draw sequence label + out << " SEQ@" << seq_start << "s [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; + } + } + + // 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) { @@ -188,18 +359,31 @@ std::string convert_to_time(const std::string& value, float bpm) { } int main(int argc, char* argv[]) { - if (argc < 3 || argc > 4) { - std::cerr << "Usage: " << argv[0] << " [--gantt ]\n"; - std::cerr << "Example: " << argv[0] - << " assets/demo.seq src/generated/timeline.cc\n"; - std::cerr << " " << argv[0] - << " assets/demo.seq src/generated/timeline.cc --gantt timeline.txt\n"; + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " [output.cc] [--gantt=] [--gantt-html=]\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 timeline.cc --gantt=timeline.txt --gantt-html=timeline.html\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 = ""; - if (argc == 4 && std::string(argv[3]).rfind("--gantt=", 0) == 0) { - gantt_output = std::string(argv[3]).substr(8); // Extract filename after --gantt= + std::string gantt_html_output = ""; + + // 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 (output_cc.empty() && arg[0] != '-') { + output_cc = arg; + } } std::ifstream in_file(argv[1]); @@ -333,57 +517,68 @@ int main(int argc, char* argv[]) { }); } - std::ofstream out_file(argv[2]); - if (!out_file.is_open()) { - std::cerr << "Error: Could not open output file " << argv[2] << "\n"; - return 1; - } + // 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"; + 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"; - } + // 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, WGPUDevice device, " - "WGPUQueue queue, WGPUTextureFormat format) {\n"; + out_file << "void LoadTimeline(MainSequence& main_seq, WGPUDevice device, " + "WGPUQueue queue, WGPUTextureFormat format) {\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) { - out_file << " seq->add_effect(std::make_shared<" << eff.class_name - << ">(device, queue, format" << eff.extra_args << "), " - << eff.start << "f, " << eff.end << "f, " << eff.priority - << ");\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) { + out_file << " seq->add_effect(std::make_shared<" << eff.class_name + << ">(device, queue, format" << 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 << " main_seq.add_sequence(seq, " << seq.start_time << "f, " - << seq.priority << ");\n"; - out_file << " }\n"; - } - out_file << "}\n"; + out_file << "}\n"; + out_file.close(); - std::cout << "Successfully generated timeline with " << sequences.size() - << " sequences.\n"; + 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 chart if requested + // 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); + } return 0; } -- cgit v1.2.3