refactor: extract timeline model types

Move project entity shapes into the model layer so sibling domains no longer depend on timeline for core data types.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Maze Winther 2026-05-03 03:49:52 +02:00
parent 79cc7d2159
commit 6ac2116daa
177 changed files with 661 additions and 676 deletions

View File

@ -1,6 +1,6 @@
import { getKeyframeById } from "@/animation";
import type { SelectedKeyframeRef } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import { elementId, type ElementRef, type TimelineElement } from "@/model";
import { PasteKeyframesCommand } from "@/commands/timeline";
import type {
ClipboardHandler,
@ -12,7 +12,7 @@ function resolveSingleSourceElement({
selectedKeyframes,
}: {
selectedKeyframes: SelectedKeyframeRef[];
}) {
}): ElementRef | null {
const firstKeyframe = selectedKeyframes[0];
if (!firstKeyframe) {
return null;
@ -20,7 +20,7 @@ function resolveSingleSourceElement({
const sourceElement = {
trackId: firstKeyframe.trackId,
elementId: firstKeyframe.elementId,
elementId: elementId(firstKeyframe.elementId),
};
const isSingleSource = selectedKeyframes.every(
(keyframe) =>

View File

@ -7,11 +7,7 @@ import type {
SelectedKeyframeRef,
} from "@/animation/types";
import type { Command } from "@/commands/base-command";
import type {
CreateTimelineElement,
ElementRef,
TrackType,
} from "@/timeline";
import type { CreateTimelineElement, ElementRef, TrackType } from "@/model";
export interface ElementClipboardItem {
trackId: string;

View File

@ -1,5 +1,5 @@
import type { EditorSelectionPatch } from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
export interface CommandResult {
selection?: EditorSelectionPatch;

View File

@ -6,7 +6,7 @@ import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
import { waveformCache } from "@/services/waveform-cache/service";
import { hasMediaId } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveMediaAssetCommand extends Command {
private savedAssets: MediaAsset[] | null = null;

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { buildDefaultScene } from "@/timeline/scenes";
export class CreateSceneCommand extends Command {

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
export class DeleteSceneCommand extends Command {

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
export class RenameSceneCommand extends Command {

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { Bookmark, TScene } from "@/timeline";
import type { Bookmark, TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,

View File

@ -6,7 +6,7 @@ import {
} from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import type { KeyframeClipboardItem } from "@/clipboard";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { SceneTracks, TimelineElement } from "@/model";
import { updateElementInSceneTracks } from "@/timeline";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import { generateUUID } from "@/utils/id";

View File

@ -4,7 +4,7 @@ import {
type CommandResult,
} from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { ElementRef, SceneTracks, TimelineElement } from "@/model";
import type { ElementClipboardItem } from "@/clipboard";
import { generateUUID } from "@/utils/id";
import {
@ -16,7 +16,7 @@ import { cloneAnimations } from "@/animation";
export class PasteCommand extends Command {
private savedState: SceneTracks | null = null;
private pastedElements: { trackId: string; elementId: string }[] = [];
private pastedElements: ElementRef[] = [];
private readonly time: number;
private readonly clipboardItems: ElementClipboardItem[];
@ -142,7 +142,7 @@ export class PasteCommand extends Command {
}
}
getPastedElements(): { trackId: string; elementId: string }[] {
getPastedElements(): ElementRef[] {
return this.pastedElements;
}
}

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/timeline";
import type { TimelineTrack } from "@/model";
function removeTrackElements<TTrack extends TimelineTrack>({
track,

View File

@ -3,7 +3,13 @@ import {
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import {
elementId,
type ElementId,
type ElementRef,
type SceneTracks,
type TimelineElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import { applyPlacement, resolveTrackPlacement } from "@/timeline/placement";
@ -14,7 +20,7 @@ interface DuplicateElementsParams {
}
export class DuplicateElementsCommand extends Command {
private duplicatedElements: { trackId: string; elementId: string }[] = [];
private duplicatedElements: ElementRef[] = [];
private savedState: SceneTracks | null = null;
private elements: DuplicateElementsParams["elements"];
@ -53,7 +59,7 @@ export class DuplicateElementsCommand extends Command {
continue;
}
const newId = generateUUID();
const newId = elementId(generateUUID());
newTrackElements.push(
buildDuplicateElement({
element,
@ -107,7 +113,7 @@ export class DuplicateElementsCommand extends Command {
}
}
getDuplicatedElements(): { trackId: string; elementId: string }[] {
getDuplicatedElements(): ElementRef[] {
return this.duplicatedElements;
}
}
@ -118,7 +124,7 @@ function buildDuplicateElement({
startTime,
}: {
element: TimelineElement;
id: string;
id: ElementId;
startTime: number;
}): TimelineElement {
return {

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
import { buildDefaultEffectInstance } from "@/effects";
function addEffectToElement({

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
function removeEffectFromElement({
element,

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
function reorderEffectsOnElement({
element,

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
export function toggleEffectOnElement({
element,

View File

@ -2,7 +2,7 @@ import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { ParamValues } from "@/params";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
function updateEffectParamsOnElement({
element,

View File

@ -1,11 +1,13 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type {
CreateTimelineElement,
SceneTracks,
TimelineElement,
TrackType,
} from "@/timeline";
import {
elementId,
type CreateTimelineElement,
type ElementId,
type SceneTracks,
type TimelineElement,
type TrackType,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { requiresMediaId } from "@/timeline/element-utils";
import type { MediaAsset } from "@/media/types";
@ -29,13 +31,13 @@ export interface InsertElementParams {
}
export class InsertElementCommand extends Command {
private elementId: string;
private elementId: ElementId;
private savedState: SceneTracks | null = null;
private targetTrackId: string | null = null;
constructor({ element, placement }: InsertElementParams) {
super();
this.elementId = generateUUID();
this.elementId = elementId(generateUUID());
this.element = element;
this.placement = placement;
}

View File

@ -3,7 +3,7 @@ import { Command, type CommandResult } from "@/commands/base-command";
import { removeEffectParamKeyframe } from "@/animation/effect-param-channel";
import { updateElementInSceneTracks } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;

View File

@ -6,7 +6,7 @@ import {
import { Command, type CommandResult } from "@/commands/base-command";
import { updateElementInSceneTracks } from "@/timeline";
import type { AnimationPath, AnimationValue } from "@/animation/types";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { SceneTracks, TimelineElement } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
function removeKeyframeAndPersist({

View File

@ -3,7 +3,7 @@ import { retimeElementKeyframe } from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import { updateElementInSceneTracks } from "@/timeline";
import type { AnimationPath } from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
export class RetimeKeyframeCommand extends Command {

View File

@ -9,7 +9,7 @@ import type {
AnimationPath,
ScalarCurveKeyframePatch,
} from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class UpdateScalarKeyframeCurveCommand extends Command {
private savedState: SceneTracks | null = null;

View File

@ -8,7 +8,7 @@ import { updateElementInSceneTracks } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import type { AnimationInterpolation } from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class UpsertEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;

View File

@ -2,7 +2,7 @@ import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { upsertPathKeyframe } from "@/animation";
import { updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import type {
AnimationPath,

View File

@ -6,7 +6,7 @@ import {
} from "@/masks/custom-path";
import type { CustomMask } from "@/masks/types";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function deletePointsFromCustomMask({
mask,

View File

@ -4,7 +4,7 @@ import { insertPointOnCustomMaskSegment } from "@/masks/definitions/custom";
import type { ElementBounds } from "@/preview/element-bounds";
import type { CustomMask } from "@/masks/types";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function insertPointIntoCustomMask({
mask,

View File

@ -1,7 +1,7 @@
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, MaskableElement } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function removeMaskFromElement({
element,

View File

@ -2,7 +2,7 @@ import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { Mask } from "@/masks/types";
import type { SceneTracks, MaskableElement } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
export function toggleMaskInvertedOnElement({
element,

View File

@ -4,7 +4,12 @@ import {
type CommandResult,
} from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline";
import {
elementId as toElementId,
type SceneTracks,
type TimelineElement,
type TimelineTrack,
} from "@/model";
import {
buildEmptyTrack,
validateElementTrackCompatibility,
@ -117,7 +122,7 @@ export class MoveElementCommand extends Command {
return createElementSelectionResult(
this.moves.map(({ elementId, targetTrackId }) => ({
trackId: targetTrackId,
elementId,
elementId: toElementId(elementId),
})),
);
}

View File

@ -3,7 +3,12 @@ import {
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import {
elementId,
type ElementRef,
type SceneTracks,
type TimelineElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import { isRetimableElement } from "@/timeline";
@ -12,7 +17,7 @@ import { getSourceSpanAtClipTime } from "@/retime";
export class SplitElementsCommand extends Command {
private savedState: SceneTracks | null = null;
private rightSideElements: { trackId: string; elementId: string }[] = [];
private rightSideElements: ElementRef[] = [];
private readonly elements: { trackId: string; elementId: string }[];
private readonly splitTime: number;
private readonly retainSide: "both" | "left" | "right";
@ -32,7 +37,7 @@ export class SplitElementsCommand extends Command {
this.retainSide = retainSide;
}
getRightSideElements(): { trackId: string; elementId: string }[] {
getRightSideElements(): ElementRef[] {
return this.rightSideElements;
}
@ -107,7 +112,7 @@ export class SplitElementsCommand extends Command {
},
];
} else if (this.retainSide === "right") {
const newId = generateUUID();
const newId = elementId(generateUUID());
this.rightSideElements.push({
trackId: track.id,
elementId: newId,
@ -126,7 +131,7 @@ export class SplitElementsCommand extends Command {
];
} else {
// "both" - split into two pieces
const secondElementId = generateUUID();
const secondElementId = elementId(generateUUID());
this.rightSideElements.push({
trackId: track.id,
elementId: secondElementId,

View File

@ -10,11 +10,12 @@ import {
resolveTrackPlacement,
} from "@/timeline/placement";
import { updateElementInSceneTracks } from "@/timeline/track-element-update";
import type {
SceneTracks,
TimelineElement,
VideoElement,
} from "@/timeline/types";
import {
elementId,
type SceneTracks,
type TimelineElement,
type VideoElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
export class ToggleSourceAudioSeparationCommand extends Command {
@ -75,7 +76,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
...buildSeparatedAudioElement({
sourceElement: videoElement,
}),
id: generateUUID(),
id: elementId(generateUUID()),
};
const placementResult = resolveTrackPlacement({
tracks: this.savedState,

View File

@ -1,6 +1,6 @@
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { SceneTracks, TimelineElement } from "@/model";
import {
findTrackInSceneTracks,
updateElementInSceneTracks,

View File

@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks, TrackType } from "@/timeline";
import type { SceneTracks, TrackType } from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import {

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveTrackCommand extends Command {
private savedState: SceneTracks | null = null;

View File

@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";

View File

@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";

View File

@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
export class TracksSnapshotCommand extends Command {

View File

@ -45,7 +45,7 @@ import {
type MediaViewMode,
useAssetsPanelStore,
} from "@/components/editor/panels/assets/assets-panel-store";
import { MASKABLE_ELEMENT_TYPES } from "@/timeline";
import { MASKABLE_ELEMENT_TYPES } from "@/model";
import type { MediaAsset } from "@/media/types";
import { cn } from "@/utils/ui";
import {

View File

@ -4,7 +4,7 @@ import {
hasKeyframesForPath,
} from "@/animation";
import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import type { TimelineElement } from "@/model";
import { upsertElementKeyframe } from "@/timeline/animation-properties";
export function useKeyframedColorProperty({

View File

@ -4,7 +4,7 @@ import {
hasKeyframesForPath,
} from "@/animation";
import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import type { TimelineElement } from "@/model";
import { upsertElementKeyframe } from "@/timeline/animation-properties";
import { snapToStep } from "@/utils/math";
import { usePropertyDraft } from "./use-property-draft";

View File

@ -16,7 +16,7 @@ import {
getAnimationParamValueKind,
} from "@/animation/animated-params";
import type { ParamDefinition } from "@/params";
import type { TimelineElement } from "@/timeline";
import type { TimelineElement } from "@/model";
export interface KeyframedParamPropertyResult {
hasAnimatedKeyframes: boolean;

View File

@ -1,17 +1,5 @@
import type { ReactNode } from "react";
import type {
EffectElement,
GraphicElement,
ImageElement,
MaskableElement,
RetimableElement,
StickerElement,
TextElement,
VisualElement,
VideoElement,
AudioElement,
TimelineElement,
} from "@/timeline";
import type { AudioElement, EffectElement, GraphicElement, ImageElement, MaskableElement, RetimableElement, StickerElement, TextElement, TimelineElement, VideoElement, VisualElement } from "@/model";
import type { MediaAsset } from "@/media/types";
import { HugeiconsIcon } from "@hugeicons/react";
import {

View File

@ -2,7 +2,7 @@ import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/commands";
import type { EditorSelectionSnapshot } from "@/selection/editor-selection";
import { applyRippleAdjustments, computeRippleAdjustments } from "@/ripple";
import type { SceneTracks } from "@/timeline/types";
import type { SceneTracks } from "@/model";
interface CommandHistoryEntry {
command: Command;

View File

@ -1,5 +1,5 @@
import type { EditorCore } from "@/core";
import type { SceneTracks, TScene } from "@/timeline";
import type { SceneTracks, TScene } from "@/model";
import { storageService } from "@/services/storage/service";
import {
getMainScene,

View File

@ -6,7 +6,7 @@ import type {
EditorSelectionSnapshot,
SelectedMaskPointSelection,
} from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
export class SelectionManager {
private selectedElements: ElementRef[] = [];

View File

@ -1,13 +1,7 @@
import type { EditorCore } from "@/core";
import type { ElementBounds } from "@/preview/element-bounds";
import type { ParamValues } from "@/params";
import type {
SceneTracks,
TrackType,
TimelineTrack,
TimelineElement,
RetimeConfig,
} from "@/timeline";
import type { RetimeConfig, SceneTracks, TimelineElement, TimelineTrack, TrackType } from "@/model";
import { calculateTotalDuration } from "@/timeline";
import { TimelineDragSource } from "@/timeline/drag-source";
import { findTrackInSceneTracks } from "@/timeline/track-element-update";

View File

@ -3,7 +3,7 @@
import { useState } from "react";
import type { ParamValues } from "@/params";
import type { Effect } from "@/effects/types";
import type { EffectElement, VisualElement } from "@/timeline";
import type { EffectElement, VisualElement } from "@/model";
import { effectsRegistry } from "@/effects";
import { useEditor } from "@/editor/use-editor";
import { useElementPreview } from "@/timeline/hooks/use-element-preview";

View File

@ -3,7 +3,7 @@ import { buildDefaultParamValues } from "@/params/registry";
import { effectsRegistry } from "./registry";
import type { ParamValues } from "@/params";
import type { Effect, EffectDefinition, EffectPass } from "@/effects/types";
import { VISUAL_ELEMENT_TYPES } from "@/timeline";
import { VISUAL_ELEMENT_TYPES } from "@/model";
export { effectsRegistry } from "./registry";
export { registerDefaultEffects } from "./definitions";

View File

@ -7,7 +7,7 @@ import {
type KeyframedParamPropertyResult,
} from "@/components/editor/panels/properties/hooks/use-keyframed-param-property";
import type { ParamDefinition, ParamValues } from "@/params";
import type { GraphicElement } from "@/timeline";
import type { GraphicElement } from "@/model";
import {
graphicsRegistry,
registerDefaultGraphics,

View File

@ -1,6 +1,6 @@
"use client";
import type { MaskableElement } from "@/timeline";
import type { MaskableElement } from "@/model";
import type { Mask, MaskType, TextMask } from "@/masks/types";
import type { NumberParamDefinition, SelectParamDefinition } from "@/params";
import { masksRegistry, buildDefaultMaskInstance } from "@/masks";

View File

@ -1,5 +1,5 @@
import type { ElementBounds } from "@/preview/element-bounds";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import { MIN_MASK_DIMENSION } from "@/masks/dimensions";
import type { RectangleMaskParams } from "@/masks/types";
@ -100,10 +100,10 @@ export function toGlobalMaskSnapLines({
bounds,
canvasSize,
}: {
lines: SnapLine[];
lines: PreviewSnapLine[];
bounds: ElementBounds;
canvasSize: { width: number; height: number };
}): SnapLine[] {
}): PreviewSnapLine[] {
const centerX = bounds.cx - canvasSize.width / 2;
const centerY = bounds.cy - canvasSize.height / 2;

View File

@ -6,7 +6,7 @@ import {
snapScale,
snapScaleAxes,
type ScaleEdgePreference,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types";
import {
@ -20,7 +20,7 @@ type SharedMaskParams = SplitMaskParams | RectangleMaskParams;
type MaskSnapResult<TParams extends SharedMaskParams> = {
params: TParams;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
};
const CORNER_SIZE_HANDLES = new Set([

View File

@ -1,5 +1,5 @@
import type { ElementBounds } from "@/preview/element-bounds";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import type { ParamDefinition } from "@/params";
import type { CustomMaskPathPoint } from "@/masks/custom-path";
import type {
@ -261,7 +261,7 @@ export interface MaskSnapResult<
TParams extends BaseMaskParams = BaseMaskParams,
> {
params: TParams;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
}
export interface MaskInteractionResult {

View File

@ -14,11 +14,11 @@ import {
} from "@/preview/element-bounds";
import {
SNAP_THRESHOLD_SCREEN_PIXELS,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import type { SelectedMaskPointSelection } from "@/selection/editor-selection";
import type { Mask, MaskInteractionResult } from "@/masks/types";
import type { MaskableElement } from "@/timeline";
import type { MaskableElement } from "@/model";
import { registerCanceller } from "@/editor/cancel-interaction";
interface DragState {
@ -94,7 +94,7 @@ function withUpdatedMaskParams<TMask extends Mask>({
export function useMaskHandles({
onSnapLinesChange,
}: {
onSnapLinesChange?: (lines: SnapLine[]) => void;
onSnapLinesChange?: (lines: PreviewSnapLine[]) => void;
}) {
const editor = useEditor();
const isShiftHeldRef = useShiftKey();
@ -552,7 +552,7 @@ export function useMaskHandles({
screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
});
const { params: nextParams, activeLines } = isShiftHeldRef.current
? { params: proposedParams, activeLines: [] as SnapLine[] }
? { params: proposedParams, activeLines: [] as PreviewSnapLine[] }
: (def.interaction.snap?.({
handleId: drag.handleId,
startParams: drag.startParams,
@ -560,7 +560,7 @@ export function useMaskHandles({
bounds: selectedWithMask.bounds,
canvasSize,
snapThreshold,
}) ?? { params: proposedParams, activeLines: [] as SnapLine[] });
}) ?? { params: proposedParams, activeLines: [] as PreviewSnapLine[] });
onSnapLinesChange?.(activeLines);

View File

@ -1,10 +1,4 @@
import type {
AudioElement,
VideoElement,
LibraryAudioElement,
RetimeConfig,
SceneTracks,
} from "@/timeline";
import type { AudioElement, LibraryAudioElement, RetimeConfig, SceneTracks, VideoElement } from "@/model";
import { shouldMaintainPitch } from "@/retime/rate";
import type { MediaAsset } from "@/media/types";
import { applyAudioMasteringToBuffer } from "@/media/audio-mastering";

View File

@ -6,7 +6,7 @@ import {
type VideoCodec,
} from "mediabunny";
import { createTimelineAudioBuffer } from "@/media/audio";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import type { MediaAsset } from "@/media/types";
import { TICKS_PER_SECOND } from "@/wasm";
import { renderThumbnailDataUrl } from "./thumbnail";

View File

@ -1,7 +1,7 @@
"use client";
import { getSourceTimeAtClipTime } from "@/retime";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
const RMS_ANALYSIS_WINDOW_SECONDS = 0.02;
const DEFAULT_SOURCE_WAVEFORM_BUCKET_SIZE = 128;

View File

@ -0,0 +1,6 @@
export interface Bookmark {
time: number;
note?: string;
color?: string;
duration?: number;
}

View File

@ -0,0 +1,6 @@
import type { ElementId } from "./ids";
export type ElementRef = {
trackId: string;
elementId: ElementId;
};

View File

@ -0,0 +1,21 @@
import type { BaseTimelineElement, RetimeConfig } from "./base";
export interface BaseAudioElement extends BaseTimelineElement {
type: "audio";
volume: number;
muted?: boolean;
buffer?: AudioBuffer;
retime?: RetimeConfig;
}
export interface UploadAudioElement extends BaseAudioElement {
sourceType: "upload";
mediaId: string;
}
export interface LibraryAudioElement extends BaseAudioElement {
sourceType: "library";
sourceUrl: string;
}
export type AudioElement = UploadAudioElement | LibraryAudioElement;

View File

@ -0,0 +1,18 @@
import type { ElementAnimations } from "@/animation/types";
import type { ElementId } from "../ids";
export interface BaseTimelineElement {
id: ElementId;
name: string;
duration: number;
startTime: number;
trimStart: number;
trimEnd: number;
sourceDuration?: number;
animations?: ElementAnimations;
}
export interface RetimeConfig {
rate: number;
maintainPitch?: boolean;
}

View File

@ -0,0 +1,8 @@
import type { ParamValues } from "@/params";
import type { BaseTimelineElement } from "./base";
export interface EffectElement extends BaseTimelineElement {
type: "effect";
effectType: string;
params: ParamValues;
}

View File

@ -0,0 +1,17 @@
import type { Effect } from "@/effects/types";
import type { Mask } from "@/masks/types";
import type { ParamValues } from "@/params";
import type { BlendMode, Transform } from "@/rendering";
import type { BaseTimelineElement } from "./base";
export interface GraphicElement extends BaseTimelineElement {
type: "graphic";
definitionId: string;
params: ParamValues;
hidden?: boolean;
transform: Transform;
opacity: number;
blendMode?: BlendMode;
effects?: Effect[];
masks?: Mask[];
}

View File

@ -0,0 +1,15 @@
import type { Effect } from "@/effects/types";
import type { Mask } from "@/masks/types";
import type { BlendMode, Transform } from "@/rendering";
import type { BaseTimelineElement } from "./base";
export interface ImageElement extends BaseTimelineElement {
type: "image";
mediaId: string;
hidden?: boolean;
transform: Transform;
opacity: number;
blendMode?: BlendMode;
effects?: Effect[];
masks?: Mask[];
}

View File

@ -0,0 +1,84 @@
import type { AudioElement, LibraryAudioElement, UploadAudioElement } from "./audio";
import type { EffectElement } from "./effect";
import type { GraphicElement } from "./graphic";
import type { ImageElement } from "./image";
import type { StickerElement } from "./sticker";
import type { TextElement } from "./text";
import type { VideoElement } from "./video";
import type { Transform } from "@/rendering";
export * from "./audio";
export * from "./base";
export * from "./effect";
export * from "./graphic";
export * from "./image";
export * from "./sticker";
export * from "./text";
export * from "./video";
export type ElementUpdatePatch =
| { transform: Transform }
| { opacity: number }
| { volume: number };
export type TimelineElement =
| AudioElement
| VideoElement
| ImageElement
| TextElement
| StickerElement
| GraphicElement
| EffectElement;
export type ElementType = TimelineElement["type"];
function elementTypes<T extends ElementType[]>(...types: T): T {
return types;
}
export const MASKABLE_ELEMENT_TYPES = elementTypes("video", "image", "graphic");
export type MaskableElement = Extract<
TimelineElement,
{ type: (typeof MASKABLE_ELEMENT_TYPES)[number] }
>;
export const RETIMABLE_ELEMENT_TYPES = elementTypes("video", "audio");
export type RetimableElement = Extract<
TimelineElement,
{ type: (typeof RETIMABLE_ELEMENT_TYPES)[number] }
>;
export const VISUAL_ELEMENT_TYPES = elementTypes(
"video",
"image",
"text",
"sticker",
"graphic",
);
export type VisualElement = Extract<
TimelineElement,
{ type: (typeof VISUAL_ELEMENT_TYPES)[number] }
>;
export type CreateUploadAudioElement = Omit<UploadAudioElement, "id">;
export type CreateLibraryAudioElement = Omit<LibraryAudioElement, "id">;
export type CreateAudioElement =
| CreateUploadAudioElement
| CreateLibraryAudioElement;
export type CreateVideoElement = Omit<VideoElement, "id">;
export type CreateImageElement = Omit<ImageElement, "id">;
export type CreateTextElement = Omit<TextElement, "id">;
export type CreateStickerElement = Omit<StickerElement, "id">;
export type CreateGraphicElement = Omit<GraphicElement, "id">;
export type CreateEffectElement = Omit<EffectElement, "id">;
export type CreateTimelineElement =
| CreateAudioElement
| CreateVideoElement
| CreateImageElement
| CreateTextElement
| CreateStickerElement
| CreateGraphicElement
| CreateEffectElement;

View File

@ -0,0 +1,16 @@
import type { Effect } from "@/effects/types";
import type { BlendMode, Transform } from "@/rendering";
import type { BaseTimelineElement } from "./base";
export interface StickerElement extends BaseTimelineElement {
type: "sticker";
stickerId: string;
/** Natural dimensions of the sticker asset, stored at insert time. Used by renderer and preview bounds to avoid split-brain geometry. */
intrinsicWidth?: number;
intrinsicHeight?: number;
hidden?: boolean;
transform: Transform;
opacity: number;
blendMode?: BlendMode;
effects?: Effect[];
}

View File

@ -0,0 +1,33 @@
import type { Effect } from "@/effects/types";
import type { BlendMode, Transform } from "@/rendering";
import type { BaseTimelineElement } from "./base";
export interface TextBackground {
enabled: boolean;
color: string;
cornerRadius?: number;
paddingX?: number;
paddingY?: number;
offsetX?: number;
offsetY?: number;
}
export interface TextElement extends BaseTimelineElement {
type: "text";
content: string;
fontSize: number;
fontFamily: string;
color: string;
background: TextBackground;
textAlign: "left" | "center" | "right";
fontWeight: "normal" | "bold";
fontStyle: "normal" | "italic";
textDecoration: "none" | "underline" | "line-through";
letterSpacing?: number;
lineHeight?: number;
hidden?: boolean;
transform: Transform;
opacity: number;
blendMode?: BlendMode;
effects?: Effect[];
}

View File

@ -0,0 +1,19 @@
import type { Effect } from "@/effects/types";
import type { Mask } from "@/masks/types";
import type { BlendMode, Transform } from "@/rendering";
import type { BaseTimelineElement, RetimeConfig } from "./base";
export interface VideoElement extends BaseTimelineElement {
type: "video";
mediaId: string;
volume?: number;
muted?: boolean;
isSourceAudioEnabled?: boolean;
hidden?: boolean;
retime?: RetimeConfig;
transform: Transform;
opacity: number;
blendMode?: BlendMode;
effects?: Effect[];
masks?: Mask[];
}

15
apps/web/src/model/ids.ts Normal file
View File

@ -0,0 +1,15 @@
export type ElementId = string & { readonly __brand: "ElementId" };
export type TrackId = string & { readonly __brand: "TrackId" };
export type SceneId = string & { readonly __brand: "SceneId" };
export function elementId(value: string): ElementId {
return value as ElementId;
}
export function trackId(value: string): TrackId {
return value as TrackId;
}
export function sceneId(value: string): SceneId {
return value as SceneId;
}

View File

@ -0,0 +1,7 @@
export * from "./bookmark";
export * from "./element-ref";
export * from "./elements";
export * from "./ids";
export * from "./scene";
export * from "./time";
export * from "./track";

View File

@ -0,0 +1,12 @@
import type { Bookmark } from "./bookmark";
import type { SceneTracks } from "./track";
export interface TScene {
id: string;
name: string;
isMain: boolean;
tracks: SceneTracks;
bookmarks: Bookmark[];
createdAt: Date;
updatedAt: Date;
}

View File

@ -0,0 +1,5 @@
export type MediaTime = number & { readonly __brand: "MediaTime" };
export function mediaTime(value: number): MediaTime {
return value as MediaTime;
}

View File

@ -0,0 +1,62 @@
import type {
AudioElement,
EffectElement,
GraphicElement,
ImageElement,
StickerElement,
TextElement,
VideoElement,
} from "./elements";
export type TrackType = "video" | "text" | "audio" | "graphic" | "effect";
export interface BaseTrack {
id: string;
name: string;
}
export interface VideoTrack extends BaseTrack {
type: "video";
elements: (VideoElement | ImageElement)[];
muted: boolean;
hidden: boolean;
}
export interface TextTrack extends BaseTrack {
type: "text";
elements: TextElement[];
hidden: boolean;
}
export interface AudioTrack extends BaseTrack {
type: "audio";
elements: AudioElement[];
muted: boolean;
}
export interface GraphicTrack extends BaseTrack {
type: "graphic";
elements: (StickerElement | GraphicElement)[];
hidden: boolean;
}
export interface EffectTrack extends BaseTrack {
type: "effect";
elements: EffectElement[];
hidden: boolean;
}
export type TimelineTrack =
| VideoTrack
| TextTrack
| AudioTrack
| GraphicTrack
| EffectTrack;
export type OverlayTrack = VideoTrack | TextTrack | GraphicTrack | EffectTrack;
export interface SceneTracks {
overlay: OverlayTrack[];
main: VideoTrack;
audio: AudioTrack[];
}

View File

@ -3,7 +3,7 @@
import { PEN_CURSOR } from "@/preview/components/cursors";
import { usePreviewViewport } from "@/preview/components/preview-viewport";
import { useMaskHandles } from "@/masks/use-mask-handles";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import {
CornerHandle,
CircleHandle,
@ -22,7 +22,7 @@ import { Rotate01Icon, FeatherIcon } from "@hugeicons/core-free-icons";
export function MaskHandles({
onSnapLinesChange,
}: {
onSnapLinesChange?: (lines: SnapLine[]) => void;
onSnapLinesChange?: (lines: PreviewSnapLine[]) => void;
}) {
const viewport = usePreviewViewport();
const {

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { usePreviewViewport } from "@/preview/components/preview-viewport";
import { usePreviewInteraction } from "@/preview/hooks/use-preview-interaction";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import { TransformHandles } from "./transform-handles";
import { MaskHandles } from "./mask-handles";
import { SnapGuides } from "./snap-guides";
@ -10,7 +10,7 @@ import { usePropertiesStore } from "@/components/editor/panels/properties/stores
import { useEditor } from "@/editor/use-editor";
export function PreviewInteractionOverlay() {
const [snapLines, setSnapLines] = useState<SnapLine[]>([]);
const [snapLines, setSnapLines] = useState<PreviewSnapLine[]>([]);
const editor = useEditor();
const viewport = usePreviewViewport();
const selectedElements = useEditor((e) => e.selection.getSelectedElements());

View File

@ -1,9 +1,9 @@
"use client";
import { usePreviewViewport } from "@/preview/components/preview-viewport";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
export function SnapGuides({ lines }: { lines: SnapLine[] }) {
export function SnapGuides({ lines }: { lines: PreviewSnapLine[] }) {
const viewport = usePreviewViewport();
if (lines.length === 0) {

View File

@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef } from "react";
import { usePreviewViewport } from "@/preview/components/preview-viewport";
import { useEditor } from "@/editor/use-editor";
import type { TextElement } from "@/timeline";
import type { TextElement } from "@/model";
import { DEFAULTS } from "@/timeline/defaults";
import {
getElementLocalTime,

View File

@ -15,19 +15,12 @@ import {
import {
SNAP_THRESHOLD_SCREEN_PIXELS,
snapPosition,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import type { TCanvasSize } from "@/project/types";
import type { Transform } from "@/rendering";
import { isVisualElement } from "@/timeline/element-utils";
import type {
ElementRef,
SceneTracks,
TextElement,
TimelineElement,
TimelineTrack,
VisualElement,
} from "@/timeline";
import type { ElementRef, SceneTracks, TextElement, TimelineElement, TimelineTrack, VisualElement } from "@/model";
const MIN_DRAG_DISTANCE = 0.5;
const PRIMARY_POINTER_BUTTON = 0;
@ -133,7 +126,7 @@ export interface PlaybackApi {
export interface PreviewOptions {
isMaskMode: () => boolean;
onSnapLinesChange?: (lines: SnapLine[]) => void;
onSnapLinesChange?: (lines: PreviewSnapLine[]) => void;
}
export interface PreviewInteractionDeps {
@ -554,7 +547,7 @@ export class PreviewInteractionController {
})
: {
snappedPosition: proposedPosition,
activeLines: [] as SnapLine[],
activeLines: [] as PreviewSnapLine[],
};
this.deps.preview.onSnapLinesChange?.(activeLines);

View File

@ -14,7 +14,7 @@ import {
snapScale,
snapScaleAxes,
type ScaleEdgePreference,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import { isVisualElement } from "@/timeline/element-utils";
import {
@ -25,12 +25,7 @@ import {
import type { ElementAnimations } from "@/animation/types";
import type { Transform } from "@/rendering";
import { resolveTransformAtTime } from "@/rendering/animation-values";
import type {
ElementRef,
SceneTracks,
TimelineElement,
VisualElement,
} from "@/timeline";
import type { ElementRef, SceneTracks, TimelineElement, VisualElement } from "@/model";
type Point = { readonly x: number; readonly y: number };
type CanvasSize = { readonly width: number; readonly height: number };
@ -137,7 +132,7 @@ export interface TimelineOps {
}
export interface PreviewOptions {
onSnapLinesChange?: (lines: SnapLine[]) => void;
onSnapLinesChange?: (lines: PreviewSnapLine[]) => void;
}
export interface TransformHandleDeps {
@ -590,7 +585,7 @@ export class TransformHandleController {
screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
});
const { snappedScale, activeLines } = this.deps.input.isShiftHeld()
? { snappedScale: scaleFactor, activeLines: [] as SnapLine[] }
? { snappedScale: scaleFactor, activeLines: [] as PreviewSnapLine[] }
: snapScale({
proposedScale: scaleFactor,
position: session.initialTransform.position,
@ -670,12 +665,12 @@ export class TransformHandleController {
x: {
snappedScale: proposedScaleX,
snapDistance: Infinity,
activeLines: [] as SnapLine[],
activeLines: [] as PreviewSnapLine[],
},
y: {
snappedScale: proposedScaleY,
snapDistance: Infinity,
activeLines: [] as SnapLine[],
activeLines: [] as PreviewSnapLine[],
},
}
: snapScaleAxes({

View File

@ -1,4 +1,4 @@
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { ElementId, SceneTracks, TimelineElement } from "@/model";
import type { MediaAsset } from "@/media/types";
import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
@ -18,7 +18,7 @@ export interface ElementBounds {
export interface ElementWithBounds {
trackId: string;
elementId: string;
elementId: ElementId;
element: TimelineElement;
bounds: ElementBounds;
}

View File

@ -1,5 +1,5 @@
import type { ElementWithBounds } from "./element-bounds";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
function pointInRotatedRect({
px,

View File

@ -2,7 +2,7 @@ import { useEffect, useReducer, useRef } from "react";
import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
import { usePreviewViewport } from "@/preview/components/preview-viewport";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import { registerCanceller } from "@/editor/cancel-interaction";
import {
PreviewInteractionController,
@ -10,7 +10,7 @@ import {
type PreviewInteractionDepsRef,
} from "@/preview/controllers/preview-interaction-controller";
export type OnSnapLinesChange = (lines: SnapLine[]) => void;
export type OnSnapLinesChange = (lines: PreviewSnapLine[]) => void;
export function usePreviewInteraction({
onSnapLinesChange,

View File

@ -1,4 +1,4 @@
export interface SnapLine {
export interface PreviewSnapLine {
type: "horizontal" | "vertical";
position: number;
}
@ -8,9 +8,9 @@ const ROTATION_SNAP_THRESHOLD_DEGREES = 5;
export const MIN_SCALE = 0.01;
export const SNAP_THRESHOLD_SCREEN_PIXELS = 8;
export interface SnapResult {
export interface PreviewSnapResult {
snappedPosition: { x: number; y: number };
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
}
type ScaleEdge = "left" | "right" | "top" | "bottom";
@ -78,7 +78,7 @@ export function snapPosition({
elementSize: { width: number; height: number };
rotation?: number;
snapThreshold: { x: number; y: number };
}): SnapResult {
}): PreviewSnapResult {
const centerX = 0;
const centerY = 0;
const left = -canvasSize.width / 2;
@ -91,11 +91,11 @@ export function snapPosition({
const sinR = Math.abs(Math.sin(rotRad));
const halfWidth = (elementSize.width * cosR + elementSize.height * sinR) / 2;
const halfHeight = (elementSize.width * sinR + elementSize.height * cosR) / 2;
const activeLines: SnapLine[] = [];
const activeLines: PreviewSnapLine[] = [];
type AxisSnapCandidate = {
snappedPosition: number;
line: SnapLine;
line: PreviewSnapLine;
distance: number;
};
@ -183,7 +183,7 @@ export function snapPosition({
export interface ScaleSnapResult {
snappedScale: number;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
}
export function snapScale({
@ -226,7 +226,7 @@ export function snapScale({
interface SnapCandidate {
scale: number;
distance: number;
lines: SnapLine[];
lines: PreviewSnapLine[];
edge: ScaleEdge;
}
@ -320,10 +320,10 @@ export function snapScale({
const snappedTop = position.y - aabbBaseHalfH * best.scale;
const snappedBottom = position.y + aabbBaseHalfH * best.scale;
const activeLines: SnapLine[] = [];
const activeLines: PreviewSnapLine[] = [];
const seenKeys = new Set<string>();
function addLine({ line }: { line: SnapLine }) {
function addLine({ line }: { line: PreviewSnapLine }) {
const key = `${line.type}-${line.position}`;
if (!seenKeys.has(key)) {
seenKeys.add(key);
@ -368,7 +368,7 @@ export interface AxisSnapResult {
snappedScale: number;
/** Infinity when no snap candidate was within threshold */
snapDistance: number;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
}
export function snapScaleAxes({
@ -413,7 +413,7 @@ export function snapScaleAxes({
interface Candidate {
scale: number;
distance: number;
line: SnapLine;
line: PreviewSnapLine;
edge: ScaleEdge;
}
@ -442,7 +442,7 @@ export function snapScaleAxes({
if (cosR > EPSILON) {
for (const T of [canvasLeft, 0, canvasRight]) {
const line: SnapLine = { type: "vertical", position: T };
const line: PreviewSnapLine = { type: "vertical", position: T };
const distLeft = Math.abs(currentLeftEdge - T);
if (distLeft <= snapThreshold.x) {
const scale = (2 * (position.x - T) - yContribW) / (baseWidth * cosR);
@ -458,7 +458,7 @@ export function snapScaleAxes({
if (sinR > EPSILON) {
for (const T of [canvasTop, 0, canvasBottom]) {
const line: SnapLine = { type: "horizontal", position: T };
const line: PreviewSnapLine = { type: "horizontal", position: T };
const distTop = Math.abs(currentTopEdge - T);
if (distTop <= snapThreshold.y) {
const scale = (2 * (position.y - T) - yContribH) / (baseWidth * sinR);
@ -480,7 +480,7 @@ export function snapScaleAxes({
if (sinR > EPSILON) {
for (const T of [canvasLeft, 0, canvasRight]) {
const line: SnapLine = { type: "vertical", position: T };
const line: PreviewSnapLine = { type: "vertical", position: T };
const distLeft = Math.abs(currentLeftEdge - T);
if (distLeft <= snapThreshold.x) {
const scale = (2 * (position.x - T) - xContribW) / (baseHeight * sinR);
@ -496,7 +496,7 @@ export function snapScaleAxes({
if (cosR > EPSILON) {
for (const T of [canvasTop, 0, canvasBottom]) {
const line: SnapLine = { type: "horizontal", position: T };
const line: PreviewSnapLine = { type: "horizontal", position: T };
const distTop = Math.abs(currentTopEdge - T);
if (distTop <= snapThreshold.y) {
const scale = (2 * (position.y - T) - xContribH) / (baseHeight * cosR);

View File

@ -1,5 +1,5 @@
import type { FrameRate } from "opencut-wasm";
import type { TScene } from "@/timeline/types";
import type { TScene } from "@/model";
export type TBackground =
| {

View File

@ -20,7 +20,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import type { BlendMode } from "@/rendering";
import type { ElementType } from "@/timeline";
import type { ElementType } from "@/model";
import type { ElementAnimations } from "@/animation/types";
import { HugeiconsIcon } from "@hugeicons/react";
import { RainDropIcon } from "@hugeicons/core-free-icons";

View File

@ -1,7 +1,7 @@
import { NumberField } from "@/components/ui/number-field";
import { useEditor } from "@/editor/use-editor";
import { clamp, isNearlyEqual } from "@/utils/math";
import type { VisualElement } from "@/timeline";
import type { VisualElement } from "@/model";
import {
Section,
SectionContent,

View File

@ -5,7 +5,7 @@ import {
getSourceTimeAtClipTime,
getTimelineDurationForSourceSpan,
} from "@/retime";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
const twoX: RetimeConfig = { rate: 2 };
const halfX: RetimeConfig = { rate: 0.5 };

View File

@ -3,7 +3,7 @@ import {
getSourceSpanAtClipTime,
splitRetimeAtClipTime,
} from "@/retime";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
describe("retime split", () => {
test("measures source span at a clip time", () => {

View File

@ -1,6 +1,6 @@
import { PitchShifter } from "soundtouchjs";
import { clampRetimeRate, shouldMaintainPitch } from "@/retime/rate";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
import { getSourceTimeAtClipTime } from "./resolve";
const RATE_EPSILON = 1e-6;

View File

@ -1,4 +1,4 @@
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
import { clampRetimeRate } from "@/retime/rate";
export function buildConstantRetime({

View File

@ -1,4 +1,4 @@
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
import { clampRetimeRate } from "@/retime/rate";
function getSafeRate({ rate }: { rate: number }): number {

View File

@ -1,4 +1,4 @@
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
import { getSourceTimeAtClipTime } from "./resolve";
export function getSourceSpanAtClipTime({

View File

@ -1,4 +1,4 @@
import type { SceneTracks, TimelineTrack } from "@/timeline/types";
import type { SceneTracks, TimelineTrack } from "@/model";
import { rippleShiftElements } from "./shift";
export interface RippleAdjustment {

View File

@ -1,4 +1,4 @@
import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline/types";
import type { SceneTracks, TimelineElement, TimelineTrack } from "@/model";
import type { RippleAdjustment } from "./apply";
interface Interval {

View File

@ -1,4 +1,4 @@
import type { TimelineElement } from "@/timeline/types";
import type { TimelineElement } from "@/model";
export function rippleShiftElements<TElement extends TimelineElement>({
elements,

View File

@ -1,5 +1,5 @@
import type { SelectedKeyframeRef } from "@/animation/types";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
export interface SelectedMaskPointSelection {
trackId: string;

View File

@ -1,5 +1,5 @@
import type { EffectPass } from "@/effects/types";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
import { BaseNode } from "./base-node";
export type BlurBackgroundNodeParams = {

Some files were not shown because too many files have changed in this diff Show More