mirror of
https://github.com/paboyle/Grid.git
synced 2026-04-29 06:56:00 +01:00
Adding Claude related files
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What This Is
|
||||
|
||||
VTK-based visualisation and analysis tools for Grid lattice QCD eigenvector density and HMC force data. All programmes link against both Grid (for reading Scidac/ILDG lattice files) and VTK (for rendering).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd /Users/peterboyle/QCD/AmSC/Grid/visualisation/build
|
||||
cmake .. -DVTK_DIR=$HOME/QCD/vtk/VTK-9.4.2-install/lib/cmake/vtk-9.4
|
||||
make <target> # e.g. make ControlledVisualise5D
|
||||
```
|
||||
|
||||
All executables are built as macOS bundles (`.app`) except `ForceAnalysis`, `FindPeak`, and `DumpField`.
|
||||
|
||||
## Programmes
|
||||
|
||||
### ControlledVisualise5D
|
||||
|
||||
Interactive VTK renderer for 5D DWF eigenvector density (`LatticeComplexD`). Driven via named pipe `/tmp/visualise_cmd`.
|
||||
|
||||
**Launch script**: `/Volumes/X9Pro/visualisation/Grid/visualisation/build/Hdwf_1_long/visualise_controlled.sh`
|
||||
|
||||
**Wire protocol** (one command per line to `/tmp/visualise_cmd`):
|
||||
|
||||
| Command | Effect |
|
||||
|---------|--------|
|
||||
| `file <N>` / `file +N` / `file -N` | Jump to file by index or relative |
|
||||
| `slice <dim> <N>` / `+N` / `-N` | Set or shift slice coordinate in dimension dim |
|
||||
| `spin <deg>` | Continuous azimuth rotation at deg/tick (100ms tick); `spin 0` stops |
|
||||
| `azimuth <deg>` | Single azimuth rotation step |
|
||||
| `elevation <deg>` | Single elevation rotation step |
|
||||
| `zoom <factor>` | Camera dolly (>1 = in) |
|
||||
| `iso <value>` | Isosurface threshold in RMS units |
|
||||
| `status` | Print current state |
|
||||
| `quit` | Exit |
|
||||
|
||||
**Dimension indices for 5D DWF grid** (`--grid 48.32.32.32.32`):
|
||||
|
||||
| dim | axis | size |
|
||||
|-----|------|------|
|
||||
| 0 | s (Ls) | 48 |
|
||||
| 1 | x | 32 |
|
||||
| 2 | y | 32 |
|
||||
| 3 | z | 32 |
|
||||
| 4 | t | 32 |
|
||||
|
||||
**MD time mapping** for trajectory 702 (241 files, τ=3.3–4.0):
|
||||
- File index N → τ = 3.300000 + N × (1/480)
|
||||
- τ → file index = round((τ − 3.3) × 480)
|
||||
|
||||
**Display axes**: `--xyz 0.3.4` shows s, z, t. The `--slice` argument sets initial values for all dims; dims not in `--xyz`, `--sum`, or `--loop` are the fixed slice dimensions (x=dim1, y=dim2 with `--xyz 0.3.4`).
|
||||
|
||||
**Spin**: Implemented via `g_spinDeg` global applied on every 100ms poll timer tick inside `CommandHandler::Execute()`. Does not flood the pipe.
|
||||
|
||||
### FindPeak
|
||||
|
||||
Reads a `LatticeComplexD` Scidac file, prints the top-N sites by real value to stderr.
|
||||
|
||||
```bash
|
||||
./FindPeak --grid 48.32.32.32.32 --mpi 1.1.1.1.1 <file> 2>peaks.txt
|
||||
```
|
||||
|
||||
Key result: At τ=3.670833 the tunneling hotsite on the s=0 wall is (x=21, y=24, z=2, t=23).
|
||||
|
||||
### ForceAnalysis
|
||||
|
||||
Reads 4D `LatticeComplexD` force snapshot files (Shuhei's snapshots at `/Volumes/X9Pro/visualisation/Shuhei/snapshots/`). Outputs TSV of RMS and hotsite value per file to stderr.
|
||||
|
||||
```bash
|
||||
./ForceAnalysis --grid 32.32.32.32 --mpi 1.1.1 --hotsite 21.24.2.23 \
|
||||
<files...> 2>force.tsv 1>/dev/null
|
||||
```
|
||||
|
||||
Force components: `Gauge_lat`, `Gauge_smr`, `Jacobian_smr`, `Ferm0047_lat`, `Ferm0047_smr`.
|
||||
|
||||
### DumpField
|
||||
|
||||
Reads a `LatticeComplexD` and dumps via Grid's `<<` operator to stdout for verification.
|
||||
|
||||
### TranscriptToVideo
|
||||
|
||||
Renders a conversation transcript to an MP4 video (1280×720, 10 fps) with a typewriter animation effect, scrolling history, and optional captions. Does **not** link against Grid — pure VTK only.
|
||||
|
||||
#### Transcript format
|
||||
|
||||
```
|
||||
[USER] First question text, possibly
|
||||
continuing on the next line.
|
||||
|
||||
A blank line within a turn creates a paragraph break (visual spacer).
|
||||
|
||||
[ASSISTANT] Response text.
|
||||
Multiple continuation lines are preserved
|
||||
as separate display lines, not merged.
|
||||
|
||||
[CAPTION] Caption text shown at bottom of screen in white italic.
|
||||
[CAPTION] (whitespace-only body clears the caption)
|
||||
[USER] Next question...
|
||||
```
|
||||
|
||||
- Lines beginning `[USER]`, `[ASSISTANT]`, `[CAPTION]` start a new turn.
|
||||
- Continuation lines (no `[TAG]` prefix) are joined with `\n` — each becomes its own wrapped display line.
|
||||
- Blank lines within a turn become paragraph-break spacers.
|
||||
- Markdown emphasis markers (`**`, `*`, `` ` ``) are stripped automatically.
|
||||
- UTF-8 smart quotes, em-dashes, ellipses, arrows are transliterated to ASCII.
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
cd /Users/peterboyle/QCD/AmSC/Grid/visualisation/build
|
||||
# Set runtime library paths first (see Runtime Environment below)
|
||||
./TranscriptToVideo <transcript_file> <output.mp4>
|
||||
```
|
||||
|
||||
Transcript files live in `/Users/peterboyle/QCD/AmSC/Grid/visualisation/` (e.g. `transcript`, `transcript2`, `transcript3`).
|
||||
|
||||
#### Visual layout
|
||||
|
||||
| Element | Detail |
|
||||
|---------|--------|
|
||||
| Background | Near-black navy `(0.04, 0.04, 0.10)` |
|
||||
| `[USER]` text | Gold `(1.00, 0.84, 0.00)` |
|
||||
| `[ASSISTANT]` text | Steel blue `(0.68, 0.85, 0.90)` |
|
||||
| History | Up to 18 lines; brightness fades linearly from 0.85 (newest) to 0.20 (oldest) |
|
||||
| Caption | Arial italic 20pt white with shadow, centred at bottom |
|
||||
| Progress bar | Blue, top of frame |
|
||||
| Typewriter speed | 50 chars/sec (5 chars/frame at 10 fps) |
|
||||
| Pause between lines | 3 frames (0.3 s) |
|
||||
| Word-wrap column | 60 chars (body only, after prefix) |
|
||||
|
||||
#### Key implementation notes
|
||||
|
||||
- **Persistent render context**: a single `vtkRenderWindow` is created once and reused for all frames. Creating a new window per frame exhausts the macOS Metal GPU command buffer after ~33 frames (`MTLCommandBufferErrorDomain Code=8`).
|
||||
- **`SanitiseASCII()`**: replaces multi-byte UTF-8 sequences before passing to VTK's font renderer (which crashes on non-ASCII input).
|
||||
- Output format is MP4 via `vtkFFMPEGWriter`. `SetOffScreenRendering(1)` is required for headless rendering.
|
||||
|
||||
## Runtime Environment
|
||||
|
||||
All executables in `build/` require Spack-installed HDF5/FFTW/GMP/MPFR on the dynamic linker path:
|
||||
|
||||
```bash
|
||||
SPACK=/Users/peterboyle/QCD/Spack/spack/opt/spack/darwin-m1
|
||||
export DYLD_LIBRARY_PATH=\
|
||||
$SPACK/hdf5-1.14.6-2265ms4kymgw6hcnwi6vqehslyfv74t4/lib:\
|
||||
$SPACK/fftw-3.3.10-aznn6h3nac5cycidlhrhgjxvntpcbg57/lib:\
|
||||
$SPACK/gmp-6.3.0-cwiz4n7ww33fnb3aban2iup4orcr6c7i/lib:\
|
||||
$SPACK/mpfr-4.2.1-exgbz4qshmet6tmmuttdewdlunfvtrlb/lib:\
|
||||
$DYLD_LIBRARY_PATH
|
||||
```
|
||||
|
||||
(These paths are also set by the ControlledVisualise5D launch script.)
|
||||
|
||||
## Key Physics Context
|
||||
|
||||
See `/Volumes/X9Pro/visualisation/analysis_notes_20260407.md` for full analysis. Summary:
|
||||
|
||||
- Near-zero mode of H_DWF localises on the two walls (s=0 and s=47) of the 5D domain wall geometry
|
||||
- Topology change transfers norm between walls, mediated by a near-zero mode of H_w (Hermitian Wilson at m=−1.8)
|
||||
- Tunneling hotsite on s=0 wall: (x=21, y=24, z=2, t=23); s=47 wall: (x=4, y=8, z=0, t=20)
|
||||
- Light fermion pseudofermion force (Ferm0047_smr) peaks at ~20× RMS at the hotsite during tunneling — this is the restoring force that causes topological bounces
|
||||
|
||||
## Grid/VTK interaction notes
|
||||
|
||||
- Grid log messages go to stdout; all data output in analysis programmes uses stderr to avoid interleaving
|
||||
- `TensorRemove()` is required when extracting a scalar from `peekSite()` result: `real(TensorRemove(peekSite(field, site)))`
|
||||
- For runtime-determined grid dimensionality use `GridDefaultSimd(latt_size.size(), vComplex::Nsimd())`
|
||||
- DYLD_LIBRARY_PATH must include Spack HDF5/FFTW/GMP/MPFR paths (see launch script)
|
||||
@@ -43,8 +43,6 @@
|
||||
#include <vtkTextActor.h>
|
||||
#include <vtkTextProperty.h>
|
||||
#include <vtkProperty2D.h>
|
||||
#include <vtkSliderWidget.h>
|
||||
#include <vtkSliderRepresentation2D.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
|
||||
#define MPEG
|
||||
@@ -61,7 +59,9 @@
|
||||
#include <atomic>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
@@ -88,6 +88,71 @@ static std::mutex g_cmdMutex;
|
||||
static std::atomic<bool> g_running{true};
|
||||
static double g_spinDeg = 0.0; // degrees per poll tick; 0 = stopped
|
||||
|
||||
// ─── MPEG recording state ─────────────────────────────────────────────────────
|
||||
static bool g_recording = false;
|
||||
static vtkFFMPEGWriter* g_mpegWriter = nullptr;
|
||||
static vtkWindowToImageFilter* g_imageFilter = nullptr;
|
||||
static std::string g_recordingFile; // AVI filename for mux step
|
||||
|
||||
// ─── Audio state (PCM audio track, synced to video frames) ───────────────────
|
||||
static const int AUDIO_RATE = 44100;
|
||||
static const double BEEP_FREQ = 800.0;
|
||||
static const int BEEP_SAMPLES = AUDIO_RATE * 4 / 100; // 40ms beep
|
||||
static std::vector<int16_t> g_audioBuffer;
|
||||
static int g_beepRemaining = 0;
|
||||
static double g_beepPhase = 0.0;
|
||||
static int g_samplesPerFrame = AUDIO_RATE / 10; // updated at record start
|
||||
|
||||
// Write one video frame worth of audio samples (beep or silence) to the buffer.
|
||||
static void GenerateAudioFrame()
|
||||
{
|
||||
for (int i = 0; i < g_samplesPerFrame; i++) {
|
||||
int16_t s = 0;
|
||||
if (g_beepRemaining > 0) {
|
||||
int pos = BEEP_SAMPLES - g_beepRemaining;
|
||||
double env = 1.0;
|
||||
int fade = AUDIO_RATE / 100; // 10ms fade
|
||||
if (pos < fade) env = (double)pos / fade;
|
||||
if (g_beepRemaining < fade) env = (double)g_beepRemaining / fade;
|
||||
s = (int16_t)(16000.0 * env * std::sin(2.0 * M_PI * BEEP_FREQ * g_beepPhase / AUDIO_RATE));
|
||||
g_beepPhase += 1.0;
|
||||
--g_beepRemaining;
|
||||
} else {
|
||||
g_beepPhase = 0.0;
|
||||
}
|
||||
g_audioBuffer.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
static void TriggerBeep() { g_beepRemaining = BEEP_SAMPLES; }
|
||||
|
||||
// Simple mono 16-bit PCM WAV writer.
|
||||
static void WriteWAV(const std::string& path, const std::vector<int16_t>& buf, int rate)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
int dataBytes = (int)(buf.size() * 2);
|
||||
int chunkSize = 36 + dataBytes;
|
||||
int byteRate = rate * 2;
|
||||
f.write("RIFF", 4); f.write((char*)&chunkSize, 4);
|
||||
f.write("WAVE", 4);
|
||||
f.write("fmt ", 4);
|
||||
int fmtSz = 16; f.write((char*)&fmtSz, 4);
|
||||
int16_t pcm = 1; f.write((char*)&pcm, 2);
|
||||
int16_t ch = 1; f.write((char*)&ch, 2);
|
||||
f.write((char*)&rate, 4);
|
||||
f.write((char*)&byteRate, 4);
|
||||
int16_t blk = 2; f.write((char*)&blk, 2);
|
||||
int16_t bps = 16; f.write((char*)&bps, 2);
|
||||
f.write("data", 4); f.write((char*)&dataBytes, 4);
|
||||
f.write((char*)buf.data(), dataBytes);
|
||||
}
|
||||
|
||||
// Play a short audible beep on the local machine (non-blocking).
|
||||
static void PlayBeepAudible()
|
||||
{
|
||||
system("afplay /System/Library/Sounds/Tink.aiff -v 0.4 &");
|
||||
}
|
||||
|
||||
// ─── Grid I/O ─────────────────────────────────────────────────────────────────
|
||||
template <class T>
|
||||
void readFile(T& out, const std::string& fname)
|
||||
@@ -295,29 +360,36 @@ private:
|
||||
const std::string& path = files[file];
|
||||
std::string tau = path.substr(path.rfind('_') + 1);
|
||||
std::stringstream ss;
|
||||
ss << "tau = " << tau << "\nSlice " << Slice << "\nLoop " << loop_coor;
|
||||
ss << "tau = " << tau << "\nSlice " << Slice;
|
||||
text->SetInput(ss.str().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
// ─── SliderCallback ───────────────────────────────────────────────────────────
|
||||
class SliderCallback : public vtkCommand
|
||||
{
|
||||
public:
|
||||
static SliderCallback* New() { return new SliderCallback; }
|
||||
virtual void Execute(vtkObject* caller, unsigned long, void*)
|
||||
{
|
||||
vtkSliderWidget* sw = vtkSliderWidget::SafeDownCast(caller);
|
||||
if (sw) contour = ((vtkSliderRepresentation*)sw->GetRepresentation())->GetValue();
|
||||
fu->posExtractor->SetValue(0, contour * fu->rms);
|
||||
fu->negExtractor->SetValue(0, -contour * fu->rms);
|
||||
fu->posExtractor->Modified();
|
||||
fu->negExtractor->Modified();
|
||||
// ─── Typewriter caption state ─────────────────────────────────────────────────
|
||||
// User caption (gold, upper line) — cleared on new user: instruction
|
||||
static std::string g_userCaptionFull;
|
||||
static size_t g_userCaptionPos = 0;
|
||||
// Claude caption (light blue, lower line) — cleared when user: arrives
|
||||
static std::string g_claudeCaptionFull;
|
||||
static size_t g_claudeCaptionPos = 0;
|
||||
static int g_captionTick = 0;
|
||||
static const int g_captionRate = 1; // ticks per character (1 x 100ms = 10 chars/sec)
|
||||
|
||||
static std::string WrapText(const std::string& s, int maxCols = 45) {
|
||||
std::istringstream words(s);
|
||||
std::string word, line, result;
|
||||
while (words >> word) {
|
||||
if (!line.empty() && (int)(line.size() + 1 + word.size()) > maxCols) {
|
||||
result += line + "\n";
|
||||
line = word;
|
||||
} else {
|
||||
if (!line.empty()) line += " ";
|
||||
line += word;
|
||||
}
|
||||
}
|
||||
static double contour;
|
||||
FrameUpdater* fu;
|
||||
};
|
||||
double SliderCallback::contour = 1.0;
|
||||
if (!line.empty()) result += line;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── CommandHandler ───────────────────────────────────────────────────────────
|
||||
// Minimal parser for the wire protocol. Natural-language interpretation
|
||||
@@ -331,15 +403,22 @@ public:
|
||||
vtkCamera* camera;
|
||||
vtkRenderer* renderer;
|
||||
vtkRenderWindowInteractor* iren;
|
||||
vtkSliderRepresentation2D* sliderRep;
|
||||
vtkTextActor* captionActor = nullptr; // claude (light blue, lower)
|
||||
vtkTextActor* userCaptionActor = nullptr; // user (gold, upper)
|
||||
int pollTimerId = -1;
|
||||
double isosurfaceLevel = 1.0; // in RMS units
|
||||
|
||||
void CaptureFrame() {
|
||||
if (g_recording && g_mpegWriter && g_imageFilter) {
|
||||
GenerateAudioFrame();
|
||||
g_imageFilter->Modified();
|
||||
g_mpegWriter->Write();
|
||||
}
|
||||
}
|
||||
|
||||
void SetIsosurface(double level)
|
||||
{
|
||||
isosurfaceLevel = std::max(0.0, std::min(10.0, level));
|
||||
sliderRep->SetValue(isosurfaceLevel);
|
||||
SliderCallback::contour = isosurfaceLevel;
|
||||
fu->posExtractor->SetValue(0, isosurfaceLevel * fu->rms);
|
||||
fu->negExtractor->SetValue(0, -isosurfaceLevel * fu->rms);
|
||||
fu->posExtractor->Modified();
|
||||
@@ -417,6 +496,96 @@ public:
|
||||
std::cout << "[cmd] spin rate = " << g_spinDeg << " deg/tick" << std::endl;
|
||||
}
|
||||
|
||||
// ── caption user: <text> / caption claude: <text> / caption ─────────
|
||||
// user: clears both lines, types user text (gold) on upper line.
|
||||
// claude: keeps user line, types response (light blue) on lower line.
|
||||
// caption alone clears both immediately.
|
||||
else if (verb == "caption") {
|
||||
std::string rest;
|
||||
std::getline(iss, rest);
|
||||
if (!rest.empty() && rest[0] == ' ') rest = rest.substr(1);
|
||||
if (rest.empty()) {
|
||||
g_userCaptionFull = ""; g_userCaptionPos = 0;
|
||||
g_claudeCaptionFull = ""; g_claudeCaptionPos = 0;
|
||||
g_captionTick = 0;
|
||||
if (userCaptionActor) userCaptionActor->SetInput("");
|
||||
if (captionActor) captionActor->SetInput("");
|
||||
iren->GetRenderWindow()->Render(); CaptureFrame();
|
||||
} else if (rest.substr(0,5) == "user:") {
|
||||
// New instruction: clear both, start typing user text
|
||||
g_claudeCaptionFull = ""; g_claudeCaptionPos = 0;
|
||||
g_userCaptionFull = WrapText(rest); g_userCaptionPos = 0;
|
||||
g_captionTick = 0;
|
||||
if (userCaptionActor) userCaptionActor->SetInput("");
|
||||
if (captionActor) captionActor->SetInput("");
|
||||
iren->GetRenderWindow()->Render(); CaptureFrame();
|
||||
} else {
|
||||
// claude: or unlabelled — keep user line, type below
|
||||
g_claudeCaptionFull = WrapText(rest); g_claudeCaptionPos = 0;
|
||||
g_captionTick = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── record start <filename> / record stop ────────────────────────────
|
||||
else if (verb == "record") {
|
||||
#ifdef MPEG
|
||||
std::string sub;
|
||||
if (!(iss >> sub)) { std::cout << "[cmd] record: expected start <file> or stop" << std::endl; return; }
|
||||
if (sub == "stop") {
|
||||
if (g_recording && g_mpegWriter) {
|
||||
g_mpegWriter->End();
|
||||
g_mpegWriter->Delete(); g_mpegWriter = nullptr;
|
||||
g_imageFilter->Delete(); g_imageFilter = nullptr;
|
||||
g_recording = false;
|
||||
std::cout << "[cmd] recording stopped: " << g_recordingFile << std::endl;
|
||||
// Write WAV and mux to MP4
|
||||
std::string wavFile = g_recordingFile + ".wav";
|
||||
WriteWAV(wavFile, g_audioBuffer, AUDIO_RATE);
|
||||
g_audioBuffer.clear();
|
||||
std::string mp4File = g_recordingFile;
|
||||
if (mp4File.size() > 4 && mp4File.substr(mp4File.size()-4) == ".avi")
|
||||
mp4File = mp4File.substr(0, mp4File.size()-4) + ".mp4";
|
||||
else
|
||||
mp4File += ".mp4";
|
||||
std::string cmd = "ffmpeg -y -i \"" + g_recordingFile + "\" -i \"" + wavFile +
|
||||
"\" -c:v copy -c:a aac -shortest \"" + mp4File + "\" 2>/dev/null";
|
||||
int ret = system(cmd.c_str());
|
||||
if (ret == 0) {
|
||||
std::cout << "[cmd] muxed output: " << mp4File << std::endl;
|
||||
unlink(wavFile.c_str()); // clean up intermediate WAV
|
||||
} else {
|
||||
std::cout << "[cmd] mux failed (ffmpeg not found?). WAV kept: " << wavFile << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "[cmd] not recording" << std::endl;
|
||||
}
|
||||
} else if (sub == "start") {
|
||||
std::string fname = "recording.avi";
|
||||
iss >> fname;
|
||||
if (g_recording) { std::cout << "[cmd] already recording" << std::endl; return; }
|
||||
g_recordingFile = fname;
|
||||
g_audioBuffer.clear();
|
||||
g_samplesPerFrame = AUDIO_RATE / std::max(1, g_framerate);
|
||||
g_beepRemaining = 0;
|
||||
g_beepPhase = 0.0;
|
||||
g_imageFilter = vtkWindowToImageFilter::New();
|
||||
g_imageFilter->SetInput(iren->GetRenderWindow());
|
||||
g_imageFilter->SetInputBufferTypeToRGB();
|
||||
g_mpegWriter = vtkFFMPEGWriter::New();
|
||||
g_mpegWriter->SetFileName(fname.c_str());
|
||||
g_mpegWriter->SetRate(g_framerate);
|
||||
g_mpegWriter->SetInputConnection(g_imageFilter->GetOutputPort());
|
||||
g_mpegWriter->Start();
|
||||
g_recording = true;
|
||||
std::cout << "[cmd] recording started: " << fname << std::endl;
|
||||
} else {
|
||||
std::cout << "[cmd] record: unknown subcommand '" << sub << "'" << std::endl;
|
||||
}
|
||||
#else
|
||||
std::cout << "[cmd] record: MPEG support not compiled" << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── iso <value> ──────────────────────────────────────────────────────
|
||||
else if (verb == "iso") {
|
||||
double val;
|
||||
@@ -481,6 +650,37 @@ public:
|
||||
for (const auto& line : pending) {
|
||||
std::cout << "[cmd] >> " << line << std::endl;
|
||||
RunLine(line);
|
||||
// CaptureFrame() called inside RunLine for caption; for other
|
||||
// rendering commands capture here (duplicate Modified() is harmless)
|
||||
CaptureFrame();
|
||||
}
|
||||
|
||||
// Typewriter: advance one character every g_captionRate ticks.
|
||||
// User line types first; claude line starts once user line is complete.
|
||||
bool typing = (g_userCaptionPos < g_userCaptionFull.size()) ||
|
||||
(g_claudeCaptionPos < g_claudeCaptionFull.size());
|
||||
if (typing) {
|
||||
if (++g_captionTick >= g_captionRate) {
|
||||
g_captionTick = 0;
|
||||
bool rendered = false;
|
||||
if (g_userCaptionPos < g_userCaptionFull.size()) {
|
||||
++g_userCaptionPos;
|
||||
if (userCaptionActor)
|
||||
userCaptionActor->SetInput(g_userCaptionFull.substr(0, g_userCaptionPos).c_str());
|
||||
PlayBeepAudible();
|
||||
TriggerBeep();
|
||||
rendered = true;
|
||||
} else if (g_claudeCaptionPos < g_claudeCaptionFull.size()) {
|
||||
++g_claudeCaptionPos;
|
||||
if (captionActor)
|
||||
captionActor->SetInput(g_claudeCaptionFull.substr(0, g_claudeCaptionPos).c_str());
|
||||
rendered = true;
|
||||
}
|
||||
if (rendered) {
|
||||
iren->GetRenderWindow()->Render();
|
||||
CaptureFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply continuous spin (if active) at poll-timer rate
|
||||
@@ -488,6 +688,7 @@ public:
|
||||
camera->Azimuth(g_spinDeg);
|
||||
renderer->ResetCameraClippingRange();
|
||||
iren->GetRenderWindow()->Render();
|
||||
CaptureFrame();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -603,11 +804,33 @@ int main(int argc, char* argv[])
|
||||
|
||||
vtkNew<vtkTextActor> TextT;
|
||||
TextT->SetInput("Initialising...");
|
||||
TextT->SetPosition(0, 0.7*1025);
|
||||
TextT->SetPosition(10, 920);
|
||||
TextT->GetTextProperty()->SetFontSize(24);
|
||||
TextT->GetTextProperty()->SetColor(colors->GetColor3d("Gold").GetData());
|
||||
|
||||
aRenderer->AddActor(TextT); aRenderer->AddActor(outline);
|
||||
// Claude response caption (light blue, lower line)
|
||||
vtkNew<vtkTextActor> CaptionT;
|
||||
CaptionT->SetInput("");
|
||||
CaptionT->SetPosition(512, 38);
|
||||
CaptionT->GetTextProperty()->SetFontSize(32);
|
||||
CaptionT->GetTextProperty()->SetColor(0.6, 0.9, 1.0);
|
||||
CaptionT->GetTextProperty()->SetJustificationToCentered();
|
||||
CaptionT->GetTextProperty()->SetBackgroundColor(0.0, 0.0, 0.0);
|
||||
CaptionT->GetTextProperty()->SetBackgroundOpacity(0.6);
|
||||
CaptionT->GetTextProperty()->BoldOn();
|
||||
|
||||
// User instruction caption (gold, upper line)
|
||||
vtkNew<vtkTextActor> UserCaptionT;
|
||||
UserCaptionT->SetInput("");
|
||||
UserCaptionT->SetPosition(512, 82);
|
||||
UserCaptionT->GetTextProperty()->SetFontSize(32);
|
||||
UserCaptionT->GetTextProperty()->SetColor(1.0, 0.85, 0.0);
|
||||
UserCaptionT->GetTextProperty()->SetJustificationToCentered();
|
||||
UserCaptionT->GetTextProperty()->SetBackgroundColor(0.0, 0.0, 0.0);
|
||||
UserCaptionT->GetTextProperty()->SetBackgroundOpacity(0.6);
|
||||
UserCaptionT->GetTextProperty()->BoldOn();
|
||||
|
||||
aRenderer->AddActor(TextT); aRenderer->AddActor(CaptionT); aRenderer->AddActor(UserCaptionT); aRenderer->AddActor(outline);
|
||||
aRenderer->AddActor(pos); aRenderer->AddActor(neg);
|
||||
|
||||
vtkNew<FrameUpdater> fu;
|
||||
@@ -627,34 +850,14 @@ int main(int argc, char* argv[])
|
||||
renWin->SetSize(1024,1024); renWin->SetWindowName("ControlledFieldDensity");
|
||||
renWin->Render(); iren->Initialize();
|
||||
|
||||
// Slider
|
||||
vtkSmartPointer<vtkSliderRepresentation2D> sliderRep = vtkSmartPointer<vtkSliderRepresentation2D>::New();
|
||||
sliderRep->SetMinimumValue(0.0); sliderRep->SetMaximumValue(10.0); sliderRep->SetValue(default_contour);
|
||||
sliderRep->SetTitleText("Fraction RMS");
|
||||
sliderRep->GetTitleProperty()->SetColor( colors->GetColor3d("AliceBlue").GetData());
|
||||
sliderRep->GetLabelProperty()->SetColor( colors->GetColor3d("AliceBlue").GetData());
|
||||
sliderRep->GetSelectedProperty()->SetColor(colors->GetColor3d("DeepPink").GetData());
|
||||
sliderRep->GetTubeProperty()->SetColor( colors->GetColor3d("MistyRose").GetData());
|
||||
sliderRep->GetCapProperty()->SetColor( colors->GetColor3d("Yellow").GetData());
|
||||
sliderRep->SetSliderLength(0.05); sliderRep->SetSliderWidth(0.025); sliderRep->SetEndCapLength(0.02);
|
||||
sliderRep->GetPoint1Coordinate()->SetCoordinateSystemToNormalizedDisplay(); sliderRep->GetPoint1Coordinate()->SetValue(0.1,0.1);
|
||||
sliderRep->GetPoint2Coordinate()->SetCoordinateSystemToNormalizedDisplay(); sliderRep->GetPoint2Coordinate()->SetValue(0.9,0.1);
|
||||
|
||||
vtkSmartPointer<vtkSliderWidget> sliderWidget = vtkSmartPointer<vtkSliderWidget>::New();
|
||||
sliderWidget->SetInteractor(iren); sliderWidget->SetRepresentation(sliderRep);
|
||||
sliderWidget->SetAnimationModeToAnimate(); sliderWidget->EnabledOn();
|
||||
|
||||
vtkSmartPointer<SliderCallback> slidercallback = vtkSmartPointer<SliderCallback>::New();
|
||||
slidercallback->fu = fu; SliderCallback::contour = default_contour;
|
||||
sliderWidget->AddObserver(vtkCommand::InteractionEvent, slidercallback);
|
||||
|
||||
// CommandHandler on fast poll timer
|
||||
vtkNew<CommandHandler> cmdHandler;
|
||||
cmdHandler->fu = fu;
|
||||
cmdHandler->camera = aCamera;
|
||||
cmdHandler->renderer = aRenderer;
|
||||
cmdHandler->iren = iren;
|
||||
cmdHandler->sliderRep = sliderRep;
|
||||
cmdHandler->captionActor = CaptionT;
|
||||
cmdHandler->userCaptionActor = UserCaptionT;
|
||||
cmdHandler->isosurfaceLevel = default_contour;
|
||||
iren->AddObserver(vtkCommand::TimerEvent, cmdHandler);
|
||||
cmdHandler->pollTimerId = iren->CreateRepeatingTimer(100);
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
// ForceAnalysis.cxx
|
||||
//
|
||||
// Reads a sequence of force snapshot files (LatticeComplexD, real part = force magnitude)
|
||||
// and produces two outputs:
|
||||
//
|
||||
// 1. Tab-separated timeseries to stdout:
|
||||
// idx Gauge_lat_rms Gauge_lat_hot Gauge_smr_rms ...
|
||||
// where _rms is the lattice RMS and _hot is the value at --hotsite.
|
||||
//
|
||||
// 2. PNG images (one per force component per snapshot) rendered via VTK
|
||||
// as isosurfaces of the force density, using the same pipeline as
|
||||
// Visualise5D. Images are written to --pngdir/<label>_<idx>.png.
|
||||
// These can be read back by Claude to interpret spatial structure.
|
||||
//
|
||||
// Usage:
|
||||
// ForceAnalysis --grid 32.32.32.32 --mpi 1.1.1.1
|
||||
// --snapdir /path/to/snapshots
|
||||
// --first 0 --last 1920 --step 10
|
||||
// --hotsite x.y.z.t
|
||||
// --pngdir /path/to/output/pngs
|
||||
// --isosurface 1.0 (contour in units of field RMS)
|
||||
// --fixediso 0.05 (fixed absolute contour, overrides --isosurface)
|
||||
// --slice t (which dimension to fix for 3D display, default: t)
|
||||
// --sliceval 2 (value of that dimension, default: 0)
|
||||
//
|
||||
// Dimension order on the 32^4 lattice: x=0 y=1 z=2 t=3
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkActor2D.h>
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkImageActor.h>
|
||||
#include <vtkImageMapper3D.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkImageMapToColors.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkNamedColors.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkOutlineFilter.h>
|
||||
#include <vtkPolyData.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkPolyDataMapper2D.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkProperty2D.h>
|
||||
#include <vtkPoints.h>
|
||||
#include <vtkCellArray.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkStripper.h>
|
||||
#include <vtkCallbackCommand.h>
|
||||
#include <vtkTextActor.h>
|
||||
#include <vtkTextProperty.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
#include <vtkPNGWriter.h>
|
||||
|
||||
#define USE_FLYING_EDGES
|
||||
#ifdef USE_FLYING_EDGES
|
||||
#include <vtkFlyingEdges3D.h>
|
||||
typedef vtkFlyingEdges3D isosurface;
|
||||
#else
|
||||
#include <vtkMarchingCubes.h>
|
||||
typedef vtkMarchingCubes isosurface;
|
||||
#endif
|
||||
|
||||
#include <Grid/Grid.h>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <sys/stat.h>
|
||||
|
||||
using namespace Grid;
|
||||
|
||||
// ─── I/O ─────────────────────────────────────────────────────────────────────
|
||||
template <class T>
|
||||
bool tryReadFile(T& out, const std::string& fname)
|
||||
{
|
||||
std::ifstream test(fname);
|
||||
if (!test.good()) return false;
|
||||
test.close();
|
||||
emptyUserRecord record;
|
||||
ScidacReader RD;
|
||||
RD.open(fname);
|
||||
RD.readScidacFieldRecord(out, record);
|
||||
RD.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Fill a 3D vtkImageData slice from a 4D lattice field ────────────────────
|
||||
// Sums over the sliced dimension at sliceval, displays the remaining 3 dims.
|
||||
void fillImageData(vtkImageData* img,
|
||||
LatticeComplexD& field,
|
||||
const Coordinate& latt_size,
|
||||
int slice_dim, int sliceval)
|
||||
{
|
||||
// Display dims = all dims except slice_dim, in order
|
||||
std::vector<int> disp;
|
||||
for (int d = 0; d < 4; d++) if (d != slice_dim) disp.push_back(d);
|
||||
|
||||
int Nx = latt_size[disp[0]];
|
||||
int Ny = latt_size[disp[1]];
|
||||
int Nz = latt_size[disp[2]];
|
||||
|
||||
for (int ix = 0; ix < Nx; ix++)
|
||||
for (int iy = 0; iy < Ny; iy++)
|
||||
for (int iz = 0; iz < Nz; iz++) {
|
||||
Coordinate site(4);
|
||||
site[disp[0]] = ix;
|
||||
site[disp[1]] = iy;
|
||||
site[disp[2]] = iz;
|
||||
site[slice_dim] = sliceval;
|
||||
RealD val = real(peekSite(field, site));
|
||||
img->SetScalarComponentFromDouble(ix, iy, iz, 0, val);
|
||||
}
|
||||
img->Modified();
|
||||
}
|
||||
|
||||
// ─── 2D heatmap: persistent context ───────────────────────────────────────────
|
||||
// Renders a fixed (dim1=v1, dim2=v2) slice of the 4D force field as a
|
||||
// diverging blue→white→red colour map, with a fixed symmetric colour scale
|
||||
// so brightness directly encodes force magnitude across all frames.
|
||||
// A white cross-hair marks the hotsite projection onto the slice.
|
||||
struct HeatmapCtx {
|
||||
// image pipeline
|
||||
vtkNew<vtkImageData> img;
|
||||
vtkNew<vtkLookupTable> lut;
|
||||
vtkNew<vtkImageMapToColors> colorMap;
|
||||
vtkNew<vtkImageActor> imgActor;
|
||||
// colour scale legend (text, avoids needing RenderingAnnotation module)
|
||||
vtkNew<vtkTextActor> cbar;
|
||||
// hotsite cross-hair (2D overlay actors)
|
||||
vtkNew<vtkPolyData> crossPD;
|
||||
vtkNew<vtkPoints> crossPts;
|
||||
vtkNew<vtkCellArray> crossLines;
|
||||
vtkNew<vtkActor2D> crossActor;
|
||||
// title
|
||||
vtkNew<vtkTextActor> titleAct;
|
||||
// renderer / window
|
||||
vtkNew<vtkRenderer> ren;
|
||||
vtkNew<vtkRenderWindow> renWin;
|
||||
vtkNew<vtkWindowToImageFilter> w2i;
|
||||
vtkNew<vtkPNGWriter> writer;
|
||||
|
||||
int Nx = 0, Ny = 0; // display dimensions of the slice
|
||||
double scale = 0.07; // colour range: [-scale, +scale]
|
||||
int hotX = -1, hotY = -1; // hotsite projection onto (Nx,Ny) plane
|
||||
// pixel coords of the image origin in the render window
|
||||
int imgOffX = 60, imgOffY = 40;
|
||||
int imgW = 0, imgH = 0; // rendered pixel size of each lattice cell
|
||||
|
||||
void init(int nx, int ny, double sc, int hx, int hy)
|
||||
{
|
||||
Nx = nx; Ny = ny; scale = sc;
|
||||
hotX = hx; hotY = hy;
|
||||
|
||||
const int WIN_W = 900, WIN_H = 700;
|
||||
// Make cells square and as large as possible within the central area
|
||||
int cellW = (WIN_W - 160) / Nx;
|
||||
int cellH = (WIN_H - 120) / Ny;
|
||||
imgW = std::min(cellW, cellH);
|
||||
imgH = imgW;
|
||||
imgOffX = (WIN_W - Nx * imgW) / 2;
|
||||
imgOffY = 60;
|
||||
|
||||
// --- Image data (scalar field, one component) ---
|
||||
img->SetDimensions(Nx, Ny, 1);
|
||||
img->SetSpacing(imgW, imgH, 1);
|
||||
img->SetOrigin(imgOffX, imgOffY, 0);
|
||||
img->AllocateScalars(VTK_DOUBLE, 1);
|
||||
|
||||
// --- Diverging LUT: blue(-scale) → white(0) → red(+scale) ---
|
||||
lut->SetNumberOfTableValues(512);
|
||||
lut->SetRange(-scale, scale);
|
||||
lut->SetNanColor(0.2, 0.2, 0.2, 1.0);
|
||||
for (int i = 0; i < 512; ++i) {
|
||||
double t = i / 511.0; // 0=blue, 0.5=white, 1=red
|
||||
double r = (t > 0.5) ? 1.0 : 2.0 * t;
|
||||
double g = (t < 0.5) ? 2.0 * t : 2.0 * (1.0 - t);
|
||||
double b = (t < 0.5) ? 1.0 : 2.0 * (1.0 - t);
|
||||
lut->SetTableValue(i, r, g, b, 1.0);
|
||||
}
|
||||
lut->Build();
|
||||
|
||||
// --- Colour map pipeline ---
|
||||
colorMap->SetInputData(img);
|
||||
colorMap->SetLookupTable(lut);
|
||||
colorMap->Update();
|
||||
|
||||
imgActor->GetMapper()->SetInputConnection(colorMap->GetOutputPort());
|
||||
|
||||
// --- Colour scale legend (text) ---
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << std::scientific << std::setprecision(2)
|
||||
<< "blue=-" << sc << " white=0 red=+" << sc;
|
||||
cbar->SetInput(ss.str().c_str());
|
||||
}
|
||||
cbar->GetTextProperty()->SetFontFamilyToCourier();
|
||||
cbar->GetTextProperty()->SetFontSize(13);
|
||||
cbar->GetTextProperty()->SetColor(0.9, 0.9, 0.9);
|
||||
cbar->SetDisplayPosition(10, 10);
|
||||
|
||||
// --- Cross-hair at hotsite (2D display coords) ---
|
||||
if (hotX >= 0 && hotY >= 0) {
|
||||
double cx = imgOffX + (hotX + 0.5) * imgW;
|
||||
double cy = imgOffY + (hotY + 0.5) * imgH;
|
||||
double arm = imgW * 0.8;
|
||||
crossPts->InsertNextPoint(cx - arm, cy, 0);
|
||||
crossPts->InsertNextPoint(cx + arm, cy, 0);
|
||||
crossPts->InsertNextPoint(cx, cy - arm, 0);
|
||||
crossPts->InsertNextPoint(cx, cy + arm, 0);
|
||||
vtkIdType seg0[2] = {0, 1};
|
||||
vtkIdType seg1[2] = {2, 3};
|
||||
crossLines->InsertNextCell(2, seg0);
|
||||
crossLines->InsertNextCell(2, seg1);
|
||||
crossPD->SetPoints(crossPts);
|
||||
crossPD->SetLines(crossLines);
|
||||
vtkNew<vtkPolyDataMapper2D> crossMap;
|
||||
crossMap->SetInputData(crossPD);
|
||||
crossActor->SetMapper(crossMap);
|
||||
crossActor->GetProperty()->SetColor(1, 1, 1);
|
||||
crossActor->GetProperty()->SetLineWidth(2.0);
|
||||
}
|
||||
|
||||
// --- Title ---
|
||||
titleAct->GetTextProperty()->SetFontFamilyToCourier();
|
||||
titleAct->GetTextProperty()->SetFontSize(16);
|
||||
titleAct->GetTextProperty()->SetColor(1, 1, 0);
|
||||
titleAct->SetDisplayPosition(10, WIN_H - 30);
|
||||
|
||||
// --- Renderer (2D parallel projection so image fills correctly) ---
|
||||
ren->SetBackground(0.08, 0.08, 0.12);
|
||||
ren->AddActor(imgActor);
|
||||
ren->AddActor2D(cbar);
|
||||
ren->AddActor2D(crossActor);
|
||||
ren->AddActor2D(titleAct);
|
||||
ren->GetActiveCamera()->ParallelProjectionOn();
|
||||
// Set up camera to look straight down at the image plane
|
||||
ren->GetActiveCamera()->SetPosition(WIN_W/2.0, WIN_H/2.0, 1000);
|
||||
ren->GetActiveCamera()->SetFocalPoint(WIN_W/2.0, WIN_H/2.0, 0);
|
||||
ren->GetActiveCamera()->SetViewUp(0, 1, 0);
|
||||
ren->GetActiveCamera()->SetParallelScale(WIN_H / 2.0);
|
||||
ren->ResetCameraClippingRange();
|
||||
|
||||
renWin->AddRenderer(ren);
|
||||
renWin->SetSize(WIN_W, WIN_H);
|
||||
renWin->SetOffScreenRendering(1);
|
||||
renWin->SetMultiSamples(0);
|
||||
|
||||
w2i->SetInput(renWin);
|
||||
w2i->SetInputBufferTypeToRGB();
|
||||
w2i->ReadFrontBufferOff();
|
||||
}
|
||||
};
|
||||
|
||||
void renderHeatmap(HeatmapCtx& ctx,
|
||||
LatticeComplexD& field,
|
||||
const Coordinate& latt_size,
|
||||
int dim1, int val1, // first fixed dimension
|
||||
int dim2, int val2, // second fixed dimension
|
||||
const std::string& title,
|
||||
const std::string& outpath)
|
||||
{
|
||||
// Display dimensions: the two dims that are NOT fixed
|
||||
std::vector<int> disp;
|
||||
for (int d = 0; d < 4; d++)
|
||||
if (d != dim1 && d != dim2) disp.push_back(d);
|
||||
|
||||
int Nx = latt_size[disp[0]];
|
||||
int Ny = latt_size[disp[1]];
|
||||
|
||||
// Fill image data
|
||||
for (int ix = 0; ix < Nx; ix++) {
|
||||
for (int iy = 0; iy < Ny; iy++) {
|
||||
Coordinate site(4);
|
||||
site[disp[0]] = ix;
|
||||
site[disp[1]] = iy;
|
||||
site[dim1] = val1;
|
||||
site[dim2] = val2;
|
||||
RealD val = real(TensorRemove(peekSite(field, site)));
|
||||
ctx.img->SetScalarComponentFromDouble(ix, iy, 0, 0, val);
|
||||
}
|
||||
}
|
||||
ctx.img->Modified();
|
||||
ctx.colorMap->Update();
|
||||
|
||||
ctx.titleAct->SetInput(title.c_str());
|
||||
|
||||
ctx.renWin->Render();
|
||||
ctx.w2i->Modified();
|
||||
ctx.w2i->Update();
|
||||
|
||||
ctx.writer->SetFileName(outpath.c_str());
|
||||
ctx.writer->SetInputConnection(ctx.w2i->GetOutputPort());
|
||||
ctx.writer->Write();
|
||||
}
|
||||
|
||||
// ─── Persistent rendering context (created once, reused every frame) ──────────
|
||||
// Avoids Metal GPU context exhaustion on macOS when rendering hundreds of frames.
|
||||
struct RenderCtx {
|
||||
vtkNew<vtkNamedColors> colors;
|
||||
vtkNew<vtkImageData> imageData;
|
||||
vtkNew<isosurface> posEx, negEx;
|
||||
vtkNew<vtkStripper> posSt, negSt;
|
||||
vtkNew<vtkPolyDataMapper> posMap, negMap, outMap;
|
||||
vtkNew<vtkActor> posAct, negAct, outAct;
|
||||
vtkNew<vtkOutlineFilter> outF;
|
||||
vtkNew<vtkTextActor> label;
|
||||
vtkNew<vtkRenderer> ren;
|
||||
vtkNew<vtkCamera> cam;
|
||||
vtkNew<vtkRenderWindow> renWin;
|
||||
vtkNew<vtkWindowToImageFilter> w2i;
|
||||
vtkNew<vtkPNGWriter> writer;
|
||||
|
||||
void init(int Nx, int Ny, int Nz)
|
||||
{
|
||||
std::array<unsigned char,4> posColor{{240,184,160,255}};
|
||||
colors->SetColor("posColor", posColor.data());
|
||||
std::array<unsigned char,4> bkg{{51,77,102,255}};
|
||||
colors->SetColor("BkgColor", bkg.data());
|
||||
|
||||
imageData->SetDimensions(Nx, Ny, Nz);
|
||||
imageData->AllocateScalars(VTK_DOUBLE, 1);
|
||||
|
||||
posEx->SetInputData(imageData); posEx->SetValue(0, 1.0);
|
||||
posSt->SetInputConnection(posEx->GetOutputPort());
|
||||
posMap->SetInputConnection(posSt->GetOutputPort());
|
||||
posMap->ScalarVisibilityOff();
|
||||
posAct->SetMapper(posMap);
|
||||
posAct->GetProperty()->SetDiffuseColor(colors->GetColor3d("posColor").GetData());
|
||||
posAct->GetProperty()->SetSpecular(0.3);
|
||||
posAct->GetProperty()->SetSpecularPower(20);
|
||||
posAct->GetProperty()->SetOpacity(0.6);
|
||||
|
||||
negEx->SetInputData(imageData); negEx->SetValue(0, -1.0);
|
||||
negSt->SetInputConnection(negEx->GetOutputPort());
|
||||
negMap->SetInputConnection(negSt->GetOutputPort());
|
||||
negMap->ScalarVisibilityOff();
|
||||
negAct->SetMapper(negMap);
|
||||
negAct->GetProperty()->SetDiffuseColor(colors->GetColor3d("Ivory").GetData());
|
||||
negAct->GetProperty()->SetOpacity(0.6);
|
||||
|
||||
outF->SetInputData(imageData);
|
||||
outMap->SetInputConnection(outF->GetOutputPort());
|
||||
outAct->SetMapper(outMap);
|
||||
outAct->GetProperty()->SetColor(colors->GetColor3d("Black").GetData());
|
||||
|
||||
label->SetPosition(10, 10);
|
||||
label->GetTextProperty()->SetFontFamilyToCourier();
|
||||
label->GetTextProperty()->SetFontSize(18);
|
||||
label->GetTextProperty()->SetColor(colors->GetColor3d("Gold").GetData());
|
||||
|
||||
ren->AddActor(posAct);
|
||||
ren->AddActor(negAct);
|
||||
ren->AddActor(outAct);
|
||||
ren->AddActor2D(label);
|
||||
ren->SetBackground(colors->GetColor3d("BkgColor").GetData());
|
||||
|
||||
cam->SetViewUp(0,0,-1);
|
||||
cam->SetPosition(0,-1000,0);
|
||||
cam->SetFocalPoint(0,0,0);
|
||||
cam->ComputeViewPlaneNormal();
|
||||
cam->Azimuth(30.0);
|
||||
cam->Elevation(30.0);
|
||||
ren->SetActiveCamera(cam);
|
||||
|
||||
renWin->AddRenderer(ren);
|
||||
renWin->SetSize(800, 600);
|
||||
renWin->SetOffScreenRendering(1);
|
||||
renWin->SetMultiSamples(0);
|
||||
|
||||
w2i->SetInput(renWin);
|
||||
w2i->SetInputBufferTypeToRGB();
|
||||
w2i->ReadFrontBufferOff();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Render one force field snapshot to a PNG (reuses existing RenderCtx) ─────
|
||||
void renderPNG(RenderCtx& ctx,
|
||||
LatticeComplexD& field,
|
||||
const Coordinate& latt_size,
|
||||
int slice_dim, int sliceval,
|
||||
double contour,
|
||||
const std::string& title,
|
||||
const std::string& outpath)
|
||||
{
|
||||
// Update image data
|
||||
fillImageData(ctx.imageData, field, latt_size, slice_dim, sliceval);
|
||||
|
||||
// Update isosurface levels
|
||||
ctx.posEx->SetValue(0, contour);
|
||||
ctx.negEx->SetValue(0, -contour);
|
||||
|
||||
// Update label
|
||||
ctx.label->SetInput(title.c_str());
|
||||
|
||||
// Reset camera to fit the (possibly new) data bounds
|
||||
ctx.ren->ResetCamera();
|
||||
ctx.cam->Dolly(1.2);
|
||||
ctx.ren->ResetCameraClippingRange();
|
||||
|
||||
ctx.renWin->Render();
|
||||
|
||||
ctx.w2i->Modified();
|
||||
ctx.w2i->Update();
|
||||
|
||||
ctx.writer->SetFileName(outpath.c_str());
|
||||
ctx.writer->SetInputConnection(ctx.w2i->GetOutputPort());
|
||||
ctx.writer->Write();
|
||||
}
|
||||
|
||||
// ─── main ─────────────────────────────────────────────────────────────────────
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
Grid_init(&argc, &argv);
|
||||
GridLogMessage.Active(0);
|
||||
GridLogIterative.Active(0);
|
||||
GridLogDebug.Active(0);
|
||||
GridLogPerformance.Active(0);
|
||||
GridLogComms.Active(0);
|
||||
GridLogDslash.Active(0);
|
||||
GridLogMemory.Active(0);
|
||||
|
||||
// ── CLI ──────────────────────────────────────────────────────────────────
|
||||
std::string snapdir = ".";
|
||||
std::string pngdir = "";
|
||||
int first = 0, last = 1920, step = 1;
|
||||
int slice_dim = 3, sliceval = 0; // default: fix t=0, display xyz
|
||||
double iso_rms = 1.0;
|
||||
double fixed_iso = -1.0; // if >0, use this absolute contour
|
||||
double tau_start = -1.0; // if >=0, display MD time tau = tau_start + idx*tau_step
|
||||
double tau_step = 0.0;
|
||||
// Heatmap mode: fix two dimensions, show 2D colour map
|
||||
bool do_heatmap = false;
|
||||
int slice_dim2 = -1, sliceval2 = 0;
|
||||
double heat_scale = -1.0; // if >0, fixed symmetric colour scale; else auto
|
||||
Coordinate hotsite({0,0,0,0});
|
||||
bool has_hotsite = false;
|
||||
|
||||
std::string arg;
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--snapdir"))
|
||||
snapdir = GridCmdOptionPayload(argv, argv+argc, "--snapdir");
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--pngdir"))
|
||||
pngdir = GridCmdOptionPayload(argv, argv+argc, "--pngdir");
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--first")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--first");
|
||||
GridCmdOptionInt(arg, first);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--last")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--last");
|
||||
GridCmdOptionInt(arg, last);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--step")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--step");
|
||||
GridCmdOptionInt(arg, step);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--slicedim")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--slicedim");
|
||||
GridCmdOptionInt(arg, slice_dim);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--sliceval")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--sliceval");
|
||||
GridCmdOptionInt(arg, sliceval);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--isosurface")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--isosurface");
|
||||
GridCmdOptionFloat(arg, iso_rms);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--fixediso")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--fixediso");
|
||||
GridCmdOptionFloat(arg, fixed_iso);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--taustart")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--taustart");
|
||||
GridCmdOptionFloat(arg, tau_start);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--taustep")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--taustep");
|
||||
GridCmdOptionFloat(arg, tau_step);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--hotsite")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--hotsite");
|
||||
GridCmdOptionIntVector(arg, hotsite);
|
||||
has_hotsite = true;
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--heatmap"))
|
||||
do_heatmap = true;
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--slicedim2")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--slicedim2");
|
||||
GridCmdOptionInt(arg, slice_dim2);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--sliceval2")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--sliceval2");
|
||||
GridCmdOptionInt(arg, sliceval2);
|
||||
}
|
||||
if (GridCmdOptionExists(argv, argv+argc, "--heatscale")) {
|
||||
arg = GridCmdOptionPayload(argv, argv+argc, "--heatscale");
|
||||
GridCmdOptionFloat(arg, heat_scale);
|
||||
}
|
||||
|
||||
bool do_png = !pngdir.empty();
|
||||
if (do_png) mkdir(pngdir.c_str(), 0755);
|
||||
|
||||
// ── Grid setup ───────────────────────────────────────────────────────────
|
||||
auto latt_size = GridDefaultLatt();
|
||||
auto simd_layout = GridDefaultSimd(Nd, vComplex::Nsimd());
|
||||
auto mpi_layout = GridDefaultMpi();
|
||||
GridCartesian grid(latt_size, simd_layout, mpi_layout);
|
||||
LatticeComplexD field(&grid);
|
||||
|
||||
// Force components
|
||||
struct ForceSpec { std::string prefix; std::string label; };
|
||||
std::vector<ForceSpec> forces = {
|
||||
{ "F_IwasakiGaugeAction_lat", "Gauge_lat" },
|
||||
{ "F_IwasakiGaugeAction_smr", "Gauge_smr" },
|
||||
{ "F_JacobianAction_smr", "Jacobian" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.0047_det_0.05_lat", "Ferm0047_lat" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.0047_det_0.05_smr", "Ferm0047_smr" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.05_det_0.1_lat", "Ferm005_lat" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.1_det_0.25_lat", "Ferm01_lat" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.25_det_0.5_lat", "Ferm025_lat" },
|
||||
{ "F_TwoFlavourEvenOddRatioPseudoFermionActiondet_0.5_det_1_lat", "Ferm05_lat" },
|
||||
};
|
||||
|
||||
// ── Stdout header ─────────────────────────────────────────────────────────
|
||||
std::cerr << "idx";
|
||||
for (auto& fs : forces) {
|
||||
std::cerr << "\t" << fs.label << "_rms";
|
||||
if (has_hotsite) std::cerr << "\t" << fs.label << "_hot";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
|
||||
// ── Persistent render contexts (one GPU context for all frames) ──────────
|
||||
std::unique_ptr<RenderCtx> ctx; // isosurface mode
|
||||
std::unique_ptr<HeatmapCtx> hctx; // heatmap mode
|
||||
|
||||
// ── Main loop ─────────────────────────────────────────────────────────────
|
||||
for (int idx = first; idx <= last; idx += step) {
|
||||
std::cerr << idx;
|
||||
|
||||
for (auto& fs : forces) {
|
||||
std::string fname = snapdir + "/" + fs.prefix + "." + std::to_string(idx);
|
||||
|
||||
if (!tryReadFile(field, fname)) {
|
||||
std::cerr << "\t-";
|
||||
if (has_hotsite) std::cerr << "\t-";
|
||||
continue;
|
||||
}
|
||||
|
||||
// RMS (real part)
|
||||
RealD sumsq = 0.0;
|
||||
for (int i = 0; i < grid.gSites(); i++) {
|
||||
Coordinate site;
|
||||
Lexicographic::CoorFromIndex(site, i, latt_size);
|
||||
RealD v = real(peekSite(field, site));
|
||||
sumsq += v * v;
|
||||
}
|
||||
RealD rms = std::sqrt(sumsq / grid.gSites());
|
||||
std::cerr << "\t" << rms;
|
||||
|
||||
if (has_hotsite) {
|
||||
RealD hval = real(TensorRemove(peekSite(field, hotsite)));
|
||||
std::cerr << "\t" << hval;
|
||||
}
|
||||
|
||||
// PNG output (isosurface or heatmap)
|
||||
if (do_png) {
|
||||
// Build title string
|
||||
std::ostringstream title;
|
||||
title << fs.label << " ";
|
||||
if (tau_start >= 0.0 && tau_step > 0.0) {
|
||||
double tau = tau_start + idx * tau_step;
|
||||
title << std::fixed << std::setprecision(6) << "tau=" << tau;
|
||||
} else {
|
||||
title << "idx=" << idx;
|
||||
}
|
||||
title << " rms=" << std::scientific << std::setprecision(3) << rms;
|
||||
|
||||
std::ostringstream outpath;
|
||||
outpath << pngdir << "/" << fs.label
|
||||
<< "_" << std::setfill('0') << std::setw(6) << idx << ".png";
|
||||
|
||||
if (do_heatmap && slice_dim2 >= 0) {
|
||||
// ── Heatmap mode ────────────────────────────────────────
|
||||
// Display dims = the two that are NOT fixed
|
||||
std::vector<int> disp;
|
||||
for (int d = 0; d < 4; d++)
|
||||
if (d != slice_dim && d != slice_dim2) disp.push_back(d);
|
||||
|
||||
if (!hctx) {
|
||||
double sc = (heat_scale > 0) ? heat_scale : rms * 20.0;
|
||||
// Hotsite projection onto display plane
|
||||
int hx = -1, hy = -1;
|
||||
if (has_hotsite) {
|
||||
hx = hotsite[disp[0]];
|
||||
hy = hotsite[disp[1]];
|
||||
}
|
||||
hctx = std::make_unique<HeatmapCtx>();
|
||||
hctx->init(latt_size[disp[0]], latt_size[disp[1]], sc, hx, hy);
|
||||
}
|
||||
|
||||
title << " scale=+-" << std::fixed << std::setprecision(4) << hctx->scale;
|
||||
renderHeatmap(*hctx, field, latt_size,
|
||||
slice_dim, sliceval,
|
||||
slice_dim2, sliceval2,
|
||||
title.str(), outpath.str());
|
||||
} else {
|
||||
// ── Isosurface mode ─────────────────────────────────────
|
||||
double contour = (fixed_iso > 0) ? fixed_iso : iso_rms * rms;
|
||||
title << " iso=" << contour;
|
||||
|
||||
if (!ctx) {
|
||||
std::vector<int> disp;
|
||||
for (int d = 0; d < 4; d++) if (d != slice_dim) disp.push_back(d);
|
||||
ctx = std::make_unique<RenderCtx>();
|
||||
ctx->init(latt_size[disp[0]], latt_size[disp[1]], latt_size[disp[2]]);
|
||||
}
|
||||
renderPNG(*ctx, field, latt_size, slice_dim, sliceval,
|
||||
contour, title.str(), outpath.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cerr << "\n";
|
||||
std::cerr.flush();
|
||||
}
|
||||
|
||||
Grid_finalize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
// TranscriptToVideo.cxx
|
||||
//
|
||||
// Reads a conversation transcript file with [User] / [Claude] turns and
|
||||
// renders it to an AVI using vtkFFMPEGWriter at 1280x720, 10 fps.
|
||||
//
|
||||
// Transcript format:
|
||||
// [USER] Some question or command, possibly spanning
|
||||
// multiple continuation lines.
|
||||
// [ASSISTANT] A response, also possibly
|
||||
// spanning multiple lines.
|
||||
// ...
|
||||
//
|
||||
// Rules:
|
||||
// - A line beginning "[User]" or "[Claude]" starts a new turn.
|
||||
// - Any subsequent non-blank line that does NOT begin with "[" is a
|
||||
// continuation of the previous turn (joined with a single space).
|
||||
// - Blank lines are ignored.
|
||||
//
|
||||
// Usage:
|
||||
// ./TranscriptToVideo <transcript.txt> <output.avi>
|
||||
//
|
||||
// Typewriter speed : 10 chars/sec → 1 frame/char at 10 fps
|
||||
// Pause after turn : 0.5 s → 5 frames
|
||||
// Word-wrap column : 62
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkActor2D.h>
|
||||
#include <vtkCellArray.h>
|
||||
#include <vtkFFMPEGWriter.h>
|
||||
#include <vtkNamedColors.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPoints.h>
|
||||
#include <vtkPolyData.h>
|
||||
#include <vtkPolyDataMapper2D.h>
|
||||
#include <vtkProperty2D.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkTextActor.h>
|
||||
#include <vtkTextProperty.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
static const int WIDTH = 1280;
|
||||
static const int HEIGHT = 720;
|
||||
static const int FPS = 10;
|
||||
static const int CHARS_PER_FRAME = 5; // 50 chars/sec at 10 fps
|
||||
static const int PAUSE_FRAMES = 3; // 0.3 s
|
||||
static const int WRAP_COLS = 60;
|
||||
static const int MAX_HISTORY = 18; // visible completed lines
|
||||
static const int FONT_SIZE = 18;
|
||||
static const int TITLE_SIZE = 23;
|
||||
static const int LINE_HEIGHT = 28; // pixels between lines
|
||||
static const int MARGIN_LEFT = 48;
|
||||
static const int MARGIN_TOP = 58; // below title bar
|
||||
|
||||
// Colours (R, G, B in 0–1)
|
||||
static const double COL_BG[3] = { 0.04, 0.04, 0.10 }; // near-black navy
|
||||
static const double COL_USER[3] = { 1.00, 0.84, 0.00 }; // gold
|
||||
static const double COL_CLAUDE[3] = { 0.68, 0.85, 0.90 }; // light steel blue
|
||||
static const double COL_TITLE[3] = { 1.00, 1.00, 1.00 }; // white
|
||||
static const double COL_BAR[3] = { 0.30, 0.55, 0.80 }; // progress bar blue
|
||||
static const double COL_LABEL[3] = { 0.65, 0.65, 0.65 }; // dim grey for speaker tag
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structs
|
||||
// ---------------------------------------------------------------------------
|
||||
enum class Speaker { User, Claude, Caption };
|
||||
|
||||
struct Turn {
|
||||
Speaker speaker;
|
||||
std::string text; // full unwrapped text
|
||||
};
|
||||
|
||||
// A rendered display line (already word-wrapped, tagged with speaker)
|
||||
struct DisplayLine {
|
||||
Speaker speaker;
|
||||
std::string prefix; // "[USER] " or "[ASSISTANT] "
|
||||
std::string body; // wrapped segment
|
||||
bool isFirst; // first line of this turn (prefix printed)
|
||||
bool isBlank; // spacer between turns (no text rendered)
|
||||
bool isCaption; // caption update — body holds text (empty = clear)
|
||||
DisplayLine() : speaker(Speaker::User), isFirst(false), isBlank(false), isCaption(false) {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Replace common multi-byte UTF-8 sequences with ASCII equivalents so that
|
||||
// VTK's font renderer (which only handles ASCII reliably) does not crash.
|
||||
// Any remaining non-ASCII byte is replaced with '?'.
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::string SanitiseASCII(const std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
const unsigned char* p = (const unsigned char*)s.data();
|
||||
const unsigned char* end = p + s.size();
|
||||
while (p < end) {
|
||||
unsigned char c = *p;
|
||||
if (c < 0x80) {
|
||||
out += (char)c;
|
||||
++p;
|
||||
} else if (c == 0xE2 && (p + 2) < end) {
|
||||
// 3-byte sequence starting 0xE2
|
||||
unsigned char b1 = *(p+1), b2 = *(p+2);
|
||||
// U+2018 ' U+2019 ' (0xE2 0x80 0x98 / 0x99)
|
||||
if (b1 == 0x80 && (b2 == 0x98 || b2 == 0x99)) { out += '\''; p += 3; }
|
||||
// U+201C " U+201D " (0xE2 0x80 0x9C / 0x9D)
|
||||
else if (b1 == 0x80 && (b2 == 0x9C || b2 == 0x9D)) { out += '"'; p += 3; }
|
||||
// U+2013 en-dash (0xE2 0x80 0x93)
|
||||
else if (b1 == 0x80 && b2 == 0x93) { out += '-'; p += 3; }
|
||||
// U+2014 em-dash (0xE2 0x80 0x94)
|
||||
else if (b1 == 0x80 && b2 == 0x94) { out += '-'; p += 3; }
|
||||
// U+2026 ellipsis (0xE2 0x80 0xA6)
|
||||
else if (b1 == 0x80 && b2 == 0xA6) { out += "..."; p += 3; }
|
||||
// U+2192 arrow (0xE2 0x86 0x92)
|
||||
else if (b1 == 0x86 && b2 == 0x92) { out += "->"; p += 3; }
|
||||
// U+00D7 multiplication sign (0xC3 0x97) — caught below, but
|
||||
// U+22xx math operators: replace with '~'
|
||||
else { out += '~'; p += 3; }
|
||||
} else if (c == 0xC3 && (p + 1) < end) {
|
||||
// 2-byte latin-1 supplement
|
||||
unsigned char b1 = *(p+1);
|
||||
if (b1 == 0x97) { out += 'x'; p += 2; } // U+00D7 ×
|
||||
else if (b1 == 0xB7) { out += '/'; p += 2; } // U+00F7 ÷
|
||||
else { out += '?'; p += 2; }
|
||||
} else {
|
||||
// Unknown multi-byte: skip the whole sequence
|
||||
out += '?';
|
||||
++p;
|
||||
while (p < end && (*p & 0xC0) == 0x80) ++p; // skip continuation bytes
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Strip markdown emphasis markers (* and `) so they don't appear in the video.
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::string StripMarkdown(const std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (c == '*' || c == '`') continue;
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Word-wrap: splits `text` into lines of at most maxCols characters.
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::vector<std::string> WrapText(const std::string& text, int maxCols)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream words(text);
|
||||
std::string word, line;
|
||||
while (words >> word) {
|
||||
if (!line.empty() && (int)(line.size() + 1 + word.size()) > maxCols) {
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
} else {
|
||||
if (!line.empty()) line += ' ';
|
||||
line += word;
|
||||
}
|
||||
}
|
||||
if (!line.empty()) lines.push_back(line);
|
||||
if (lines.empty()) lines.push_back("");
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse transcript file into list of Turns.
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::vector<Turn> ParseTranscript(const std::string& path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
if (!f) {
|
||||
std::cerr << "Cannot open transcript: " << path << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
std::vector<Turn> turns;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
// Trim trailing CR (Windows files)
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
|
||||
if (line.empty()) {
|
||||
// Blank line within a dialogue turn = paragraph break.
|
||||
// Caption turns don't support paragraph breaks.
|
||||
if (!turns.empty() && turns.back().speaker != Speaker::Caption) {
|
||||
turns.back().text += '\n'; // '\n' sentinel: expands to blank spacer
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.size() >= 6 && line.substr(0, 6) == "[USER]") {
|
||||
Turn t;
|
||||
t.speaker = Speaker::User;
|
||||
t.text = line.substr(6);
|
||||
while (!t.text.empty() && t.text.front() == ' ') t.text.erase(t.text.begin());
|
||||
t.text = SanitiseASCII(t.text);
|
||||
turns.push_back(std::move(t));
|
||||
} else if (line.size() >= 11 && line.substr(0, 11) == "[ASSISTANT]") {
|
||||
Turn t;
|
||||
t.speaker = Speaker::Claude;
|
||||
t.text = line.substr(11);
|
||||
while (!t.text.empty() && t.text.front() == ' ') t.text.erase(t.text.begin());
|
||||
t.text = SanitiseASCII(t.text);
|
||||
turns.push_back(std::move(t));
|
||||
} else if (line.size() >= 9 && line.substr(0, 9) == "[CAPTION]") {
|
||||
Turn t;
|
||||
t.speaker = Speaker::Caption;
|
||||
t.text = line.substr(9);
|
||||
while (!t.text.empty() && t.text.front() == ' ') t.text.erase(t.text.begin());
|
||||
t.text = SanitiseASCII(t.text);
|
||||
turns.push_back(std::move(t));
|
||||
} else if (!turns.empty()) {
|
||||
// Continuation line — strip leading whitespace, preserve as separate line
|
||||
size_t start = line.find_first_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
if (!turns.back().text.empty()) turns.back().text += '\n';
|
||||
turns.back().text += SanitiseASCII(line.substr(start));
|
||||
}
|
||||
}
|
||||
}
|
||||
return turns;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Expand all Turns into DisplayLines (word-wrapped).
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::vector<DisplayLine> ExpandToDisplayLines(const std::vector<Turn>& turns)
|
||||
{
|
||||
std::vector<DisplayLine> out;
|
||||
// Prefix widths kept equal for alignment
|
||||
const std::string userPfx = "[USER] ";
|
||||
const std::string claudePfx = "[ASSISTANT] ";
|
||||
|
||||
// Track previous non-caption speaker to know when to insert blank spacers
|
||||
Speaker prevSpeaker = Speaker::Caption; // sentinel: no spacer before first real turn
|
||||
for (size_t ti = 0; ti < turns.size(); ++ti) {
|
||||
const Turn& t = turns[ti];
|
||||
|
||||
// Caption turn: emit one special DisplayLine, no spacer, no history entry
|
||||
if (t.speaker == Speaker::Caption) {
|
||||
DisplayLine dl;
|
||||
dl.isCaption = true;
|
||||
dl.speaker = Speaker::Caption;
|
||||
// body: whitespace-only → clear; otherwise wrap lines joined with \n
|
||||
std::string trimmed = t.text;
|
||||
size_t first = trimmed.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) {
|
||||
dl.body = ""; // signal to clear caption
|
||||
} else {
|
||||
// Wrap to ~90 cols for the wider caption zone
|
||||
auto lines = WrapText(trimmed, 90);
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
if (i > 0) dl.body += "\n";
|
||||
dl.body += lines[i];
|
||||
}
|
||||
}
|
||||
out.push_back(dl);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert a blank spacer before each new dialogue turn (not before the first)
|
||||
if (prevSpeaker != Speaker::Caption) {
|
||||
DisplayLine blank;
|
||||
blank.isBlank = true;
|
||||
blank.speaker = t.speaker;
|
||||
out.push_back(blank);
|
||||
}
|
||||
prevSpeaker = t.speaker;
|
||||
|
||||
const std::string& pfx = (t.speaker == Speaker::User) ? userPfx : claudePfx;
|
||||
int bodyWidth = WRAP_COLS - (int)pfx.size();
|
||||
if (bodyWidth < 20) bodyWidth = 20;
|
||||
|
||||
// Split the turn text on '\n' to get individual source lines.
|
||||
// Empty source lines become blank spacers (paragraph breaks within a turn).
|
||||
// Non-empty source lines are stripped of markdown markers and word-wrapped.
|
||||
std::vector<std::string> srcLines;
|
||||
{
|
||||
std::string seg;
|
||||
for (char ch : t.text) {
|
||||
if (ch == '\n') { srcLines.push_back(seg); seg.clear(); }
|
||||
else seg += ch;
|
||||
}
|
||||
srcLines.push_back(seg);
|
||||
}
|
||||
|
||||
bool firstOfTurn = true;
|
||||
for (const auto& srcLine : srcLines) {
|
||||
// Strip markdown emphasis markers
|
||||
std::string stripped = StripMarkdown(srcLine);
|
||||
// Trim leading/trailing whitespace
|
||||
size_t f = stripped.find_first_not_of(" \t");
|
||||
if (f == std::string::npos) {
|
||||
// Blank — paragraph break spacer within the turn
|
||||
DisplayLine blank;
|
||||
blank.isBlank = true;
|
||||
blank.speaker = t.speaker;
|
||||
out.push_back(blank);
|
||||
continue;
|
||||
}
|
||||
size_t l = stripped.find_last_not_of(" \t");
|
||||
stripped = stripped.substr(f, l - f + 1);
|
||||
|
||||
auto wrapped = WrapText(stripped, bodyWidth);
|
||||
for (size_t i = 0; i < wrapped.size(); ++i) {
|
||||
DisplayLine dl;
|
||||
dl.speaker = t.speaker;
|
||||
dl.prefix = pfx;
|
||||
dl.body = wrapped[i];
|
||||
dl.isFirst = firstOfTurn && (i == 0);
|
||||
out.push_back(dl);
|
||||
}
|
||||
firstOfTurn = false;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: set actor text to `s`, configure font/colour, position.
|
||||
// ---------------------------------------------------------------------------
|
||||
static void ConfigureTextActor(vtkTextActor* a, int fontSize,
|
||||
double r, double g, double b)
|
||||
{
|
||||
a->GetTextProperty()->SetFontFamilyToCourier();
|
||||
a->GetTextProperty()->SetFontSize(fontSize);
|
||||
a->GetTextProperty()->SetColor(r, g, b);
|
||||
a->GetTextProperty()->SetBold(0);
|
||||
a->GetTextProperty()->SetItalic(0);
|
||||
a->GetTextProperty()->ShadowOff();
|
||||
a->GetTextProperty()->SetJustificationToLeft();
|
||||
a->GetTextProperty()->SetVerticalJustificationToBottom();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Create a thin horizontal progress bar actor (2D polygon).
|
||||
// Returns the actor; caller adds to renderer.
|
||||
// ---------------------------------------------------------------------------
|
||||
static vtkActor2D* MakeProgressBar(vtkPolyData*& pd, vtkPoints*& pts)
|
||||
{
|
||||
pts = vtkPoints::New();
|
||||
vtkCellArray* cells = vtkCellArray::New();
|
||||
|
||||
// 4 points, updated every frame
|
||||
pts->SetNumberOfPoints(4);
|
||||
pts->SetPoint(0, 0, HEIGHT - 8, 0);
|
||||
pts->SetPoint(1, 0, HEIGHT - 2, 0);
|
||||
pts->SetPoint(2, 100, HEIGHT - 2, 0);
|
||||
pts->SetPoint(3, 100, HEIGHT - 8, 0);
|
||||
|
||||
vtkIdType quad[4] = { 0, 1, 2, 3 };
|
||||
cells->InsertNextCell(4, quad);
|
||||
|
||||
pd = vtkPolyData::New();
|
||||
pd->SetPoints(pts);
|
||||
pd->SetPolys(cells);
|
||||
cells->Delete();
|
||||
|
||||
vtkPolyDataMapper2D* mapper = vtkPolyDataMapper2D::New();
|
||||
mapper->SetInputData(pd);
|
||||
|
||||
vtkActor2D* actor = vtkActor2D::New();
|
||||
actor->SetMapper(mapper);
|
||||
actor->GetProperty()->SetColor(COL_BAR[0], COL_BAR[1], COL_BAR[2]);
|
||||
mapper->Delete();
|
||||
return actor;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: TranscriptToVideo <transcript.txt> <output.avi>\n";
|
||||
return 1;
|
||||
}
|
||||
const std::string transcriptPath = argv[1];
|
||||
const std::string outputPath = argv[2];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Parse & expand
|
||||
// -----------------------------------------------------------------------
|
||||
auto turns = ParseTranscript(transcriptPath);
|
||||
auto displayLines = ExpandToDisplayLines(turns);
|
||||
const int totalLines = (int)displayLines.size();
|
||||
|
||||
if (totalLines == 0) {
|
||||
std::cerr << "Transcript has no parseable turns.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Total frames (rough estimate for progress bar denominator)
|
||||
// Each display line: on average ~40 chars + PAUSE_FRAMES
|
||||
// We'll compute exact total below after knowing char counts.
|
||||
int totalFrames = 0;
|
||||
for (const auto& dl : displayLines) {
|
||||
int chars = (int)dl.body.size();
|
||||
totalFrames += (chars + CHARS_PER_FRAME - 1) / CHARS_PER_FRAME + 1 + PAUSE_FRAMES;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// VTK Setup
|
||||
// -----------------------------------------------------------------------
|
||||
vtkNew<vtkRenderer> ren;
|
||||
vtkNew<vtkRenderWindow> renWin;
|
||||
|
||||
ren->SetBackground(COL_BG[0], COL_BG[1], COL_BG[2]);
|
||||
renWin->AddRenderer(ren);
|
||||
renWin->SetSize(WIDTH, HEIGHT);
|
||||
renWin->SetOffScreenRendering(1);
|
||||
renWin->SetMultiSamples(0);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Title actor
|
||||
// -----------------------------------------------------------------------
|
||||
vtkNew<vtkTextActor> titleActor;
|
||||
titleActor->SetInput("Transcript");
|
||||
ConfigureTextActor(titleActor, TITLE_SIZE,
|
||||
COL_TITLE[0], COL_TITLE[1], COL_TITLE[2]);
|
||||
titleActor->GetTextProperty()->SetBold(1);
|
||||
titleActor->GetTextProperty()->SetJustificationToCentered();
|
||||
titleActor->SetDisplayPosition(WIDTH / 2, HEIGHT - 36);
|
||||
ren->AddActor2D(titleActor);
|
||||
|
||||
// Thin separator line below title (drawn as a narrow quad)
|
||||
{
|
||||
vtkPoints* lpts = vtkPoints::New();
|
||||
vtkCellArray* lcell = vtkCellArray::New();
|
||||
lpts->InsertNextPoint(0, HEIGHT - 44, 0);
|
||||
lpts->InsertNextPoint(WIDTH, HEIGHT - 44, 0);
|
||||
lpts->InsertNextPoint(WIDTH, HEIGHT - 42, 0);
|
||||
lpts->InsertNextPoint(0, HEIGHT - 42, 0);
|
||||
vtkIdType q[4] = {0,1,2,3};
|
||||
lcell->InsertNextCell(4, q);
|
||||
vtkPolyData* lpd = vtkPolyData::New();
|
||||
lpd->SetPoints(lpts);
|
||||
lpd->SetPolys(lcell);
|
||||
vtkPolyDataMapper2D* lmap = vtkPolyDataMapper2D::New();
|
||||
lmap->SetInputData(lpd);
|
||||
vtkActor2D* lineActor = vtkActor2D::New();
|
||||
lineActor->SetMapper(lmap);
|
||||
lineActor->GetProperty()->SetColor(0.3, 0.3, 0.5);
|
||||
ren->AddActor2D(lineActor);
|
||||
lpts->Delete(); lcell->Delete(); lpd->Delete();
|
||||
lmap->Delete(); lineActor->Delete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// History text actors (MAX_HISTORY lines, reused with shifting content)
|
||||
// -----------------------------------------------------------------------
|
||||
std::vector<vtkTextActor*> histActors(MAX_HISTORY);
|
||||
for (int i = 0; i < MAX_HISTORY; ++i) {
|
||||
histActors[i] = vtkTextActor::New();
|
||||
histActors[i]->SetInput("");
|
||||
ConfigureTextActor(histActors[i], FONT_SIZE, 0.5, 0.5, 0.5);
|
||||
// Position: bottom of history area = just above current-line area
|
||||
int y = MARGIN_TOP + (MAX_HISTORY - 1 - i) * LINE_HEIGHT;
|
||||
histActors[i]->SetDisplayPosition(MARGIN_LEFT, HEIGHT - y);
|
||||
ren->AddActor2D(histActors[i]);
|
||||
}
|
||||
|
||||
// Current (actively typing) line actor — two actors: prefix + body
|
||||
vtkNew<vtkTextActor> curPfxActor; // "[User] " in dim colour
|
||||
vtkNew<vtkTextActor> curBodyActor; // body text in vivid colour
|
||||
vtkNew<vtkTextActor> cursorActor; // blinking block
|
||||
|
||||
ConfigureTextActor(curPfxActor, FONT_SIZE, COL_LABEL[0], COL_LABEL[1], COL_LABEL[2]);
|
||||
ConfigureTextActor(curBodyActor, FONT_SIZE, 1, 1, 1); // will be overridden per turn
|
||||
ConfigureTextActor(cursorActor, FONT_SIZE, 1, 1, 1);
|
||||
|
||||
cursorActor->SetInput("|");
|
||||
|
||||
int curLineY = HEIGHT - (MARGIN_TOP + MAX_HISTORY * LINE_HEIGHT);
|
||||
// Place current line below history
|
||||
curPfxActor->SetDisplayPosition(MARGIN_LEFT, curLineY);
|
||||
ren->AddActor2D(curPfxActor);
|
||||
ren->AddActor2D(curBodyActor);
|
||||
ren->AddActor2D(cursorActor);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Progress bar
|
||||
// -----------------------------------------------------------------------
|
||||
vtkPolyData* barPD = nullptr;
|
||||
vtkPoints* barPts = nullptr;
|
||||
vtkActor2D* barActor = MakeProgressBar(barPD, barPts);
|
||||
ren->AddActor2D(barActor);
|
||||
|
||||
// Progress label
|
||||
vtkNew<vtkTextActor> progLabelActor;
|
||||
progLabelActor->SetInput("");
|
||||
ConfigureTextActor(progLabelActor, 13,
|
||||
COL_LABEL[0], COL_LABEL[1], COL_LABEL[2]);
|
||||
progLabelActor->SetDisplayPosition(MARGIN_LEFT, 12);
|
||||
ren->AddActor2D(progLabelActor);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Caption actor — bottom-centre, Arial, white, initially hidden
|
||||
// -----------------------------------------------------------------------
|
||||
vtkNew<vtkTextActor> captionActor;
|
||||
captionActor->SetInput("");
|
||||
captionActor->GetTextProperty()->SetFontFamilyToArial();
|
||||
captionActor->GetTextProperty()->SetFontSize(20);
|
||||
captionActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0);
|
||||
captionActor->GetTextProperty()->SetBold(0);
|
||||
captionActor->GetTextProperty()->SetItalic(1);
|
||||
captionActor->GetTextProperty()->ShadowOn();
|
||||
captionActor->GetTextProperty()->SetShadowOffset(1, -1);
|
||||
captionActor->GetTextProperty()->SetJustificationToCentered();
|
||||
captionActor->GetTextProperty()->SetVerticalJustificationToBottom();
|
||||
// Position: centred horizontally, in the gap between typing line and progress label
|
||||
captionActor->SetDisplayPosition(WIDTH / 2, 32);
|
||||
ren->AddActor2D(captionActor);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// FFMPEG writer
|
||||
// -----------------------------------------------------------------------
|
||||
vtkNew<vtkWindowToImageFilter> w2i;
|
||||
w2i->SetInput(renWin);
|
||||
w2i->SetScale(1);
|
||||
w2i->ReadFrontBufferOff();
|
||||
|
||||
vtkNew<vtkFFMPEGWriter> writer;
|
||||
writer->SetInputConnection(w2i->GetOutputPort());
|
||||
writer->SetFileName(outputPath.c_str());
|
||||
writer->SetRate(FPS);
|
||||
writer->SetBitRate(4000);
|
||||
writer->SetBitRateTolerance(400);
|
||||
writer->Start();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helper: measure pixel width of a string in the current font
|
||||
// (approximate: Courier is monospace, so width ≈ chars × charWidth)
|
||||
// At font size 17 in Courier, one character ≈ 10.2px wide.
|
||||
// -----------------------------------------------------------------------
|
||||
const double CHAR_PX = 10.2;
|
||||
|
||||
auto bodyX = [&](const std::string& pfx) -> int {
|
||||
return MARGIN_LEFT + (int)(pfx.size() * CHAR_PX);
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Render one frame
|
||||
// -----------------------------------------------------------------------
|
||||
int frameCount = 0;
|
||||
auto renderFrame = [&]() {
|
||||
renWin->Render();
|
||||
w2i->Modified();
|
||||
writer->Write();
|
||||
++frameCount;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// History ring — holds completed display lines (most-recent last)
|
||||
// -----------------------------------------------------------------------
|
||||
std::vector<DisplayLine> history;
|
||||
|
||||
auto refreshHistory = [&]() {
|
||||
// slot 0 = bottom row (newest), slot MAX_HISTORY-1 = top row (oldest).
|
||||
// Brightness fades linearly from 0.85 (slot 0) to 0.20 (slot MAX-1).
|
||||
// Blank spacer lines show as empty strings.
|
||||
// The [USER]/[ASSISTANT] prefix stays at a fixed bright level so the
|
||||
// speaker is always identifiable even in dim history.
|
||||
int n = (int)history.size();
|
||||
for (int slot = 0; slot < MAX_HISTORY; ++slot) {
|
||||
int idx = n - 1 - slot; // slot 0 → newest, slot MAX-1 → oldest
|
||||
if (idx < 0) {
|
||||
histActors[slot]->SetInput("");
|
||||
continue;
|
||||
}
|
||||
const auto& hl = history[idx];
|
||||
if (hl.isBlank) {
|
||||
histActors[slot]->SetInput("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Graduated brightness: bright near bottom, dim near top
|
||||
double bodyBright = 0.20 + 0.65 * (1.0 - (double)slot / (MAX_HISTORY - 1));
|
||||
|
||||
const double* col = (hl.speaker == Speaker::User) ? COL_USER : COL_CLAUDE;
|
||||
|
||||
if (hl.isFirst) {
|
||||
// Prefix stays vivid; body fades
|
||||
// We render prefix + body as one string but colour the whole line
|
||||
// at body brightness — a compromise since VTK TextActor is single-colour.
|
||||
// Use a slightly higher floor for the prefix line so the tag is readable.
|
||||
double pfxBright = std::min(1.0, bodyBright + 0.25);
|
||||
std::string txt = hl.prefix + hl.body;
|
||||
histActors[slot]->SetInput(txt.c_str());
|
||||
histActors[slot]->GetTextProperty()->SetColor(
|
||||
col[0] * pfxBright, col[1] * pfxBright, col[2] * pfxBright);
|
||||
} else {
|
||||
std::string txt = std::string(hl.prefix.size(), ' ') + hl.body;
|
||||
histActors[slot]->SetInput(txt.c_str());
|
||||
histActors[slot]->GetTextProperty()->SetColor(
|
||||
col[0] * bodyBright, col[1] * bodyBright, col[2] * bodyBright);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main animation loop
|
||||
// -----------------------------------------------------------------------
|
||||
int dlIdx = 0; // index into displayLines
|
||||
|
||||
// Count total turns to feed into progress label
|
||||
// (display line index → turn index: count isFirst lines)
|
||||
int totalTurns = 0;
|
||||
for (const auto& dl : displayLines) if (dl.isFirst) ++totalTurns;
|
||||
int turnsSeen = 0;
|
||||
|
||||
for (int li = 0; li < totalLines; ++li) {
|
||||
const DisplayLine& dl = displayLines[li];
|
||||
|
||||
// Caption update: swap the bottom caption, no history, no typewriter
|
||||
if (dl.isCaption) {
|
||||
captionActor->SetInput(dl.body.c_str());
|
||||
renderFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blank spacer: push to history silently with no typewriter frames
|
||||
if (dl.isBlank) {
|
||||
history.push_back(dl);
|
||||
refreshHistory();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vivid colour for current speaker
|
||||
double cr, cg, cb;
|
||||
if (dl.speaker == Speaker::User) {
|
||||
cr = COL_USER[0]; cg = COL_USER[1]; cb = COL_USER[2];
|
||||
} else {
|
||||
cr = COL_CLAUDE[0]; cg = COL_CLAUDE[1]; cb = COL_CLAUDE[2];
|
||||
}
|
||||
curBodyActor->GetTextProperty()->SetColor(cr, cg, cb);
|
||||
cursorActor->GetTextProperty()->SetColor(cr, cg, cb);
|
||||
|
||||
// Prefix: vivid speaker colour for first line, indent for continuation
|
||||
if (dl.isFirst) {
|
||||
curPfxActor->SetInput(dl.prefix.c_str());
|
||||
curPfxActor->GetTextProperty()->SetColor(cr, cg, cb);
|
||||
} else {
|
||||
curPfxActor->SetInput(std::string(dl.prefix.size(), ' ').c_str());
|
||||
curPfxActor->GetTextProperty()->SetColor(0, 0, 0);
|
||||
}
|
||||
|
||||
int bx = bodyX(dl.prefix);
|
||||
curBodyActor->SetDisplayPosition(bx, curLineY);
|
||||
cursorActor->SetDisplayPosition(bx, curLineY);
|
||||
|
||||
if (dl.isFirst) ++turnsSeen;
|
||||
|
||||
// Update progress label
|
||||
{
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "Turn %d / %d", turnsSeen, totalTurns);
|
||||
progLabelActor->SetInput(buf);
|
||||
}
|
||||
|
||||
// Update progress bar width
|
||||
{
|
||||
double frac = (totalFrames > 0) ? (double)frameCount / totalFrames : 0.0;
|
||||
double barW = frac * (WIDTH - 2 * MARGIN_LEFT);
|
||||
barPts->SetPoint(0, MARGIN_LEFT, HEIGHT - 8, 0);
|
||||
barPts->SetPoint(1, MARGIN_LEFT, HEIGHT - 2, 0);
|
||||
barPts->SetPoint(2, MARGIN_LEFT + barW, HEIGHT - 2, 0);
|
||||
barPts->SetPoint(3, MARGIN_LEFT + barW, HEIGHT - 8, 0);
|
||||
barPts->Modified();
|
||||
barPD->Modified();
|
||||
}
|
||||
|
||||
// Typewriter: reveal CHARS_PER_FRAME characters per rendered frame.
|
||||
// Always render the fully-complete state last.
|
||||
const std::string& body = dl.body;
|
||||
int ci = 0;
|
||||
while (true) {
|
||||
curBodyActor->SetInput(body.substr(0, ci).c_str());
|
||||
double cx = bx + ci * CHAR_PX;
|
||||
cursorActor->SetDisplayPosition((int)cx, curLineY);
|
||||
cursorActor->SetVisibility((frameCount % 2 == 0) ? 1 : 0);
|
||||
renderFrame();
|
||||
if (ci >= (int)body.size()) break;
|
||||
ci = std::min(ci + CHARS_PER_FRAME, (int)body.size());
|
||||
}
|
||||
|
||||
// Line is complete — move it to history
|
||||
history.push_back(dl);
|
||||
refreshHistory();
|
||||
|
||||
// Clear current line display
|
||||
curPfxActor->SetInput("");
|
||||
curBodyActor->SetInput("");
|
||||
cursorActor->SetVisibility(0);
|
||||
|
||||
// Pause frames
|
||||
for (int p = 0; p < PAUSE_FRAMES; ++p) {
|
||||
// Update progress bar
|
||||
{
|
||||
double frac = (totalFrames > 0) ? (double)frameCount / totalFrames : 0.0;
|
||||
double barW = frac * (WIDTH - 2 * MARGIN_LEFT);
|
||||
barPts->SetPoint(0, MARGIN_LEFT, HEIGHT - 8, 0);
|
||||
barPts->SetPoint(1, MARGIN_LEFT, HEIGHT - 2, 0);
|
||||
barPts->SetPoint(2, MARGIN_LEFT + barW, HEIGHT - 2, 0);
|
||||
barPts->SetPoint(3, MARGIN_LEFT + barW, HEIGHT - 8, 0);
|
||||
barPts->Modified();
|
||||
barPD->Modified();
|
||||
}
|
||||
renderFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Finish
|
||||
// -----------------------------------------------------------------------
|
||||
writer->End();
|
||||
|
||||
// Tidy up manual ref-counted objects
|
||||
barActor->Delete();
|
||||
barPD->Delete();
|
||||
barPts->Delete();
|
||||
for (auto* a : histActors) a->Delete();
|
||||
|
||||
std::cerr << "Wrote " << frameCount << " frames ("
|
||||
<< frameCount / FPS << " s) to " << outputPath << "\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user