feat(timeline): 吸附迟滞+多探针 / 链接 offset 角标 / 音量橡皮筋 (#99)#120
feat(timeline): 吸附迟滞+多探针 / 链接 offset 角标 / 音量橡皮筋 (#99)#120cuic19053-hue wants to merge 6 commits into
Conversation
appergb
left a comment
There was a problem hiding this comment.
@cuic19053-hue 自动审核结论:请修改(REQUEST_CHANGES)。snap 迟滞/多探针、链接 offset 角标、音量橡皮筋方向不错,但有阻塞:
- 与刚合并的 #119 关键帧路线冲突:你在
web/src/store/editActions.ts重复定义了stampKeyframe/moveKeyframe(纯前端 read-modify-write),而 #119 已合并并确立后端命令路线(走真命令 + undo + 全属性含 pair/crop)。两者同名同签名,同时进 main 会 TS 重复导出编译失败 + 语义分叉(RMW 有 TOCTOU 竞态、仅支持 scalar)。请删除本 PR 的前端 RMWstampKeyframe/moveKeyframe,改调 #119 的后端命令(或本 PR 专注 snap/offset/音量,移除关键帧段)。 - 与 #123 在
timelineCanvas.ts的 DragPaint 类型冲突:你加volumeKf变体,#123 把move变体改为对象——rebase 时手动整合 DragPaint 与 paintTimeline。 - 请 rebase 到含 #119 的最新 main(#119 已改 editActions/clipRenderer/types,本 PR 现已冲突)。
…ppergb#120-123 request-changes
appergb
left a comment
There was a problem hiding this comment.
@cuic19053-hue Sonnet 4.6 复审:主体功能(吸附迟滞+多探针、音量橡皮筋、链接 offset 角标)已实现,但有三处阻塞:
- 角标位置违反 1:1。 本 PR 放左上角(
clipRenderer.tsbx = rect.x + CLIP.stripWidth + 3),上游ClipRenderer.swift:640-644是右上角(x = rect.maxX - handleW - badgeWidth - 2)。且与 #139 重复实现同名drawOffsetBadge+linkOffset字段,合并必冲突——请与 #139 二选一/协调(#139 位置与上游一致)。 - move 拖拽错误吸附 playhead。
TimelineContainer.tsxmove 分支collectTargets(timeline, excluded, activeFrame)传了 activeFrame,使拖移可吸附播放头;上游SnapEngine.swiftmove 拖拽includePlayhead默认 false(仅 razor 传 true)。注意此处与 #138 同行冲突,需协调。 - 与 #123 的 DragPaint 冲突。 本 PR 新增
volumeKfvariant、#123 改movevariant,同改timelineCanvas.ts的DragPaint/paintTimeline,合并顺序不定必产生 TS 冲突,需手动整合。
请处理后重提。
…view appergb#120) Two PR appergb#120 review request-changes fixes, both for spec 5.7 / 5.4 1:1 port correctness: 1. drawOffsetBadge anchored to the right edge of the clip, just inside the right trim handle (ClipRenderer.swift:640-644). The old top-left position sat on top of the color strip and label, and the new width-guard reserves room for the trim handle so the badge never overlaps it. 2. Move drag no longer includes the playhead in the snap target set. The old collectTargets(timeline, excluded, activeFrame) made moving clips stick to the playhead, which felt like a bug. Pass null (the same exclusion the trim path uses) so a move only snaps to other clip edges and the playhead stays a passive reference. pnpm tsc --noEmit + pnpm build + pnpm test 52/52 green.
…view appergb#120) Two PR appergb#120 review request-changes fixes, both for spec 5.7 / 5.4 1:1 port correctness: 1. drawOffsetBadge anchored to the right edge of the clip, just inside the right trim handle (ClipRenderer.swift:640-644). The old top-left position sat on top of the color strip and label, and the new width-guard reserves room for the trim handle so the badge never overlaps it. 2. Move drag no longer includes the playhead in the snap target set. The old collectTargets(timeline, excluded, activeFrame) made moving clips stick to the playhead, which felt like a bug. Pass null (the same exclusion the trim path uses) so a move only snaps to other clip edges and the playhead stays a passive reference. pnpm tsc --noEmit + pnpm build + pnpm test 52/52 green.
7d6e507 to
32f0d9a
Compare
|
@appergb 请求重新审查。此前反馈已全部修复,CI 双绿(Rust ✅ Web ✅,commit \32f0d9a\):
DragPaint 合并顺序:本 PR 与 #123 在 \ imelineCanvas.ts\ 的 \DragPaint\ 类型有冲突。本 PR 新增 \�olumeKf\ 变体(纯新增,风险低),#123 给 \move\ 变体加 \duplicate\ 字段。建议先合并本 PR(#120),#123 rebase 整合后再合并。 请 re-review,谢谢! |
* feat(timeline): copy / cut / paste clips (⌘C / ⌘X / ⌘V) (appergb#94) Adds the standard clipboard shortcuts that were completely missing. Only ⌘C/⌘X/⌘V were absent — the unmodified C/V already switch tools (razor / pointer), and the mod-prefixed branch had no handlers. Frontend only: - clipboardStore: new Zustand store holding deep snapshots of the selected clips plus the source first-frame, so a paste can re-place the group relative to the current playhead. UI-only, never persisted. - editActions: copyClips / cutClips / pasteClipsAtPlayhead. - copy: snapshot selected clips + their track index + min startFrame. - cut: copy then deleteSelectedClips. - paste: offset each clip's startFrame by `activeFrame - sourceFirstFrame`, clear addLinkedAudio so the paste stands alone (mirrors upstream `pasteClipsAtPlayhead` link re-reflection), and select the new clips. Clips whose source track no longer exists are skipped. - useKeyboardShortcuts: wire ⌘C / ⌘X / ⌘V inside the existing `if (mod)` block — no conflict with the unmodified C/V tool switches. - i18n: 4 new keys (zh-CN + en) for copy / cut / paste / clipboardEmpty. Closes appergb#94. * fix(appergb#94): rebase onto main + linkGroup re-mapping + empty-clipboard toast Address review feedback on PR appergb#105: 1. Rebased onto latest main (resolved import conflict: kept both trimToPlayheadEdits and useClipboardStore). 2. copyClips now expands link groups: if a selected clip has a linkGroupId, all linked companions are included in the clipboard (mirrors upstream copyClips), so a paste reproduces the video+audio pair. 3. pasteClipsAtPlayhead now re-establishes link groups after addClips: clips that shared a linkGroupId in the clipboard are re-linked via linkClips, preserving video+audio linkage. 4. Empty-clipboard paste now shows a toast (edit.clipboardEmpty) instead of silently doing nothing. Added toast mechanism to uiStore + Toast component in App.tsx. --------- Co-authored-by: baiqing <lbx12309@icloud.com>
* feat(swap-media): 实现 SwapMedia 编辑命令,支持替换 clip 媒体 (appergb#101) 后端: - 新增 EditCommand::SwapMedia 变体,替换 clip 的 media_ref - 校验新媒体存在于 manifest,若时长不足自动截断 duration + 调整 trim_end - 保留所有编辑属性(transform/crop/keyframe tracks/grade/masks/effects/fade) - media_type 隐含 source_clip_type(spec "sync media_type" 场景) - 新增 EditRequest::SwapMedia DTO + into_command 映射 - 6 个单元测试:等长替换/较短截断/媒体不存在/同步 media_type/clip 不存在/undo 前端: - types.ts 新增 swapMedia EditRequest 变体 - editActions.ts 新增 swapMedia(clipId, mediaRef, options?) action - Inspector 新增「替换媒体」section + 内联媒体选择器 - i18n 中英文翻译 Closes appergb#101 * style: fix cargo fmt in command.rs and tests (appergb#101) * fix: correct cargo fmt in command_apply.rs (appergb#101) * fix: align trailing comment with 43 spaces (appergb#101) * chore: trim playback whitespace * fix(swap-media): simplify DTO to 2-arg + frontend type-consistency filter (review appergb#121) * fix(swap-media): singleLinkGroup gate + extract SwapMediaSection out of Inspector (appergb#101) --------- Co-authored-by: baiqing <lbx12309@icloud.com>
* feat(inspector): live sampling + missing fields (crop/fade/flip) (appergb#97) Backend (opentake-ops + src-tauri): - Extend ClipProperties with crop, fade_in/out_frames, fade_in/out_interpolation, flip_horizontal, flip_vertical - set_clip_properties writes new fields; fade clamps to clip duration; flip_* writes to transform.flip_* - ClipPropertiesDto mirrors fields with serde camelCase - 5 unit tests: crop sets+clears track, fade frames+interp, fade clamps, flip writes to transform, multiple fields at once Frontend (web): - clip.ts: 1:1 port of Rust Clip::*_at sampling methods (opacity/volume/ rotation/size/topLeft/crop), fadeMultiplier, db<->linear, generic sampleKeyframeTrack with number/AnimPair/Crop lerp - Inspector.tsx: read activeFrame from uiStore; show sampled values at playhead; switch to ReadOnlyValue + AnimatedHint when a track is active - 4 new sections: Position (top-left x/y), Crop (4 edge insets 0-1), Flip (2 checkboxes), Fade (in/out frames + interpolation selects) - Fade section appears on both video and audio tabs - types.ts: extend ClipPropertiesReq with camelCase fields - dict.ts: i18n keys for new sections (zh-CN + en) Closes appergb#97 * style: fix cargo fmt import in command.rs (appergb#97) * fix: add ..Default::default() for new ClipProperties fields (appergb#97) * fix(appergb#97): use clip.opacity/volume for editable fields, sampled* only for animated (review appergb#122)
* feat(appergb#93): add clip right-click context menu Closes appergb#93. - New ClipContextMenu component with Split / Delete / Link-Unlink - TimelineContainer: onContextMenu hit-tests the clip, selects it if needed, and opens the menu; closes on outside click or Escape - i18n: contextMenu.split/delete/link/unlink (zh-CN + en) * fix(appergb#93): menu cursor positioning + viewport flip; remove render-phase onClose() Blocking items from review: 1. Menu now follows cursor (x/y from onContextMenu -> ClipContextMenu left/top) with useLayoutEffect viewport-boundary flip (right/bottom overflow -> open left/up). 2. Removed onClose() call during render; clip-missing now returns null and reports close via useEffect (no parent setState mid-render). Minor items: - Added disabled placeholder items: Swap Media / Save as Media / Extract Audio. - Replaced key={i} with stable key={item.id}. - Replaced imperative onMouseEnter/Leave DOM mutation with CSS :hover. * fix(timeline): remove duplicate context menu handler * feat(inspector/swap-media): gate + picker modal for Swap Media entry Wire the Swap Media context-menu action in ClipContextMenu.tsx: - Availability gate: enabled only when the clip is non-text AND alone in its link group (SPEC §5.10 "非 text 且单链组" = upstream TimelineView.menu). Multi-clip link groups (e.g. linked A/V pairs) stay disabled to avoid desyncing partners. - On click, opens a media-picker modal pre-filtered by strict type equality (item.type === clip.mediaType, mirroring upstream isAssetCompatibleWithPendingSwap). Backend re-validates as a safety net. New files: - web/src/components/timeline/SwapMediaPicker.tsx: modal list of compatible library assets; calls edit.swapMedia() on selection; shows backend error message (e.g. type-mismatch refusal) inline; Esc-to-close. New helpers / state: - web/src/lib/clip.ts: isSingleLinkGroup(clip, timeline) helper. - web/src/store/uiStore.ts: pendingSwapClipId + setPendingSwapClipId. Touched: - web/src/components/timeline/ClipContextMenu.tsx: gate + open picker. - web/src/components/timeline/TimelineContainer.tsx: render SwapMediaPicker. - web/src/i18n/dict.ts: swapMedia.noCandidates (zh + en). - web/src/lib/types.ts: swapMedia EditRequest variant. - web/src/store/editActions.ts: 2-arg swapMedia wrapper around editApply. Pairs with feat-101-swap-media (the backend `replaceClipMediaRef( resetTrim=false)` route). tsc --noEmit + pnpm build green. * fix(appergb#93): bind onContextMenu to content canvas (TS6133 unused) --------- Co-authored-by: baiqing <lbx12309@icloud.com>
1. 吸附迟滞 + 多探针
- snap.ts: findSnapDelta 扩展接受 currentlySnapped + probeOffsets,
返回 probeOffset,支持 sticky band 跨 pointer 事件保持
- TimelineContainer.tsx: 新增 snapStateRef 跨事件保持吸附状态;
onPointerMove move 分支收集所有 companions 的 start+end 作为探针组,
改用 findSnapDelta(不再传 null);onPointerUp 清空 snapStateRef
2. 链接 offset 角标
- clip.ts: 新增 linkOffsetForClip 计算链接组内帧偏移(相对 lead clip)
- clipRenderer.ts: 新增 drawOffsetBadge 绘制红色圆角徽章 "+N"/"-N"
- timelineCanvas.ts: clip 绘制参数增加 linkOffset,调用 drawOffsetBadge
3. 音量橡皮筋
- clipRenderer.ts: 新增 drawVolumeEnvelope 绘制 volumeTrack 折线 + kf 圆点
(半径 5px,黄色填充白色边框);拖拽时 ghost dot 跟随光标
- hitTest.ts: 新增 audioVolumeKfHit 命中测试(8px 容差)
- TimelineContainer.tsx: 新增 audioVolumeKf DragState + 拖拽逻辑;
Cmd+click 空白处调 stampKeyframe
- editActions.ts: moveKeyframe / stampKeyframe 实现为前端 wrapper
(read-modify-write over setKeyframes,因后端仅暴露 SetKeyframes)
验证:pnpm tsc --noEmit 通过;pnpm build 通过;52 项测试全通过
…view appergb#120) Two PR appergb#120 review request-changes fixes, both for spec 5.7 / 5.4 1:1 port correctness: 1. drawOffsetBadge anchored to the right edge of the clip, just inside the right trim handle (ClipRenderer.swift:640-644). The old top-left position sat on top of the color strip and label, and the new width-guard reserves room for the trim handle so the badge never overlaps it. 2. Move drag no longer includes the playhead in the snap target set. The old collectTargets(timeline, excluded, activeFrame) made moving clips stick to the playhead, which felt like a bug. Pass null (the same exclusion the trim path uses) so a move only snaps to other clip edges and the playhead stays a passive reference. pnpm tsc --noEmit + pnpm build + pnpm test 52/52 green.
32f0d9a to
6adf576
Compare
* feat(timeline): 吸附迟滞+多探针 / 链接 offset 角标 / 音量橡皮筋 (appergb#99) 1. 吸附迟滞 + 多探针 - snap.ts: findSnapDelta 扩展接受 currentlySnapped + probeOffsets, 返回 probeOffset,支持 sticky band 跨 pointer 事件保持 - TimelineContainer.tsx: 新增 snapStateRef 跨事件保持吸附状态; onPointerMove move 分支收集所有 companions 的 start+end 作为探针组, 改用 findSnapDelta(不再传 null);onPointerUp 清空 snapStateRef 2. 链接 offset 角标 - clip.ts: 新增 linkOffsetForClip 计算链接组内帧偏移(相对 lead clip) - clipRenderer.ts: 新增 drawOffsetBadge 绘制红色圆角徽章 "+N"/"-N" - timelineCanvas.ts: clip 绘制参数增加 linkOffset,调用 drawOffsetBadge 3. 音量橡皮筋 - clipRenderer.ts: 新增 drawVolumeEnvelope 绘制 volumeTrack 折线 + kf 圆点 (半径 5px,黄色填充白色边框);拖拽时 ghost dot 跟随光标 - hitTest.ts: 新增 audioVolumeKfHit 命中测试(8px 容差) - TimelineContainer.tsx: 新增 audioVolumeKf DragState + 拖拽逻辑; Cmd+click 空白处调 stampKeyframe - editActions.ts: moveKeyframe / stampKeyframe 实现为前端 wrapper (read-modify-write over setKeyframes,因后端仅暴露 SetKeyframes) 验证:pnpm tsc --noEmit 通过;pnpm build 通过;52 项测试全通过 * fix(pr-120): offset badge top-right + move drag excludes playhead (review appergb#120) Two PR appergb#120 review request-changes fixes, both for spec 5.7 / 5.4 1:1 port correctness: 1. drawOffsetBadge anchored to the right edge of the clip, just inside the right trim handle (ClipRenderer.swift:640-644). The old top-left position sat on top of the color strip and label, and the new width-guard reserves room for the trim handle so the badge never overlaps it. 2. Move drag no longer includes the playhead in the snap target set. The old collectTargets(timeline, excluded, activeFrame) made moving clips stick to the playhead, which felt like a bug. Pass null (the same exclusion the trim path uses) so a move only snaps to other clip edges and the playhead stays a passive reference. pnpm tsc --noEmit + pnpm build + pnpm test 52/52 green.
|
@appergb 请求重新审查。R2 反馈已逐条核对落实,CI 双绿(Rust ✅ Web ✅,commit
DragPaint 整合计划:本 PR 与 #123 都改 请 re-review,谢谢! |
|
Local integration follow-up for the editing parity branch.\n\nI independently rechecked the #120 request-changes blockers against the current integration branch, not just this PR ref. Two integration regressions were still present locally after combining #120/#123/#139:\n\n- Move drag had regressed to collecting the playhead as a snap target. Fixed locally by routing move drag through collectMoveSnapTargets(), which delegates to collectTargets(timeline, excluded, null). Razor/trim still opt into playhead snapping separately.\n- The link-offset badge and Option/Alt duplicate ghost '+' badge could collide in the top-right. Fixed locally by having drawOffsetBadge return its rect and bounding/skipping the duplicate badge when there is no legal space left of the offset badge.\n\nRegression tests added locally:\n- web/src/components/timeline/TimelineContainer.test.ts verifies move snap targets omit the playhead and exclude dragged clip edges.\n- web/src/components/timeline/clipRenderer.test.ts verifies wide clips keep duplicate and offset badges separate, and narrow offset-eligible clips skip the duplicate badge instead of overlapping.\n\nVerification:\n- pnpm -C web exec vitest run src/components/timeline/TimelineContainer.test.ts src/components/timeline/clipRenderer.test.ts src/components/timeline/hitTest.test.ts src/store/editActions.test.ts src/lib/clip.test.ts -> 53 tests passed.\n- pnpm -C web test -> 21 files / 133 tests passed.\n- pnpm -C web build -> passed; only the known Tauri dynamic/static import warning remains.\n- git diff --check -> passed.\n- Separate TypeScript reviewer approved the revised local integration fix and reran tsc --noEmit plus focused timeline tests.\n\nNote: this is local integration evidence; cloud PR review state is still CHANGES_REQUESTED until the PR itself is re-reviewed/updated. |
|
Closing as superseded by #155, which has been merged into main and includes this work in the integrated codebase. Keeping this PR closed avoids duplicate/conflicting review paths. |
|
Superseded by merged integration PR #155. |
What
解决 #99:吸附迟滞+多探针 / 链接 offset 角标 / 音量橡皮筋
How
1. 吸附迟滞 + 多探针
2. 链接 offset 角标
3. 音量橡皮筋
额外说明
#95(keyframe editing)尚未合并到 upstream/main,后端
EditCommand仅暴露SetKeyframes。为保持纯前端 PR,
edit.moveKeyframe/edit.stampKeyframe实现为前端 wrapper(read-modify-write over
setKeyframes):读取当前 timeline mirror → 修改 kf 数组 → 调用已有的
setKeyframes命令。运行时可用,无需后端改动。Testing
Limitation
Closes #99