Skip to content

拖拽片段交换 + 标准导出全套 + 预览/素材体验修复#172

Open
appergb wants to merge 10 commits into
mainfrom
feat/drag-swap-and-export
Open

拖拽片段交换 + 标准导出全套 + 预览/素材体验修复#172
appergb wants to merge 10 commits into
mainfrom
feat/drag-swap-and-export

Conversation

@appergb

@appergb appergb commented Jun 29, 2026

Copy link
Copy Markdown
Owner

基于真机测试反馈的一批剪辑体验修复 + 导出能力扩展。4 个 commit,本地五项关卡全绿(cargo fmt --check / clippy -D warnings / cargo test --workspace / pnpm build / pnpm test 212)。

拖拽交互(commit 1)

  • 跨轨片段级交换:把片段拖到另一轨已有片段上,两者互换「轨道 + 起始帧」,不再用覆盖语义吞掉目标内容。新增无损 swap_clip_positions op + EditCommand::SwapClips(换位后若与第三个片段重叠则拒绝、零丢失)+ camelCase IPC DTO(附反序列化回归测试)。拖动时实时预览被挤走的片段滑入让出的槽位。仅在「单片段跨轨且落点恰好压住一个片段」触发,其余保持原 move 行为。
  • 拖入位置指示:整宽淡填充 lane("整条亮起来")改为细虚线插入线,clip 大小的灰块在任何落点都清晰可辨。

标准导出全套(commit 2–3)

  • XMEML 正名export_fcpxml 产物其实是 XMEML 4(FCP7 XML,Premiere/达芬奇/剪映可导入)——新增诚实的 export_xmeml,旧名保留为弃用别名。
  • 新增 EDL(CMX3600)/ OTIO(对照官方样本验证)/ 现代 FCPXML 1.10(可带文本叠加)三个纯函数导出模块 + Tauri 命令。
  • "导出"按钮变四格式选择菜单,各自原生保存对话框 + 正确扩展名。

预览 / 素材 / 导出 UI 修复(commit 4)

  • A 拖入新片段后预览不显示:拖放是 HTML5 drop(无 pointerdown),媒体→时间线预览切换没触发;drop 后清 previewMediaId,预览切到时间线合成。
  • B scrub 抽搐 / 轨道头部漂移:scrub 结束时把暂停媒体 settle 到最终帧 + 暂停态时间线变化重新 seek,避免 scrub 新进入的片段停在源文件帧 0。
  • C 素材库加载慢:LibraryView 用 IntersectionObserver 懒挂载——无缓存 poster 的视频卡原本直接在 <video> 里加载整段源文件,几十张同时解码;现在滚到可见才加载。
  • D 拖入卡顿:新增 preload_media 命令(worker 线程预热 poster + 时间线 filmstrip sprite + 波形缓存),媒体面板在选中 / 拖拽开始时 best-effort 触发,预览和落点都走缓存、不在交互路径上解码。
  • E 导出菜单透明 + 渲染按钮:定义缺失的 --bg-elevated token(所有弹层之前引用未定义→背景透明、透出后面面板);导出菜单加"渲染为视频(MP4)"项(空时间线置灰)。

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace(含 swap op 5 + IPC camelCase + 各导出格式 51 测试)
  • pnpm -C web build + pnpm -C web test(212)
  • 真机 tauri build release 打包成功
  • 交互目视:拖入灰条 / 跨轨交换不吞帧 / 预览切换 / 导出菜单 / 四格式导入 Premiere·达芬奇·FCP(computer-use 被 Dock 拦截,需人工确认)

🤖 Generated with Claude Code

baiqing and others added 10 commits June 29, 2026 16:32
Dragging a clip onto another track now swaps the two clips' positions
(track + start frame) instead of overwriting/swallowing the destination,
with a live preview of the displaced clip sliding into the vacated slot.

- New lossless `swap_clip_positions` op + `EditCommand::SwapClips`; the op
  refuses (no change) if a swap would overlap a third clip, so nothing is
  ever destroyed. camelCase IPC DTO (`swapClips`/clipA/clipB) with a
  deserialize regression test (guards the recurring DTO camelCase trap).
- Frontend dispatches the swap only on a single-clip cross-track drop that
  lands on exactly one existing clip; everything else keeps the prior move.

