summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/group-move/snap.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/timeline/group-move/snap.ts')
-rw-r--r--apps/web/src/timeline/group-move/snap.ts85
1 files changed, 85 insertions, 0 deletions
diff --git a/apps/web/src/timeline/group-move/snap.ts b/apps/web/src/timeline/group-move/snap.ts
new file mode 100644
index 00000000..32c0a2f1
--- /dev/null
+++ b/apps/web/src/timeline/group-move/snap.ts
@@ -0,0 +1,85 @@
+import type { SceneTracks } from "@/timeline";
+import {
+ buildTimelineSnapPoints,
+ getTimelineSnapThresholdInTicks,
+ resolveTimelineSnap,
+ type SnapPoint,
+} from "@/timeline/snapping";
+import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
+import { getPlayheadSnapPoints } from "@/timeline/playhead-snap-source";
+import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
+import type { MoveGroup } from "./types";
+
+export function snapGroupEdges({
+ group,
+ anchorStartTime,
+ tracks,
+ playheadTime,
+ zoomLevel,
+}: {
+ group: MoveGroup;
+ anchorStartTime: number;
+ tracks: SceneTracks;
+ playheadTime: number;
+ zoomLevel: number;
+}): {
+ snappedAnchorStartTime: number;
+ snapPoint: SnapPoint | null;
+} {
+ const excludeElementIds = new Set(
+ group.members.map((member) => member.elementId),
+ );
+ const snapPoints = buildTimelineSnapPoints({
+ sources: [
+ () => getElementEdgeSnapPoints({ tracks, excludeElementIds }),
+ () => getPlayheadSnapPoints({ playheadTime }),
+ () =>
+ getAnimationKeyframeSnapPointsForTimeline({
+ tracks,
+ excludeElementIds,
+ }),
+ ],
+ });
+ const maxSnapDistance = getTimelineSnapThresholdInTicks({ zoomLevel });
+
+ let closestSnapDistance = Infinity;
+ let snappedAnchorStartTime = anchorStartTime;
+ let snapPoint: SnapPoint | null = null;
+
+ for (const member of group.members) {
+ const memberStartTime = anchorStartTime + member.timeOffset;
+ const memberStartSnap = resolveTimelineSnap({
+ targetTime: memberStartTime,
+ snapPoints,
+ maxSnapDistance,
+ });
+ if (
+ memberStartSnap.snapPoint &&
+ memberStartSnap.snapDistance < closestSnapDistance
+ ) {
+ closestSnapDistance = memberStartSnap.snapDistance;
+ snappedAnchorStartTime = memberStartSnap.snappedTime - member.timeOffset;
+ snapPoint = memberStartSnap.snapPoint;
+ }
+
+ const memberEndSnap = resolveTimelineSnap({
+ targetTime: memberStartTime + member.duration,
+ snapPoints,
+ maxSnapDistance,
+ });
+ if (
+ memberEndSnap.snapPoint &&
+ memberEndSnap.snapDistance < closestSnapDistance
+ ) {
+ closestSnapDistance = memberEndSnap.snapDistance;
+ snappedAnchorStartTime =
+ memberEndSnap.snappedTime - member.duration - member.timeOffset;
+ snapPoint = memberEndSnap.snapPoint;
+ }
+ }
+
+ return {
+ snappedAnchorStartTime,
+ snapPoint,
+ };
+}