Skip to content

Commit fb48936

Browse files
committed
docs(components): correct the documented component count
Refs #220
1 parent fbaf997 commit fb48936

4 files changed

Lines changed: 246 additions & 36 deletions

File tree

‎README.md‎

Lines changed: 128 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,117 @@ Once installed, Claude Code automatically loads the skills when you work in that
8787

8888
## CLI Reference
8989

90+
### `rustmotion validate`
91+
92+
Schema + geometry checks, with no render. This is the gate every generated scenario is expected to pass before use.
93+
94+
| Flag | Description | Default |
95+
|---|---|---|
96+
| `-f, --file <FILE>` | Path to the JSON scenario file | (required) |
97+
| `--report <FILE>` | Write a machine-readable JSON report of all violations | |
98+
| `--fix` | Auto-fix safe violations in place (`auto_scroll: true`, drop `white-space` back to wrap, `text-autofit: true`) — refuses templated scenarios (`include`/`for-each`/`use`) | `false` |
99+
| `--strict-anim` | Sample animated frames and reapply renderer transforms to detect per-frame viewport overflow (slower) | `false` |
100+
| `--lenient` | Treat geometry violations as warnings instead of errors | `false` |
101+
| `--props <FILE>` | Load variable overrides from a JSON object file | |
102+
| `--var <KEY=VALUE>` | Set a single variable override (repeatable); `--var` wins over `--props` | |
103+
90104
### `rustmotion render`
91105

