feat(ops): freeze frame composite command (split + insert image clip)#213
Open
appergb wants to merge 1 commit into
Open
feat(ops): freeze frame composite command (split + insert image clip)#213appergb wants to merge 1 commit into
appergb wants to merge 1 commit into
Conversation
- EditCommand::FreezeFrame { clip_id, at_frame, duration_frames, media_ref }
- composite transaction: split at at_frame + ripple-insert still image clip
- one undo step (withTimelineSwap snapshot); right half + sync-locked followers shift right
- validation: at_frame strictly inside clip, duration >= 1, video/image only
- src-tauri edit_apply intercepts: composites frame, imports PNG, injects real
media_ref before the transaction (capture failure aborts with timeline untouched)
- pure into_command mapping mints virtual 'freeze:<clip>:<frame>' placeholder
for MCP/test callers that can't capture
- UI: right-click 'Freeze Frame' (video/image only) with duration prompt
- i18n: zh '冻结帧' / en 'Freeze Frame'
- user request 2026-07-01 (HANDOFF §3.7); no upstream equivalent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Freeze Frame feature requested 2026-07-01 (HANDOFF §3.7). OpenTake extension — no upstream Palmier Pro equivalent (OpenTake 反超).
Freeze Frame = split the clip at
at_frame, capture the composited frame as a still, and ripple-insert that still forduration_framesat the split point (the right half shifts right). Packaged as oneEditCommandcomposite transaction → one undo step.Design
Composite transaction (one
withTimelineSwap)EditCommand::FreezeFrame { clip_id, at_frame, duration_frames, media_ref }runs inside a singletransact():ops::split_clip(clip_id, at_frame)— left half keeps the id (now ends atat_frame), right half gets a fresh id starting atat_frame.ops::ripple::ripple_insert(image_spec, at_frame)— the right half (start_frame == at_frame, so>= at_framematches the ripple-push threshold) shifts right byduration_frames; the image clip fills the freed gap. Sync-locked followers ride the ripple too (A/V sync preserved).Undo restores the original in one step (verified by
freeze_frame_undo_restores_original_in_one_step).Real pixels via capture-before-apply
edit_applyinterceptsEditRequest::FreezeFrame(src-tauri/commands.rs): it composites the timeline atat_frame→ PNG → imports as a still → passes the real asset id asmedia_refinto theEditCommand. Capture failure aborts before the edit transaction starts, so the timeline stays untouched (no half-applied split).render::capture_freeze_framereusescomposite_rgba(full-res, no preview cap) +media::import_one.Virtual placeholder (MCP / tests)
The pure
into_commandmapping mintsfreeze:<clip_id>:<at_frame>as a placeholdermedia_reffor callers that can't capture (MCP / unit tests). The timeline structure is correct either way; the render layer resolves real pixels when available.Validation (ops layer, before mutation)
at_framestrictly inside(start_frame, end_frame)(endpoints rejected)duration_frames >= 1media_typeisVideoorImage(audio / text / lottie rejected)Files
crates/opentake-ops/src/command.rsFreezeFramevariant +freeze_frame()+ 10 unit testssrc-tauri/src/commands.rsEditRequest::FreezeFrameDTO (camelCase) +into_commandmapping +edit_applyintercept (render+media State) + serde testsrc-tauri/src/render.rscapture_freeze_frame()helper (composite → PNG → import → media_ref)web/src/lib/types.tsfreezeFrameEditRequest variantweb/src/lib/api.tsfreezeFrame()wrapperweb/src/store/editActions.tsfreezeFrame()+freezeClipAtPlayhead()+DEFAULT_FREEZE_FRAMESweb/src/components/timeline/ClipContextMenu.tsxweb/src/i18n/dict.tscontextMenu.freezeFrame+freezeFramePrompt(zh/en)IPC camelCase discipline
freezeFrame→clipId/atFrame/durationFrames(three-side sync: Rust DTO, TSEditRequest, call site). Guarded bydeserializes_freeze_frame_camelcase_and_maps_to_command.Verification
cargo fmt --all --check✅cargo clippy --workspace --all-targets -- -D warnings✅ (zero warnings)cargo clippy -p opentake-tauri --no-default-features --all-targets -- -D warnings✅cargo test --workspace✅ (all pass)cargo test -p opentake-ops✅ — 10/10 freeze_frame tests passpnpm -C web build✅pnpm -C web test✅ (437 pass)Design note:
media_reffieldThe HANDOFF spec listed the variant as
{ clip_id, at_frame, duration_frames }(nomedia_ref), with ops deriving the virtualfreeze:prefix and src-tauri backfilling real pixels. The only way to honor the hard constraints — (1) one-transaction undo, (2) capture-failure aborts with no timeline change, (3) real pixels land — is to carrymedia_refon the command so src-tauri can inject the real imported id beforeapply. The virtualfreeze:prefix is still produced (byinto_command) as the default/placeholder, satisfying the spec's intent; src-tauri'sedit_applysubstitutes the real id on the UI path. Documented in the variant's doc-comment.