diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-05 00:01:11 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-05 00:01:11 +0100 |
| commit | c1fc6be0e9e06e0955e71cff11620cdd88eb3f5a (patch) | |
| tree | d31ad5092d25b0d9dc6b064facb0b35eeb6dd3c8 /tools/seq_compiler.cc | |
| parent | 850932428ceea8422c9a0eef10f5e4df3be22c5d (diff) | |
feat: Add validation-only mode and HTML/SVG Gantt charts to seq_compiler
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=<file.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.
Diffstat (limited to 'tools/seq_compiler.cc')
| -rw-r--r-- | tools/seq_compiler.cc | 291 |
1 files changed, 243 insertions, 48 deletions
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<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; + } + + // 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 << "<!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 + 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 += 5) { + 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 (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 << " <!-- 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 [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 [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; + } + } + + // 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) { @@ -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] << " <input.seq> <output.cc> [--gantt <gantt.txt>]\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] << " <input.seq> [output.cc] [--gantt=<file.txt>] [--gantt-html=<file.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<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) { - 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<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) { + 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; } |