The media drop-in indicator no longer floods the whole row: the
full-width "new track" fill becomes a thin dashed insertion line and the
clip-sized gray ghost is bolder, so the landing spot is always legible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honest naming for the existing exporter and three new standard
timeline-interchange formats, all in opentake-project as pure functions
with Tauri command wrappers.

- Rename: the existing `export_fcpxml` command always produced XMEML 4
  (FCP7 XML); add an honest `export_xmeml` command and keep
  `export_fcpxml` as a thin deprecated alias. The XMEML output bytes are
  unchanged (it is a faithful 1:1 port of upstream XMLExporter.swift; no
  conformance bug found).
- EDL (crates/opentake-project/src/edl.rs): CMX3600 edit decision list —
  TITLE / FCM, numbered events, reel/channel/transition, source+record
  timecodes at timeline fps (drop/non-drop per fps), `* FROM CLIP NAME:`.
  Video track only (format limitation documented in the output).
- OTIO (crates/opentake-project/src/otio.rs): OpenTimelineIO JSON, shape
  validated against the reference samples (Timeline.1 → Stack.1 → Track.1
  → Clip.1/Gap.1, RationalTime.1/TimeRange.1, ExternalReference.1 with
  file:// target_url + available_range). Hand-written via serde_json (no
  maintained GPL-compatible OTIO crate exists).
- FCPXML modern (crates/opentake-project/src/fcpxml_modern.rs): native
  Final Cut Pro X FCPXML 1.10 — resources/assets/formats, library/event/
  project/sequence/spine, asset-clip/title, adjust-transform/adjust-volume.
  Carries text overlays XMEML can't. Premiere does NOT import FCPXML
  (noted in code).
- xmlnode.rs: small shared XML document tree for the FCPXML emitter.

New Tauri commands: export_xmeml, export_edl, export_otio,
export_fcpxml_modern (export_fcpxml kept as alias). All return
Result<(), String> at the boundary.

103 new unit tests across the four modules; full workspace builds, clippy
clean (-D warnings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the single "导出" / "Export" button into a format menu offering the
four standard timeline-interchange formats, mirroring the existing
subtitle popover.

- web/src/lib/api.ts: add exportXmeml / exportEdl / exportOtio /
  exportFcpxmlModern path-only wrappers; keep exportFcpxml as a
  @deprecated alias for exportXmeml. All no-op outside Tauri.
- web/src/components/shell/TitleBar.tsx: replace onExport (XMEML-only)
  with an INTERCHANGE_FORMATS-driven popup menu — XMEML (Premiere・
  DaVinci・剪映), FCPXML (Final Cut Pro), OTIO (达芬奇・工业标准),
  EDL (CMX3600). Each opens the native save dialog with the right
  extension (.xml/.fcpxml/.otio/.edl) then calls its command + toasts.
  Extracted a shared useDismissable hook for both popovers.
- web/src/i18n/dict.ts: zh-CN + en labels, dialog titles, filter names,
  and done/failed toasts for all four formats.
- TitleBar.visual.test.ts: lock in the four formats → extension/command
  mapping and the popup-menu shape.

web build (tsc + vite) green; 211 web tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ary, preload-on-select, opaque export menu

Preview
- Dropping media onto the timeline is an HTML5 drop (no pointerdown), so the
  media-preview→timeline switch never fired and the preview stayed on the
  dropped asset's standalone view. Clear the selected media on drop so the
  preview shows the timeline composite at the playhead.
- Settle paused media to the final frame when a scrub ends, and re-run the
  paused sync when the timeline changes while paused, so a clip the scrub/edit
  just brought under the playhead no longer holds its source frame 0.

Media loading
- Library grid lazy-mounts each card's thumbnail via IntersectionObserver: a
  video entry with no cached poster otherwise loaded its full source file in a
  <video>, so dozens of cards decoded at once. Off-screen cards now stay a
  placeholder until scrolled into view.
- New `preload_media` command warms the poster + timeline filmstrip sprite +
  waveform caches on a worker thread; the media panel fires it (best-effort) on
  select and drag-start, so preview and the subsequent timeline drop read from
  cache instead of decoding on the interaction path.

Export menu
- Define the missing `--bg-elevated` token: every popover (export / subtitle /
  clip context menu / swap-media picker) referenced it undefined and rendered
  with a transparent background that bled the panel behind it through.
- Add a "Render to Video (MP4)" item to the export menu (greyed when the
  timeline is empty) so video render is reachable alongside the interchange
  formats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op on newer stable)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The export render device was created with `Limits::downlevel_defaults()`, whose
