Skip to content

feat(timeline): drag-drop new track + Option/Alt-drag duplicate (#98)#123

Open
cuic19053-hue wants to merge 14 commits into
appergb:mainfrom
cuic19053-hue:feat-98-drag-drop-new-track-option-duplicate
Open

feat(timeline): drag-drop new track + Option/Alt-drag duplicate (#98)#123
cuic19053-hue wants to merge 14 commits into
appergb:mainfrom
cuic19053-hue:feat-98-drag-drop-new-track-option-duplicate

Conversation

@cuic19053-hue

Copy link
Copy Markdown
Contributor

What

Implements issue #98: 拖放新建轨道 + Option/Alt-拖拽复制功能 (drag-drop to create new tracks + Option/Alt-drag to duplicate clips).

  • Drag a clip below the last track → a new track is created on drop and the clip lands there.
  • Hold Option (macOS) / Alt (Windows) while dragging → the clip is duplicated instead of moved (original stays put, a deep copy lands at the target).

How

Backend (Rust)

  • crates/opentake-ops/src/ops/duplicate.rs (new): duplicate_clips(timeline, clip_ids, offset_frames, target_track_indexes, ids) -> Vec<String>. Deep-copies each clip via Clip: Clone (keyframe tracks / grade / chroma / masks / effects / text / transform / crop / fades), mints a fresh id, shifts start_frame by offset_frames (clamped >= 0), lands on target_track_indexes[i], clears link_group_id (a copy is not linked to the original's group), and clears the destination range overwrite-style first (mirrors move_clips). 11 unit tests.
  • crates/opentake-ops/src/command.rs: EditCommand::DuplicateClips { clip_ids, offset_frames, target_track_indexes } + duplicate_clips_cmd apply dispatch with validation (empty ids / length mismatch / missing clips) and the standard transact wrapper. 7 command-level tests.
  • src-tauri/src/commands.rs: EditRequest::DuplicateClips DTO + into_command mapping.

Frontend (React + TypeScript)

  • web/src/lib/types.ts: duplicateClips variant on EditRequest.
  • web/src/store/editActions.ts: duplicateClips(clipIds, offsetFrames, targetTrackIndexes) action.
  • web/src/components/timeline/TimelineContainer.tsx:
    • isDuplicate: boolean on the move DragState (set from e.altKey at pointer-down).
    • DropTarget discriminated union: { kind: "existing"; trackIndex } | { kind: "newTrack"; trackType }.
    • newTrackTypeFor(clip): audio → "audio", else → "video" (matches addMediaToTimeline / upstream placeClip).
    • onPointerMove: computes dropTargettrackAt hit → existing; below the last track bottom → newTrack with the derived type.
    • onPointerUp: newTrack branch → edit.insertTrack(trackType)forceRefresh()edit.duplicateClips/edit.moveClips with the new track index; existing-track branch → group-floor-clamped move or duplicate.
  • web/src/components/timeline/timelineCanvas.ts: DragPaint move variant extended with isDuplicate? + newTrackType?. Dashed new-track drop indicator below the last track; ghost rendered at the new-track Y when newTrackType is set; isDuplicate passed to drawClip.
  • web/src/components/timeline/clipRenderer.ts: isDuplicate? on DrawOpts; yellow "+" badge in the top-right corner when ghost && isDuplicate.

Testing

  • Rust unit tests (18 total): duplicate_clips (11) + duplicate_clips_cmd (7). Cover original retention, deep copy of keyframes/grade/masks/effects/text/transform, link_group_id clearing, multi-track targets, relative spacing, overwrite blocking, frame clamping, missing-clip skip, incompatible-track skip, validation errors (empty ids / length mismatch / missing clips), and undoability.
  • TypeScript: pnpm tsc --noEmit ✅ + pnpm build ✅ (1648 modules, 300.50 kB gzipped).
  • Rust IDE diagnostics: clean (no errors/warnings).

Limitation

  • The new-track drop zone is the area below the last track only; dropping above the first track (into the ruler/drop zone) keeps the previous target (no new track created there).
  • target_track_indexes length must match clip_ids length (one target per source, by index); mismatched lengths are a validation error at the command layer and a silent skip at the ops layer.
  • The new track is appended at the end of timeline.tracks; it is not inserted into the correct zone (video/audio) by the frontend. The backend InsertTrack command clamps into its zone, so the resulting index may differ from tracks.length - 1 if the timeline already has tracks of the other type. The frontend reads tl.tracks.length - 1 after forceRefresh, which is correct because insertTrack appends then the zone clamp is a no-op for an empty zone (the common case for "drop below last track").

Closes #98.

@appergb appergb left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cuic19053-hue 自动审核结论:请修改(REQUEST_CHANGES)。落点路由 + 拖到新轨 + Option 复制方向不错、不碰 #91,但有 1:1 阻塞:

  1. 链接组重映射缺陷(HIGH):duplicate_clips 把每个拷贝的 link_group_id 清为 None(duplicate.rs:375)。上游 duplicateClipsToPositions 语义是链接组重映射到新 group id——多选含 A/V 链接对一起 Option-拖拽时,当前实现会把它们拆成无链接孤儿、丢失成对联动。请对每组源 link_group_id 生成一个新 group id 并在该组拷贝间共享重映射(单剪辑/无链接仍 None)。

建议(非阻塞,可一并修):① 补测『小 offset 使目标与源重叠』的 clear_region overwrite 边界;② 新轨拖放异步竞态——onPointerUp 依赖 forceRefresh 后读 tracks.length-1 取新轨 index,若 timeline_changed 事件迟到会落错轨;建议让 insertTrack 返回新轨 index,或后端在一个事务内完成『建轨+复制』。

请修链接组重映射后 rebase 到含 #119 的 main。

@appergb appergb left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cuic19053-hue Sonnet 4.6 复审:仍需修改。

[CRITICAL] 链接组重映射缺陷。 crates/opentake-ops/src/ops/duplicate.rs:410 无条件 clip.link_group_id = None;,与上游 EditorViewModel+Clipboard.swift:145-155groupCounts/groupRemap 不符。上游语义:多个被复制 clip 共享同一 linkGroupId(A/V 联动对同时 Option-拖拽)时,拷贝之间应映射到新的共享 group id;仅当某 group 本次只出现一次(孤立 clip)才清为 nil。当前一律清 nil → A/V 联动对 Option-拖拽后拷贝间链接丢失

请实现 groupCounts + groupRemap 映射表,并补「含链接对的多 clip 复制后拷贝间仍保持 link_group_id 一致」测试。另:本 PR 与 #120 都改 timelineCanvas.tsDragPaint/paintTimeline,合并时会冲突,二者需协调。改完重提。

@cuic19053-hue

Copy link
Copy Markdown
Contributor Author

@appergb 请求重新审查。此前反馈已全部修复,CI 双绿(Rust ✅ Web ✅,commit \1f4267e\):

  1. 链接组重映射:实现 \groupCounts\ + \groupRemap\ 映射表,同 \linkGroupId\ 多 clip 拷贝共享新 group id,孤立 clip 清 nil(对齐上游 \EditorViewModel+Clipboard.swift:145-155\)
  2. 补测试:\duplicate_clips_remaps_link_group_for_multi_clip_group\ + \duplicate_linked_pair_keeps_copies_linked\

DragPaint 合并顺序:本 PR 与 #120 在 \ imelineCanvas.ts\ 的 \DragPaint\ 类型有冲突。本 PR 给 \move\ 变体加 \duplicate\ 字段(Option/Alt-drag),#120 新增 \�olumeKf\ 变体。建议 #120 先合并,本 PR rebase 整合(保留 \�olumeKf\ 变体 + \move.duplicate\ 字段)后再合并。

请 re-review,谢谢!

cuic19053-hue and others added 14 commits June 25, 2026 22:36
* 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>
* 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.
…rgb#98)

Backend (Rust):
- Add `opentake_ops::ops::duplicate::duplicate_clips` — deep-copies each
  clip (keyframe tracks / grade / chroma / masks / effects / text /
  transform / crop / fades via `Clip: Clone`), mints a fresh id, shifts
  `start_frame` by `offset_frames`, lands on `target_track_indexes[i]`,
  clears `link_group_id`, and clears the destination range overwrite-style
  first (mirrors `move_clips`). 11 unit tests cover original retention,
  link-group clearing, keyframe deep copy, grade/masks/effects deep copy,
  multi-track targets, relative spacing, overwrite blocking, frame
  clamping, missing-clip skip, incompatible-track skip, and text/transform.
- Add `EditCommand::DuplicateClips` variant + `duplicate_clips_cmd` apply
  dispatch with validation (empty ids / length mismatch / missing clips)
  and the standard transact wrapper (snapshot -> mutate -> commit-if-changed
  -> version++). 7 command-level tests (creates copy, deep copies keyframes,
  clears link_group_id, missing clip errors, length mismatch errors, empty
  ids errors, undoable).
- Add `EditRequest::DuplicateClips` DTO in `src-tauri/src/commands.rs` +
  `into_command` mapping (direct field pass-through).

Frontend (React + TypeScript):
- Add `duplicateClips` variant to `EditRequest` in `types.ts`.
- Add `duplicateClips()` action in `editActions.ts` (applyAndRefresh).
- `TimelineContainer.tsx`: add `isDuplicate` flag (Alt key detection at
  pointer-down), `DropTarget` discriminated union (`existing` | `newTrack`),
  `newTrackTypeFor` helper (audio -> "audio", else -> "video"). `onPointerMove`
  computes `dropTarget` (existing track via `trackAt`, or `newTrack` when
  below the last track bottom). `onPointerUp` branches: newTrack ->
  `edit.insertTrack` -> `forceRefresh` -> `edit.duplicateClips`/`moveClips`
  with the new track index; existing track -> group-floor-clamped move or
  duplicate.
- `timelineCanvas.ts`: extend `DragPaint` move variant with `isDuplicate?`
  and `newTrackType?`. Render a dashed new-track drop indicator below the
  last track; render the ghost at the new-track Y when `newTrackType` is
  set; pass `isDuplicate` to `drawClip`.
- `clipRenderer.ts`: add `isDuplicate?` to `DrawOpts`; draw a yellow "+"
  badge in the top-right corner when `ghost && isDuplicate` so the user
  sees the gesture will copy rather than move.

Closes appergb#98.
@cuic19053-hue cuic19053-hue force-pushed the feat-98-drag-drop-new-track-option-duplicate branch from 1f4267e to 854cc1a Compare June 25, 2026 15:18
cuic19053-hue added a commit to cuic19053-hue/OpenTake that referenced this pull request Jun 25, 2026
* feat(timeline): drag-drop new track + Option/Alt-drag duplicate (appergb#98)

Backend (Rust):
- Add `opentake_ops::ops::duplicate::duplicate_clips` — deep-copies each
  clip (keyframe tracks / grade / chroma / masks / effects / text /
  transform / crop / fades via `Clip: Clone`), mints a fresh id, shifts
  `start_frame` by `offset_frames`, lands on `target_track_indexes[i]`,
  clears `link_group_id`, and clears the destination range overwrite-style
  first (mirrors `move_clips`). 11 unit tests cover original retention,
  link-group clearing, keyframe deep copy, grade/masks/effects deep copy,
  multi-track targets, relative spacing, overwrite blocking, frame
  clamping, missing-clip skip, incompatible-track skip, and text/transform.
- Add `EditCommand::DuplicateClips` variant + `duplicate_clips_cmd` apply
  dispatch with validation (empty ids / length mismatch / missing clips)
  and the standard transact wrapper (snapshot -> mutate -> commit-if-changed
  -> version++). 7 command-level tests (creates copy, deep copies keyframes,
  clears link_group_id, missing clip errors, length mismatch errors, empty
  ids errors, undoable).
- Add `EditRequest::DuplicateClips` DTO in `src-tauri/src/commands.rs` +
  `into_command` mapping (direct field pass-through).

Frontend (React + TypeScript):
- Add `duplicateClips` variant to `EditRequest` in `types.ts`.
- Add `duplicateClips()` action in `editActions.ts` (applyAndRefresh).
- `TimelineContainer.tsx`: add `isDuplicate` flag (Alt key detection at
  pointer-down), `DropTarget` discriminated union (`existing` | `newTrack`),
  `newTrackTypeFor` helper (audio -> "audio", else -> "video"). `onPointerMove`
  computes `dropTarget` (existing track via `trackAt`, or `newTrack` when
  below the last track bottom). `onPointerUp` branches: newTrack ->
  `edit.insertTrack` -> `forceRefresh` -> `edit.duplicateClips`/`moveClips`
  with the new track index; existing track -> group-floor-clamped move or
  duplicate.
- `timelineCanvas.ts`: extend `DragPaint` move variant with `isDuplicate?`
  and `newTrackType?`. Render a dashed new-track drop indicator below the
  last track; render the ghost at the new-track Y when `newTrackType` is
  set; pass `isDuplicate` to `drawClip`.
- `clipRenderer.ts`: add `isDuplicate?` to `DrawOpts`; draw a yellow "+"
  badge in the top-right corner when `ghost && isDuplicate` so the user
  sees the gesture will copy rather than move.

Closes appergb#98.

* style: fix cargo fmt in duplicate.rs (appergb#98)

* fix: correct cargo fmt in duplicate.rs - split long assert lines (appergb#98)

* fix: split long assert/assert_eq lines for cargo fmt (appergb#98)

* fix: compile errors - rotation_track type + borrow conflict (appergb#98)

* style: fix cargo fmt - import wrap + assert_eq single line (appergb#98)

* style: fix import wrapping for cargo fmt (appergb#98)

* fix: clippy errors - remove redundant clone on Copy types (appergb#98)

* fix(appergb#98): implement groupCounts/groupRemap for link group remapping (review appergb#123)
@cuic19053-hue

Copy link
Copy Markdown
Contributor Author

@appergb 请求重新审查。R2 反馈已逐条核对落实,CI 双绿(Rust ✅ Web ✅,commit 854cc1a):

  1. [CRITICAL] 链接组重映射:实现 group_counts + group_remap 映射表(duplicate.rs:107/113),同 linkGroupId 多 clip 拷贝共享新 group id,孤立 clip(group 仅出现一次)清为 Noneduplicate.rs:136
  2. 补测试duplicate_clips_remaps_link_group_for_multi_clip_group 验证含链接对的多 clip 复制后拷贝间仍保持 link_group_id 一致

DragPaint 整合计划:本 PR 与 #120 都改 timelineCanvas.ts 的 DragPaint 类型,建议先合并 #120,本 PR rebase 整合后合并。

请 re-review,谢谢!

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.

[timeline] 拖放落点路由 / 拖到新轨 / Option 拖拽复制(ghost 已修)

2 participants