92106
| Flag | Description | Default |
93107
|---|---|---|
94-
| `input` | Path to the JSON scenario file | (required) |
108+
| `-f, --file <FILE>` | Path to the JSON scenario file (or `--json <STRING>` for inline input) | (required) |
95109
| `-o, --output` | Output file path | `output.mp4` |
96110
| `--frame <N>` | Render a single frame to PNG (0-indexed) | |
111+
| `--frames <START-END>` | Render only frames `START..=END` as a standalone segment with its own windowed audio slice, for joining later with `rustmotion concat`. Mutually exclusive with `--frame`/`--watch`; only mp4/webm/mov are implemented for a range | |
97112
| `--codec <CODEC>` | Video codec: `h264`, `h265`, `vp9`, `prores` | `h264` |
98113
| `--crf <0-51>` | Constant Rate Factor (lower = better quality) | `23` |
99114
| `--format <FMT>` | Output format: `mp4`, `webm`, `mov`, `gif`, `png-seq` | auto from extension |
100115
| `--transparent` | Transparent background (PNG sequence, WebM, ProRes 4444) | `false` |
116+
| `--hardware-acceleration` | Probe `ffmpeg -encoders` and use this machine's hardware encoder (VideoToolbox/NVENC/QSV/AMF) when available; explicit message and software fallback otherwise | `false` |
117+
| `-w, --watch` | Watch the input file and re-render on change (not compatible with `--props`/`--var`) | `false` |
118+
| `--no-validate` | Skip the implicit validate pass (schema + geometry + variables) before rendering | `false` |
119+
| `--lenient` | Treat geometry violations as warnings during the implicit validate pass | `false` |
120+
| `--strict-anim` | Sample animated frames for per-frame viewport overflow during the implicit validate pass | `false` |
121+
| `--props <FILE>` | Load variable overrides from a JSON object file | |
122+
| `--var <KEY=VALUE>` | Set a single variable override (repeatable); `--var` wins over `--props` | |
101123
| `--output-format json` | Machine-readable JSON output for CI pipelines | |
102124
| `-q, --quiet` | Suppress all output except errors | |
125+
| `--threads <N>` | Number of parallel rendering threads (global flag) | all cores |
126+
127+
### `rustmotion concat`
128+
129+
Joins segment files — e.g. several `render --frames a-b` outputs from the same scenario — via ffmpeg's concat demuxer (`-c copy`, no re-encoding). Requires ffmpeg on PATH.
130+
131+
```bash
132+
rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4
133+
```
134+
135+
### `rustmotion still`
136+
137+
Exports a single frame as a still image (PNG/JPEG/WebP).
138+
139+
| Flag | Description | Default |
140+
|---|---|---|
141+
| `-f, --file <FILE>` | Path to the JSON scenario file | (required) |
142+
| `-o, --output` | Output file path | `still.png` |
143+
| `--time <SECONDS>` | Time to capture | `0.0` |
144+
| `--format <FMT>` | Image format: `png`, `jpeg`, `webp` | from extension |
145+
| `--quality <1-100>` | JPEG quality | `90` |
146+
| `--props` / `--var` | Variable overrides, same as `render` | |
147+
148+
### `rustmotion captions`
149+
150+
Generates word-level caption timings from audio (via a local `whisper.cpp` binary) or by importing subtitles.
151+
152+
```bash
153+
rustmotion captions voice.mp3 -o words.json
154+
rustmotion captions --from-srt subs.srt -o words.json
155+
```
156+
157+
| Flag | Description | Default |
158+
|---|---|---|
159+
| `audio` | Audio file to transcribe (mutually exclusive with `--from-srt`/`--from-vtt`) | |
160+
| `-o, --output` | Output JSON file (stdout if omitted) | |
161+
| `--model` | Whisper model name (`tiny`, `base`, `small`, `medium`, `large-v3`) or a path to a `.bin` | `base` |
162+
| `--lang` | Spoken language code (auto-detected if omitted) | |
163+
| `--from-srt` / `--from-vtt` | Import cues from a subtitle file instead of transcribing | |
164+
165+
### `rustmotion batch`
166+
167+
Renders one video per line of a JSONL data file, substituting each line's fields as variable overrides.
168+
169+
| Flag | Description | Default |
170+
|---|---|---|
171+
| `-f, --file <FILE>` | Path to the scenario template (JSON or HTML dialect) | (required) |
172+
| `--data <FILE>` | JSONL file, one object of variable overrides per line | (required) |
173+
| `--output-dir <DIR>` | Directory to write output files into | (required) |
174+
| `--name-template` | Output filename template (`{field}`, `{index}`) | `"{index}.mp4"` |
175+
| `--codec` / `--crf` / `--format` / `--transparent` | Same as `render` | |
176+
| `--jobs <N>` | Videos to render in parallel (the render itself already uses all cores via rayon) | `1` |
177+
178+
### `rustmotion schema`
179+
180+
Prints the JSON Schema for scenario files (editor autocompletion, LLM prompts).
181+
182+
```bash
183+
rustmotion schema -o schema.json
184+
```
185+
186+
### `rustmotion info`
187+
188+
Shows information about a scenario (duration, scene count, dimensions, ...).
189+
190+
```bash
191+
rustmotion info scenario.json
192+
```
193+
194+
### `rustmotion skills`
195+
196+
Manages the built-in Claude Code skills — `install [--global]`, `uninstall [--global]`, `list`, `show <name>`. See [Claude Code Skills](#claude-code-skills).
197+
198+
### `rustmotion completions`
199+
200+
Generates or installs shell completions — `install`, `uninstall`, `generate <shell>`. See [Shell Completions](#shell-completions).
103201

104202
---
105203

@@ -2025,42 +2123,42 @@ Transparency is supported with `--transparent` for PNG sequences, WebM (VP9), an
20252123
- **JSON Schema:** schemars (auto-generated from Rust types)
20262124
- **Parallelism:** rayon (multi-threaded frame rendering)
20272125

2028-
## Architecture
2126+
rustmotion ships 60 components, each implementing the `Painter` trait, through a CSS-inspired **box_tree → layout_pass → paint_pass** pipeline:
20292127

2030-
rustmotion uses a Flutter-inspired **measure → layout → paint** pipeline built on Skia:
2128+
1. **box_tree** — builds a tree of `BoxNode { css: CssStyle, children, intrinsic }` from the resolved JSON components
2129+
2. **layout_pass** — runs [taffy](https://github.com/DioxusLabs/taffy) to compute each node's `BoxLayout { x, y, width, height }`; leaves that carry an `IntrinsicMeasure` (text, images, codeblocks, ...) are measured through a `measure_fn`
2130+
3. **paint_pass** — walks the tree top-down, applies transform/opacity, paints decorations (background, border, shadow), and delegates content painting to the component's `Painter` implementation
20312131

2032-
```
2033-
src/
2034-
├── components/ # 51 components (each implements Widget trait)
2035-
│ ├── chart/ # Chart sub-modules (bar, line, pie, radar, etc.)
2036-
│ └── *.rs # One file per component
2037-
├── engine/
2038-
│ ├── render/ # Render pipeline (component, scene, background, transforms)
2039-
│ ├── codeblock/ # Codeblock rendering (highlight, chrome, reveal, diff)
2040-
│ ├── animator.rs # Animation resolver, easing, spring solver
2041-
│ └── renderer.rs # Skia drawing primitives
2042-
├── schema/ # Data models
2043-
│ ├── scenario.rs # Scenario, View, Scene, VideoConfig
2044-
│ ├── style.rs # LayerStyle, FontWeight, layout types
2045-
│ ├── background.rs # Animated backgrounds
2046-
│ ├── animation.rs # EasingType, presets
2047-
│ └── video.rs # AnimationEffect, shapes, fills
2048-
├── layout/ # Flex/grid layout engines
2049-
├── traits/ # Widget, Styled, Animatable, Timed, Container
2050-
└── macros.rs # impl_traits! macro
2132+
```rust
2133+
pub trait Painter {
2134+
fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx);
2135+
fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None }
2136+
}
20512137
```
20522138

2053-
Every component implements the `Widget` trait:
2139+
`PaintCtx` carries `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`.
20542140

2055-
```rust
2056-
trait Widget {
2057-
fn paint(&self, canvas: &Canvas, ctx: &PaintContext) -> Result<()>;
2058-
fn measure(&self, constraints: &Constraints) -> (f32, f32);
2059-
fn layout(&self, constraints: &Constraints) -> LayoutNode;
2060-
}
2141+
### Workspace layout
2142+
2143+
```
2144+
crates/
2145+
├── rustmotion-core/src/
2146+
│ ├── css/ # CssStyle, units, cascade, taffy bridge, animation resolution
2147+
│ ├── engine/ # box_tree, layout_pass, paint_pass, animator, transitions, Skia primitives
2148+
│ ├── schema/ # Scenario, Scene, VideoConfig, style, background, animation, codeblock models
2149+
│ └── traits/ # Painter, Animatable, Timed, Styled
2150+
├── rustmotion-components/src/
2151+
│ ├── lib.rs # `Component` enum (60 variants) + dispatch (as_painter, as_animatable, ...)
2152+
│ ├── box_builder.rs # JSON components → BuiltScene (box tree + stagger delays)
2153+
│ ├── chart/ # bar/line/pie/radar/scatter/radial/funnel/waterfall sub-modules
2154+
│ └── *.rs # one file per component (Painter implementation)
2155+
└── rustmotion/src/
2156+
├── cli/ # the `rustmotion` binary (clap subcommands)
2157+
├── encode/ # video/audio encoders and muxing
2158+
└── loader.rs # JSON/HTML → ResolvedScenario
20612159
```
20622160

2063-
`PaintContext` provides timing, layout dimensions, parent info, and resolved animated properties in a single struct.
2161+
The `rustmotion` crate is where the binary lives — a crate with only a `[lib]` target installs nothing executable via `cargo install`.
20642162

20652163
## License
20662164

‎crates/rustmotion-components/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "rustmotion-components"
33
version.workspace = true
44
edition = "2021"
5-
description = "Component library for rustmotion (51 components)"
5+
description = "Component library for rustmotion (60 components)"
66
license = "MIT"
77
repository = "https://github.com/LeadcodeDev/rustmotion"
88
readme = "../../README.md"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Regression tests for the workstream K (docs, schema, CI) audit findings:
2+
//! documentation drift and inert schema fields.
3+
4+
/// Text immediately following the component count in the README's
5+
/// Architecture section (see `README.md`'s "rustmotion ships N components,
6+
/// each implementing the `Painter` trait" sentence).
7+
const README_COUNT_MARKER: &str = " components, each implementing the `Painter` trait";
8+
9+
/// Text immediately following the component count in
10+
/// `crates/rustmotion-components/Cargo.toml`'s `description` — the string
11+
/// crates.io displays for the published crate.
12+
const CARGO_TOML_COUNT_MARKER: &str = " components)\"";
13+
14+
/// Read the integer that appears immediately before `marker` in `haystack`,
15+
/// skipping trailing whitespace. Panics with the marker text on failure so a
16+
/// reworded sentence names exactly what moved instead of a bare parse error.
17+
fn number_before(haystack: &str, marker: &str, haystack_name: &str) -> u32 {
18+
let idx = haystack.find(marker).unwrap_or_else(|| {
19+
panic!(
20+
"marker {marker:?} not found in {haystack_name} — did the component count sentence \
21+
move or get reworded? Update this test's marker to match."
22+
)
23+
});
24+
let digits: String = haystack[..idx]
25+
.chars()
26+
.rev()
27+
.skip_while(|c| c.is_whitespace())
28+
.take_while(|c| c.is_ascii_digit())
29+
.collect();
30+
let digits: String = digits.chars().rev().collect();
31+
digits.parse().unwrap_or_else(|_| {
32+
panic!("no number found immediately before marker {marker:?} in {haystack_name}")
33+
})
34+
}
35+
36+
/// Count the variants of `pub enum Component` in
37+
/// `crates/rustmotion-components/src/lib.rs`, by counting non-empty,
38+
/// non-attribute lines between its opening `{` and closing `}`. Every
39+
/// variant in that enum is declared on its own line (`Name(Type),`); the
40+
/// only other lines in the block are `#[serde(...)]` attributes.
41+
fn count_component_variants(lib_rs: &str) -> usize {
42+
let start_marker = "pub enum Component {";
43+
let start = lib_rs.find(start_marker).unwrap_or_else(|| {
44+
panic!("{start_marker:?} not found in rustmotion-components/src/lib.rs")
45+
}) + start_marker.len();
46+
let rest = &lib_rs[start..];
47+
let end = rest
48+
.find("\n}")
49+
.expect("no closing '}' found for `pub enum Component` block");
50+
let body = &rest[..end];
51+
body.lines()
52+
.map(str::trim)
53+
.filter(|line| !line.is_empty() && !line.starts_with('#'))
54+
.count()
55+
}
56+
57+
/// README.md and `rustmotion-components/Cargo.toml` (the text
58+
/// crates.io shows for the published crate) both claimed "51 components"
59+
/// while `Component` actually had 60 variants — and nothing kept the two in
60+
/// sync. Locks the documented counts to the real one so a future component
61+
/// addition/removal that forgets to update the docs fails CI instead of
62+
/// drifting silently again.
63+
#[test]
64+
fn documented_component_count_matches_enum_variant_count() {
65+
let lib_rs = include_str!("../../rustmotion-components/src/lib.rs");
66+
let actual = count_component_variants(lib_rs);
67+
assert!(
68+
actual > 0,
69+
"found zero variants in `pub enum Component` — count parsing is broken"
70+
);
71+
72+
let readme = include_str!("../../../README.md");
73+
let readme_count = number_before(readme, README_COUNT_MARKER, "README.md");
74+
assert_eq!(
75+
readme_count as usize, actual,
76+
"README.md claims {readme_count} components but `Component` has {actual} variants"
77+
);
78+
79+
let cargo_toml = include_str!("../../rustmotion-components/Cargo.toml");
80+
let cargo_toml_count = number_before(
81+
cargo_toml,
82+
CARGO_TOML_COUNT_MARKER,
83+
"rustmotion-components/Cargo.toml",
84+
);
85+
assert_eq!(
86+
cargo_toml_count as usize, actual,
87+
"rustmotion-components/Cargo.toml's description claims {cargo_toml_count} components but \
88+
`Component` has {actual} variants"
89+
);
90+
}

0 commit comments

Comments
 (0)