`max_texture_dimension_2d` is 2048. The downscaled preview stays under that, but
a full-resolution export (FHD is borderline, 2K/4K exceed it) made
`Device::create_texture` fail with an uncaptured wgpu error that panics — so
"export video" aborted (SIGABRT) the whole app. Request the adapter's real
limits instead (Metal reports 16384), covering every realistic export size.

Adds a 4K export integration test (renders 3840x2160) that SIGABRTs on the old
device and passes on the fixed one. Existing exports only covered 720p (1280x720
< 2048), which is why CI never caught the crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed frame

The timeline preview stacked clips by DOM paint order (no z-index), which React
reconciliation could shuffle as clips enter/leave during scrub — so the preview
could disagree with the final composite. Pin each layer's z-index to its track
order (lower index = higher layer, matching opentake-render's track-0-topmost
blend), so the preview and the exported frame agree exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reload

The Tauri asset protocol honors HTTP Range (verified in tauri-2.11.x
src/protocol/asset.rs: 206 Partial Content, Accept-Ranges, ~1 MiB chunks),
so WKWebView already streams <video> progressively from disk. The cold-click
slowness was the <video> defaulting to preload="auto" (eager buffering) with no
poster (blank/spinner until the first frame paints). Fix without a segment
pipeline:

- Preview <video> now uses preload="metadata" + an instant HI-RES first-frame
  poster, so a cold click shows a sharp frame immediately and the rest streams
  lazily during playback/pause. De-blurs (poster is up to 1920x1080, not the
  120x68 grid thumbnail) and removes the blank/spinner.
- New preview_poster(mediaRef, timeSecs?) Tauri command + video_preview_poster
  decode (shared decode_poster_to helper with video_poster). Cached as
  {key}.preview[.{ms}].png, keyed separately from the grid {key}.thumb.png so
  the two sizes never clobber. api.ts previewPoster() wrapper; Preview fetches
  it on select (video only, cleared between items).
- Re-purpose preload_media: warm ONLY the hi-res preview poster (video). Drop
  the heavy 240-frame filmstrip sprite + waveform warming — neither speeds
  actual <video> playback, and the timeline loads them lazily itself when a clip
  lands (TimelineContainer generateThumbnail/getWaveform).
- Page-aware media grid: a video card pre-warms its preview poster when it
  scrolls into view (same IntersectionObserver gate, so far-out cards aren't
  touched). Bound the in-memory thumbnail-path cache with a small LRU
  (BoundedCache, 256) so a long scrolled library can't grow memory without
  limit.

Gates: fmt --check, clippy --workspace -D warnings, test --workspace, web build,
web test (217) all green. New tests: preview-poster cache keying (Rust),
BoundedCache LRU (vitest). Preview is GPU/codec-dependent — needs real-app
visual verification for sharpness + instant first-frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (upstream parity)

- New-track insertion indicator is now a solid YELLOW line (1:1 with upstream's
  NSColor.systemYellow), replacing the dashed white hint.
- Snap feedback: on macOS a clip-edge / media-drop snap fires a light trackpad
  "alignment" haptic via a new `snap_haptic` command (NSHapticFeedbackManager
  through objc2-app-kit, dispatched on the main thread, best-effort). Other
  platforms get a short quiet tick sound. Deduped — fires once per fresh
  engagement, mirroring upstream's SnapEngine haptic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… tick)

Per the user, the playhead should magnetize to the nearest clip start/end while
scrubbing — both on the timeline ruler and the preview scrub bar — with the same
snap tick as clip drags. (A slight, deliberate enhancement over the upstream
code, which scrubs the playhead freely.)

- Timeline ruler scrub uses the existing sticky snap engine (findSnapDelta) over
  clip-edge targets only (not the playhead itself), with its own scrubSnapRef so
  it doesn't fight the clip-move snap.
- Preview scrub bar uses a new frame-based snapFrameToEdge (~0.25s threshold),
  since the bar carries no timeline zoom. Unit-tested.
- Both fire maybeSnapFeedback on engage (deduped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant