From fa591f4db45152b41fecc1eefe5573fcf0932053 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Thu, 20 Aug 2026 12:25:24 +0200 Subject: [PATCH] feat(cli): ship the binary from the published crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo install rustmotion` could not work. The README documents it, and the crate's own description calls it "a CLI tool", but the published crate carried only a `[lib]`: the `rustmotion` binary was declared in `rustmotion-cli`, which is `publish = false`. Installing it gave a library and no command. The binary now lives in `crates/rustmotion` and `rustmotion-cli` is gone. The studio subcommand could not come along, and not for want of trying: an optional dependency does not help, because `rustmotion-studio` depends on `rustmotion`, so cargo rejects the cycle before compiling anything. No capability is lost — `rustmotion-studio` already declares its own binary of that name, so the studio is launched directly instead of through a subcommand. Moving the CLI exposed a second defect, latent while the crate was unpublished and the same class as the themes bug fixed in #216: `build.rs` walked `.claude/skills/rustmotion/` and `skills.rs` embedded `CLAUDE.md`, both outside the package directory. `cargo package` archives only what lives under the crate, so the tarball did not compile — the verify step failed on a missing SKILL.md. Both now live inside the crate, with symlinks at their old paths so Claude Code still finds them. One copy, not two: this repository already learned in #165 what a duplicated skill list costs when it drifts, and the test that caught that drift is what still guards it. The install destination is unchanged. `build.rs` embeds the path the file will be written to rather than the path it came from, and running the built binary in an empty directory writes 59 files to `.claude/skills/rustmotion/` as before. --- .claude/skills/rustmotion | 1 + .github/workflows/publish.yaml | 2 +- CLAUDE.md | 231 +---------------- Cargo.lock | 24 +- Cargo.toml | 3 +- crates/rustmotion-cli/Cargo.toml | 27 -- crates/rustmotion-components/src/intrinsic.rs | 4 +- crates/rustmotion-components/src/lib.rs | 2 +- crates/rustmotion-core/src/css/style.rs | 8 +- crates/rustmotion-core/src/engine/animator.rs | 4 +- .../src/engine/renderer/assets.rs | 2 +- .../rustmotion-core/tests/spring_duration.rs | 2 +- crates/rustmotion/CLAUDE.md | 238 ++++++++++++++++++ crates/rustmotion/Cargo.toml | 22 +- .../{rustmotion-cli => rustmotion}/build.rs | 30 ++- .../rustmotion/skills}/SKILL.md | 0 .../skills}/rules/1600-brutalist-style.md | 0 .../skills}/rules/3d-perspective.md | 0 .../rules/animation-completion-budget.md | 0 .../skills}/rules/audio-reactive.md | 0 .../skills}/rules/badge-video-sizing.md | 0 .../skills}/rules/captions-workflow.md | 0 .../skills}/rules/card-flex-layout.md | 0 .../rustmotion/skills}/rules/card-parallax.md | 0 .../rustmotion/skills}/rules/card-resize.md | 0 .../skills}/rules/char-animation-tuning.md | 0 .../rustmotion/skills}/rules/chart-types.md | 0 .../skills}/rules/color-palettes.md | 0 .../rules/component-field-placement.md | 0 .../skills}/rules/continuous-presets.md | 0 .../skills}/rules/counter-standalone.md | 0 .../skills}/rules/data-viz-components.md | 0 .../skills}/rules/depth-layering.md | 0 .../skills}/rules/dot-map-coordinates.md | 0 .../rustmotion/skills}/rules/dynamic-depth.md | 0 .../skills}/rules/easing-guidelines.md | 0 .../skills}/rules/even-dimensions.md | 0 .../skills}/rules/geometry-safety.md | 0 .../rustmotion/skills}/rules/glassmorphism.md | 0 .../skills}/rules/gradient-quality.md | 0 .../skills}/rules/grid-card-height.md | 0 .../rustmotion/skills}/rules/hex-colors.md | 0 .../skills}/rules/html-css-mental-model.md | 0 .../skills}/rules/hyperframes-mapping.md | 0 .../rustmotion/skills}/rules/icon-format.md | 0 .../skills}/rules/icon-sizing-hierarchy.md | 0 .../rustmotion/skills}/rules/layer-order.md | 0 .../skills}/rules/module-structure.md | 2 +- .../rustmotion/skills}/rules/motion-path.md | 0 .../skills}/rules/notification-stacking.md | 0 .../rustmotion/skills}/rules/number-wheel.md | 0 .../rustmotion/skills}/rules/paint-context.md | 0 .../skills}/rules/pixel-product-register.md | 0 .../skills}/rules/pointer-walkthrough.md | 0 .../skills}/rules/prefer-presets.md | 0 .../skills}/rules/responsive-device-sizing.md | 0 .../rustmotion/skills}/rules/scene-pacing.md | 0 .../skills}/rules/stagger-animations.md | 0 .../rustmotion/skills}/rules/stat-cards.md | 0 .../skills}/rules/streaming-text.md | 0 .../skills}/rules/templates-and-iteration.md | 0 .../skills}/rules/text-background.md | 0 .../rustmotion/skills}/rules/text-polish.md | 0 .../skills}/rules/time-remapping.md | 0 .../skills}/rules/timeline-sequencing.md | 0 .../skills}/rules/timing-constraints.md | 0 .../skills}/rules/typography-readability.md | 0 .../rustmotion/skills}/rules/ui-controls.md | 0 .../rustmotion/skills}/rules/validate-json.md | 0 .../skills}/rules/vertical-align.md | 0 .../rustmotion/skills}/rules/video-wizard.md | 0 .../skills}/rules/wiggle-additive.md | 0 .../rustmotion/skills}/rules/world-view.md | 2 +- .../src/bin/main.rs | 2 +- .../src => rustmotion/src/cli}/claude_md.rs | 0 .../src/cli}/commands/batch.rs | 6 +- .../src/cli}/commands/captions.rs | 0 .../src/cli}/commands/geometry.rs | 2 +- .../src/cli}/commands/info.rs | 0 .../src/cli}/commands/mod.rs | 0 .../src/cli}/commands/render.rs | 6 +- .../src/cli}/commands/schema.rs | 0 .../src/cli}/commands/still.rs | 0 .../src/cli}/commands/validate.rs | 0 .../src/cli}/commands/validate_attrs.rs | 2 +- .../src/cli}/commands/validate_schema.rs | 2 +- .../src/cli}/commands/validation.rs | 0 .../src/lib.rs => rustmotion/src/cli/mod.rs} | 35 +-- .../src => rustmotion/src/cli}/skills.rs | 6 +- .../src => rustmotion/src/cli}/tui.rs | 0 crates/rustmotion/src/encode/audio.rs | 2 +- crates/rustmotion/src/lib.rs | 8 + .../tests/motion_path_strict_anim.rs | 6 +- .../tests/skill_files_match_disk.rs | 8 +- 94 files changed, 332 insertions(+), 357 deletions(-) create mode 120000 .claude/skills/rustmotion mode change 100644 => 120000 CLAUDE.md delete mode 100644 crates/rustmotion-cli/Cargo.toml create mode 100644 crates/rustmotion/CLAUDE.md rename crates/{rustmotion-cli => rustmotion}/build.rs (81%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/SKILL.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/1600-brutalist-style.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/3d-perspective.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/animation-completion-budget.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/audio-reactive.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/badge-video-sizing.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/captions-workflow.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/card-flex-layout.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/card-parallax.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/card-resize.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/char-animation-tuning.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/chart-types.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/color-palettes.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/component-field-placement.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/continuous-presets.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/counter-standalone.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/data-viz-components.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/depth-layering.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/dot-map-coordinates.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/dynamic-depth.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/easing-guidelines.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/even-dimensions.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/geometry-safety.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/glassmorphism.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/gradient-quality.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/grid-card-height.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/hex-colors.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/html-css-mental-model.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/hyperframes-mapping.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/icon-format.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/icon-sizing-hierarchy.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/layer-order.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/module-structure.md (98%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/motion-path.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/notification-stacking.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/number-wheel.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/paint-context.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/pixel-product-register.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/pointer-walkthrough.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/prefer-presets.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/responsive-device-sizing.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/scene-pacing.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/stagger-animations.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/stat-cards.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/streaming-text.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/templates-and-iteration.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/text-background.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/text-polish.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/time-remapping.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/timeline-sequencing.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/timing-constraints.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/typography-readability.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/ui-controls.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/validate-json.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/vertical-align.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/video-wizard.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/wiggle-additive.md (100%) rename {.claude/skills/rustmotion => crates/rustmotion/skills}/rules/world-view.md (99%) rename crates/{rustmotion-cli => rustmotion}/src/bin/main.rs (65%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/claude_md.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/batch.rs (99%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/captions.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/geometry.rs (99%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/info.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/mod.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/render.rs (99%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/schema.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/still.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/validate.rs (100%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/validate_attrs.rs (99%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/validate_schema.rs (99%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/commands/validation.rs (100%) rename crates/{rustmotion-cli/src/lib.rs => rustmotion/src/cli/mod.rs} (97%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/skills.rs (97%) rename crates/{rustmotion-cli/src => rustmotion/src/cli}/tui.rs (100%) rename crates/{rustmotion-cli => rustmotion}/tests/motion_path_strict_anim.rs (97%) rename crates/{rustmotion-cli => rustmotion}/tests/skill_files_match_disk.rs (96%) diff --git a/.claude/skills/rustmotion b/.claude/skills/rustmotion new file mode 120000 index 0000000..d1b1299 --- /dev/null +++ b/.claude/skills/rustmotion @@ -0,0 +1 @@ +../../crates/rustmotion/skills \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 5a2a02e..8a70df1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -62,6 +62,6 @@ jobs: # pouvait donc jamais reussir, et la release 0.6.0 s'est arretee la — apres # avoir publie rustmotion-core, definitivement. # - # studio et cli portent `publish = false` : cargo les saute tout seul. + # rustmotion-studio porte `publish = false` : cargo la saute tout seul. - name: Publish all crates run: cargo publish --workspace --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index fd93be2..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,230 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code when working with this repository. -See .claude/skills/ for detailed instructions on generating rustmotion scenarios. - -## Règle obligatoire - -Tout JSON de scénario généré doit être validé avec `rustmotion validate` avant d'être présenté à l'utilisateur. Le validateur fait deux passes : **schema** et **geometry** (détection de débordement viewport). Les deux doivent passer. - -## Sécurité géométrique (viewport) - -Aucun contenu textuel ne doit dépasser du device. Quatre propriétés contrôlent ce comportement : - -- `style.white-space` (default `normal`, donc wrap actif) sur `text` : le texte wrap sur la largeur du parent par défaut. `white-space: "nowrap"` (ou `"pre"`) est légitime uniquement si un `max-width` fini + `font-size` raisonnable garantissent que la ligne tient. Le validateur émet `unwrappable_text_overflow` sinon. Il n'existe pas de champ `style.wrap` — c'est un vocabulaire hérité de l'ancien modèle de style, supprimé de `CssStyle`. Voir [rules/geometry-safety.md](.claude/skills/rustmotion/rules/geometry-safety.md). -- `auto_scroll` (default `true`) sur `codeblock` et `terminal` : quand le contenu dépasse la hauteur du `size`, le moteur scrolle (clip + translate) sans réduire la `font-size`. `auto_scroll: false` → `auto_scroll_disabled_overflow`. -- `style.text-autofit` (default absent) sur `text` et `gradient_text` : réduit la `font-size` jusqu'à ce que le contenu tienne dans sa boîte. À réserver au texte piloté par des données, dont on ne peut pas connaître la longueur à l'avance — pas pour compenser une mise en page qu'on peut simplement dimensionner. Le rétrécissement s'arrête à un plancher de lisibilité calibré ; si ça ne suffit pas, **la violation est toujours signalée**. Seuls ces deux composants l'implémentent : le déclarer ailleurs est inerte. -- `style.overflow` (default `visible`) sur les conteneurs : sémantique CSS. `hidden` clippe au bord du parent. Le validateur ne se plaint que si le contenu sort du **viewport**, pas d'un parent `visible`. - -`marquee` et `cursor` sont exemptés (leur rôle est de bleed). - -CLI : -- `rustmotion validate -f file.json` — schema + geometry -- `--fix` — auto-fix sûr : `auto_scroll: true` sur `auto_scroll_disabled_overflow`, retrait de `style.white-space` sur `unwrappable_text_overflow` (retour au wrapping), et `text-autofit: true` sur `content_overflows_box` pour `text`/`gradient_text`. Les débordements de viewport restent non corrigés : ils demandent un arbitrage de mise en page. `--fix` **refuse** d'écrire sur un scénario templaté, utilisant `include`, ou utilisant `for-each`/`use` — les index de chemin ne correspondraient plus à la source. -- `--report r.json` — rapport JSON -- `--strict-anim` — vérification frame par frame ; ajoute la détection `animated_text_overflow` (transform animé qui sort du viewport à un instant échantillonné). L'échantillonnage s'arrête à `scene.freeze_at`, puisque rien n'est rendu au-delà. -- `--strict-attrs` — promeut en erreurs les attributs inconnus (détection schéma + did-you-mean, activée par défaut en warnings) -- `--lenient` — warnings au lieu d'errors - -## Encodage - -- ffmpeg est auto-détecté et utilisé par défaut (10-bit H.264, meilleure qualité sur les gradients sombres) -- `--hardware-acceleration` sonde `ffmpeg -encoders` et bascule sur VideoToolbox/NVENC/QSV/AMF si la machine en offre un. Indisponible → message explicite et repli logiciel, jamais de bascule silencieuse. Le CRF n'a pas de sens sur la plupart des encodeurs matériels : le passer avec l'accélération produit un avertissement. -- `--frames a-b` rend une plage de frames en segment autonome, avec **sa** tranche d'audio (les pistes ne repartent pas de zéro). `rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4` les recolle via le concat demuxer de ffmpeg. C'est la brique d'un rendu distribué. -- Sans ffmpeg, le fallback openh264 intégré encode en 8-bit -- Pour les vidéos avec des gradients sombres, recommander `--codec prores` pour une qualité maximale - -## Factorisation : `components`, `for-each`, `use` - -Ne duplique pas un sous-arbre. Si dix cartes ne diffèrent que par leurs données, écris-en une et itère — c'est le mode d'échec le plus fréquent de la génération, chaque copie étant une occasion de diverger. - -```json -"components": { "stat_card": { "params": { "label": { "type": "string" } }, "template": { … } } }, -"children": [{ - "for-each": [ { "label": "Revenue" }, { "label": "Users" } ], - "template": { "use": "stat_card", "props": { "label": "$label" } } -}] -``` - -Chaque élément du `for-each` lie ses champs directement (`$label`), plus `$index` et `$item`. `params` a la forme de `config` ; omettre `default` rend le paramètre requis. La clé d'overrides est **`props`**, pas `config` — ce nom-là est réservé et serait sauté par la substitution. - -`components` est local au fichier qui le déclare. On peut itérer sur un tableau venu d'une variable ; on ne peut pas instancier un composant défini dans un fichier inclus. Toute erreur — cycle, tableau manquant, composant inconnu, paramètre absent — est nommée et située. Voir [rules/templates-and-iteration.md](.claude/skills/rustmotion/rules/templates-and-iteration.md). - -> `--fix` refuse de réécrire un scénario qui utilise ces directives : les index de chemin ne correspondent plus à la source. `validate` fonctionne normalement, sur l'arbre expansé. - -## Composition : `scenes` vs `composition` (vues `slide` / `world`) - -Un scénario est soit une liste plate `scenes` (racine) — implicitement enveloppée dans une seule vue `slide` — soit un `composition: [...]` explicite, un tableau de **vues** typées `"slide"` ou `"world"`. Les deux sont mutuellement exclusifs (`CompositionAndScenesConflict` si les deux sont présents). - -Dans une vue `slide`, les `transition` entre scènes sont des **composites pixel de deux frame-buffers déjà rendus** (fade, wipe, zoom, flip, iris, slide, chromatic_wipe…) : aucun élément ne survit à la coupe, seuls les pixels sont mélangés. - -> `chromatic_wipe` is a fast slide whose reveal edge splits into red/cyan at its peak, then recomposes. `direction` (`left`/`right`/`up`/`down`) orients it, `aberration` sets how far the channels split (`0` = a plain slide, `2` = fully doubled). The flash is zero at both ends: a leftover fringe on the last frame would bleed into the next scene. - -La vue **`world`** est le seul mécanisme qui produit une continuité réelle entre beats : une caméra virtuelle se déplace en continu à travers un espace 2D où chaque scène occupe une position (`world-position`), avec un fondu de recouvrement pendant le pan au lieu d'une coupe. C'est la brique à utiliser pour une vidéo qui doit se lire comme un plan continu, sans limite de scène perceptible. Voir [rules/world-view.md](.claude/skills/rustmotion/rules/world-view.md) pour le modèle de coordonnées (le piège `world-position` = waypoint caméra, pas origine de scène), la recette du halo ambiant en `view.background`, et un exemple multi-beat validé. - -**Piège de casing à connaître :** `world-position` (scène) est en kebab-case, alors que son voisin `freeze_at` (même struct `Scene`) est en snake_case. Vraie inconsistance du schéma, pas une faute de frappe — copier la casse telle quelle. - -## Animated backgrounds - -`scene["animated-background"]` (kebab-case, like `world-position`) accepts a `preset` from `gradient_shift`, `grid_dots`, `grid_lines`, `concentric_circles`, `halo`, `pixel_grid`, `heropattern`, with its config under a key of the same name, plus the common `x`/`y`/`speed`/`direction` that drift the texture. - -> `grid_dots` marks the intersections and reads as a texture; `grid_lines` is a grid of ruled **lines** and reads as a structure — the one to put behind a chart or a code panel. Config: `cell`, `weight`, `color`, plus `major_every`/`major_weight` for the graph-paper effect. - -## Composants disponibles (60) - -### Basiques -`text`, `shape`, `image`, `icon`, `svg`, `video`, `gif`, `caption`, `rich_text`, `gradient_text` - -### Conteneurs -`card`, `flex`, `grid`, `div` (alias de `container`), `container`, `positioned` - -> `div` = layout pur sans décoration visuelle (HTML `
`). `card` = même chose mais avec fond/border-radius/ombre attendus. - -### Data Visualization -- `chart` — 12 types: bar, line, pie, donut, horizontal_bar, area, stacked_bar, radar, scatter, radial_bar, funnel, waterfall. Supporte axes/grilles/labels. -- `gauge` — jauge semi-circulaire pour KPIs -- `sparkline` — mini-chart inline sans axes -- `stat` — carte KPI composite (valeur + label + tendance + sparkline) -- `heatmap` — grille colorée type GitHub contributions -- `treemap` — rectangles proportionnels (slice-and-dice) -- `dot_map` — carte mondiale en dot-pattern avec points de données, pulse, lat/lng -- `progress` — barre linéaire ou circulaire -- `counter` — compteur animé (standalone uniquement, pas dans les cards) -- `number_wheel` — digits that scroll like a mechanical odometer and land on the figure. Not to be confused with `counter`, which interpolates a value and rewrites the number (its glyphs jump). See [rules/number-wheel.md](.claude/skills/rustmotion/rules/number-wheel.md). -- `table` — tableau avec column_widths, column_align, cell_padding, show_borders - -### UI Components -- `badge` — pill avec icon, dot indicator, pulse animation, count badge -- `avatar` / `avatar_group` — avatar circulaire / groupe empilé avec "+N" -- `switch` — toggle animé on/off avec toggle_at -- `slider` — curseur horizontal animé avec animate_to/animate_at -- `rating` — étoiles avec remplissage partiel animé -- `kbd` — touche clavier visuelle (effet 3D) -- `tooltip` — label flottant avec flèche directionnelle -- `notification` — toast fade-in/out avec stack push (info/success/warning/error) -- `pill_nav` — tabs avec pill indicator animé entre onglets -- `list` — liste bullet/numbered/checklist avec icônes -- `stepper` — étapes numérotées connectées avec progression animée -- `comparison` — vue avant/après avec divider animé -- `countdown` — timer digital flip-clock style -- `marquee` — texte défilant continu -- `skeleton` — placeholder de chargement avec shimmer (rectangle/circle/text) -- `tag_cloud` — nuage de mots avec tailles pondérées -- `callout` — bulle avec flèche -- `divider` — séparateur visuel -- `success_check` — a checkmark that draws itself inside a halo, with a pop and a settling rotation -- `pointer` — a simulated **mouse** cursor (arrow + click ring) following waypoints. `cursor` is a text caret, not this. See [rules/pointer-walkthrough.md](.claude/skills/rustmotion/rules/pointer-walkthrough.md). - -### Code & Terminal -- `codeblock` — code syntax-highlighted avec reveal, diff mode (`diff: true`), state transitions -- `terminal` — terminal avec chrome macOS, reveal typewriter + curseur clignotant - -### Diagrammes -`arrow`, `connector`, `timeline`, `line` - -> Pour faire suivre une trajectoire à un composant, utilise l'effet d'animation `motion_path` (données de chemin SVG, orientation optionnelle selon la tangente) plutôt que d'empiler des `translate`. Voir [rules/motion-path.md](.claude/skills/rustmotion/rules/motion-path.md). - -### Média -`mockup`, `lottie`, `cursor`, `particle`, `qr_code` - -### Audio -- `waveform` — visualisation d'onde audio réactive au volume de la piste -- `audio_spectrum` — barres de spectre audio réactives (FFT) - -> Voir [rules/audio-reactive.md](.claude/skills/rustmotion/rules/audio-reactive.md) pour lier un composant à une piste `audio` via `style.audio-reactive`. - -## Text finishes - -Four mechanisms that each used to demand a hand-assembled sub-tree. Details and pitfalls in [rules/text-polish.md](.claude/skills/rustmotion/rules/text-polish.md). - -- **`shimmer`** — animation effect: a band of light sweeps over the element. Composited `SrcATop` inside the node's layer, so it only lights up **pixels that are actually painted** (the glyphs, not the box). Combined with `char_blur_in`, this is "text stagger". -- **`text.states` + `text.swap`** — a label that becomes another one: the outgoing one rises while blurring, the incoming one rises from below while unblurring. The box is measured on the **longest** label, not the first one. -- **`text.caret`** — a caret (`line` or `block`) pinned to a `typewriter`'s reveal head. A `cursor` composited alongside would stay where it was placed while the text grows underneath it. -- **`pop_in`** — a two-beat entry preset: a back-out scale that *places* the element, then a short elastic pulse that draws the eye back to it. - -The seven `char_*` presets are tuned via `direction` (up/down/left/right), `distance`, `scale_from`, `jitter`+`seed` (deterministic stagger irregularity) and `ink_from` (each unit's starting colour). See [rules/char-animation-tuning.md](.claude/skills/rustmotion/rules/char-animation-tuning.md) and, for tokens arriving, [rules/streaming-text.md](.claude/skills/rustmotion/rules/streaming-text.md). - -> If you're asked for an effect named in the Hyperframes vocabulary ("streaming text", "number wheel", "badge pop", "top-down letters"…), check the mapping table first: [rules/hyperframes-mapping.md](.claude/skills/rustmotion/rules/hyperframes-mapping.md). - -## Architecture - -### Render Pipeline (CSS engine) - -Le moteur utilise un pipeline **box_tree → layout_pass → paint_pass** inspiré des navigateurs web : - -1. **box_tree** (`box_builder.rs`) — construit un arbre de `BoxNode { css: CssStyle, children, intrinsic }` depuis les composants JSON résolus -2. **layout_pass** (`engine/layout_pass.rs`) — orchestre taffy pour calculer les `BoxLayout { x, y, width, height }` de chaque nœud. Les feuilles avec un `IntrinsicMeasure` (texte, image, codeblock) sont mesurées via une `measure_fn`. -3. **paint_pass** (`engine/paint_pass.rs`) — descend l'arbre, applique transform/opacity, peint les décorations (background, border, shadow), délègue au `Painter` du composant pour le contenu. - -Chaque composant implémente le trait `Painter` : - -```rust -pub trait Painter { - fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx); - fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None } -} -``` - -`PaintCtx` contient : `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`. - -### Structure des crates - -``` -crates/ -├── rustmotion-core/src/ -│ ├── css/ # Modèle CSS -│ │ ├── style.rs # CssStyle (propriétés CSS kebab-case) -│ │ ├── units.rs # Length, LengthPercentage (px, %, em, rem, vw, vh) -│ │ ├── cascade.rs # Héritage color/font-* parent → enfant -│ │ ├── taffy_bridge.rs # CssStyle → taffy::Style -│ │ └── animation.rs # Résolution des animations → override CssStyle -│ ├── engine/ -│ │ ├── box_tree.rs # BoxNode, BoxKind, IntrinsicMeasure -│ │ ├── layout_pass.rs # Orchestration taffy, BoxLayout résultant -│ │ ├── paint_pass.rs # Walk top-down, décorations, dispatch Painter -│ │ ├── animator.rs # Résolution animations, easing, spring solver -│ │ ├── transition.rs # Transitions entre scènes -│ │ ├── renderer/ # Primitives Skia (colors, fonts, shapes, text) -│ │ └── text/cosmic.rs # Bridge cosmic-text — PAS branché sur le rendu réel -│ ├── schema/ # Modèles de données JSON -│ │ ├── scenario.rs # Scenario, ResolvedScenario, View, Scene, VideoConfig -│ │ ├── style.rs # Specialized types (CardBorder, CardShadow, Fill, etc.) -│ │ ├── background.rs # AnimatedBackground, BackgroundPreset -│ │ ├── animation.rs # EasingType, AnimationPreset, PresetConfig -│ │ ├── codeblock_types.rs # CodeblockChrome, CodeblockState -│ │ └── video.rs # AnimationEffect, Size, ShapeType, Stroke -│ └── traits/ -│ ├── painter.rs # Painter trait + PaintCtx + AvailableSize + MeasureCtx -│ ├── animatable.rs # Animatable trait -│ ├── timed.rs # Timed trait + TimingConfig -│ └── styled.rs # Styled trait -│ -├── rustmotion-components/src/ -│ ├── lib.rs # Enum Component + dispatch (as_painter, as_animatable, etc.) -│ ├── box_builder.rs # build_scene() → BuiltScene (components + stagger_delays) -│ ├── intrinsic.rs # TextIntrinsic, BadgeIntrinsic, CounterIntrinsic, etc. -│ ├── legacy_dispatch.rs # LegacyPaintDispatcher (bridge NodeId → Painter) -│ ├── chart/ # 10 fichiers (mod + bar/line/pie/radar/scatter/radial/funnel/waterfall/axes) -│ └── *.rs # Un fichier par composant (impl Painter) -│ -└── rustmotion-cli/src/ - └── commands/ # validate, render, schema, info -``` - -### Ajouter un nouveau composant - -1. Créer `crates/rustmotion-components/src/mon_composant.rs` avec struct serde + `impl Painter` (`paint_content`) -2. Ajouter `rustmotion_core::impl_traits!(MonComposant { Animatable => animation, Timed => timing, Styled => style });` -3. Ajouter le variant dans l'enum `Component` dans `lib.rs` -4. Ajouter les match arms dans les méthodes de dispatch (`as_painter`, `as_animatable`, `as_timed`, `as_styled`) -5. Ajouter `pub mod mon_composant;` et `pub use mon_composant::MonComposant;` dans `lib.rs` -6. Si le composant a une taille fixe: la déclarer via apply_intrinsic_overrides dans box_builder.rs -7. Si le composant mesure son propre contenu : ajouter `XxxIntrinsic` dans `intrinsic.rs` - -### Tests - -```bash -cargo test --workspace # ~200 tests (layout + serde round-trip + pixel regressions + smoke) -cargo check # Vérification compilation -rustmotion validate file.json # Validation scénario -``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..7ddc0a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +crates/rustmotion/CLAUDE.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 061d4b0..6f47668 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5053,11 +5053,16 @@ dependencies = [ name = "rustmotion" version = "0.6.1" dependencies = [ + "clap", + "clap_complete", + "crossterm", + "dirs", "gif 0.13.3", "image", "minimp4", "notify", "openh264", + "ratatui", "rayon", "resvg", "rubato", @@ -5077,25 +5082,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "rustmotion-cli" -version = "0.6.1" -dependencies = [ - "clap", - "clap_complete", - "crossterm", - "dirs", - "image", - "notify", - "ratatui", - "rayon", - "rustmotion", - "rustmotion-studio", - "schemars", - "serde", - "serde_json", -] - [[package]] name = "rustmotion-components" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 498dbb3..1ee1fd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,11 @@ members = [ "crates/rustmotion-core", "crates/rustmotion-components", "crates/rustmotion", - "crates/rustmotion-cli", "crates/rustmotion-studio", "crates/rustmotion-html", ] -# Les six crates avancent ensemble. La version se change ici, et nulle part +# Les cinq crates avancent ensemble. La version se change ici, et nulle part # ailleurs : elle était auparavant répétée dix fois — une par manifeste, plus une # par dépendance interne — et une seule oubliée fait échouer la publication après # que les précédentes soient parties, ce qui ne se rattrape pas. diff --git a/crates/rustmotion-cli/Cargo.toml b/crates/rustmotion-cli/Cargo.toml deleted file mode 100644 index df660b7..0000000 --- a/crates/rustmotion-cli/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "rustmotion-cli" -version.workspace = true -edition = "2021" -publish = false - -[lib] -path = "src/lib.rs" - -[[bin]] -name = "rustmotion" -path = "src/bin/main.rs" - -[dependencies] -rustmotion = { path = "../rustmotion" } -rustmotion-studio = { path = "../rustmotion-studio" } -clap = { version = "4", features = ["derive"] } -clap_complete = "4.6.0" -ratatui = "0.29" -crossterm = "0.28" -notify = "7" -schemars = "0.8" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -dirs = "6.0.0" -rayon = "1" -image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } diff --git a/crates/rustmotion-components/src/intrinsic.rs b/crates/rustmotion-components/src/intrinsic.rs index 3375e19..eb016cf 100644 --- a/crates/rustmotion-components/src/intrinsic.rs +++ b/crates/rustmotion-components/src/intrinsic.rs @@ -116,7 +116,7 @@ pub struct TextIntrinsic { impl TextIntrinsic { /// M1: `white-space: nowrap|pre` disables wrapping — the geometry /// validator's `unwrappable_text_overflow`/`ContentOverflowsBox` checks - /// (crates/rustmotion-cli/src/commands/geometry.rs) already branch on + /// (crates/rustmotion/src/cli/commands/geometry.rs) already branch on /// exactly this pair of variants and re-measure via this same /// `TextIntrinsic`, so the wrap decision here must match theirs exactly /// or the validator's assumption about what the renderer produces is @@ -164,7 +164,7 @@ impl TextIntrinsic { // No *real* `LengthContext` (real viewport, real parent width) is // reachable here without changing this constructor's signature — // its only callers are `box_builder.rs` and - // `rustmotion-cli/src/commands/geometry.rs`, both outside this + // `rustmotion/src/cli/commands/geometry.rs`, both outside this // workstream's scope (box_builder.rs is a sibling's live file this // wave; the geometry validator re-measures via this exact type and // must keep agreeing with it byte-for-byte, so changing what it diff --git a/crates/rustmotion-components/src/lib.rs b/crates/rustmotion-components/src/lib.rs index f889ac5..2ac0556 100644 --- a/crates/rustmotion-components/src/lib.rs +++ b/crates/rustmotion-components/src/lib.rs @@ -319,7 +319,7 @@ pub struct ChildComponent { /// `style` property — `CssStyle` is `deny_unknown_fields` and belongs to /// no one this wave. Defaults to `false`: no existing scenario changes /// behaviour. Exempts only `viewport_overflow` and `animated_text_overflow` - /// (see `crates/rustmotion-cli/src/commands/geometry.rs`); it does NOT + /// (see `crates/rustmotion/src/cli/commands/geometry.rs`); it does NOT /// exempt `content_overflows_box` — content larger than its own box stays /// a reported defect regardless of `bleed`. Applies to this component /// only: a bled container does not suppress checks on its children, since diff --git a/crates/rustmotion-core/src/css/style.rs b/crates/rustmotion-core/src/css/style.rs index 01545ff..6be471f 100644 --- a/crates/rustmotion-core/src/css/style.rs +++ b/crates/rustmotion-core/src/css/style.rs @@ -13,10 +13,10 @@ use super::units::{Length, LengthContext, LengthPercentage, ParsedLength}; // shape either way — a css-local mirror would only duplicate the struct. use crate::schema::{deserialize_animation_effects, AnimationEffect, GradientBorder, InnerShadow}; -// ─── Legibility floor (relocated from `rustmotion-cli/src/commands/ +// ─── Legibility floor (relocated from `rustmotion/src/cli/commands/ // geometry.rs`'s `check_legibility`, issue #110/#102 — moved here, not // duplicated, so `text-autofit` below can shrink down to the exact same -// calibrated threshold instead of inventing a second one; `rustmotion-cli` +// calibrated threshold instead of inventing a second one; `rustmotion` // depends on `rustmotion-core`, never the other way around, so the shared // value has to live on this side of that boundary) ───────────────────────── // @@ -124,7 +124,7 @@ pub struct CssStyle { /// instead of overflowing it. This is what lets an author declare "this /// text must fit here" and closes `ContentOverflowsBox` as a possible /// validator failure for that node — see `apply_fixes` - /// (`rustmotion-cli/src/commands/validate.rs`)'s comment on why it + /// (`rustmotion/src/cli/commands/validate.rs`)'s comment on why it /// deliberately refuses to auto-fix that violation today: growing the /// box, shrinking the font, and shortening the copy are all legitimate /// fixes, and picking one was never this tool's call to make silently. @@ -163,7 +163,7 @@ pub struct CssStyle { /// /// **The floor.** Never shrinks below [`TEXT_AUTOFIT_MIN_FONT_PX`] — the /// same calibrated legibility ratio `check_legibility` - /// (`rustmotion-cli/src/commands/geometry.rs`) already enforces, not a + /// (`rustmotion/src/cli/commands/geometry.rs`) already enforces, not a /// new threshold. If the content still doesn't fit at the floor, the /// floor size is used anyway (illegible-but-smallest beats an even /// larger overflow) and the geometry validator's `ContentOverflowsBox` diff --git a/crates/rustmotion-core/src/engine/animator.rs b/crates/rustmotion-core/src/engine/animator.rs index 89e9331..152c796 100644 --- a/crates/rustmotion-core/src/engine/animator.rs +++ b/crates/rustmotion-core/src/engine/animator.rs @@ -596,7 +596,7 @@ fn spring_settle_time(damping: f64, stiffness: f64, mass: f64, threshold: f64, m /// set, else `DEFAULT_SPRING_REST_THRESHOLD`, floored so `spring_settle_time` /// always has a well-defined (nonzero) target — the same belt-and-suspenders /// pattern `spring_value` already applies to `damping`/`stiffness`/`mass`. -/// `rustmotion-cli`'s `check_spring_config` rejects non-positive or absurd +/// the CLI's `check_spring_config` rejects non-positive or absurd /// (`>= 1.0`) values at the author-facing layer; this floor is the /// solver-side backstop. fn spring_rest_threshold(config: &SpringConfig) -> f64 { @@ -1369,7 +1369,7 @@ fn motion_path_sample(cfg: &MotionPathConfig, time: f64) -> MotionPathSample { /// apply_animated_props` already bridges into `css.transform`'s /// `translate`/`rotate` functions. That bridge — not a new one — is what /// makes a `motion_path` excursion past the viewport visible to -/// `--strict-anim` (`rustmotion-cli::commands::geometry:: +/// `--strict-anim` (`rustmotion::cli::commands::geometry:: /// apply_static_node_transform`, which folds `css.transform` to detect /// overflow): this function must never write position/orientation anywhere /// else, or that detection silently stops seeing it. diff --git a/crates/rustmotion-core/src/engine/renderer/assets.rs b/crates/rustmotion-core/src/engine/renderer/assets.rs index 070e947..fdd65d9 100644 --- a/crates/rustmotion-core/src/engine/renderer/assets.rs +++ b/crates/rustmotion-core/src/engine/renderer/assets.rs @@ -252,7 +252,7 @@ pub fn extract_video_frame(src: &str, time: f64, width: u32, height: u32) -> Res // ─── Media metadata probing ──────────────────────────────────────────────── // // "How long is this audio file? What are the dimensions of this image?" — -// `rustmotion info` (`crates/rustmotion-cli/src/commands/info.rs`) answers +// `rustmotion info` (`crates/rustmotion/src/cli/commands/info.rs`) answers // these by calling the functions below, one per asset kind. Two rules tie // them together: // diff --git a/crates/rustmotion-core/tests/spring_duration.rs b/crates/rustmotion-core/tests/spring_duration.rs index 90e80e9..631cb4d 100644 --- a/crates/rustmotion-core/tests/spring_duration.rs +++ b/crates/rustmotion-core/tests/spring_duration.rs @@ -1,6 +1,6 @@ //! Issue #167 lot E: `SpringConfig::duration`/`rest_threshold` and //! `engine::animator::spring_rest_time` are meant to be used from outside -//! `rustmotion-core` (by `rustmotion-cli`'s `info` command, in particular). +//! `rustmotion-core` (by `rustmotion`'s `info` command, in particular). //! This is a black-box check that the public surface actually works end to //! end — the white-box coverage (settle-time correctness against a //! brute-force reference, over/under-damped edge cases, shape preservation) diff --git a/crates/rustmotion/CLAUDE.md b/crates/rustmotion/CLAUDE.md new file mode 100644 index 0000000..670d7a6 --- /dev/null +++ b/crates/rustmotion/CLAUDE.md @@ -0,0 +1,238 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with this repository. +See .claude/skills/ for detailed instructions on generating rustmotion scenarios. + +## Règle obligatoire + +Tout JSON de scénario généré doit être validé avec `rustmotion validate` avant d'être présenté à l'utilisateur. Le validateur fait deux passes : **schema** et **geometry** (détection de débordement viewport). Les deux doivent passer. + +## Sécurité géométrique (viewport) + +Aucun contenu textuel ne doit dépasser du device. Quatre propriétés contrôlent ce comportement : + +- `style.white-space` (default `normal`, donc wrap actif) sur `text` : le texte wrap sur la largeur du parent par défaut. `white-space: "nowrap"` (ou `"pre"`) est légitime uniquement si un `max-width` fini + `font-size` raisonnable garantissent que la ligne tient. Le validateur émet `unwrappable_text_overflow` sinon. Il n'existe pas de champ `style.wrap` — c'est un vocabulaire hérité de l'ancien modèle de style, supprimé de `CssStyle`. Voir [rules/geometry-safety.md](.claude/skills/rustmotion/rules/geometry-safety.md). +- `auto_scroll` (default `true`) sur `codeblock` et `terminal` : quand le contenu dépasse la hauteur du `size`, le moteur scrolle (clip + translate) sans réduire la `font-size`. `auto_scroll: false` → `auto_scroll_disabled_overflow`. +- `style.text-autofit` (default absent) sur `text` et `gradient_text` : réduit la `font-size` jusqu'à ce que le contenu tienne dans sa boîte. À réserver au texte piloté par des données, dont on ne peut pas connaître la longueur à l'avance — pas pour compenser une mise en page qu'on peut simplement dimensionner. Le rétrécissement s'arrête à un plancher de lisibilité calibré ; si ça ne suffit pas, **la violation est toujours signalée**. Seuls ces deux composants l'implémentent : le déclarer ailleurs est inerte. +- `style.overflow` (default `visible`) sur les conteneurs : sémantique CSS. `hidden` clippe au bord du parent. Le validateur ne se plaint que si le contenu sort du **viewport**, pas d'un parent `visible`. + +`marquee` et `cursor` sont exemptés (leur rôle est de bleed). + +CLI : +- `rustmotion validate -f file.json` — schema + geometry +- `--fix` — auto-fix sûr : `auto_scroll: true` sur `auto_scroll_disabled_overflow`, retrait de `style.white-space` sur `unwrappable_text_overflow` (retour au wrapping), et `text-autofit: true` sur `content_overflows_box` pour `text`/`gradient_text`. Les débordements de viewport restent non corrigés : ils demandent un arbitrage de mise en page. `--fix` **refuse** d'écrire sur un scénario templaté, utilisant `include`, ou utilisant `for-each`/`use` — les index de chemin ne correspondraient plus à la source. +- `--report r.json` — rapport JSON +- `--strict-anim` — vérification frame par frame ; ajoute la détection `animated_text_overflow` (transform animé qui sort du viewport à un instant échantillonné). L'échantillonnage s'arrête à `scene.freeze_at`, puisque rien n'est rendu au-delà. +- `--strict-attrs` — promeut en erreurs les attributs inconnus (détection schéma + did-you-mean, activée par défaut en warnings) +- `--lenient` — warnings au lieu d'errors + +## Encodage + +- ffmpeg est auto-détecté et utilisé par défaut (10-bit H.264, meilleure qualité sur les gradients sombres) +- `--hardware-acceleration` sonde `ffmpeg -encoders` et bascule sur VideoToolbox/NVENC/QSV/AMF si la machine en offre un. Indisponible → message explicite et repli logiciel, jamais de bascule silencieuse. Le CRF n'a pas de sens sur la plupart des encodeurs matériels : le passer avec l'accélération produit un avertissement. +- `--frames a-b` rend une plage de frames en segment autonome, avec **sa** tranche d'audio (les pistes ne repartent pas de zéro). `rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4` les recolle via le concat demuxer de ffmpeg. C'est la brique d'un rendu distribué. +- Sans ffmpeg, le fallback openh264 intégré encode en 8-bit +- Pour les vidéos avec des gradients sombres, recommander `--codec prores` pour une qualité maximale + +## Factorisation : `components`, `for-each`, `use` + +Ne duplique pas un sous-arbre. Si dix cartes ne diffèrent que par leurs données, écris-en une et itère — c'est le mode d'échec le plus fréquent de la génération, chaque copie étant une occasion de diverger. + +```json +"components": { "stat_card": { "params": { "label": { "type": "string" } }, "template": { … } } }, +"children": [{ + "for-each": [ { "label": "Revenue" }, { "label": "Users" } ], + "template": { "use": "stat_card", "props": { "label": "$label" } } +}] +``` + +Chaque élément du `for-each` lie ses champs directement (`$label`), plus `$index` et `$item`. `params` a la forme de `config` ; omettre `default` rend le paramètre requis. La clé d'overrides est **`props`**, pas `config` — ce nom-là est réservé et serait sauté par la substitution. + +`components` est local au fichier qui le déclare. On peut itérer sur un tableau venu d'une variable ; on ne peut pas instancier un composant défini dans un fichier inclus. Toute erreur — cycle, tableau manquant, composant inconnu, paramètre absent — est nommée et située. Voir [rules/templates-and-iteration.md](.claude/skills/rustmotion/rules/templates-and-iteration.md). + +> `--fix` refuse de réécrire un scénario qui utilise ces directives : les index de chemin ne correspondent plus à la source. `validate` fonctionne normalement, sur l'arbre expansé. + +## Composition : `scenes` vs `composition` (vues `slide` / `world`) + +Un scénario est soit une liste plate `scenes` (racine) — implicitement enveloppée dans une seule vue `slide` — soit un `composition: [...]` explicite, un tableau de **vues** typées `"slide"` ou `"world"`. Les deux sont mutuellement exclusifs (`CompositionAndScenesConflict` si les deux sont présents). + +Dans une vue `slide`, les `transition` entre scènes sont des **composites pixel de deux frame-buffers déjà rendus** (fade, wipe, zoom, flip, iris, slide, chromatic_wipe…) : aucun élément ne survit à la coupe, seuls les pixels sont mélangés. + +> `chromatic_wipe` is a fast slide whose reveal edge splits into red/cyan at its peak, then recomposes. `direction` (`left`/`right`/`up`/`down`) orients it, `aberration` sets how far the channels split (`0` = a plain slide, `2` = fully doubled). The flash is zero at both ends: a leftover fringe on the last frame would bleed into the next scene. + +La vue **`world`** est le seul mécanisme qui produit une continuité réelle entre beats : une caméra virtuelle se déplace en continu à travers un espace 2D où chaque scène occupe une position (`world-position`), avec un fondu de recouvrement pendant le pan au lieu d'une coupe. C'est la brique à utiliser pour une vidéo qui doit se lire comme un plan continu, sans limite de scène perceptible. Voir [rules/world-view.md](.claude/skills/rustmotion/rules/world-view.md) pour le modèle de coordonnées (le piège `world-position` = waypoint caméra, pas origine de scène), la recette du halo ambiant en `view.background`, et un exemple multi-beat validé. + +**Piège de casing à connaître :** `world-position` (scène) est en kebab-case, alors que son voisin `freeze_at` (même struct `Scene`) est en snake_case. Vraie inconsistance du schéma, pas une faute de frappe — copier la casse telle quelle. + +## Animated backgrounds + +`scene["animated-background"]` (kebab-case, like `world-position`) accepts a `preset` from `gradient_shift`, `grid_dots`, `grid_lines`, `concentric_circles`, `halo`, `pixel_grid`, `heropattern`, with its config under a key of the same name, plus the common `x`/`y`/`speed`/`direction` that drift the texture. + +> `grid_dots` marks the intersections and reads as a texture; `grid_lines` is a grid of ruled **lines** and reads as a structure — the one to put behind a chart or a code panel. Config: `cell`, `weight`, `color`, plus `major_every`/`major_weight` for the graph-paper effect. + +## Composants disponibles (60) + +### Basiques +`text`, `shape`, `image`, `icon`, `svg`, `video`, `gif`, `caption`, `rich_text`, `gradient_text` + +### Conteneurs +`card`, `flex`, `grid`, `div` (alias de `container`), `container`, `positioned` + +> `div` = layout pur sans décoration visuelle (HTML `
`). `card` = même chose mais avec fond/border-radius/ombre attendus. + +### Data Visualization +- `chart` — 12 types: bar, line, pie, donut, horizontal_bar, area, stacked_bar, radar, scatter, radial_bar, funnel, waterfall. Supporte axes/grilles/labels. +- `gauge` — jauge semi-circulaire pour KPIs +- `sparkline` — mini-chart inline sans axes +- `stat` — carte KPI composite (valeur + label + tendance + sparkline) +- `heatmap` — grille colorée type GitHub contributions +- `treemap` — rectangles proportionnels (slice-and-dice) +- `dot_map` — carte mondiale en dot-pattern avec points de données, pulse, lat/lng +- `progress` — barre linéaire ou circulaire +- `counter` — compteur animé (standalone uniquement, pas dans les cards) +- `number_wheel` — digits that scroll like a mechanical odometer and land on the figure. Not to be confused with `counter`, which interpolates a value and rewrites the number (its glyphs jump). See [rules/number-wheel.md](.claude/skills/rustmotion/rules/number-wheel.md). +- `table` — tableau avec column_widths, column_align, cell_padding, show_borders + +### UI Components +- `badge` — pill avec icon, dot indicator, pulse animation, count badge +- `avatar` / `avatar_group` — avatar circulaire / groupe empilé avec "+N" +- `switch` — toggle animé on/off avec toggle_at +- `slider` — curseur horizontal animé avec animate_to/animate_at +- `rating` — étoiles avec remplissage partiel animé +- `kbd` — touche clavier visuelle (effet 3D) +- `tooltip` — label flottant avec flèche directionnelle +- `notification` — toast fade-in/out avec stack push (info/success/warning/error) +- `pill_nav` — tabs avec pill indicator animé entre onglets +- `list` — liste bullet/numbered/checklist avec icônes +- `stepper` — étapes numérotées connectées avec progression animée +- `comparison` — vue avant/après avec divider animé +- `countdown` — timer digital flip-clock style +- `marquee` — texte défilant continu +- `skeleton` — placeholder de chargement avec shimmer (rectangle/circle/text) +- `tag_cloud` — nuage de mots avec tailles pondérées +- `callout` — bulle avec flèche +- `divider` — séparateur visuel +- `success_check` — a checkmark that draws itself inside a halo, with a pop and a settling rotation +- `pointer` — a simulated **mouse** cursor (arrow + click ring) following waypoints. `cursor` is a text caret, not this. See [rules/pointer-walkthrough.md](.claude/skills/rustmotion/rules/pointer-walkthrough.md). + +### Code & Terminal +- `codeblock` — code syntax-highlighted avec reveal, diff mode (`diff: true`), state transitions +- `terminal` — terminal avec chrome macOS, reveal typewriter + curseur clignotant + +### Diagrammes +`arrow`, `connector`, `timeline`, `line` + +> Pour faire suivre une trajectoire à un composant, utilise l'effet d'animation `motion_path` (données de chemin SVG, orientation optionnelle selon la tangente) plutôt que d'empiler des `translate`. Voir [rules/motion-path.md](.claude/skills/rustmotion/rules/motion-path.md). + +### Média +`mockup`, `lottie`, `cursor`, `particle`, `qr_code` + +### Audio +- `waveform` — visualisation d'onde audio réactive au volume de la piste +- `audio_spectrum` — barres de spectre audio réactives (FFT) + +> Voir [rules/audio-reactive.md](.claude/skills/rustmotion/rules/audio-reactive.md) pour lier un composant à une piste `audio` via `style.audio-reactive`. + +## Text finishes + +Four mechanisms that each used to demand a hand-assembled sub-tree. Details and pitfalls in [rules/text-polish.md](.claude/skills/rustmotion/rules/text-polish.md). + +- **`shimmer`** — animation effect: a band of light sweeps over the element. Composited `SrcATop` inside the node's layer, so it only lights up **pixels that are actually painted** (the glyphs, not the box). Combined with `char_blur_in`, this is "text stagger". +- **`text.states` + `text.swap`** — a label that becomes another one: the outgoing one rises while blurring, the incoming one rises from below while unblurring. The box is measured on the **longest** label, not the first one. +- **`text.caret`** — a caret (`line` or `block`) pinned to a `typewriter`'s reveal head. A `cursor` composited alongside would stay where it was placed while the text grows underneath it. +- **`pop_in`** — a two-beat entry preset: a back-out scale that *places* the element, then a short elastic pulse that draws the eye back to it. + +The seven `char_*` presets are tuned via `direction` (up/down/left/right), `distance`, `scale_from`, `jitter`+`seed` (deterministic stagger irregularity) and `ink_from` (each unit's starting colour). See [rules/char-animation-tuning.md](.claude/skills/rustmotion/rules/char-animation-tuning.md) and, for tokens arriving, [rules/streaming-text.md](.claude/skills/rustmotion/rules/streaming-text.md). + +> If you're asked for an effect named in the Hyperframes vocabulary ("streaming text", "number wheel", "badge pop", "top-down letters"…), check the mapping table first: [rules/hyperframes-mapping.md](.claude/skills/rustmotion/rules/hyperframes-mapping.md). + +## Architecture + +### Render Pipeline (CSS engine) + +Le moteur utilise un pipeline **box_tree → layout_pass → paint_pass** inspiré des navigateurs web : + +1. **box_tree** (`box_builder.rs`) — construit un arbre de `BoxNode { css: CssStyle, children, intrinsic }` depuis les composants JSON résolus +2. **layout_pass** (`engine/layout_pass.rs`) — orchestre taffy pour calculer les `BoxLayout { x, y, width, height }` de chaque nœud. Les feuilles avec un `IntrinsicMeasure` (texte, image, codeblock) sont mesurées via une `measure_fn`. +3. **paint_pass** (`engine/paint_pass.rs`) — descend l'arbre, applique transform/opacity, peint les décorations (background, border, shadow), délègue au `Painter` du composant pour le contenu. + +Chaque composant implémente le trait `Painter` : + +```rust +pub trait Painter { + fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx); + fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None } +} +``` + +`PaintCtx` contient : `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`. + +### Structure des crates + +``` +crates/ +├── rustmotion-core/src/ +│ ├── css/ # Modèle CSS +│ │ ├── style.rs # CssStyle (propriétés CSS kebab-case) +│ │ ├── units.rs # Length, LengthPercentage (px, %, em, rem, vw, vh) +│ │ ├── cascade.rs # Héritage color/font-* parent → enfant +│ │ ├── taffy_bridge.rs # CssStyle → taffy::Style +│ │ └── animation.rs # Résolution des animations → override CssStyle +│ ├── engine/ +│ │ ├── box_tree.rs # BoxNode, BoxKind, IntrinsicMeasure +│ │ ├── layout_pass.rs # Orchestration taffy, BoxLayout résultant +│ │ ├── paint_pass.rs # Walk top-down, décorations, dispatch Painter +│ │ ├── animator.rs # Résolution animations, easing, spring solver +│ │ ├── transition.rs # Transitions entre scènes +│ │ ├── renderer/ # Primitives Skia (colors, fonts, shapes, text) +│ │ └── text/cosmic.rs # Bridge cosmic-text — PAS branché sur le rendu réel +│ ├── schema/ # Modèles de données JSON +│ │ ├── scenario.rs # Scenario, ResolvedScenario, View, Scene, VideoConfig +│ │ ├── style.rs # Specialized types (CardBorder, CardShadow, Fill, etc.) +│ │ ├── background.rs # AnimatedBackground, BackgroundPreset +│ │ ├── animation.rs # EasingType, AnimationPreset, PresetConfig +│ │ ├── codeblock_types.rs # CodeblockChrome, CodeblockState +│ │ └── video.rs # AnimationEffect, Size, ShapeType, Stroke +│ └── traits/ +│ ├── painter.rs # Painter trait + PaintCtx + AvailableSize + MeasureCtx +│ ├── animatable.rs # Animatable trait +│ ├── timed.rs # Timed trait + TimingConfig +│ └── styled.rs # Styled trait +│ +├── rustmotion-components/src/ +│ ├── lib.rs # Enum Component + dispatch (as_painter, as_animatable, etc.) +│ ├── box_builder.rs # build_scene() → BuiltScene (components + stagger_delays) +│ ├── intrinsic.rs # TextIntrinsic, BadgeIntrinsic, CounterIntrinsic, etc. +│ ├── legacy_dispatch.rs # LegacyPaintDispatcher (bridge NodeId → Painter) +│ ├── chart/ # 10 fichiers (mod + bar/line/pie/radar/scatter/radial/funnel/waterfall/axes) +│ └── *.rs # Un fichier par composant (impl Painter) +│ +└── rustmotion/src/ + ├── cli/ # Le binaire `rustmotion` (clap + sous-commandes) + │ └── commands/ # validate, render, schema, info + ├── encode/ # Encodeurs vidéo/audio, mux + └── loader.rs # Chargement JSON/HTML → ResolvedScenario +``` + +> Le binaire vit dans la crate publiée `rustmotion` : une crate qui n'a qu'une +> `[lib]` n'installe rien d'exécutable via `cargo install`. Il n'y a pas de +> sous-commande `studio` — `rustmotion-studio` dépend de `rustmotion`, donc la +> dépendance inverse serait un cycle. Le studio s'ouvre par son propre binaire. + +### Ajouter un nouveau composant + +1. Créer `crates/rustmotion-components/src/mon_composant.rs` avec struct serde + `impl Painter` (`paint_content`) +2. Ajouter `rustmotion_core::impl_traits!(MonComposant { Animatable => animation, Timed => timing, Styled => style });` +3. Ajouter le variant dans l'enum `Component` dans `lib.rs` +4. Ajouter les match arms dans les méthodes de dispatch (`as_painter`, `as_animatable`, `as_timed`, `as_styled`) +5. Ajouter `pub mod mon_composant;` et `pub use mon_composant::MonComposant;` dans `lib.rs` +6. Si le composant a une taille fixe: la déclarer via apply_intrinsic_overrides dans box_builder.rs +7. Si le composant mesure son propre contenu : ajouter `XxxIntrinsic` dans `intrinsic.rs` + +### Tests + +```bash +cargo test --workspace # ~200 tests (layout + serde round-trip + pixel regressions + smoke) +cargo check # Vérification compilation +rustmotion validate file.json # Validation scénario +``` diff --git a/crates/rustmotion/Cargo.toml b/crates/rustmotion/Cargo.toml index fad36df..eb212f5 100644 --- a/crates/rustmotion/Cargo.toml +++ b/crates/rustmotion/Cargo.toml @@ -9,6 +9,16 @@ repository = "https://github.com/LeadcodeDev/rustmotion" [lib] path = "src/lib.rs" +# Le binaire vit ici, et pas dans une crate à part, parce que c'est la seule +# façon que `cargo install rustmotion` livre une commande : une crate qui n'a +# qu'une `[lib]` installe une bibliothèque et rien d'exécutable. La +# sous-commande `studio` ne peut pas y être : `rustmotion-studio` dépend de +# `rustmotion`, donc la dépendance inverse — même optionnelle — est un cycle +# que cargo refuse. Le studio garde son propre binaire `rustmotion-studio`. +[[bin]] +name = "rustmotion" +path = "src/bin/main.rs" + [dependencies] rustmotion-core.workspace = true rustmotion-components = { workspace = true, features = ["lottie-native"] } @@ -16,6 +26,15 @@ rustmotion-html.workspace = true skia-safe = "0.82" serde = { version = "1", features = ["derive"] } serde_json = "1" +clap = { version = "4", features = ["derive"] } +clap_complete = "4.6.0" +ratatui = "0.29" +crossterm = "0.28" +dirs = "6.0.0" +# Le CLI produit le JSON Schema (`rustmotion schema`) et s'en sert pour la +# détection d'attributs inconnus : schemars passe donc de dev-dependency à +# dépendance normale. +schemars = "0.8" openh264 = "0.6" minimp4 = { version = "0.1", features = ["aac"] } gif = "0.13" @@ -38,6 +57,3 @@ ffmpeg_integration = [] ## Re-expose native Lottie decoding (default-on). Activating this feature here ## propagates to rustmotion-components so consumers only need to touch rustmotion. lottie-native = ["rustmotion-components/lottie-native"] - -[dev-dependencies] -schemars = "0.8" diff --git a/crates/rustmotion-cli/build.rs b/crates/rustmotion/build.rs similarity index 81% rename from crates/rustmotion-cli/build.rs rename to crates/rustmotion/build.rs index 2525a63..a815fd5 100644 --- a/crates/rustmotion-cli/build.rs +++ b/crates/rustmotion/build.rs @@ -1,5 +1,5 @@ //! Generates the `SKILL_FILES` table embedded into the `rustmotion` binary by -//! `src/skills.rs`. +//! `src/cli/skills.rs`. //! //! `rustmotion skills install` is the only channel through which an LLM agent //! receives the generation rules under `.claude/skills/rustmotion/`. Before @@ -42,19 +42,13 @@ fn collect_md_files(dir: &Path, out: &mut Vec) { fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo")); - // crates/rustmotion-cli -> crates -> - let workspace_root = manifest_dir - .parent() - .and_then(Path::parent) - .unwrap_or_else(|| { - panic!( - "expected {} to live at /crates/rustmotion-cli", - manifest_dir.display() - ) - }) - .to_path_buf(); - let skills_root = workspace_root.join(".claude/skills/rustmotion"); + // L'arbre des skills vit DANS la crate, et `.claude/skills/rustmotion` à la + // racine du dépôt n'en est qu'un lien symbolique. C'est ce qui permet à + // `cargo package` de l'embarquer : il n'archive que ce qui est sous le + // répertoire du paquet, et une lecture hors de ce périmètre produit un + // tarball qui ne compile chez personne. + let skills_root = manifest_dir.join("skills"); // Rerun whenever any file under the skills tree is added, removed, or // edited — Cargo scans directories given to rerun-if-changed recursively. @@ -84,10 +78,14 @@ fn main() { let mut generated = String::from("&[\n"); for path in &all_files { - let rel_path = path - .strip_prefix(&workspace_root) - .unwrap_or(path) + // Le chemin embarqué est celui que `skills install` écrira chez + // l'utilisateur, pas celui d'où le fichier vient : la source a beau + // vivre dans la crate, la destination reste l'emplacement que Claude + // Code sait découvrir. + let rel_path = Path::new(".claude/skills/rustmotion") + .join(path.strip_prefix(&skills_root).unwrap_or(path)) .to_str() + .map(str::to_owned) .unwrap_or_else(|| panic!("non-UTF-8 skill file path: {}", path.display())) .replace('\\', "/"); // normalize separators if built on Windows let abs_path = path diff --git a/.claude/skills/rustmotion/SKILL.md b/crates/rustmotion/skills/SKILL.md similarity index 100% rename from .claude/skills/rustmotion/SKILL.md rename to crates/rustmotion/skills/SKILL.md diff --git a/.claude/skills/rustmotion/rules/1600-brutalist-style.md b/crates/rustmotion/skills/rules/1600-brutalist-style.md similarity index 100% rename from .claude/skills/rustmotion/rules/1600-brutalist-style.md rename to crates/rustmotion/skills/rules/1600-brutalist-style.md diff --git a/.claude/skills/rustmotion/rules/3d-perspective.md b/crates/rustmotion/skills/rules/3d-perspective.md similarity index 100% rename from .claude/skills/rustmotion/rules/3d-perspective.md rename to crates/rustmotion/skills/rules/3d-perspective.md diff --git a/.claude/skills/rustmotion/rules/animation-completion-budget.md b/crates/rustmotion/skills/rules/animation-completion-budget.md similarity index 100% rename from .claude/skills/rustmotion/rules/animation-completion-budget.md rename to crates/rustmotion/skills/rules/animation-completion-budget.md diff --git a/.claude/skills/rustmotion/rules/audio-reactive.md b/crates/rustmotion/skills/rules/audio-reactive.md similarity index 100% rename from .claude/skills/rustmotion/rules/audio-reactive.md rename to crates/rustmotion/skills/rules/audio-reactive.md diff --git a/.claude/skills/rustmotion/rules/badge-video-sizing.md b/crates/rustmotion/skills/rules/badge-video-sizing.md similarity index 100% rename from .claude/skills/rustmotion/rules/badge-video-sizing.md rename to crates/rustmotion/skills/rules/badge-video-sizing.md diff --git a/.claude/skills/rustmotion/rules/captions-workflow.md b/crates/rustmotion/skills/rules/captions-workflow.md similarity index 100% rename from .claude/skills/rustmotion/rules/captions-workflow.md rename to crates/rustmotion/skills/rules/captions-workflow.md diff --git a/.claude/skills/rustmotion/rules/card-flex-layout.md b/crates/rustmotion/skills/rules/card-flex-layout.md similarity index 100% rename from .claude/skills/rustmotion/rules/card-flex-layout.md rename to crates/rustmotion/skills/rules/card-flex-layout.md diff --git a/.claude/skills/rustmotion/rules/card-parallax.md b/crates/rustmotion/skills/rules/card-parallax.md similarity index 100% rename from .claude/skills/rustmotion/rules/card-parallax.md rename to crates/rustmotion/skills/rules/card-parallax.md diff --git a/.claude/skills/rustmotion/rules/card-resize.md b/crates/rustmotion/skills/rules/card-resize.md similarity index 100% rename from .claude/skills/rustmotion/rules/card-resize.md rename to crates/rustmotion/skills/rules/card-resize.md diff --git a/.claude/skills/rustmotion/rules/char-animation-tuning.md b/crates/rustmotion/skills/rules/char-animation-tuning.md similarity index 100% rename from .claude/skills/rustmotion/rules/char-animation-tuning.md rename to crates/rustmotion/skills/rules/char-animation-tuning.md diff --git a/.claude/skills/rustmotion/rules/chart-types.md b/crates/rustmotion/skills/rules/chart-types.md similarity index 100% rename from .claude/skills/rustmotion/rules/chart-types.md rename to crates/rustmotion/skills/rules/chart-types.md diff --git a/.claude/skills/rustmotion/rules/color-palettes.md b/crates/rustmotion/skills/rules/color-palettes.md similarity index 100% rename from .claude/skills/rustmotion/rules/color-palettes.md rename to crates/rustmotion/skills/rules/color-palettes.md diff --git a/.claude/skills/rustmotion/rules/component-field-placement.md b/crates/rustmotion/skills/rules/component-field-placement.md similarity index 100% rename from .claude/skills/rustmotion/rules/component-field-placement.md rename to crates/rustmotion/skills/rules/component-field-placement.md diff --git a/.claude/skills/rustmotion/rules/continuous-presets.md b/crates/rustmotion/skills/rules/continuous-presets.md similarity index 100% rename from .claude/skills/rustmotion/rules/continuous-presets.md rename to crates/rustmotion/skills/rules/continuous-presets.md diff --git a/.claude/skills/rustmotion/rules/counter-standalone.md b/crates/rustmotion/skills/rules/counter-standalone.md similarity index 100% rename from .claude/skills/rustmotion/rules/counter-standalone.md rename to crates/rustmotion/skills/rules/counter-standalone.md diff --git a/.claude/skills/rustmotion/rules/data-viz-components.md b/crates/rustmotion/skills/rules/data-viz-components.md similarity index 100% rename from .claude/skills/rustmotion/rules/data-viz-components.md rename to crates/rustmotion/skills/rules/data-viz-components.md diff --git a/.claude/skills/rustmotion/rules/depth-layering.md b/crates/rustmotion/skills/rules/depth-layering.md similarity index 100% rename from .claude/skills/rustmotion/rules/depth-layering.md rename to crates/rustmotion/skills/rules/depth-layering.md diff --git a/.claude/skills/rustmotion/rules/dot-map-coordinates.md b/crates/rustmotion/skills/rules/dot-map-coordinates.md similarity index 100% rename from .claude/skills/rustmotion/rules/dot-map-coordinates.md rename to crates/rustmotion/skills/rules/dot-map-coordinates.md diff --git a/.claude/skills/rustmotion/rules/dynamic-depth.md b/crates/rustmotion/skills/rules/dynamic-depth.md similarity index 100% rename from .claude/skills/rustmotion/rules/dynamic-depth.md rename to crates/rustmotion/skills/rules/dynamic-depth.md diff --git a/.claude/skills/rustmotion/rules/easing-guidelines.md b/crates/rustmotion/skills/rules/easing-guidelines.md similarity index 100% rename from .claude/skills/rustmotion/rules/easing-guidelines.md rename to crates/rustmotion/skills/rules/easing-guidelines.md diff --git a/.claude/skills/rustmotion/rules/even-dimensions.md b/crates/rustmotion/skills/rules/even-dimensions.md similarity index 100% rename from .claude/skills/rustmotion/rules/even-dimensions.md rename to crates/rustmotion/skills/rules/even-dimensions.md diff --git a/.claude/skills/rustmotion/rules/geometry-safety.md b/crates/rustmotion/skills/rules/geometry-safety.md similarity index 100% rename from .claude/skills/rustmotion/rules/geometry-safety.md rename to crates/rustmotion/skills/rules/geometry-safety.md diff --git a/.claude/skills/rustmotion/rules/glassmorphism.md b/crates/rustmotion/skills/rules/glassmorphism.md similarity index 100% rename from .claude/skills/rustmotion/rules/glassmorphism.md rename to crates/rustmotion/skills/rules/glassmorphism.md diff --git a/.claude/skills/rustmotion/rules/gradient-quality.md b/crates/rustmotion/skills/rules/gradient-quality.md similarity index 100% rename from .claude/skills/rustmotion/rules/gradient-quality.md rename to crates/rustmotion/skills/rules/gradient-quality.md diff --git a/.claude/skills/rustmotion/rules/grid-card-height.md b/crates/rustmotion/skills/rules/grid-card-height.md similarity index 100% rename from .claude/skills/rustmotion/rules/grid-card-height.md rename to crates/rustmotion/skills/rules/grid-card-height.md diff --git a/.claude/skills/rustmotion/rules/hex-colors.md b/crates/rustmotion/skills/rules/hex-colors.md similarity index 100% rename from .claude/skills/rustmotion/rules/hex-colors.md rename to crates/rustmotion/skills/rules/hex-colors.md diff --git a/.claude/skills/rustmotion/rules/html-css-mental-model.md b/crates/rustmotion/skills/rules/html-css-mental-model.md similarity index 100% rename from .claude/skills/rustmotion/rules/html-css-mental-model.md rename to crates/rustmotion/skills/rules/html-css-mental-model.md diff --git a/.claude/skills/rustmotion/rules/hyperframes-mapping.md b/crates/rustmotion/skills/rules/hyperframes-mapping.md similarity index 100% rename from .claude/skills/rustmotion/rules/hyperframes-mapping.md rename to crates/rustmotion/skills/rules/hyperframes-mapping.md diff --git a/.claude/skills/rustmotion/rules/icon-format.md b/crates/rustmotion/skills/rules/icon-format.md similarity index 100% rename from .claude/skills/rustmotion/rules/icon-format.md rename to crates/rustmotion/skills/rules/icon-format.md diff --git a/.claude/skills/rustmotion/rules/icon-sizing-hierarchy.md b/crates/rustmotion/skills/rules/icon-sizing-hierarchy.md similarity index 100% rename from .claude/skills/rustmotion/rules/icon-sizing-hierarchy.md rename to crates/rustmotion/skills/rules/icon-sizing-hierarchy.md diff --git a/.claude/skills/rustmotion/rules/layer-order.md b/crates/rustmotion/skills/rules/layer-order.md similarity index 100% rename from .claude/skills/rustmotion/rules/layer-order.md rename to crates/rustmotion/skills/rules/layer-order.md diff --git a/.claude/skills/rustmotion/rules/module-structure.md b/crates/rustmotion/skills/rules/module-structure.md similarity index 98% rename from .claude/skills/rustmotion/rules/module-structure.md rename to crates/rustmotion/skills/rules/module-structure.md index 6d1ffc5..471d410 100644 --- a/.claude/skills/rustmotion/rules/module-structure.md +++ b/crates/rustmotion/skills/rules/module-structure.md @@ -8,7 +8,7 @@ The workspace is split into three crates: |---|---| | `rustmotion-core` | Engine, CSS model, schema types, Painter trait | | `rustmotion-components` | 57 component structs + Painter impls + box builder | -| `rustmotion-cli` | CLI commands (render, validate, schema, info) | +| `rustmotion` | Encode/loader pipeline + the `rustmotion` binary (`src/cli/`) | --- diff --git a/.claude/skills/rustmotion/rules/motion-path.md b/crates/rustmotion/skills/rules/motion-path.md similarity index 100% rename from .claude/skills/rustmotion/rules/motion-path.md rename to crates/rustmotion/skills/rules/motion-path.md diff --git a/.claude/skills/rustmotion/rules/notification-stacking.md b/crates/rustmotion/skills/rules/notification-stacking.md similarity index 100% rename from .claude/skills/rustmotion/rules/notification-stacking.md rename to crates/rustmotion/skills/rules/notification-stacking.md diff --git a/.claude/skills/rustmotion/rules/number-wheel.md b/crates/rustmotion/skills/rules/number-wheel.md similarity index 100% rename from .claude/skills/rustmotion/rules/number-wheel.md rename to crates/rustmotion/skills/rules/number-wheel.md diff --git a/.claude/skills/rustmotion/rules/paint-context.md b/crates/rustmotion/skills/rules/paint-context.md similarity index 100% rename from .claude/skills/rustmotion/rules/paint-context.md rename to crates/rustmotion/skills/rules/paint-context.md diff --git a/.claude/skills/rustmotion/rules/pixel-product-register.md b/crates/rustmotion/skills/rules/pixel-product-register.md similarity index 100% rename from .claude/skills/rustmotion/rules/pixel-product-register.md rename to crates/rustmotion/skills/rules/pixel-product-register.md diff --git a/.claude/skills/rustmotion/rules/pointer-walkthrough.md b/crates/rustmotion/skills/rules/pointer-walkthrough.md similarity index 100% rename from .claude/skills/rustmotion/rules/pointer-walkthrough.md rename to crates/rustmotion/skills/rules/pointer-walkthrough.md diff --git a/.claude/skills/rustmotion/rules/prefer-presets.md b/crates/rustmotion/skills/rules/prefer-presets.md similarity index 100% rename from .claude/skills/rustmotion/rules/prefer-presets.md rename to crates/rustmotion/skills/rules/prefer-presets.md diff --git a/.claude/skills/rustmotion/rules/responsive-device-sizing.md b/crates/rustmotion/skills/rules/responsive-device-sizing.md similarity index 100% rename from .claude/skills/rustmotion/rules/responsive-device-sizing.md rename to crates/rustmotion/skills/rules/responsive-device-sizing.md diff --git a/.claude/skills/rustmotion/rules/scene-pacing.md b/crates/rustmotion/skills/rules/scene-pacing.md similarity index 100% rename from .claude/skills/rustmotion/rules/scene-pacing.md rename to crates/rustmotion/skills/rules/scene-pacing.md diff --git a/.claude/skills/rustmotion/rules/stagger-animations.md b/crates/rustmotion/skills/rules/stagger-animations.md similarity index 100% rename from .claude/skills/rustmotion/rules/stagger-animations.md rename to crates/rustmotion/skills/rules/stagger-animations.md diff --git a/.claude/skills/rustmotion/rules/stat-cards.md b/crates/rustmotion/skills/rules/stat-cards.md similarity index 100% rename from .claude/skills/rustmotion/rules/stat-cards.md rename to crates/rustmotion/skills/rules/stat-cards.md diff --git a/.claude/skills/rustmotion/rules/streaming-text.md b/crates/rustmotion/skills/rules/streaming-text.md similarity index 100% rename from .claude/skills/rustmotion/rules/streaming-text.md rename to crates/rustmotion/skills/rules/streaming-text.md diff --git a/.claude/skills/rustmotion/rules/templates-and-iteration.md b/crates/rustmotion/skills/rules/templates-and-iteration.md similarity index 100% rename from .claude/skills/rustmotion/rules/templates-and-iteration.md rename to crates/rustmotion/skills/rules/templates-and-iteration.md diff --git a/.claude/skills/rustmotion/rules/text-background.md b/crates/rustmotion/skills/rules/text-background.md similarity index 100% rename from .claude/skills/rustmotion/rules/text-background.md rename to crates/rustmotion/skills/rules/text-background.md diff --git a/.claude/skills/rustmotion/rules/text-polish.md b/crates/rustmotion/skills/rules/text-polish.md similarity index 100% rename from .claude/skills/rustmotion/rules/text-polish.md rename to crates/rustmotion/skills/rules/text-polish.md diff --git a/.claude/skills/rustmotion/rules/time-remapping.md b/crates/rustmotion/skills/rules/time-remapping.md similarity index 100% rename from .claude/skills/rustmotion/rules/time-remapping.md rename to crates/rustmotion/skills/rules/time-remapping.md diff --git a/.claude/skills/rustmotion/rules/timeline-sequencing.md b/crates/rustmotion/skills/rules/timeline-sequencing.md similarity index 100% rename from .claude/skills/rustmotion/rules/timeline-sequencing.md rename to crates/rustmotion/skills/rules/timeline-sequencing.md diff --git a/.claude/skills/rustmotion/rules/timing-constraints.md b/crates/rustmotion/skills/rules/timing-constraints.md similarity index 100% rename from .claude/skills/rustmotion/rules/timing-constraints.md rename to crates/rustmotion/skills/rules/timing-constraints.md diff --git a/.claude/skills/rustmotion/rules/typography-readability.md b/crates/rustmotion/skills/rules/typography-readability.md similarity index 100% rename from .claude/skills/rustmotion/rules/typography-readability.md rename to crates/rustmotion/skills/rules/typography-readability.md diff --git a/.claude/skills/rustmotion/rules/ui-controls.md b/crates/rustmotion/skills/rules/ui-controls.md similarity index 100% rename from .claude/skills/rustmotion/rules/ui-controls.md rename to crates/rustmotion/skills/rules/ui-controls.md diff --git a/.claude/skills/rustmotion/rules/validate-json.md b/crates/rustmotion/skills/rules/validate-json.md similarity index 100% rename from .claude/skills/rustmotion/rules/validate-json.md rename to crates/rustmotion/skills/rules/validate-json.md diff --git a/.claude/skills/rustmotion/rules/vertical-align.md b/crates/rustmotion/skills/rules/vertical-align.md similarity index 100% rename from .claude/skills/rustmotion/rules/vertical-align.md rename to crates/rustmotion/skills/rules/vertical-align.md diff --git a/.claude/skills/rustmotion/rules/video-wizard.md b/crates/rustmotion/skills/rules/video-wizard.md similarity index 100% rename from .claude/skills/rustmotion/rules/video-wizard.md rename to crates/rustmotion/skills/rules/video-wizard.md diff --git a/.claude/skills/rustmotion/rules/wiggle-additive.md b/crates/rustmotion/skills/rules/wiggle-additive.md similarity index 100% rename from .claude/skills/rustmotion/rules/wiggle-additive.md rename to crates/rustmotion/skills/rules/wiggle-additive.md diff --git a/.claude/skills/rustmotion/rules/world-view.md b/crates/rustmotion/skills/rules/world-view.md similarity index 99% rename from .claude/skills/rustmotion/rules/world-view.md rename to crates/rustmotion/skills/rules/world-view.md index cd70893..5ccb02c 100644 --- a/.claude/skills/rustmotion/rules/world-view.md +++ b/crates/rustmotion/skills/rules/world-view.md @@ -70,7 +70,7 @@ Only set `world-position` explicitly when the path isn't a straight horizontal f A scene's `children` are laid out exactly like a `slide`-view scene: `0..video_width` × `0..video_height`, independent of that scene's `world-position`. The camera translation is applied to the whole scene as one block, after layout — a child never needs to know where its scene sits in world space. -This was verified against the geometry validator (`crates/rustmotion-cli/src/commands/geometry.rs`, `validate_geometry`): it checks every scene's children against `(scenario.video.width, scenario.video.height)` regardless of that scene's `world-position`. Placing a child at what looks like the world-space X (e.g. `2880` for the second beat) is rejected as viewport overflow: +This was verified against the geometry validator (`crates/rustmotion/src/cli/commands/geometry.rs`, `validate_geometry`): it checks every scene's children against `(scenario.video.width, scenario.video.height)` regardless of that scene's `world-position`. Placing a child at what looks like the world-space X (e.g. `2880` for the second beat) is rejected as viewport overflow: ```json // Scene 2 has "world-position": {"x": 2880, ...}. A child positioned at diff --git a/crates/rustmotion-cli/src/bin/main.rs b/crates/rustmotion/src/bin/main.rs similarity index 65% rename from crates/rustmotion-cli/src/bin/main.rs rename to crates/rustmotion/src/bin/main.rs index d29d970..e00d27e 100644 --- a/crates/rustmotion-cli/src/bin/main.rs +++ b/crates/rustmotion/src/bin/main.rs @@ -1,5 +1,5 @@ fn main() { - if let Err(e) = rustmotion_cli::run() { + if let Err(e) = rustmotion::cli::run() { eprintln!("Error: {}", e); std::process::exit(1); } diff --git a/crates/rustmotion-cli/src/claude_md.rs b/crates/rustmotion/src/cli/claude_md.rs similarity index 100% rename from crates/rustmotion-cli/src/claude_md.rs rename to crates/rustmotion/src/cli/claude_md.rs diff --git a/crates/rustmotion-cli/src/commands/batch.rs b/crates/rustmotion/src/cli/commands/batch.rs similarity index 99% rename from crates/rustmotion-cli/src/commands/batch.rs rename to crates/rustmotion/src/cli/commands/batch.rs index 1220601..b8ec364 100644 --- a/crates/rustmotion-cli/src/commands/batch.rs +++ b/crates/rustmotion/src/cli/commands/batch.rs @@ -22,8 +22,8 @@ use std::collections::HashMap; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, Mutex}; -use crate::commands::render::cmd_render; -use crate::commands::validation::{self, ValidationSource}; +use crate::cli::commands::render::cmd_render; +use crate::cli::commands::validation::{self, ValidationSource}; /// One row parsed from the JSONL data file. struct BatchRow { @@ -872,7 +872,7 @@ mod batch_integration_tests { /// because `std::env::set_var`/`remove_var` are `unsafe`: the standard /// library only guarantees soundness when nothing else in the process /// reads or writes the environment concurrently. This is the only test - /// file in `rustmotion-cli` whose tests spawn a real (non `png-seq` / + /// file in the CLI whose tests spawn a real (non `png-seq` / /// `gif` / `raw`) video encode — every other test in this crate's test /// binary never reads `PATH` — so this lock only needs to protect /// against concurrent runs of tests within this file. diff --git a/crates/rustmotion-cli/src/commands/captions.rs b/crates/rustmotion/src/cli/commands/captions.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/captions.rs rename to crates/rustmotion/src/cli/commands/captions.rs diff --git a/crates/rustmotion-cli/src/commands/geometry.rs b/crates/rustmotion/src/cli/commands/geometry.rs similarity index 99% rename from crates/rustmotion-cli/src/commands/geometry.rs rename to crates/rustmotion/src/cli/commands/geometry.rs index 707f741..929f592 100644 --- a/crates/rustmotion-cli/src/commands/geometry.rs +++ b/crates/rustmotion/src/cli/commands/geometry.rs @@ -4122,7 +4122,7 @@ mod legibility_tests { #[test] fn legibility_never_blocks_validation() { - use crate::commands::validate_schema::validate_scenario; + use crate::cli::commands::validate_schema::validate_scenario; let json = r##"{ "video": { "width": 1920, "height": 1080 }, "scenes": [{ diff --git a/crates/rustmotion-cli/src/commands/info.rs b/crates/rustmotion/src/cli/commands/info.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/info.rs rename to crates/rustmotion/src/cli/commands/info.rs diff --git a/crates/rustmotion-cli/src/commands/mod.rs b/crates/rustmotion/src/cli/commands/mod.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/mod.rs rename to crates/rustmotion/src/cli/commands/mod.rs diff --git a/crates/rustmotion-cli/src/commands/render.rs b/crates/rustmotion/src/cli/commands/render.rs similarity index 99% rename from crates/rustmotion-cli/src/commands/render.rs rename to crates/rustmotion/src/cli/commands/render.rs index 3172bbb..2c6a11d 100644 --- a/crates/rustmotion-cli/src/commands/render.rs +++ b/crates/rustmotion/src/cli/commands/render.rs @@ -1,12 +1,12 @@ -use crate::tui; -use crate::OutputFormat; +use crate::cli::tui; +use crate::cli::OutputFormat; use rustmotion::encode; use rustmotion::engine; use rustmotion::error::{Result, RustmotionError}; use rustmotion::schema::ResolvedScenario; use std::path::{Path, PathBuf}; -use crate::commands::validation::{self, ValidationSource}; +use crate::cli::commands::validation::{self, ValidationSource}; /// Load + validate a scenario for watch mode. On validation failure prints the /// report and returns the typed error so the caller can decide how to handle it. diff --git a/crates/rustmotion-cli/src/commands/schema.rs b/crates/rustmotion/src/cli/commands/schema.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/schema.rs rename to crates/rustmotion/src/cli/commands/schema.rs diff --git a/crates/rustmotion-cli/src/commands/still.rs b/crates/rustmotion/src/cli/commands/still.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/still.rs rename to crates/rustmotion/src/cli/commands/still.rs diff --git a/crates/rustmotion-cli/src/commands/validate.rs b/crates/rustmotion/src/cli/commands/validate.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/validate.rs rename to crates/rustmotion/src/cli/commands/validate.rs diff --git a/crates/rustmotion-cli/src/commands/validate_attrs.rs b/crates/rustmotion/src/cli/commands/validate_attrs.rs similarity index 99% rename from crates/rustmotion-cli/src/commands/validate_attrs.rs rename to crates/rustmotion/src/cli/commands/validate_attrs.rs index ce85c47..cd8c30d 100644 --- a/crates/rustmotion-cli/src/commands/validate_attrs.rs +++ b/crates/rustmotion/src/cli/commands/validate_attrs.rs @@ -185,7 +185,7 @@ fn truncate(msg: &str, max: usize) -> String { #[cfg(test)] mod tests { use super::*; - use crate::commands::validation::{load, run_checks, ValidationSource}; + use crate::cli::commands::validation::{load, run_checks, ValidationSource}; fn resolved(children: serde_json::Value) -> ResolvedScenario { let json = serde_json::json!({ diff --git a/crates/rustmotion-cli/src/commands/validate_schema.rs b/crates/rustmotion/src/cli/commands/validate_schema.rs similarity index 99% rename from crates/rustmotion-cli/src/commands/validate_schema.rs rename to crates/rustmotion/src/cli/commands/validate_schema.rs index 518c4b2..a42a28c 100644 --- a/crates/rustmotion-cli/src/commands/validate_schema.rs +++ b/crates/rustmotion/src/cli/commands/validate_schema.rs @@ -983,7 +983,7 @@ mod style_warning_tests { fn negative_spring_damping_is_an_error() { // Constat #6: damping < 0 makes the spring solver diverge instead of // settle (a `SpringConfig` accepts any f64 — nothing in - // `rustmotion-cli` checked `damping`/`stiffness` before this). + // `rustmotion`'s CLI checked `damping`/`stiffness` before this). let child: ChildComponent = serde_json::from_value(serde_json::json!({ "type": "text", "content": "hi", diff --git a/crates/rustmotion-cli/src/commands/validation.rs b/crates/rustmotion/src/cli/commands/validation.rs similarity index 100% rename from crates/rustmotion-cli/src/commands/validation.rs rename to crates/rustmotion/src/cli/commands/validation.rs diff --git a/crates/rustmotion-cli/src/lib.rs b/crates/rustmotion/src/cli/mod.rs similarity index 97% rename from crates/rustmotion-cli/src/lib.rs rename to crates/rustmotion/src/cli/mod.rs index ea95cf4..0ca9926 100644 --- a/crates/rustmotion-cli/src/lib.rs +++ b/crates/rustmotion/src/cli/mod.rs @@ -1,11 +1,19 @@ +//! Le binaire `rustmotion` : analyse des arguments et aiguillage vers +//! `commands`. +//! +//! Il n'y a pas de sous-commande `studio` ici. Le studio est une app Dioxus +//! qui dépend de cette crate ; l'appeler depuis ce module ferait dépendre +//! `rustmotion` de `rustmotion-studio`, donc d'elle-même, et cargo refuse le +//! cycle. Le studio s'ouvre par son propre binaire, `rustmotion-studio -f +//! scenario.json`. + mod claude_md; mod commands; mod skills; -pub mod tui; +mod tui; use clap::{CommandFactory, Parser, Subcommand}; use rustmotion::error::{Result, RustmotionError}; -use rustmotion::loader::load_input; use rustmotion::schema::ResolvedScenario; use std::collections::HashMap; use std::io::Write; @@ -32,13 +40,6 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Open a live preview window with hot-reload - Studio { - /// Path to the JSON scenario file - #[arg(short, long)] - file: PathBuf, - }, - /// Render a JSON scenario to video or a single frame Render { /// Path to the JSON scenario file @@ -651,10 +652,6 @@ pub fn run() -> Result<()> { } match cli.command { - Commands::Studio { file } => match load_input(&file) { - Ok(scenario) => rustmotion_studio::run_preview(scenario, Some(file), true), - Err(e) => rustmotion_studio::run_preview_with_error(format!("{}", e), Some(file), true), - }, Commands::Render { file, json, @@ -884,12 +881,6 @@ pub fn run() -> Result<()> { "rustmotion", &mut std::io::stdout(), ); - clap_complete::generate( - shell, - &mut rustmotion_studio::command(), - "rustmotion-studio", - &mut std::io::stdout(), - ); Ok(()) } CompletionsAction::Install => install_completions(), @@ -920,12 +911,6 @@ fn install_completions() -> Result<()> { let mut buf = Vec::new(); clap_complete::generate(shell, &mut Cli::command(), "rustmotion", &mut buf); - clap_complete::generate( - shell, - &mut rustmotion_studio::command(), - "rustmotion-studio", - &mut buf, - ); let completions = String::from_utf8(buf).unwrap(); let home = dirs::home_dir() diff --git a/crates/rustmotion-cli/src/skills.rs b/crates/rustmotion/src/cli/skills.rs similarity index 97% rename from crates/rustmotion-cli/src/skills.rs rename to crates/rustmotion/src/cli/skills.rs index 7fbced6..34f3c89 100644 --- a/crates/rustmotion-cli/src/skills.rs +++ b/crates/rustmotion/src/cli/skills.rs @@ -10,7 +10,7 @@ struct SkillFile { } /// CLAUDE.md project instructions (written at project root). -const CLAUDE_MD: &str = include_str!("../../../CLAUDE.md"); +const CLAUDE_MD: &str = include_str!("../../CLAUDE.md"); /// All skill files embedded at compile time. /// @@ -79,7 +79,7 @@ pub fn install(global: bool) -> Result<()> { if !global { let claude_path = root.join("CLAUDE.md"); let existing = std::fs::read_to_string(&claude_path).ok(); - let merged = crate::claude_md::merge(existing.as_deref(), CLAUDE_MD); + let merged = crate::cli::claude_md::merge(existing.as_deref(), CLAUDE_MD); if write_if_changed(&claude_path, &merged)? { written += 1; } else { @@ -194,7 +194,7 @@ pub fn uninstall(global: bool) -> Result<()> { if !global { let claude_path = root.join("CLAUDE.md"); if let Ok(existing) = std::fs::read_to_string(&claude_path) { - match crate::claude_md::strip(&existing) { + match crate::cli::claude_md::strip(&existing) { Some(remaining) => { if remaining != existing { std::fs::write(&claude_path, remaining)?; diff --git a/crates/rustmotion-cli/src/tui.rs b/crates/rustmotion/src/cli/tui.rs similarity index 100% rename from crates/rustmotion-cli/src/tui.rs rename to crates/rustmotion/src/cli/tui.rs diff --git a/crates/rustmotion/src/encode/audio.rs b/crates/rustmotion/src/encode/audio.rs index 4752fc9..85f0979 100644 --- a/crates/rustmotion/src/encode/audio.rs +++ b/crates/rustmotion/src/encode/audio.rs @@ -808,7 +808,7 @@ mod tests { // ── media-io: probe_audio_metadata ────────────────────────────────────── // - // `rustmotion info` (crates/rustmotion-cli/src/commands/info.rs) needs + // `rustmotion info` (crates/rustmotion/src/cli/commands/info.rs) needs // "how long is this audio file, at what rate/channel count" for every // `audio[].src` a scenario declares. The brief for that fix is explicit: // reuse `decode_audio_file` — the exact decode `mix_audio_tracks_segment` diff --git a/crates/rustmotion/src/lib.rs b/crates/rustmotion/src/lib.rs index d7aba3c..70a89d6 100644 --- a/crates/rustmotion/src/lib.rs +++ b/crates/rustmotion/src/lib.rs @@ -1,3 +1,10 @@ +// `cli` était une crate séparée qui consommait celle-ci sous le nom +// `rustmotion::`. Cet alias garde ces chemins valides depuis l'intérieur, ce +// qui laisse le module CLI écrire `rustmotion::schema::…` exactement comme +// n'importe quel consommateur externe — sans quoi il faudrait réécrire des +// centaines de chemins en `crate::` pour un déplacement de fichiers. +extern crate self as rustmotion; + // Re-export core and components for downstream users pub use rustmotion_core as core; pub use rustmotion_core::error; @@ -19,6 +26,7 @@ pub mod engine { // Local modules pub mod assets; +pub mod cli; pub mod encode; pub mod include; pub mod loader; diff --git a/crates/rustmotion-cli/tests/motion_path_strict_anim.rs b/crates/rustmotion/tests/motion_path_strict_anim.rs similarity index 97% rename from crates/rustmotion-cli/tests/motion_path_strict_anim.rs rename to crates/rustmotion/tests/motion_path_strict_anim.rs index 99df072..315b992 100644 --- a/crates/rustmotion-cli/tests/motion_path_strict_anim.rs +++ b/crates/rustmotion/tests/motion_path_strict_anim.rs @@ -13,8 +13,8 @@ //! viewport while following its curve would pass validation silently. This //! test is run against the actual compiled `rustmotion` binary (not an //! internal call into `validate_geometry_animated`) because -//! `rustmotion-cli::commands` is a private module (`mod commands;` in -//! `src/lib.rs`) — the CLI subprocess is the only externally-observable +//! `rustmotion::cli::commands` is a private module (`mod commands;` in +//! `src/cli/mod.rs`) — the CLI subprocess is the only externally-observable //! contract for "`--strict-anim` sees this". //! //! Three scenarios, each isolating one variable: @@ -36,7 +36,7 @@ struct ScratchFile(PathBuf); impl ScratchFile { fn new(label: &str) -> Self { let unique = format!( - "rustmotion-cli-motion-path-test-{label}-{}-{}", + "rustmotion-motion-path-test-{label}-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/crates/rustmotion-cli/tests/skill_files_match_disk.rs b/crates/rustmotion/tests/skill_files_match_disk.rs similarity index 96% rename from crates/rustmotion-cli/tests/skill_files_match_disk.rs rename to crates/rustmotion/tests/skill_files_match_disk.rs index 98f4bf5..9efc3be 100644 --- a/crates/rustmotion-cli/tests/skill_files_match_disk.rs +++ b/crates/rustmotion/tests/skill_files_match_disk.rs @@ -1,6 +1,6 @@ //! Guards `rustmotion skills install` against silently dropping rule files. //! -//! `SKILL_FILES` (embedded by `build.rs`, see `src/skills.rs`) is meant to +//! `SKILL_FILES` (embedded by `build.rs`, see `src/cli/skills.rs`) is meant to //! mirror every `.md` file under `.claude/skills/rustmotion/` exactly. //! Before `build.rs` existed, that table was a hand-maintained literal list //! that fell 17 files behind disk without ever failing a build or a test @@ -18,12 +18,12 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -/// `crates/rustmotion-cli` -> `crates` -> ``. +/// `crates/rustmotion` -> `crates` -> ``. fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(Path::parent) - .expect("rustmotion-cli is expected at /crates/rustmotion-cli") + .expect("rustmotion is expected at /crates/rustmotion") .to_path_buf() } @@ -60,7 +60,7 @@ struct ScratchDir(PathBuf); impl ScratchDir { fn new(label: &str) -> Self { let unique = format!( - "rustmotion-cli-test-{label}-{}-{}", + "rustmotion-skills-test-{label}-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)