mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
commit
bdca69e6e3
37 files changed
+245
-95
No files matched your search
@@ -28,8 +28,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
windows-arm64:
|
||||
# Immutable supporting workflow revision until the new install contract lands upstream.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@main
|
||||
with:
|
||||
app-name: effectcraft
|
||||
app-dir: EffectCraft
|
||||
|
||||
@@ -144,3 +144,8 @@ as PhotoCraft does with `cargo xtask corpus`.
|
||||
GitHub profiles). Never hand-edit `contributors.json`.
|
||||
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
|
||||
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
|
||||
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
|
||||
@@ -30,3 +30,8 @@ EffectCraft is a clean-room, open-source, pure-Rust motion graphics and visual e
|
||||
- For UI work, **look at the result**. Headless (no window, works while the user is busy on the machine): `cargo run -p effectcraft-ui-egui --example snapshot -- --out ui.png --step '{"method":"engine.execute","params":{"command":"layer.select","params":{"layers":["#2"]}}}'` (steps are control-channel requests; `--script steps.jsonl`; `{"method":"snap","params":{"path":…}}` for intermediate shots). Or drive the live app via the control channel and take `ui.screenshot` (needs the window visible on screen).
|
||||
- After Effects reference: drive AE via ExtendScript (`osascript -e 'tell application "Adobe After Effects 2026" to DoScriptFile "x.jsx"'`) and capture only AE's window (`screencapture -l <windowid>`); keep captures in `plan/aftereffects/ref/` (gitignored), never commit them, never capture the Home screen.
|
||||
- Parallel agents: separate git worktrees and `CARGO_TARGET_DIR=target/agent-<name>`; keep every `Cargo.toml` valid (the `crates/*` glob).
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
|
||||
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
|
||||
Generated
+5
-5
@@ -4293,7 +4293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4885,9 +4885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.21.1"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||
checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -7470,9 +7470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.0"
|
||||
version = "9.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704"
|
||||
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
|
||||
+15
-15
@@ -1874,7 +1874,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("comp.settings", "Composition Settings...", "コンポジション設定..."),
|
||||
("comp.setPosterTime", "Set Poster Time", "ポスター時間を設定"),
|
||||
("comp.trimToWorkArea", "Trim Comp to Work Area", "コンポジションをワークエリアにトリミング"),
|
||||
("comp.cropToRegionOfInterest", "Crop Comp to Region of Interest", "コンポジションを目標範囲にクロップ"),
|
||||
("comp.cropToRegionOfInterest", "Crop Comp to Region of Interest", "コンポジションを関心領域にクロップ"),
|
||||
("comp.cropToLayerBounds", "Crop Comp to Selected Layer(s) Bounds", "コンポジションを選択レイヤーの境界にクロップ"),
|
||||
("render.addOutputModule", "Add Output Module", "出力モジュールを追加"),
|
||||
("", "Preview", "プレビュー"),
|
||||
@@ -1927,7 +1927,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.mask.reset", "Reset Mask", "マスクをリセット"),
|
||||
("layer.mask.remove", "Remove Mask", "マスクを削除"),
|
||||
("layer.mask.removeAll", "Remove All Masks", "すべてのマスクを削除"),
|
||||
("track.mask", "Track Mask", "マスクをトラック"),
|
||||
("track.mask", "Track Mask", "マスクをトラッキング"),
|
||||
("", "Mode", "モード"),
|
||||
("layer.mask.mode", "None", "なし"),
|
||||
("layer.mask.mode", "Add", "加算"),
|
||||
@@ -1992,8 +1992,8 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.transform", "Fit to Comp Height", "コンポジションの高さに合わせる"),
|
||||
("layer.autoOrient", "Auto-Orient...", "自動方向..."),
|
||||
("", "Time", "時間"),
|
||||
("layer.enableTimeRemap", "Enable Time Remapping", "タイムリマップ使用可能"),
|
||||
("layer.timeReverse", "Time-Reverse Layer", "時間反転レイヤー"),
|
||||
("layer.enableTimeRemap", "Enable Time Remapping", "時間リマップを有効化"),
|
||||
("layer.timeReverse", "Time-Reverse Layer", "レイヤーを時間反転"),
|
||||
("layer.timeStretch", "Time Stretch...", "時間伸縮..."),
|
||||
("layer.freezeFrame", "Freeze Frame", "フレームを固定"),
|
||||
("layer.freezeOnLastFrame", "Freeze On Last Frame", "最後のフレームで固定"),
|
||||
@@ -2061,7 +2061,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.trackMatte", "Matte with Layer Above", "上のレイヤーをマットにする"),
|
||||
("layer.trackMatte", "Matte with Layer Below", "下のレイヤーをマットにする"),
|
||||
("", "Layer Styles", "レイヤースタイル"),
|
||||
("layer.style.options", "Layer Style Options...", "レイヤースタイルオプション..."),
|
||||
("layer.style.options", "Layer Style Options...", "レイヤースタイルのオプション..."),
|
||||
("layer.style.convertToEditable", "Convert to Editable Styles", "編集可能なスタイルに変換"),
|
||||
("layer.style.showAll", "Show All", "すべてを表示"),
|
||||
("layer.style.removeAll", "Remove All", "すべてを削除"),
|
||||
@@ -2090,7 +2090,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.create", "Convert to Editable Text", "編集可能なテキストに変換"),
|
||||
("layer.create", "Create Shapes from Text", "テキストからシェイプを作成"),
|
||||
("layer.create", "Create Masks from Text", "テキストからマスクを作成"),
|
||||
("layer.create", "Create Shapes from Vector Layer", "ベクトルレイヤーからシェイプを作成"),
|
||||
("layer.create", "Create Shapes from Vector Layer", "ベクターレイヤーからシェイプを作成"),
|
||||
("layer.create", "Create Keyframes from Data", "データからキーフレームを作成"),
|
||||
("layer.create", "Null Controllers for Positional Points", "位置のポイント用ヌルコントローラー"),
|
||||
("layer.create", "Null Controllers for Path Points", "パスのポイント用ヌルコントローラー"),
|
||||
@@ -2140,7 +2140,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Jitter", "ジッター"),
|
||||
("layer.applyTextPreset", "Drop In Lines", "行をドロップイン"),
|
||||
("anim.addKeyframe", "Add Keyframe", "キーフレームを追加"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "停止したキーフレームの切り替え"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "ホールドキーフレームの切り替え"),
|
||||
("keys.setMuted", "Mute Keyframes", "キーフレームをミュート"),
|
||||
("keys.setMuted", "Unmute Keyframes", "キーフレームのミュートを解除"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "キーフレーム補間法..."),
|
||||
@@ -2156,7 +2156,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.sequence", "Sequence Layers...", "シーケンスレイヤー..."),
|
||||
("keys.timeReverse", "Time-Reverse Keyframes", "時間反転キーフレーム"),
|
||||
("", "Animate Text", "テキストをアニメート"),
|
||||
("layer.enablePerChar3D", "Enable Per-character 3D", "文字ごとの3Dを使用可能にする"),
|
||||
("layer.enablePerChar3D", "Enable Per-character 3D", "文字ごとの3Dを有効化"),
|
||||
("layer.addTextAnimator", "Anchor Point", "アンカーポイント"),
|
||||
("layer.addTextAnimator", "Position", "位置"),
|
||||
("layer.addTextAnimator", "Scale", "スケール"),
|
||||
@@ -2168,7 +2168,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "RGB", "RGB"),
|
||||
("layer.addTextAnimator", "Hue", "色相"),
|
||||
("layer.addTextAnimator", "Saturation", "彩度"),
|
||||
("layer.addTextAnimator", "Brightness", "明度"),
|
||||
("layer.addTextAnimator", "Brightness", "明るさ"),
|
||||
("", "Stroke Color", "線のカラー"),
|
||||
("layer.addTextAnimator", "Stroke Width", "線幅"),
|
||||
("", "Front", "前面"),
|
||||
@@ -2190,12 +2190,12 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("text.removeAllAnimators", "Remove All Text Animators", "すべてのテキストアニメーターを削除"),
|
||||
("prop.setExpression", "Add Expression", "エクスプレッションを追加"),
|
||||
("essential.addProperty", "Add Property to Essential Graphics", "エッセンシャルグラフィックスにプロパティを追加"),
|
||||
("prop.separateDimensions", "Separate Dimensions", "次元に分割"),
|
||||
("track.camera", "Track Camera", "カメラをトラック"),
|
||||
("prop.separateDimensions", "Separate Dimensions", "次元を分離"),
|
||||
("track.camera", "Track Camera", "カメラをトラッキング"),
|
||||
("track.warpStabilizer", "Warp Stabilizer VFX", "ワープスタビライザーVFX"),
|
||||
("track.motion", "Track Motion", "モーションをトラック"),
|
||||
("track.stabilize", "Stabilize Motion", "モーションをスタビライズ"),
|
||||
("track.property", "Track this Property", "このプロパティをトラック"),
|
||||
("track.motion", "Track Motion", "モーションをトラッキング"),
|
||||
("track.stabilize", "Stabilize Motion", "モーションを安定化"),
|
||||
("track.property", "Track this Property", "このプロパティをトラッキング"),
|
||||
("anim.reveal", "Reveal Properties with Keyframes", "キーフレームのあるプロパティを表示"),
|
||||
("anim.reveal", "Reveal Properties with Animation", "アニメーションのあるプロパティを表示"),
|
||||
("anim.reveal", "Reveal All Modified Properties", "変更されたすべてのプロパティを表示"),
|
||||
@@ -2275,7 +2275,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("window.workspace", "Default", "初期設定"),
|
||||
("window.workspace", "Review", "レビュー"),
|
||||
("window.workspace", "Learn", "学習"),
|
||||
("window.workspace", "Small Screen", "小画面"),
|
||||
("window.workspace", "Small Screen", "小さい画面"),
|
||||
("window.workspace", "Standard", "標準"),
|
||||
("window.workspace", "All Panels", "すべてのパネル"),
|
||||
("window.workspace", "Animation", "アニメーション"),
|
||||
|
||||
@@ -455,7 +455,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("None", "なし"),
|
||||
("Not applied: tick it in the list to add it.", "未適用:リストでチェックを入れると追加されます。"),
|
||||
("Not assigned", "未割り当て"),
|
||||
("Note: Shadows are only cast from layers with Cast Shadows enabled.", "注意:シャドウは「シャドウを投影」が有効なレイヤーからのみ投影されます。"),
|
||||
("Note: Shadows are only cast from layers with Cast Shadows enabled.", "注意:「シャドウを落とす」が有効なレイヤーだけがシャドウを落とします。"),
|
||||
("Notify", "通知"),
|
||||
("OK", "OK"),
|
||||
("Offset:", "オフセット:"),
|
||||
@@ -537,7 +537,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Select a command to change its shortcut.", "ショートカットを変更するコマンドを選択してください。"),
|
||||
("Select a layer to see its effects", "レイヤーを選択するとエフェクトが表示されます"),
|
||||
("Select a layer to see its properties", "レイヤーを選択するとプロパティが表示されます"),
|
||||
("({}) Selected Objects", "({}) 個の選択されたオブジェクト"),
|
||||
("({}) Selected Objects", "選択したオブジェクト({})"),
|
||||
(
|
||||
"Select a layer with a mask or shape path (or the path property), or Puppet pins.",
|
||||
"マスクまたはシェイプパスのあるレイヤー(またはパスプロパティ)か、パペットピンを選択してください。",
|
||||
@@ -685,7 +685,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Import Image Sequence", "画像シーケンスを読み込み"),
|
||||
("Import PDF / Illustrator File", "PDF / Illustratorファイルを読み込み"),
|
||||
("Import Photoshop File", "Photoshopファイルを読み込み"),
|
||||
("Interpret Unlabeled Alpha As", "ラベルのないアルファを変換"),
|
||||
("Interpret Unlabeled Alpha As", "ラベルのないアルファの解釈"),
|
||||
("Join the ArtCraft Discord", "ArtCraftのDiscordに参加"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPUアクセラレーション({})"),
|
||||
("Mercury Software Only", "Mercuryソフトウェアのみ"),
|
||||
@@ -803,7 +803,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Cycle Mask Colors", "新規マスクに別のカラーを使用"),
|
||||
("Use Gradients", "グラデーションを使用"),
|
||||
("Menu Bar", "メニューバー"),
|
||||
("Grids & Guides", "グリッド&ガイド"),
|
||||
("Grids & Guides", "グリッドとガイド"),
|
||||
("Gridline Every", "グリッドの間隔"),
|
||||
("Subdivisions", "分割数"),
|
||||
("Style", "スタイル"),
|
||||
@@ -835,7 +835,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Ask User", "ユーザーに確認"),
|
||||
("Guess", "推測"),
|
||||
("Ignore", "無視"),
|
||||
("Straight", "そのまま"),
|
||||
("Straight", "ストレート"),
|
||||
("Premultiplied", "プリマルチプライ"),
|
||||
("Default Drag Import As", "複数アイテムのドラッグ読み込みの初期設定"),
|
||||
("Footage", "フッテージ"),
|
||||
@@ -885,7 +885,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Scripting & Expressions", "スクリプトとエクスプレッション"),
|
||||
("Application Scripting", "アプリケーションのスクリプト"),
|
||||
("Allow Scripts to Write Files and Access Network", "スクリプトによるファイルへの書き込みとネットワークへのアクセスを許可"),
|
||||
("Warn User When Executing Files", "ファイルの実行時にカーソルに警告"),
|
||||
("Warn User When Executing Files", "ファイルの実行時にユーザーに警告"),
|
||||
("Enable JavaScript Debugger", "JavaScript デバッガーを使用"),
|
||||
("Expressions Editor", "エクスプレッションエディター"),
|
||||
("Font Size", "フォントサイズ"),
|
||||
@@ -894,7 +894,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Auto-complete", "オートコンプリート"),
|
||||
("Bracket Matching", "対応する括弧をハイライト"),
|
||||
("Word Wrap", "折り返し"),
|
||||
("Show Expression Error Banner", "式のエラーがある場合に警告バナーを表示"),
|
||||
("Show Expression Error Banner", "エクスプレッションのエラーがある場合に警告バナーを表示"),
|
||||
(
|
||||
"Expressions use the JavaScript engine. Script errors report their file and line in the Script Console; there is no step debugger.",
|
||||
"エクスプレッションはJavaScriptエンジンを使用します。スクリプトのエラーはファイルと行をスクリプトコンソールに報告します。ステップ実行デバッガーはありません。",
|
||||
@@ -1003,8 +1003,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Create Shapes from Vector Layer", "ベクターレイヤーからシェイプを作成"),
|
||||
("Create Shapes from Text", "テキストからシェイプを作成"),
|
||||
("Create Masks from Text", "テキストからマスクを作成"),
|
||||
("New Mask", "新しいマスク"),
|
||||
("Center In View", "ビュー内で中央揃え"),
|
||||
("New Mask", "新規マスク"),
|
||||
("Center In View", "ビューの中央に配置"),
|
||||
("Fit to Comp", "コンポジションに合わせる"),
|
||||
("Flip Horizontal", "水平方向に反転"),
|
||||
("Flip Vertical", "垂直方向に反転"),
|
||||
@@ -1033,8 +1033,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("All Transform Properties", "すべてのトランスフォームプロパティ"),
|
||||
("Line Anchor", "行のアンカー"),
|
||||
("Line Spacing", "行間"),
|
||||
("Character Offset", "文字オフセット"),
|
||||
("Character Value", "文字値"),
|
||||
("Character Offset", "文字のオフセット"),
|
||||
("Character Value", "文字の値"),
|
||||
("Fill Color: RGB", "塗りの色: RGB"),
|
||||
("Fill Color: Hue", "塗りの色: 色相"),
|
||||
("Fill Color: Saturation", "塗りの色: 彩度"),
|
||||
@@ -1060,7 +1060,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("ScriptUI Panel", "ScriptUI パネル"),
|
||||
("Lumetri Scopes", "Lumetri スコープ"),
|
||||
("Media Browser", "メディアブラウザー"),
|
||||
("Content-Aware Fill", "コンテンツに応じた塗り"),
|
||||
("Content-Aware Fill", "コンテンツに応じた塗りつぶし"),
|
||||
("Create Nulls From Paths", "パスからヌルを作成"),
|
||||
("VR Comp Editor", "VR コンポジションエディター"),
|
||||
("Media Duration", "メディアの長さ"),
|
||||
@@ -1095,7 +1095,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Background Color", "背景色"),
|
||||
("Basic", "基本"),
|
||||
("Shutter Angle", "シャッター角度"),
|
||||
("Shutter Phase", "シャッター位相"),
|
||||
("Shutter Phase", "シャッターフェーズ"),
|
||||
("Samples Per Frame", "フレームあたりのサンプル数"),
|
||||
("Adaptive Sample Limit", "適応サンプル数の上限"),
|
||||
("X and Y", "X と Y"),
|
||||
|
||||
@@ -296,21 +296,13 @@ fn marker(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, comp:
|
||||
hx += 5.0;
|
||||
}
|
||||
}
|
||||
p.line_segment([pos2(xe, y0), pos2(xe, y0 + h)], Stroke::new(1.0, col));
|
||||
}
|
||||
p.add(egui::Shape::convex_polygon(marker_shape(x, y0, h), col, Stroke::NONE));
|
||||
if !m.comment.is_empty() {
|
||||
// The name sits in a small outlined box beside the flag, as in After Effects. It is drawn in the theme's field
|
||||
// colours, so it follows Dark, Darker and Light.
|
||||
let galley = p.layout_no_wrap(m.comment.lines().next().unwrap_or_default().to_string(), Tokens::ui(10.0), t.text);
|
||||
let (box_w, mut box_x) = (galley.size().x + 8.0, x + 6.0);
|
||||
// A duration that ends under the box would be hidden by its fill: the box steps past the end cap instead.
|
||||
if let Some(xe) = end
|
||||
&& xe > box_x
|
||||
&& xe < box_x + box_w
|
||||
{
|
||||
box_x = xe + 3.0;
|
||||
}
|
||||
let box_w = galley.size().x + 8.0;
|
||||
let box_x = name_box_x(x, box_w, end, p.clip_rect());
|
||||
let box_rect = Rect::from_min_size(pos2(box_x, y0), vec2(box_w, h.max(galley.size().y + 2.0)));
|
||||
p.rect(box_rect, t.radius_sm, t.field_bg, Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
|
||||
p.galley(pos2(box_rect.min.x + 4.0, box_rect.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
@@ -385,6 +377,33 @@ fn marker(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, comp:
|
||||
});
|
||||
}
|
||||
|
||||
fn name_box_x(x: f32, width: f32, end: Option<f32>, clip: Rect) -> f32 {
|
||||
let start = x + 6.0;
|
||||
let right = match end {
|
||||
Some(end) if end > start && end < start + width => end + 3.0,
|
||||
_ => start,
|
||||
};
|
||||
if right + width <= clip.right() {
|
||||
return right;
|
||||
}
|
||||
let left = x - 6.0 - width;
|
||||
if left >= clip.left() {
|
||||
return left;
|
||||
}
|
||||
start
|
||||
}
|
||||
|
||||
/// Flags and duration handles sit above every name box in their marker strip.
|
||||
fn marker_glyphs(p: &egui::Painter, tokens: &Tokens, tm: TMap, marker: &Marker, time: f64, y0: f32, y1: f32) {
|
||||
let color = marker_color(tokens, marker.label);
|
||||
let h = (y1 - y0).min(10.0);
|
||||
p.add(egui::Shape::convex_polygon(marker_shape(tm.x(time), y0, h), color, Stroke::NONE));
|
||||
if marker.duration > Tick(0) {
|
||||
let end = tm.x(time + marker.duration.seconds());
|
||||
p.line_segment([pos2(end, y0), pos2(end, y0 + h)], Stroke::new(1.0, color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Composition markers in the ruler strip `[y0, y1]`, protected regions over `rows`.
|
||||
pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Comp, tm: TMap, strip: Rect, rows: Rect) {
|
||||
let p = ui.painter().with_clip_rect(strip);
|
||||
@@ -398,6 +417,9 @@ pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &C
|
||||
for (i, m) in comp.markers.iter().enumerate() {
|
||||
marker(app, ui, &p, comp, tm, m, m.time.seconds(), MarkerRef { layer: None, index: i }, strip.min.y, strip.max.y);
|
||||
}
|
||||
for m in &comp.markers {
|
||||
marker_glyphs(&p, &app.tokens, tm, m, m.time.seconds(), strip.min.y, strip.max.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// The composition marker bin at the right end of the time ruler (`bin`), as in After Effects
|
||||
@@ -439,6 +461,9 @@ pub(crate) fn layer_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, clip: R
|
||||
for (i, m) in layer.markers.iter().enumerate() {
|
||||
marker(app, ui, &p, comp, tm, m, layer.comp_time(m.time).seconds(), MarkerRef { layer: Some(layer.id.0), index: i }, r.min.y + 3.0, r.max.y - 3.0);
|
||||
}
|
||||
for m in &layer.markers {
|
||||
marker_glyphs(&p, &app.tokens, tm, m, layer.comp_time(m.time).seconds(), r.min.y + 3.0, r.max.y - 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The nested comp's markers on a precomp layer's bar (`markers.nested`).
|
||||
@@ -488,6 +513,21 @@ fn nested_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn duration_name_box_stays_visible_near_the_right_clip_edge() {
|
||||
let clip = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 20.0));
|
||||
let x = name_box_x(70.0, 20.0, Some(85.0), clip);
|
||||
assert!(x >= clip.left() && x + 20.0 <= clip.right());
|
||||
assert!(x + 20.0 < 70.0 - 4.0, "the left-hand box must not cover its own marker flag");
|
||||
assert!(85.0 < x || 85.0 > x + 20.0, "the end cap must remain outside the box");
|
||||
assert_eq!(name_box_x(20.0, 20.0, Some(35.0), clip), 38.0);
|
||||
for end in [None, Some(72.0), Some(110.0)] {
|
||||
let x = name_box_x(90.0, 20.0, end, clip);
|
||||
assert!(x >= clip.left() && x + 20.0 <= clip.right(), "all right-edge names must remain visible: {end:?}");
|
||||
assert!(x + 20.0 < 90.0 - 4.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marker_dialog_reads_and_writes_every_field() {
|
||||
let mut s = effectcraft_engine::Session::default();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Shared device-creation gate for the integration-test executable.
|
||||
|
||||
static GPU_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
thread_local! {
|
||||
static HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
pub(crate) struct Guard {
|
||||
held: Option<std::sync::MutexGuard<'static, ()>>,
|
||||
}
|
||||
|
||||
impl Drop for Guard {
|
||||
fn drop(&mut self) {
|
||||
if self.held.is_some() {
|
||||
HELD.with(|held| held.set(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GPU-backed harnesses hold this for their whole test; screenshots may enter it again.
|
||||
pub(crate) fn lock() -> Guard {
|
||||
let held = if HELD.with(std::cell::Cell::get) {
|
||||
None
|
||||
} else {
|
||||
let guard = GPU_TEST_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
HELD.with(|held| held.set(true));
|
||||
Some(guard)
|
||||
};
|
||||
Guard { held }
|
||||
}
|
||||
|
||||
/// Plain harnesses create their GPU renderer lazily on the first screenshot.
|
||||
pub(crate) trait RenderSerial {
|
||||
fn render_serialized(&mut self) -> Result<image::RgbaImage, String>;
|
||||
}
|
||||
|
||||
impl<State> RenderSerial for egui_kittest::Harness<'_, State> {
|
||||
fn render_serialized(&mut self) -> Result<image::RgbaImage, String> {
|
||||
let _device = lock();
|
||||
self.render().map_err(|error| format!("{error:?}"))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The crate's integration tests, one binary (each module is a former `tests/<name>.rs`).
|
||||
|
||||
mod gpu_test;
|
||||
mod native_menu_text_keys;
|
||||
mod render_queue_snapshot;
|
||||
mod shared_docking_ui;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! adapter); run with
|
||||
//! `RQ_SNAPSHOT=/abs/out.png cargo test -p effectcraft-ui-egui --test it render_queue_snapshot:: -- --ignored`.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::render_queue::RenderStatus;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -34,7 +35,7 @@ fn render_queue_panel_snapshot() {
|
||||
h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique("rq-ui"), (ids, None::<u64>)));
|
||||
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::RenderQueue);
|
||||
h.run_steps(4);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
let out = std::env::var("RQ_SNAPSHOT").unwrap_or_else(|_| concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out/render_queue.png").into());
|
||||
if let Some(d) = std::path::Path::new(&out).parent() {
|
||||
std::fs::create_dir_all(d).unwrap();
|
||||
@@ -57,7 +58,7 @@ fn templates_dialog_snapshot() {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| app.take().expect("app"));
|
||||
h.run_steps(4);
|
||||
assert!(h.state().dialog.is_some(), "the Templates dialog is open");
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
let out = std::env::var("RQ_TEMPLATES_SNAPSHOT").unwrap_or_else(|_| concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out/rq_templates.png").into());
|
||||
if let Some(d) = std::path::Path::new(&out).parent() {
|
||||
std::fs::create_dir_all(d).unwrap();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! with wgpu and writes PNGs when `EC_SNAPSHOT_DIR` is set (`cargo test -p effectcraft-ui-egui
|
||||
//! --test it ui_3d:: -- --ignored`); the other test only runs the UI logic.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::render::three_d::View3D;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -109,6 +110,7 @@ fn camera_and_light_dialogs_create_layers() {
|
||||
#[test]
|
||||
#[ignore = "renders with wgpu; run with --ignored (set EC_SNAPSHOT_DIR to keep PNGs)"]
|
||||
fn snapshot() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let dir = std::env::var("EC_SNAPSHOT_DIR").ok();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_pixels_per_point(1.0).wgpu().build_eframe(|_| app());
|
||||
let shots: [(&str, Option<&str>); 4] =
|
||||
@@ -118,7 +120,7 @@ fn snapshot() {
|
||||
h.state_mut().session.execute(c, json!({})).unwrap();
|
||||
}
|
||||
settle(&mut h);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
if let Some(d) = &dir {
|
||||
img.save(format!("{d}/ui3d_{name}.png")).unwrap();
|
||||
}
|
||||
@@ -126,14 +128,14 @@ fn snapshot() {
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "layer.newCamera", json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
if let Some(d) = &dir {
|
||||
img.save(format!("{d}/ui3d_camera_dialog.png")).unwrap();
|
||||
}
|
||||
h.state_mut().dialog = None;
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "layer.newLight", json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
if let Some(d) = &dir {
|
||||
img.save(format!("{d}/ui3d_light_dialog.png")).unwrap();
|
||||
}
|
||||
@@ -143,7 +145,7 @@ fn snapshot() {
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": ["Card Blue"]})).unwrap();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.reveal.rotation", json!({})).unwrap();
|
||||
settle(&mut h);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
if let Some(d) = &dir {
|
||||
img.save(format!("{d}/ui3d_reveal_rotation.png")).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Right-click actions in the blank Project and Timeline areas, and on Timeline property names.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::ItemKind;
|
||||
use effectcraft_ui_egui::dock::WorkspaceExt as _;
|
||||
@@ -20,7 +21,7 @@ fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
}
|
||||
fn snapshot(h: &mut Harness<'_, EffectcraftApp>, name: &str) {
|
||||
if let Ok(dir) = std::env::var("BLANK_CONTEXT_MENU_SNAPSHOTS") {
|
||||
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
fn right_click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! The solve is a synthetic one written into the effect (the analysis itself is tested in the
|
||||
//! engine and track crates).
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::effects::camera_tracker as ct;
|
||||
use effectcraft_engine::track::camtrack::{CameraSolve, ShotType, SolveMethod, SolvedFrame, SolvedPoint};
|
||||
@@ -186,6 +187,7 @@ fn blender_export_button_opens_python_dialog_and_writes_scene() {
|
||||
let shot = std::env::var("EFFECTCRAFT_BLENDER_UI_SNAPSHOT").ok();
|
||||
let builder = Harness::builder().with_size(egui::vec2(1600.0, 1000.0));
|
||||
let builder = if shot.is_some() { builder.wgpu() } else { builder };
|
||||
let _device = shot.is_some().then(crate::gpu_test::lock);
|
||||
let mut h = builder.build_eframe(|_| a);
|
||||
h.state_mut().show_panel(PanelKind::EffectControls);
|
||||
h.run_steps(5);
|
||||
@@ -272,6 +274,7 @@ fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
#[test]
|
||||
#[ignore = "analyses the demo's 3D Showcase and renders with wgpu; run with --ignored (set EC_SNAPSHOT_DIR to keep PNGs)"]
|
||||
fn snapshot_showcase_track() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let dir = std::env::var("EC_SNAPSHOT_DIR").ok();
|
||||
let mut s = Session::default();
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
@@ -290,7 +293,7 @@ fn snapshot_showcase_track() {
|
||||
h.input_mut().events.push(egui::Event::PointerMoved(r.center() + egui::vec2(12.0, 12.0)));
|
||||
h.run_steps(2);
|
||||
if let Some(d) = &dir {
|
||||
h.render().expect("render").save(format!("{d}/camera_tracker_hover.png")).unwrap();
|
||||
h.render_serialized().expect("render").save(format!("{d}/camera_tracker_hover.png")).unwrap();
|
||||
}
|
||||
let sel: Vec<u64> = pts.iter().take(5).filter_map(|i| i.rsplit('.').next()?.parse().ok()).collect();
|
||||
h.state_mut().session.execute("camera.selectPoints", json!({"points": sel})).unwrap();
|
||||
@@ -298,6 +301,6 @@ fn snapshot_showcase_track() {
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": ["#3"]})).unwrap();
|
||||
settle(&mut h);
|
||||
if let Some(d) = &dir {
|
||||
h.render().expect("render").save(format!("{d}/camera_tracker_solid.png")).unwrap();
|
||||
h.render_serialized().expect("render").save(format!("{d}/camera_tracker_solid.png")).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Character Fill and Stroke can sample a displayed Composition pixel.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::{Session, commands::text_edit::layer_doc, project::LayerId};
|
||||
use effectcraft_ui_egui::{
|
||||
EffectcraftApp,
|
||||
@@ -37,6 +38,7 @@ fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
|
||||
#[test]
|
||||
fn character_fill_eyedropper_samples_viewer_and_keeps_text_selection() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 320, "height": 180, "duration": 2})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Background", "color": "#20c040"})).unwrap();
|
||||
@@ -64,6 +66,7 @@ fn character_fill_eyedropper_samples_viewer_and_keeps_text_selection() {
|
||||
|
||||
#[test]
|
||||
fn character_stroke_eyedropper_samples_viewer() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 320, "height": 180, "duration": 2})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "brat green", "color": "#8ACE00"})).unwrap();
|
||||
@@ -79,7 +82,7 @@ fn character_stroke_eyedropper_samples_viewer() {
|
||||
settle(&mut h);
|
||||
click_auto(&mut h, "character.stroke.eyedropper");
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().unwrap().save(format!("{dir}/character-eyedropper.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/character-eyedropper.png")).unwrap();
|
||||
}
|
||||
let sample = comp_to_screen(&h.ctx, [20.0, 20.0]).unwrap();
|
||||
click(&mut h, sample);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Shared pane resizing in the full application, including application shortcut ownership.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use craft_ui::docking::{Divider, DockArea, Hit};
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -16,7 +17,7 @@ fn snapshots(h: &mut Harness<'_, EffectcraftApp>, directory: Option<&std::path::
|
||||
let ctx = h.ctx.clone();
|
||||
h.state_mut().set_theme(&ctx, theme);
|
||||
h.run_steps(3);
|
||||
h.render().unwrap().save(directory.join(format!("effectcraft-{phase}-{name}.png"))).unwrap();
|
||||
h.render_serialized().unwrap().save(directory.join(format!("effectcraft-{phase}-{name}.png"))).unwrap();
|
||||
}
|
||||
let ctx = h.ctx.clone();
|
||||
h.state_mut().set_theme(&ctx, ThemeKind::Dark);
|
||||
@@ -32,6 +33,7 @@ fn first_size(h: &Harness<'_, EffectcraftApp>) -> f32 {
|
||||
|
||||
#[test]
|
||||
fn focused_dock_resize_owns_arrows_and_keeps_pointer_dragging() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let mut session = Session::default();
|
||||
session.execute("comp.new", json!({"name": "Split test", "width": 160, "height": 90, "duration": 1})).unwrap();
|
||||
session.execute("layer.newSolid", json!({"name": "Selected", "color": [1.0, 0.0, 0.0, 1.0]})).unwrap();
|
||||
|
||||
@@ -44,6 +44,7 @@ fn same_adapter(name: String) -> eframe::egui_wgpu::WgpuSetup {
|
||||
/// viewer's GPU frames change when the light is added.
|
||||
#[test]
|
||||
fn environment_light_lights_the_scene_on_the_gpu() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let Some(probe) = effectcraft_gpu::Gpu::headless() else {
|
||||
eprintln!("no GPU adapter: skipping");
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Real keyboard/pointer coverage for the font menus in both text panels.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::{Session, commands::text_edit::layer_doc, project::LayerId};
|
||||
use effectcraft_ui_egui::{EffectcraftApp, dock::PanelKind};
|
||||
use egui::{Event, Key, Modifiers, PointerButton};
|
||||
@@ -29,6 +30,7 @@ fn type_text(h: &mut Harness<'_, EffectcraftApp>, text: &str) {
|
||||
|
||||
#[test]
|
||||
fn font_search_filters_selects_and_undoes_in_both_panels() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
for (panel, prefix) in [(PanelKind::Character, "character.font"), (PanelKind::Properties, "properties.text.font")] {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 320, "height": 180})).unwrap();
|
||||
@@ -56,7 +58,7 @@ fn font_search_filters_selects_and_undoes_in_both_panels() {
|
||||
assert!(!rows.is_empty());
|
||||
assert!(rows.iter().all(|r| r.label.to_lowercase().contains("jetbrains")));
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().unwrap().save(format!("{dir}/{prefix}-search.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/{prefix}-search.png")).unwrap();
|
||||
}
|
||||
assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().font, "Inter", "typing only filters");
|
||||
key(&mut h, Key::Enter);
|
||||
@@ -76,7 +78,7 @@ fn font_search_filters_selects_and_undoes_in_both_panels() {
|
||||
// Reopening clears the previous query, and mouse selection still works.
|
||||
h.run_steps(2);
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().unwrap().save(format!("{dir}/{prefix}-reopened.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/{prefix}-reopened.png")).unwrap();
|
||||
}
|
||||
assert!(h.state().auto.query(&format!("{prefix}.option.")).len() > 1);
|
||||
type_text(&mut h, "JetBrains");
|
||||
|
||||
@@ -53,6 +53,7 @@ fn small_session() -> Session {
|
||||
/// composites on the CPU. The worker is held on a gate so the "preparing" state is observable.
|
||||
#[test]
|
||||
fn the_interface_draws_and_composites_on_the_cpu_while_the_gpu_compositor_is_built() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
if !any_adapter() {
|
||||
eprintln!("no wgpu adapter: skipping");
|
||||
return;
|
||||
@@ -111,6 +112,7 @@ fn the_interface_draws_and_composites_on_the_cpu_while_the_gpu_compositor_is_bui
|
||||
/// thread, not under the UI: the window keeps drawing on the CPU and says why.
|
||||
#[test]
|
||||
fn a_compositor_build_that_panics_leaves_the_viewer_on_the_cpu() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
if !any_adapter() {
|
||||
eprintln!("no wgpu adapter: skipping");
|
||||
return;
|
||||
@@ -153,6 +155,7 @@ fn same_adapter(name: String) -> eframe::egui_wgpu::WgpuSetup {
|
||||
|
||||
#[test]
|
||||
fn viewer_draws_gpu_frames_that_match_the_cpu() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let Some(probe) = effectcraft_gpu::Gpu::headless() else {
|
||||
eprintln!("no GPU adapter: skipping");
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Guide Layer in the layer context menus (Timeline rows and the Composition viewer), a plain item
|
||||
//! (no check mark) that toggles the switch on the selection, and the guide mark by a guide layer's
|
||||
//! name.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::LayerId;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -45,7 +46,7 @@ fn guide(h: &Harness<'_, EffectcraftApp>, id: LayerId) -> bool {
|
||||
|
||||
fn snapshot(h: &mut Harness<'_, EffectcraftApp>, name: &str) {
|
||||
if let Ok(dir) = std::env::var("GUIDE_LAYER_SNAPSHOTS") {
|
||||
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Effect Controls' layer parameters (a Displacement Map's layer) pick through the same widget,
|
||||
//! and both write an undoable `prop.set`.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::keyframe::Value;
|
||||
use effectcraft_engine::project::LayerId;
|
||||
@@ -183,6 +184,7 @@ fn effect_controls_layer_param_picks_through_the_shared_widget() {
|
||||
#[test]
|
||||
#[ignore = "renders with wgpu; run with --ignored (set EC_SNAPSHOT_DIR to keep PNGs)"]
|
||||
fn snapshot_source_dropdown_open_and_chosen() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 1280, "height": 720, "frameRate": 30, "duration": 5})).unwrap();
|
||||
s.execute("comp.settings", json!({"renderer": "advanced3D"})).unwrap();
|
||||
@@ -200,7 +202,7 @@ fn snapshot_source_dropdown_open_and_chosen() {
|
||||
h.run_steps(4);
|
||||
let save = |h: &mut Harness<'_, EffectcraftApp>, name: &str| {
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().expect("render").save(format!("{dir}/{name}")).expect("save");
|
||||
h.render_serialized().expect("render").save(format!("{dir}/{name}")).expect("save");
|
||||
}
|
||||
};
|
||||
save(&mut h, "source_unset.png");
|
||||
|
||||
@@ -8,16 +8,13 @@ use effectcraft_ui_egui::theme::ThemeKind;
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
/// One render at a time: two tests creating a GPU device together crash some Vulkan loaders.
|
||||
static RENDER: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
fn close(got: [u8; 4], want: egui::Color32) -> bool {
|
||||
got[..3].iter().zip(want.to_array()).all(|(a, b)| a.abs_diff(b) <= 3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_name_box_follows_the_theme() {
|
||||
let _one_at_a_time = RENDER.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _one_at_a_time = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Song", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Bed", "color": "#204060", "width": 640, "height": 360})).unwrap();
|
||||
@@ -58,7 +55,7 @@ fn the_name_box_follows_the_theme() {
|
||||
/// stays visible: the cap is drawn, the bar shows left of it, and the box starts after it.
|
||||
#[test]
|
||||
fn a_short_duration_stays_visible_beside_the_name_box() {
|
||||
let _one_at_a_time = RENDER.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _one_at_a_time = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Song", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Bed", "color": "#204060", "width": 640, "height": 360})).unwrap();
|
||||
@@ -95,3 +92,29 @@ fn a_short_duration_stays_visible_beside_the_name_box() {
|
||||
let fill = at(xe + 3.0 + 2.5, mid);
|
||||
assert!(close(fill, tokens.field_bg), "the box begins after the end cap: {fill:?}, field_bg {:?}", tokens.field_bg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_later_name_box_does_not_hide_an_earlier_duration_cap() {
|
||||
let _one_at_a_time = crate::gpu_test::lock();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
s.execute("markers.set", json!({"new": true, "time": 1.0, "duration": 0.5, "label": "Red"})).unwrap();
|
||||
s.execute("markers.set", json!({"new": true, "time": 1.3, "comment": "This name overlaps the earlier endpoint", "label": "Blue"})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(move |_| EffectcraftApp::new(s));
|
||||
h.run_steps(5);
|
||||
let a = h.state().auto.find("timeline.marker.0").unwrap().rect;
|
||||
let b = h.state().auto.find("timeline.marker.1").unwrap().rect;
|
||||
let x = |r: [f32; 4]| r[0] + r[2] / 2.0;
|
||||
let end = x(a) + (x(b) - x(a)) * 0.5 / 0.3;
|
||||
let y = a[1] + 1.0 + (a[3] - 1.0).min(10.0) / 2.0;
|
||||
let image = h.render().unwrap();
|
||||
if let Some(dir) = std::env::var_os("MARKER_NAMES_SNAPSHOT") {
|
||||
image.save(std::path::Path::new(&dir).join("marker-overlapping-cap.png")).unwrap();
|
||||
}
|
||||
let scale = image.width() as f32 / 1600.0;
|
||||
let cap = (-1..=1)
|
||||
.map(|offset| image.get_pixel(((end + offset as f32) * scale) as u32, (y * scale) as u32).0)
|
||||
.max_by_key(|pixel| i32::from(pixel[0]) - i32::from(pixel[1]))
|
||||
.unwrap();
|
||||
assert!(i32::from(cap[0]) - i32::from(cap[1]) > 40, "the earlier red duration cap stays above the later name: {cap:?}");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -140,6 +141,6 @@ fn browser_cache_refreshes_filters_navigates_and_recovers_from_errors() {
|
||||
assert_eq!(names(&listing(&h, false).1), ["Sub", "Added.csv", "Clip.csv", "From action.csv"]);
|
||||
assert!(h.state().auto.find("mediaBrowser.entry.Clip.csv").is_some());
|
||||
if let Ok(path) = std::env::var("MEDIA_BROWSER_SNAPSHOT") {
|
||||
h.render().unwrap().save(path).unwrap();
|
||||
h.render_serialized().unwrap().save(path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_engine::{
|
||||
@@ -74,7 +75,7 @@ fn file_folder_and_virtual_paths_copy_from_the_context_menu_without_document_cha
|
||||
if name == "作品 clip.csv"
|
||||
&& let Ok(file) = std::env::var("MEDIA_BROWSER_COPY_SNAPSHOT")
|
||||
{
|
||||
h.render().unwrap().save(file).unwrap();
|
||||
h.render_serialized().unwrap().save(file).unwrap();
|
||||
}
|
||||
assert_eq!(click(&mut h, &id, PointerButton::Primary), [path]);
|
||||
h.run_steps(3);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The menu bar from the keyboard, and the app's shortcuts waiting while a menu is open (#279)
|
||||
//! (egui_kittest, UI logic only).
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui::{Event, Key, Modifiers, Pos2, Rect, pos2, vec2};
|
||||
@@ -226,7 +227,7 @@ fn relink_is_reachable_through_the_dependencies_menu() {
|
||||
h.run_steps(8);
|
||||
let relink = h.query_by_label_contains("Relink Missing Footage").unwrap().rect();
|
||||
if let Some(path) = std::env::var_os("EFFECTCRAFT_RELINK_MENU_SNAPSHOT") {
|
||||
h.render().unwrap().save(path).unwrap();
|
||||
h.render_serialized().unwrap().save(path).unwrap();
|
||||
}
|
||||
click_at(&mut h, relink.center());
|
||||
assert!(picked.get());
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! registers its automation ids; the Footage panel's buttons edit into the comp; the Progress
|
||||
//! panel lists and cancels a job; Lumetri Scopes switch scope through their dropdown.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_ui_egui::dock::WorkspaceExt as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -448,7 +449,7 @@ fn panels_snapshot() {
|
||||
}
|
||||
open(&mut h, panel);
|
||||
h.run_steps(3);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
img.save(format!("{dir}/{panel}{scope}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Effects & Presets: copy the configured folder path through the contents menu.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::panels::effects_presets::PresetFile;
|
||||
use effectcraft_ui_egui::{ControlRequest, EffectcraftApp, control, dock::PanelKind};
|
||||
@@ -52,7 +53,7 @@ fn presets_menu_copies_the_configured_folder_without_editing_the_project() {
|
||||
click(&mut h, "effects.menu");
|
||||
h.run_steps(3);
|
||||
if let Ok(path) = std::env::var("PRESETS_FOLDER_SNAPSHOT") {
|
||||
h.render().unwrap().save(path).unwrap();
|
||||
h.render_serialized().unwrap().save(path).unwrap();
|
||||
}
|
||||
assert_eq!(click(&mut h, "effects.copyPresetsFolderPath"), [expected.as_str()]);
|
||||
h.run_steps(2);
|
||||
@@ -156,7 +157,7 @@ fn shared_preset_rows_search_apply_and_undo_the_selected_definition() {
|
||||
assert_eq!(h.state().session.active_comp().unwrap().layers[0].props.prop("transform/opacity").unwrap().value.as_f64(), expected);
|
||||
assert_eq!(h.state().session.history.undo.len(), history + 1);
|
||||
if let Ok(dir) = std::env::var("PRESET_CACHE_SNAPSHOTS") {
|
||||
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
}
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Window ▸ Script Console: the panel shows, runs JavaScript through `script.run` (persistent
|
||||
//! context) and logs output, results and errors (egui_kittest, UI logic only).
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use egui_kittest::Harness;
|
||||
@@ -59,7 +60,7 @@ fn script_console_snapshot() {
|
||||
}
|
||||
h.state_mut().ui.script_console.input = "app.project.numItems".into();
|
||||
h.run_steps(3);
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
let out = std::env::var("SC_SNAPSHOT").unwrap_or_else(|_| concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out/script_console.png").into());
|
||||
if let Some(d) = std::path::Path::new(&out).parent() {
|
||||
std::fs::create_dir_all(d).unwrap();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! windows (clicks reach the script's handlers), the branching History panel, and the Window /
|
||||
//! File ▸ Scripts menus listing scripts (egui_kittest, UI logic only).
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use effectcraft_ui_egui::dock::WorkspaceExt as _;
|
||||
@@ -210,6 +211,7 @@ fn history_panel_jumps_between_branches() {
|
||||
/// onDraw fills of concave paths and real images (drawImage, image controls), rendered.
|
||||
#[test]
|
||||
fn on_draw_concave_fills_and_images_render() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let dir = std::env::temp_dir().join(format!("effectcraft-scriptui-img-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let png = dir.join("green.png");
|
||||
@@ -244,7 +246,7 @@ fn on_draw_concave_fills_and_images_render() {
|
||||
let g = &win.root.children[0];
|
||||
assert!(matches!(&g.draw[1], effectcraft_engine::scriptui::DrawOp::Image { image: Some(i), .. } if i.src.as_deref() == Some(png.as_str())));
|
||||
assert_eq!(win.root.children[1].image.as_ref().and_then(|i| i.src.clone()).as_deref(), Some(png.as_str()));
|
||||
let img = h.render().expect("render");
|
||||
let img = h.render_serialized().expect("render");
|
||||
if let Ok(d) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
img.save(format!("{d}/scriptui_paint.png")).unwrap();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! as its own Timeline tab (not a rename, and not in place of the comp shown), the tabs switch and
|
||||
//! close comps, and a dragged tab lands where its insertion mark shows.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::ItemId;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -135,7 +136,7 @@ fn tab_drag_snapshot() {
|
||||
}
|
||||
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out");
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
h.render().expect("render").save(format!("{dir}/tab-drag.png")).unwrap();
|
||||
h.render_serialized().expect("render").save(format!("{dir}/tab-drag.png")).unwrap();
|
||||
}
|
||||
|
||||
/// A precomp layer's bar shows its comp's markers; a double-click opens that comp at the marker.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::{Session, config::MemoryConfig, prefs::Prefs};
|
||||
use effectcraft_ui_egui::{EffectcraftApp, menus};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, pos2, vec2};
|
||||
@@ -49,7 +50,7 @@ fn star_controls_save_and_favorite_menu_selects_the_family() {
|
||||
assert_eq!(h.state().session.prefs.appearance.favorite_themes, vec!["light"]);
|
||||
if let Some(path) = std::env::var_os("FAVORITES_SCREENSHOT_DIR") {
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
h.render().unwrap().save(std::path::Path::new(&path).join("effectcraft-favorites.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(std::path::Path::new(&path).join("effectcraft-favorites.png")).unwrap();
|
||||
}
|
||||
assert_eq!(h.state().session.prefs.appearance.appearance_mode, "dark");
|
||||
let p = at(&h, "settings.ok");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! interest drawing, rulers and guides, snapping, the shape Pen and motion-path key drags
|
||||
//! (egui_kittest, UI logic only).
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::commands::shape_tool::PaintKind;
|
||||
use effectcraft_engine::project::LayerId;
|
||||
@@ -448,7 +449,7 @@ fn mid_drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, mods: egu
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.render().expect("the mid-drag frame")
|
||||
h.render_serialized().expect("the mid-drag frame")
|
||||
}
|
||||
|
||||
/// Release the button where the drag left it.
|
||||
@@ -490,7 +491,7 @@ fn shape_drag_ghost_shows_the_tools_own_shape_while_dragging() {
|
||||
h.state_mut().ui.tool = tool;
|
||||
settle_viewer(&mut h);
|
||||
let (a, b) = (screen(&h, from), screen(&h, to));
|
||||
let before = h.render().expect("the frame before the drag");
|
||||
let before = h.render_serialized().expect("the frame before the drag");
|
||||
let mid = mid_drag(&mut h, a, b, egui::Modifiers::default());
|
||||
let fill = fill_rgb(&h);
|
||||
|
||||
@@ -526,7 +527,7 @@ fn shape_drag_ghost_normalises_the_same_way_as_the_release() {
|
||||
h.state_mut().ui.tool = Tool::Ellipse;
|
||||
settle_viewer(&mut h);
|
||||
let (a, b) = (screen(&h, from.map(|v| v as f32)), screen(&h, to.map(|v| v as f32)));
|
||||
let before = h.render().expect("the frame before the drag");
|
||||
let before = h.render_serialized().expect("the frame before the drag");
|
||||
// Measured with the button down, so this is the ghost and not the committed shape.
|
||||
let mid = mid_drag(&mut h, a, b, mods);
|
||||
let on = [centre[0] as f32 + size[0] as f32 / 4.0, centre[1] as f32 + size[1] as f32 / 4.0];
|
||||
@@ -595,7 +596,7 @@ fn a_layer_drag_moves_the_picture_while_the_button_is_down() {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
// Where the layer is not (yet): the plate behind it.
|
||||
let plate = px_at(&h.render().unwrap(), &h, [440.0, 200.0]);
|
||||
let plate = px_at(&h.render_serialized().unwrap(), &h, [440.0, 200.0]);
|
||||
|
||||
// Drag in small steps, still holding the button, and give the renders a chance to land.
|
||||
let mut moved = false;
|
||||
@@ -606,7 +607,7 @@ fn a_layer_drag_moves_the_picture_while_the_button_is_down() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(15));
|
||||
// 30 comp px behind the pointer: inside the layer, well away from its outline, its anchor
|
||||
// icon and where it started.
|
||||
let at = px_at(&h.render().expect("mid-drag frame"), &h, [320.0 + 200.0 * (i as f32 / 40.0) - 30.0, 200.0]);
|
||||
let at = px_at(&h.render_serialized().expect("mid-drag frame"), &h, [320.0 + 200.0 * (i as f32 / 40.0) - 30.0, 200.0]);
|
||||
if i >= 30 && delta(at, plate) > 30 {
|
||||
moved = true;
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! comp goes to an unlocked viewer (a new one when every viewer is locked); a click on another
|
||||
//! viewer makes it the active one; closing a viewer forgets it.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::ItemId;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -171,5 +172,5 @@ fn viewers_snapshot() {
|
||||
h.run_steps(4);
|
||||
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out");
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
h.render().unwrap().save(format!("{dir}/viewers.png")).unwrap();
|
||||
h.render_serialized().unwrap().save(format!("{dir}/viewers.png")).unwrap();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Opt-in visual review of every built-in panel, workspaces and common dialogs.
|
||||
//! Original fixture; captures are review outputs, not shipped assets.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp, dock::PanelKind};
|
||||
use egui_kittest::Harness;
|
||||
@@ -19,18 +20,18 @@ fn all_ui_review_captures() {
|
||||
std::sync::Arc::make_mut(&mut s.project).settings.gpu_acceleration = false;
|
||||
let mut h = Harness::builder().with_size(size).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(4);
|
||||
h.render().unwrap().save(out.join(format!("{label}-workspace.png"))).unwrap();
|
||||
h.render_serialized().unwrap().save(out.join(format!("{label}-workspace.png"))).unwrap();
|
||||
let ctx = h.ctx.clone();
|
||||
h.state_mut().set_theme(&ctx, effectcraft_ui_egui::theme::ThemeKind::Light);
|
||||
h.run_steps(4);
|
||||
h.render().unwrap().save(out.join(format!("{label}-workspace-light.png"))).unwrap();
|
||||
h.render_serialized().unwrap().save(out.join(format!("{label}-workspace-light.png"))).unwrap();
|
||||
h.state_mut().set_theme(&ctx, effectcraft_ui_egui::theme::ThemeKind::Dark);
|
||||
for p in PanelKind::ALL {
|
||||
h.state_mut().show_panel(p);
|
||||
h.state_mut().ui.maximized = Some(p);
|
||||
h.run_steps(4);
|
||||
assert!(h.state().auto.find(&format!("panel.{}", p.id())).is_some(), "{} opens", p.title());
|
||||
h.render().unwrap().save(out.join(format!("{label}-{}.png", p.id()))).unwrap();
|
||||
h.render_serialized().unwrap().save(out.join(format!("{label}-{}.png", p.id()))).unwrap();
|
||||
}
|
||||
h.state_mut().ui.maximized = None;
|
||||
for (name, dialog) in [
|
||||
@@ -42,7 +43,7 @@ fn all_ui_review_captures() {
|
||||
] {
|
||||
h.state_mut().dialog = Some(dialog);
|
||||
h.run_steps(4);
|
||||
h.render().unwrap().save(out.join(format!("{label}-dialog-{name}.png"))).unwrap();
|
||||
h.render_serialized().unwrap().save(out.join(format!("{label}-dialog-{name}.png"))).unwrap();
|
||||
h.state_mut().dialog = None;
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ fn cpu_pixels(f: &FrameImage) -> Vec<egui::Color32> {
|
||||
|
||||
#[test]
|
||||
fn gpu_worker_renders_effect_frames_and_the_disk_cache_survives_a_reload() {
|
||||
let _device = crate::gpu_test::lock();
|
||||
let (Some(page_gpu), Some(worker_gpu)) = (Gpu::headless(), Gpu::headless_deferred()) else {
|
||||
eprintln!("no GPU adapter: skipped");
|
||||
return;
|
||||
|
||||
@@ -10,10 +10,6 @@ use effectcraft_engine::offload::{Inbox, Offload, Post, WorkerReply, WorkerReque
|
||||
use effectcraft_gpu::Gpu;
|
||||
use serde_json::json;
|
||||
|
||||
/// One device at a time in this test binary: wgpu's OpenGL backend (Mesa llvmpipe on FreeBSD,
|
||||
/// Linux without Vulkan) panics or crashes when tests create and drive devices concurrently.
|
||||
static GPU_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Holds offloaded jobs until the test runs them.
|
||||
#[derive(Default)]
|
||||
struct Manual(Mutex<Vec<(WorkerRequest, Arc<Inbox>)>>);
|
||||
@@ -63,7 +59,7 @@ fn page() -> (Session, Arc<Manual>) {
|
||||
|
||||
#[test]
|
||||
fn render_queue_jobs_render_on_the_worker_gpu() {
|
||||
let _gpu = GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _gpu = crate::gpu_test::lock();
|
||||
let Some(g) = Gpu::headless_deferred() else {
|
||||
eprintln!("no GPU adapter: skipped");
|
||||
return;
|
||||
@@ -102,7 +98,7 @@ fn render_queue_jobs_render_on_the_worker_gpu() {
|
||||
|
||||
#[test]
|
||||
fn analyses_render_their_input_on_the_worker_gpu() {
|
||||
let _gpu = GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _gpu = crate::gpu_test::lock();
|
||||
let Some(g) = Gpu::headless_deferred() else {
|
||||
eprintln!("no GPU adapter: skipped");
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! User-reported selection, Project deletion, and render-queue drop regressions.
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::{Session, project::LayerId};
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp, dock::PanelKind};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
|
||||
@@ -273,7 +274,7 @@ fn viewer_right_click_targets_hit_and_duplicate_preserves_group() {
|
||||
}
|
||||
assert!(h.state().frames.is_cached(&key));
|
||||
h.run_steps(3);
|
||||
h.render().expect("offscreen context menu render").save(out).unwrap();
|
||||
h.render_serialized().expect("offscreen context menu render").save(out).unwrap();
|
||||
}
|
||||
h.get_by_label("Duplicate").click();
|
||||
h.run_steps(3);
|
||||
@@ -433,7 +434,7 @@ fn workflow_snapshot() {
|
||||
}
|
||||
assert!(h.state().frames.is_cached(&key), "current composition rendered before capture");
|
||||
h.run_steps(3);
|
||||
h.render().expect("offscreen UI render").save(out).unwrap();
|
||||
h.render_serialized().expect("offscreen UI render").save(out).unwrap();
|
||||
}
|
||||
|
||||
/// Filled rects painted this frame (flattening nested shape lists).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Inspect the actual viewer's painted colors and pointer-driven control geometry.
|
||||
|
||||
use crate::gpu_test::RenderSerial;
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::{
|
||||
EffectcraftApp,
|
||||
@@ -269,10 +270,10 @@ fn viewer_theme_and_width_visual_matrix() {
|
||||
}
|
||||
|
||||
fn save_stable_snapshot(h: &mut Harness<'_, EffectcraftApp>, path: &str, width: f32) {
|
||||
let image = h.render().unwrap();
|
||||
let image = h.render_serialized().unwrap();
|
||||
image.save(path).unwrap();
|
||||
h.run_steps(3);
|
||||
let repeated = h.render().unwrap();
|
||||
let repeated = h.render_serialized().unwrap();
|
||||
assert!(image == repeated, "settled synthetic viewer pixels must repeat exactly: {path}");
|
||||
if width == 900.0 {
|
||||
let times: Vec<_> = h
|
||||
|
||||
Reference in new issue
Block a user