diff --git a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx b/apps/web/src/actions/components/shortcuts-dialog.tsx similarity index 94% rename from apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx rename to apps/web/src/actions/components/shortcuts-dialog.tsx index 70802b5b..cd9c0242 100644 --- a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx +++ b/apps/web/src/actions/components/shortcuts-dialog.tsx @@ -5,8 +5,8 @@ import { toast } from "sonner"; import { type KeyboardShortcut, useKeyboardShortcutsHelp, -} from "@/hooks/use-keyboard-shortcuts-help"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; +} from "@/actions/use-keyboard-shortcuts-help"; +import { useKeybindingsStore } from "@/actions/keybindings-store"; import { Button } from "@/components/ui/button"; import { Dialog, diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/actions/definitions.ts similarity index 99% rename from apps/web/src/lib/actions/definitions.ts rename to apps/web/src/actions/definitions.ts index f6846846..6f0ba700 100644 --- a/apps/web/src/lib/actions/definitions.ts +++ b/apps/web/src/actions/definitions.ts @@ -1,7 +1,7 @@ import type { KeybindingConfig, ShortcutKey, -} from "@/lib/actions/keybinding"; +} from "@/actions/keybinding"; import type { TActionWithOptionalArgs } from "./types"; export type TActionCategory = diff --git a/apps/web/src/lib/actions/index.ts b/apps/web/src/actions/index.ts similarity index 100% rename from apps/web/src/lib/actions/index.ts rename to apps/web/src/actions/index.ts diff --git a/apps/web/src/lib/actions/keybinding.ts b/apps/web/src/actions/keybinding.ts similarity index 100% rename from apps/web/src/lib/actions/keybinding.ts rename to apps/web/src/actions/keybinding.ts diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/actions/keybindings-store.ts similarity index 97% rename from apps/web/src/stores/keybindings-store.ts rename to apps/web/src/actions/keybindings-store.ts index ff77e0b4..c0a1a5e2 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/actions/keybindings-store.ts @@ -2,11 +2,11 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import type { TActionWithOptionalArgs } from "@/lib/actions"; -import { getDefaultShortcuts } from "@/lib/actions"; +import type { TActionWithOptionalArgs } from "@/actions"; +import { getDefaultShortcuts } from "@/actions"; import { isTypableDOMElement } from "@/utils/browser"; import { isAppleDevice } from "@/utils/platform"; -import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding"; +import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding"; import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations"; const defaultKeybindings: KeybindingConfig = getDefaultShortcuts(); diff --git a/apps/web/src/stores/keybindings/migrations/index.ts b/apps/web/src/actions/keybindings/migrations/index.ts similarity index 100% rename from apps/web/src/stores/keybindings/migrations/index.ts rename to apps/web/src/actions/keybindings/migrations/index.ts diff --git a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts b/apps/web/src/actions/keybindings/migrations/v2-to-v3.ts similarity index 78% rename from apps/web/src/stores/keybindings/migrations/v2-to-v3.ts rename to apps/web/src/actions/keybindings/migrations/v2-to-v3.ts index 6ebff92c..0171c5a5 100644 --- a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts +++ b/apps/web/src/actions/keybindings/migrations/v2-to-v3.ts @@ -1,5 +1,5 @@ -import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding"; -import type { TActionWithOptionalArgs } from "@/lib/actions"; +import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding"; +import type { TActionWithOptionalArgs } from "@/actions"; interface V2State { keybindings: KeybindingConfig; diff --git a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts b/apps/web/src/actions/keybindings/migrations/v3-to-v4.ts similarity index 70% rename from apps/web/src/stores/keybindings/migrations/v3-to-v4.ts rename to apps/web/src/actions/keybindings/migrations/v3-to-v4.ts index 0f9b35da..8541071b 100644 --- a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts +++ b/apps/web/src/actions/keybindings/migrations/v3-to-v4.ts @@ -1,6 +1,6 @@ -import type { TActionWithOptionalArgs } from "@/lib/actions"; -import type { ShortcutKey } from "@/lib/actions/keybinding"; -import type { KeybindingConfig } from "@/lib/actions/keybinding"; +import type { TActionWithOptionalArgs } from "@/actions"; +import type { ShortcutKey } from "@/actions/keybinding"; +import type { KeybindingConfig } from "@/actions/keybinding"; interface V3State { keybindings: KeybindingConfig; diff --git a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts b/apps/web/src/actions/keybindings/migrations/v4-to-v5.ts similarity index 79% rename from apps/web/src/stores/keybindings/migrations/v4-to-v5.ts rename to apps/web/src/actions/keybindings/migrations/v4-to-v5.ts index 880df1dc..f6937c39 100644 --- a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts +++ b/apps/web/src/actions/keybindings/migrations/v4-to-v5.ts @@ -1,4 +1,4 @@ -import type { KeybindingConfig } from "@/lib/actions/keybinding"; +import type { KeybindingConfig } from "@/actions/keybinding"; interface V4State { keybindings: KeybindingConfig; diff --git a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts b/apps/web/src/actions/keybindings/migrations/v5-to-v6.ts similarity index 80% rename from apps/web/src/stores/keybindings/migrations/v5-to-v6.ts rename to apps/web/src/actions/keybindings/migrations/v5-to-v6.ts index ebabb7c0..09b44307 100644 --- a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts +++ b/apps/web/src/actions/keybindings/migrations/v5-to-v6.ts @@ -1,4 +1,4 @@ -import type { KeybindingConfig } from "@/lib/actions/keybinding"; +import type { KeybindingConfig } from "@/actions/keybinding"; interface V5State { keybindings: KeybindingConfig; diff --git a/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts b/apps/web/src/actions/keybindings/migrations/v6-to-v7.ts similarity index 86% rename from apps/web/src/stores/keybindings/migrations/v6-to-v7.ts rename to apps/web/src/actions/keybindings/migrations/v6-to-v7.ts index 8c5fd96c..46a99ac1 100644 --- a/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts +++ b/apps/web/src/actions/keybindings/migrations/v6-to-v7.ts @@ -1,4 +1,4 @@ -import type { KeybindingConfig } from "@/lib/actions/keybinding"; +import type { KeybindingConfig } from "@/actions/keybinding"; interface V6State { keybindings: KeybindingConfig; diff --git a/apps/web/src/lib/actions/registry.ts b/apps/web/src/actions/registry.ts similarity index 100% rename from apps/web/src/lib/actions/registry.ts rename to apps/web/src/actions/registry.ts diff --git a/apps/web/src/lib/actions/types.ts b/apps/web/src/actions/types.ts similarity index 100% rename from apps/web/src/lib/actions/types.ts rename to apps/web/src/actions/types.ts diff --git a/apps/web/src/hooks/actions/use-action-handler.ts b/apps/web/src/actions/use-action-handler.ts similarity index 93% rename from apps/web/src/hooks/actions/use-action-handler.ts rename to apps/web/src/actions/use-action-handler.ts index 3f6dbd21..a8bb0054 100644 --- a/apps/web/src/hooks/actions/use-action-handler.ts +++ b/apps/web/src/actions/use-action-handler.ts @@ -5,8 +5,8 @@ import type { TActionHandlerOptions, TArgOfAction, TInvocationTrigger, -} from "@/lib/actions"; -import { bindAction, unbindAction } from "@/lib/actions"; +} from "@/actions"; +import { bindAction, unbindAction } from "@/actions"; export function useActionHandler( action: A, diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/actions/use-editor-actions.ts similarity index 93% rename from apps/web/src/hooks/actions/use-editor-actions.ts rename to apps/web/src/actions/use-editor-actions.ts index 873fd33a..e62a8ce0 100644 --- a/apps/web/src/hooks/actions/use-editor-actions.ts +++ b/apps/web/src/actions/use-editor-actions.ts @@ -1,21 +1,21 @@ "use client"; import { useEffect, useRef } from "react"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { useActionHandler } from "@/hooks/actions/use-action-handler"; -import { useEditor } from "../use-editor"; -import { useElementSelection } from "../timeline/element/use-element-selection"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; -import { useKeyframeSelection } from "../timeline/element/use-keyframe-selection"; -import { getElementsAtTime, hasMediaId } from "@/lib/timeline"; -import { cancelInteraction } from "@/lib/cancel-interaction"; -import { invokeAction } from "@/lib/actions"; -import { canToggleSourceAudio } from "@/lib/timeline/audio-separation"; +import { useTimelineStore } from "@/timeline/timeline-store"; +import { useActionHandler } from "@/actions/use-action-handler"; +import { useEditor } from "@/editor/use-editor"; +import { useElementSelection } from "@/timeline/hooks/element/use-element-selection"; +import { TICKS_PER_SECOND } from "@/wasm"; +import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection"; +import { getElementsAtTime, hasMediaId } from "@/timeline"; +import { cancelInteraction } from "@/editor/cancel-interaction"; +import { invokeAction } from "@/actions"; +import { canToggleSourceAudio } from "@/timeline/audio-separation"; import { activateScope, clearActiveScope, type ScopeEntry, -} from "@/lib/selection/scope"; +} from "@/selection/scope"; export function useEditorActions() { const editor = useEditor(); diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/actions/use-keybindings.ts similarity index 93% rename from apps/web/src/hooks/use-keybindings.ts rename to apps/web/src/actions/use-keybindings.ts index 0c524214..c129795c 100644 --- a/apps/web/src/hooks/use-keybindings.ts +++ b/apps/web/src/actions/use-keybindings.ts @@ -1,7 +1,7 @@ import { useEffect } from "react"; -import { invokeAction } from "@/lib/actions"; -import { useEditor } from "@/hooks/use-editor"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; +import { invokeAction } from "@/actions"; +import { useEditor } from "@/editor/use-editor"; +import { useKeybindingsStore } from "@/actions/keybindings-store"; import { isTypableDOMElement } from "@/utils/browser"; /** diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/actions/use-keyboard-shortcuts-help.ts similarity index 93% rename from apps/web/src/hooks/use-keyboard-shortcuts-help.ts rename to apps/web/src/actions/use-keyboard-shortcuts-help.ts index 9a2f6cf5..7681e664 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts +++ b/apps/web/src/actions/use-keyboard-shortcuts-help.ts @@ -1,8 +1,8 @@ "use client"; import { useMemo } from "react"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; -import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions"; +import { useKeybindingsStore } from "@/actions/keybindings-store"; +import { ACTIONS, type TActionWithOptionalArgs } from "@/actions"; import { getPlatformAlternateKey, getPlatformSpecialKey, diff --git a/apps/web/src/lib/animation/__tests__/binding-values.test.ts b/apps/web/src/animation/__tests__/binding-values.test.ts similarity index 91% rename from apps/web/src/lib/animation/__tests__/binding-values.test.ts rename to apps/web/src/animation/__tests__/binding-values.test.ts index c5a354e4..a1f2010c 100644 --- a/apps/web/src/lib/animation/__tests__/binding-values.test.ts +++ b/apps/web/src/animation/__tests__/binding-values.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { composeAnimationValue, createAnimationBinding, -} from "@/lib/animation/binding-values"; +} from "@/animation/binding-values"; describe("binding values", () => { test("formats composed animated colors as hex", () => { diff --git a/apps/web/src/lib/animation/bezier.ts b/apps/web/src/animation/bezier.ts similarity index 96% rename from apps/web/src/lib/animation/bezier.ts rename to apps/web/src/animation/bezier.ts index 5a9758ed..6f9a1d18 100644 --- a/apps/web/src/lib/animation/bezier.ts +++ b/apps/web/src/animation/bezier.ts @@ -1,4 +1,4 @@ -import type { ScalarAnimationKey } from "@/lib/animation/types"; +import type { ScalarAnimationKey } from "@/animation/types"; const BEZIER_SOLVE_ITERATIONS = 20; diff --git a/apps/web/src/lib/animation/binding-values.ts b/apps/web/src/animation/binding-values.ts similarity index 99% rename from apps/web/src/lib/animation/binding-values.ts rename to apps/web/src/animation/binding-values.ts index 66fb2b5d..a46aa9a6 100644 --- a/apps/web/src/lib/animation/binding-values.ts +++ b/apps/web/src/animation/binding-values.ts @@ -12,7 +12,7 @@ import type { DiscreteValue, Vector2AnimationBinding, VectorValue, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { clamp } from "@/utils/math"; interface LinearRgba { diff --git a/apps/web/src/lib/animation/curve-bridge.ts b/apps/web/src/animation/curve-bridge.ts similarity index 97% rename from apps/web/src/lib/animation/curve-bridge.ts rename to apps/web/src/animation/curve-bridge.ts index 385fb448..af411dd1 100644 --- a/apps/web/src/lib/animation/curve-bridge.ts +++ b/apps/web/src/animation/curve-bridge.ts @@ -1,12 +1,12 @@ import { getDefaultLeftHandle, getDefaultRightHandle, -} from "@/lib/animation/bezier"; +} from "@/animation/bezier"; import type { CurveHandle, NormalizedCubicBezier, ScalarAnimationKey, -} from "@/lib/animation/types"; +} from "@/animation/types"; const VALUE_EPSILON = 1e-6; diff --git a/apps/web/src/lib/animation/effect-param-channel.ts b/apps/web/src/animation/effect-param-channel.ts similarity index 94% rename from apps/web/src/lib/animation/effect-param-channel.ts rename to apps/web/src/animation/effect-param-channel.ts index 3080e6ff..c635bb82 100644 --- a/apps/web/src/lib/animation/effect-param-channel.ts +++ b/apps/web/src/animation/effect-param-channel.ts @@ -1,9 +1,9 @@ -import type { ParamValues } from "@/lib/params"; -import type { Effect } from "@/lib/effects/types"; +import type { ParamValues } from "@/params"; +import type { Effect } from "@/effects/types"; import type { ElementAnimations, EffectParamPath, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { removeElementKeyframe } from "./keyframes"; import { resolveAnimationPathValueAtTime } from "./resolve"; diff --git a/apps/web/src/lib/animation/graph-channels.ts b/apps/web/src/animation/graph-channels.ts similarity index 98% rename from apps/web/src/lib/animation/graph-channels.ts rename to apps/web/src/animation/graph-channels.ts index 19d53e27..d6482602 100644 --- a/apps/web/src/lib/animation/graph-channels.ts +++ b/apps/web/src/animation/graph-channels.ts @@ -5,7 +5,7 @@ import type { ScalarAnimationChannel, ScalarGraphChannel, ScalarGraphKeyframeContext, -} from "@/lib/animation/types"; +} from "@/animation/types"; export interface EditableScalarChannels { binding: AnimationBindingInstance; diff --git a/apps/web/src/lib/animation/graphic-param-channel.ts b/apps/web/src/animation/graphic-param-channel.ts similarity index 94% rename from apps/web/src/lib/animation/graphic-param-channel.ts rename to apps/web/src/animation/graphic-param-channel.ts index de595060..ebf0f940 100644 --- a/apps/web/src/lib/animation/graphic-param-channel.ts +++ b/apps/web/src/animation/graphic-param-channel.ts @@ -1,12 +1,12 @@ import type { ElementAnimations, GraphicParamPath, -} from "@/lib/animation/types"; -import type { ParamValues } from "@/lib/params"; +} from "@/animation/types"; +import type { ParamValues } from "@/params"; import { getGraphicDefinition, resolveGraphicParams, -} from "@/lib/graphics"; +} from "@/graphics"; import { resolveAnimationPathValueAtTime } from "./resolve"; export const GRAPHIC_PARAM_PATH_PREFIX = "params."; diff --git a/apps/web/src/lib/animation/index.ts b/apps/web/src/animation/index.ts similarity index 100% rename from apps/web/src/lib/animation/index.ts rename to apps/web/src/animation/index.ts diff --git a/apps/web/src/lib/animation/interpolation.ts b/apps/web/src/animation/interpolation.ts similarity index 99% rename from apps/web/src/lib/animation/interpolation.ts rename to apps/web/src/animation/interpolation.ts index 1a3c0fed..277bfbb7 100644 --- a/apps/web/src/lib/animation/interpolation.ts +++ b/apps/web/src/animation/interpolation.ts @@ -7,7 +7,7 @@ import type { ScalarAnimationChannel, ScalarAnimationKey, ScalarSegmentType, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { clamp } from "@/utils/math"; import { getBezierPoint, diff --git a/apps/web/src/lib/animation/keyframe-query.ts b/apps/web/src/animation/keyframe-query.ts similarity index 99% rename from apps/web/src/lib/animation/keyframe-query.ts rename to apps/web/src/animation/keyframe-query.ts index dbcd041c..231aa5f2 100644 --- a/apps/web/src/lib/animation/keyframe-query.ts +++ b/apps/web/src/animation/keyframe-query.ts @@ -4,7 +4,7 @@ import type { AnimationPath, ElementAnimations, ElementKeyframe, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { type AnimationComponentValue, composeAnimationValue, diff --git a/apps/web/src/lib/animation/keyframes.ts b/apps/web/src/animation/keyframes.ts similarity index 99% rename from apps/web/src/lib/animation/keyframes.ts rename to apps/web/src/animation/keyframes.ts index a5efb6ee..12dc777d 100644 --- a/apps/web/src/lib/animation/keyframes.ts +++ b/apps/web/src/animation/keyframes.ts @@ -13,7 +13,7 @@ import type { ScalarAnimationKey, ScalarCurveKeyframePatch, ScalarSegmentType, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { generateUUID } from "@/utils/id"; import { cloneAnimationBinding, diff --git a/apps/web/src/lib/animation/property-groups.ts b/apps/web/src/animation/property-groups.ts similarity index 86% rename from apps/web/src/lib/animation/property-groups.ts rename to apps/web/src/animation/property-groups.ts index 1e475dac..7b71af15 100644 --- a/apps/web/src/lib/animation/property-groups.ts +++ b/apps/web/src/animation/property-groups.ts @@ -2,8 +2,8 @@ import type { AnimationPropertyGroup, AnimationPropertyPath, ElementAnimations, -} from "@/lib/animation/types"; -import { ANIMATION_PROPERTY_GROUPS } from "@/lib/animation/types"; +} from "@/animation/types"; +import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types"; import { getKeyframeAtTime } from "./keyframe-query"; export interface GroupKeyframeRef { diff --git a/apps/web/src/lib/animation/property-registry.ts b/apps/web/src/animation/property-registry.ts similarity index 96% rename from apps/web/src/lib/animation/property-registry.ts rename to apps/web/src/animation/property-registry.ts index 56340fd8..aacb9972 100644 --- a/apps/web/src/lib/animation/property-registry.ts +++ b/apps/web/src/animation/property-registry.ts @@ -3,20 +3,20 @@ import type { AnimationInterpolation, AnimationPropertyPath, AnimationValue, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { parseColorToLinearRgba } from "./binding-values"; -import type { TimelineElement } from "@/lib/timeline"; -import { MIN_TRANSFORM_SCALE } from "@/lib/animation/transform"; +import type { TimelineElement } from "@/timeline"; +import { MIN_TRANSFORM_SCALE } from "@/animation/transform"; import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN, -} from "@/lib/text/background"; +} from "@/text/background"; import { canElementHaveAudio, isVisualElement, -} from "@/lib/timeline/element-utils"; -import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +} from "@/timeline/element-utils"; +import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants"; +import { DEFAULTS } from "@/timeline/defaults"; import { snapToStep } from "@/utils/math"; export interface NumericSpec { diff --git a/apps/web/src/lib/animation/resolve.ts b/apps/web/src/animation/resolve.ts similarity index 98% rename from apps/web/src/lib/animation/resolve.ts rename to apps/web/src/animation/resolve.ts index 42c1c73c..1b51c126 100644 --- a/apps/web/src/lib/animation/resolve.ts +++ b/apps/web/src/animation/resolve.ts @@ -5,8 +5,8 @@ import type { AnimationPropertyPath, AnimationValueForPath, ElementAnimations, -} from "@/lib/animation/types"; -import type { Transform } from "@/lib/rendering"; +} from "@/animation/types"; +import type { Transform } from "@/rendering"; import { type AnimationComponentValue, composeAnimationValue, diff --git a/apps/web/src/lib/animation/target-resolver.ts b/apps/web/src/animation/target-resolver.ts similarity index 93% rename from apps/web/src/lib/animation/target-resolver.ts rename to apps/web/src/animation/target-resolver.ts index 64bb8709..32e7917c 100644 --- a/apps/web/src/lib/animation/target-resolver.ts +++ b/apps/web/src/animation/target-resolver.ts @@ -3,20 +3,20 @@ import type { AnimationInterpolation, AnimationPath, AnimationValue, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { parseEffectParamPath, isEffectParamPath, -} from "@/lib/animation/effect-param-channel"; +} from "@/animation/effect-param-channel"; import { isGraphicParamPath, parseGraphicParamPath, -} from "@/lib/animation/graphic-param-channel"; -import type { ParamDefinition } from "@/lib/params"; -import { effectsRegistry, registerDefaultEffects } from "@/lib/effects"; -import { getGraphicDefinition } from "@/lib/graphics"; -import type { TimelineElement } from "@/lib/timeline"; -import { isVisualElement } from "@/lib/timeline/element-utils"; +} from "@/animation/graphic-param-channel"; +import type { ParamDefinition } from "@/params"; +import { effectsRegistry, registerDefaultEffects } from "@/effects"; +import { getGraphicDefinition } from "@/graphics"; +import type { TimelineElement } from "@/timeline"; +import { isVisualElement } from "@/timeline/element-utils"; import { snapToStep } from "@/utils/math"; import { coerceAnimationValueForProperty, diff --git a/apps/web/src/animation/timeline-snap-points.ts b/apps/web/src/animation/timeline-snap-points.ts new file mode 100644 index 00000000..f7bf549a --- /dev/null +++ b/apps/web/src/animation/timeline-snap-points.ts @@ -0,0 +1,35 @@ +import { getElementKeyframes } from "@/animation"; +import type { SceneTracks } from "@/timeline"; +import type { SnapPoint } from "@/timeline/snapping"; + +export function getAnimationKeyframeSnapPointsForTimeline({ + tracks, + excludeElementIds, +}: { + tracks: SceneTracks; + excludeElementIds?: Set; +}): SnapPoint[] { + const snapPoints: SnapPoint[] = []; + const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio]; + + for (const track of orderedTracks) { + for (const element of track.elements) { + if (excludeElementIds?.has(element.id)) { + continue; + } + + for (const keyframe of getElementKeyframes({ + animations: element.animations, + })) { + snapPoints.push({ + time: element.startTime + keyframe.time, + type: "keyframe", + elementId: element.id, + trackId: track.id, + }); + } + } + } + + return snapPoints; +} diff --git a/apps/web/src/lib/animation/transform.ts b/apps/web/src/animation/transform.ts similarity index 100% rename from apps/web/src/lib/animation/transform.ts rename to apps/web/src/animation/transform.ts diff --git a/apps/web/src/lib/animation/types.ts b/apps/web/src/animation/types.ts similarity index 99% rename from apps/web/src/lib/animation/types.ts rename to apps/web/src/animation/types.ts index 115b4dbf..3afddfa1 100644 --- a/apps/web/src/lib/animation/types.ts +++ b/apps/web/src/animation/types.ts @@ -1,4 +1,4 @@ -import type { ParamValues } from "@/lib/params"; +import type { ParamValues } from "@/params"; export const ANIMATION_PROPERTY_PATHS = [ "transform.positionX", diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts index 544457e2..edf0d0c2 100644 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@/lib/auth/server"; +import { auth } from "@/auth/server"; import { toNextJsHandler } from "better-auth/next-js"; export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/feedback/route.ts b/apps/web/src/app/api/feedback/route.ts index b13ec2bf..cd853eea 100644 --- a/apps/web/src/app/api/feedback/route.ts +++ b/apps/web/src/app/api/feedback/route.ts @@ -1,7 +1,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { checkRateLimit } from "@/lib/rate-limit"; -import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/lib/feedback"; +import { checkRateLimit } from "@/auth/rate-limit"; +import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/feedback"; const submitSchema = z.object({ message: z diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts index c4d065a9..aa7a1b60 100644 --- a/apps/web/src/app/api/sounds/search/route.ts +++ b/apps/web/src/app/api/sounds/search/route.ts @@ -1,7 +1,7 @@ -import { webEnv } from "@/lib/env/web"; +import { webEnv } from "@/env/web"; import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { checkRateLimit } from "@/lib/rate-limit"; +import { checkRateLimit } from "@/auth/rate-limit"; const searchParamsSchema = z.object({ q: z.string().max(500, "Query too long").optional(), diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index b581de95..dba9b504 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -4,8 +4,8 @@ import { notFound } from "next/navigation"; import { BasePage } from "@/app/base-page"; import Prose from "@/components/ui/prose"; import { Separator } from "@/components/ui/separator"; -import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query"; -import type { Author, Post } from "@/lib/blog/types"; +import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query"; +import type { Author, Post } from "@/blog/types"; type PageProps = { params: Promise<{ slug: string }>; diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx index 9f5a8972..b40bc2fc 100644 --- a/apps/web/src/app/blog/page.tsx +++ b/apps/web/src/app/blog/page.tsx @@ -2,8 +2,8 @@ import type { Metadata } from "next"; import Link from "next/link"; import { BasePage } from "@/app/base-page"; import { Separator } from "@/components/ui/separator"; -import { getPosts } from "@/lib/blog/query"; -import type { Post } from "@/lib/blog/types"; +import { getPosts } from "@/blog/query"; +import type { Post } from "@/blog/types"; export const metadata: Metadata = { title: "Blog - OpenCut", diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx index f069832b..6c529ce1 100644 --- a/apps/web/src/app/changelog/[version]/page.tsx +++ b/apps/web/src/app/changelog/[version]/page.tsx @@ -3,15 +3,15 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { BasePage } from "@/app/base-page"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils"; +import { getReleaseByVersion, getSortedReleases } from "@/changelog/utils"; import { ReleaseArticle, ReleaseMeta, ReleaseTitle, ReleaseDescription, ReleaseChanges, -} from "@/lib/changelog/components/release"; -import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button"; +} from "@/changelog/components/release"; +import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button"; type Props = { params: Promise<{ version: string }> }; diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx index 8440eaa2..25ae25db 100644 --- a/apps/web/src/app/changelog/page.tsx +++ b/apps/web/src/app/changelog/page.tsx @@ -4,14 +4,14 @@ import { Separator } from "@/components/ui/separator"; import { type Release as ReleaseType, getSortedReleases, -} from "@/lib/changelog/utils"; +} from "@/changelog/utils"; import { ReleaseArticle, ReleaseMeta, ReleaseTitle, ReleaseDescription, ReleaseChanges, -} from "@/lib/changelog/components/release"; +} from "@/changelog/components/release"; export const metadata: Metadata = { title: "Changelog - OpenCut", diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx index b15d9f27..f4d13732 100644 --- a/apps/web/src/app/contributors/page.tsx +++ b/apps/web/src/app/contributors/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { GitHubContributeSection } from "@/components/gitHub-contribute-section"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Card, CardContent } from "@/components/ui/card"; -import { EXTERNAL_TOOLS } from "@/lib/site/external-tools"; +import { EXTERNAL_TOOLS } from "@/site/external-tools"; import { BasePage } from "../base-page"; export const metadata: Metadata = { diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index a3a7afa5..b3d40a37 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -8,21 +8,32 @@ import { } from "@/components/ui/resizable"; import { AssetsPanel } from "@/components/editor/panels/assets"; import { PropertiesPanel } from "@/components/editor/panels/properties"; -import { Timeline } from "@/components/editor/panels/timeline"; -import { PreviewPanel } from "@/components/editor/panels/preview"; +import { Timeline } from "@/timeline/components"; +import { PreviewPanel } from "@/preview/components"; import { EditorHeader } from "@/components/editor/editor-header"; import { EditorProvider } from "@/components/providers/editor-provider"; import { Onboarding } from "@/components/editor/onboarding"; -import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog"; -import { usePanelStore } from "@/stores/panel-store"; -import { usePasteMedia } from "@/hooks/use-paste-media"; +import { MigrationDialog } from "@/project/components/migration-dialog"; +import { usePanelStore } from "@/editor/panel-store"; +import { usePasteMedia } from "@/media/use-paste-media"; import { MobileGate } from "@/components/editor/mobile-gate"; -import { useState } from "react"; -import { useEditor } from "@/hooks/use-editor"; +import { useMemo, useState } from "react"; +import { useEditor } from "@/editor/use-editor"; import { Cancel01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Button } from "@/components/ui/button"; -import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification"; +import { ChangelogNotification } from "@/changelog/components/changelog-notification"; +import { + createPreviewOverlayControl, + isPreviewOverlayVisible, + mergePreviewOverlaySources, +} from "@/preview/overlays"; +import { usePreviewStore } from "@/preview/preview-store"; +import { getGuidePreviewOverlaySource } from "@/guides"; +import { + bookmarkNotesPreviewOverlay, + getBookmarkPreviewOverlaySource, +} from "@/timeline/bookmarks/index"; export default function Editor() { const params = useParams(); @@ -70,6 +81,49 @@ function DegradedRendererBanner() { function EditorLayout() { usePasteMedia(); const { panels, setPanel } = usePanelStore(); + const activeScene = useEditor((editor) => + editor.scenes.getActiveSceneOrNull(), + ); + const currentTime = useEditor((editor) => editor.playback.getCurrentTime()); + const activeGuide = usePreviewStore((state) => state.activeGuide); + const overlays = usePreviewStore((state) => state.overlays); + const setOverlayVisibility = usePreviewStore( + (state) => state.setOverlayVisibility, + ); + const showBookmarkNotes = isPreviewOverlayVisible({ + overlay: bookmarkNotesPreviewOverlay, + overlays, + }); + + const overlaySource = useMemo( + () => + mergePreviewOverlaySources({ + sources: [ + getGuidePreviewOverlaySource({ + guideId: activeGuide, + }), + activeScene + ? getBookmarkPreviewOverlaySource({ + bookmarks: activeScene.bookmarks, + time: currentTime, + isVisible: showBookmarkNotes, + }) + : { + definitions: [bookmarkNotesPreviewOverlay], + instances: [], + }, + ], + }), + [activeGuide, activeScene, currentTime, showBookmarkNotes], + ); + + const overlayControls = useMemo( + () => + overlaySource.definitions.map((overlay) => + createPreviewOverlayControl({ overlay, overlays }), + ), + [overlaySource.definitions, overlays], + ); return ( - + diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 6d4878c5..558ae538 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,11 +2,11 @@ import { ThemeProvider } from "next-themes"; import Script from "next/script"; import "./globals.css"; import { Toaster } from "../components/ui/sonner"; -import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification"; +import { ChangelogNotification } from "@/changelog/components/changelog-notification"; import { TooltipProvider } from "../components/ui/tooltip"; import { baseMetaData } from "./metadata"; import { BotIdClient } from "botid/client"; -import { webEnv } from "@/lib/env/web"; +import { webEnv } from "@/env/web"; import { Inter } from "next/font/google"; const siteFont = Inter({ subsets: ["latin"] }); diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts index 06276baa..aa11c43e 100644 --- a/apps/web/src/app/metadata.ts +++ b/apps/web/src/app/metadata.ts @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { SITE_INFO, SITE_URL } from "@/lib/site/brand"; +import { SITE_INFO, SITE_URL } from "@/site/brand"; export const baseMetaData: Metadata = { metadataBase: new URL(SITE_URL), diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 460e8bc3..6084a912 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -2,7 +2,7 @@ import { Hero } from "@/components/landing/hero"; import { Header } from "@/components/header"; import { Footer } from "@/components/footer"; import type { Metadata } from "next"; -import { SITE_URL } from "@/lib/site/brand"; +import { SITE_URL } from "@/site/brand"; export const metadata: Metadata = { alternates: { diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx index 4963e2ab..6362146e 100644 --- a/apps/web/src/app/privacy/page.tsx +++ b/apps/web/src/app/privacy/page.tsx @@ -7,7 +7,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Separator } from "@/components/ui/separator"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { SOCIAL_LINKS } from "@/site/social"; export const metadata: Metadata = { title: "Privacy Policy - OpenCut", diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index e8b09291..c39c4fd9 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -7,20 +7,20 @@ import type { KeyboardEvent, MouseEvent } from "react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import type { EditorCore } from "@/core"; -import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog"; -import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog"; +import { MigrationDialog } from "@/project/components/migration-dialog"; +import { StoragePersistenceDialog } from "@/services/storage/components/storage-persistence-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { useProjectsStore } from "./store"; import type { TProjectMetadata, TProjectSortKey, TProjectSortOption, -} from "@/lib/project/types"; +} from "@/project/types"; import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm"; import { formatDate } from "@/utils/date"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -62,11 +62,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog"; -import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog"; -import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog"; +import { DeleteProjectDialog } from "@/project/components/delete-project-dialog"; +import { ProjectInfoDialog } from "@/project/components/project-info-dialog"; +import { RenameProjectDialog } from "@/project/components/rename-project-dialog"; import { cn } from "@/utils/ui"; -import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification"; +import { ChangelogNotification } from "@/changelog/components/changelog-notification"; const formatProjectDuration = ({ duration, }: { diff --git a/apps/web/src/app/projects/store.ts b/apps/web/src/app/projects/store.ts index 71f89b4a..82d2744e 100644 --- a/apps/web/src/app/projects/store.ts +++ b/apps/web/src/app/projects/store.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import type { TProjectSortKey } from "@/lib/project/types"; +import type { TProjectSortKey } from "@/project/types"; export type ProjectsViewMode = "grid" | "list"; diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts index 38f6f487..1b1ca865 100644 --- a/apps/web/src/app/robots.ts +++ b/apps/web/src/app/robots.ts @@ -1,5 +1,5 @@ import type { MetadataRoute } from "next"; -import { SITE_URL } from "@/lib/site/brand"; +import { SITE_URL } from "@/site/brand"; export default function robots(): MetadataRoute.Robots { return { diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts index 4dfccd59..d4e9570a 100644 --- a/apps/web/src/app/rss.xml/route.ts +++ b/apps/web/src/app/rss.xml/route.ts @@ -1,6 +1,6 @@ import { Feed } from "feed"; -import { getPosts } from "@/lib/blog/query"; -import { SITE_INFO, SITE_URL } from "@/lib/site/brand"; +import { getPosts } from "@/blog/query"; +import { SITE_INFO, SITE_URL } from "@/site/brand"; export async function GET() { try { diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index 7b404642..73b4180f 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -1,5 +1,5 @@ -import { SITE_URL } from "@/lib/site/brand"; -import { getPosts } from "@/lib/blog/query"; +import { SITE_URL } from "@/site/brand"; +import { getPosts } from "@/blog/query"; import type { MetadataRoute } from "next"; export default async function sitemap(): Promise { diff --git a/apps/web/src/app/sponsors/page.tsx b/apps/web/src/app/sponsors/page.tsx index 0de78c85..510419f0 100644 --- a/apps/web/src/app/sponsors/page.tsx +++ b/apps/web/src/app/sponsors/page.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { BasePage } from "@/app/base-page"; import { Card, CardContent } from "@/components/ui/card"; -import { SPONSORS, type Sponsor } from "@/lib/site/sponsors"; +import { SPONSORS, type Sponsor } from "@/site/sponsors"; import { HugeiconsIcon } from "@hugeicons/react"; import { LinkSquare02Icon } from "@hugeicons/core-free-icons"; import { cn } from "@/utils/ui"; diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx index 73a6cce1..0845892c 100644 --- a/apps/web/src/app/terms/page.tsx +++ b/apps/web/src/app/terms/page.tsx @@ -7,7 +7,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Separator } from "@/components/ui/separator"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { SOCIAL_LINKS } from "@/site/social"; export const metadata: Metadata = { title: "Terms of Service - OpenCut", diff --git a/apps/web/src/lib/auth/client.ts b/apps/web/src/auth/client.ts similarity index 80% rename from apps/web/src/lib/auth/client.ts rename to apps/web/src/auth/client.ts index 3bec5072..58be51c3 100644 --- a/apps/web/src/lib/auth/client.ts +++ b/apps/web/src/auth/client.ts @@ -1,5 +1,5 @@ import { createAuthClient } from "better-auth/react"; -import { webEnv } from "@/lib/env/web"; +import { webEnv } from "@/env/web"; export const { signIn, signUp, useSession } = createAuthClient({ baseURL: webEnv.NEXT_PUBLIC_SITE_URL, diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/auth/rate-limit.ts similarity index 93% rename from apps/web/src/lib/rate-limit.ts rename to apps/web/src/auth/rate-limit.ts index 234bf48f..c10c3819 100644 --- a/apps/web/src/lib/rate-limit.ts +++ b/apps/web/src/auth/rate-limit.ts @@ -1,6 +1,6 @@ import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; -import { webEnv } from "@/lib/env/web"; +import { webEnv } from "@/env/web"; const redis = new Redis({ url: webEnv.UPSTASH_REDIS_REST_URL, diff --git a/apps/web/src/lib/auth/server.ts b/apps/web/src/auth/server.ts similarity index 92% rename from apps/web/src/lib/auth/server.ts rename to apps/web/src/auth/server.ts index 39fce38f..5e49387a 100644 --- a/apps/web/src/lib/auth/server.ts +++ b/apps/web/src/auth/server.ts @@ -1,8 +1,8 @@ import { betterAuth, type RateLimit } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { Redis } from "@upstash/redis"; -import { db } from "@/lib/db"; -import { webEnv } from "@/lib/env/web"; +import { db } from "@/db"; +import { webEnv } from "@/env/web"; const redis = new Redis({ url: webEnv.UPSTASH_REDIS_REST_URL, diff --git a/apps/web/src/lib/background/blur.ts b/apps/web/src/background/blur.ts similarity index 100% rename from apps/web/src/lib/background/blur.ts rename to apps/web/src/background/blur.ts diff --git a/apps/web/src/lib/background/color.ts b/apps/web/src/background/color.ts similarity index 100% rename from apps/web/src/lib/background/color.ts rename to apps/web/src/background/color.ts diff --git a/apps/web/src/lib/blog/query.ts b/apps/web/src/blog/query.ts similarity index 94% rename from apps/web/src/lib/blog/query.ts rename to apps/web/src/blog/query.ts index 91c2a514..a7742db2 100644 --- a/apps/web/src/lib/blog/query.ts +++ b/apps/web/src/blog/query.ts @@ -5,7 +5,7 @@ import type { MarblePost, MarblePostList, MarbleTagList, -} from "@/lib/blog/types"; +} from "@/blog/types"; import { unified } from "unified"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; diff --git a/apps/web/src/lib/blog/types.ts b/apps/web/src/blog/types.ts similarity index 100% rename from apps/web/src/lib/blog/types.ts rename to apps/web/src/blog/types.ts diff --git a/apps/web/src/lib/canvas/sizes.ts b/apps/web/src/canvas/sizes.ts similarity index 82% rename from apps/web/src/lib/canvas/sizes.ts rename to apps/web/src/canvas/sizes.ts index dce75bcb..76361336 100644 --- a/apps/web/src/lib/canvas/sizes.ts +++ b/apps/web/src/canvas/sizes.ts @@ -1,4 +1,4 @@ -import type { TCanvasSize } from "@/lib/project/types"; +import type { TCanvasSize } from "@/project/types"; export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [ { width: 1920, height: 1080 }, diff --git a/apps/web/src/lib/changelog/components/changelog-notification.tsx b/apps/web/src/changelog/components/changelog-notification.tsx similarity index 100% rename from apps/web/src/lib/changelog/components/changelog-notification.tsx rename to apps/web/src/changelog/components/changelog-notification.tsx diff --git a/apps/web/src/lib/changelog/components/copy-markdown-button.tsx b/apps/web/src/changelog/components/copy-markdown-button.tsx similarity index 100% rename from apps/web/src/lib/changelog/components/copy-markdown-button.tsx rename to apps/web/src/changelog/components/copy-markdown-button.tsx diff --git a/apps/web/src/lib/changelog/components/release.tsx b/apps/web/src/changelog/components/release.tsx similarity index 100% rename from apps/web/src/lib/changelog/components/release.tsx rename to apps/web/src/changelog/components/release.tsx diff --git a/apps/web/src/lib/changelog/entries/0.1.0.md b/apps/web/src/changelog/entries/0.1.0.md similarity index 100% rename from apps/web/src/lib/changelog/entries/0.1.0.md rename to apps/web/src/changelog/entries/0.1.0.md diff --git a/apps/web/src/lib/changelog/entries/0.2.0.md b/apps/web/src/changelog/entries/0.2.0.md similarity index 100% rename from apps/web/src/lib/changelog/entries/0.2.0.md rename to apps/web/src/changelog/entries/0.2.0.md diff --git a/apps/web/src/lib/changelog/entries/0.3.0.md b/apps/web/src/changelog/entries/0.3.0.md similarity index 100% rename from apps/web/src/lib/changelog/entries/0.3.0.md rename to apps/web/src/changelog/entries/0.3.0.md diff --git a/apps/web/src/lib/changelog/entries/0.4.0.md b/apps/web/src/changelog/entries/0.4.0.md similarity index 100% rename from apps/web/src/lib/changelog/entries/0.4.0.md rename to apps/web/src/changelog/entries/0.4.0.md diff --git a/apps/web/src/lib/changelog/utils.ts b/apps/web/src/changelog/utils.ts similarity index 100% rename from apps/web/src/lib/changelog/utils.ts rename to apps/web/src/changelog/utils.ts diff --git a/apps/web/src/lib/clipboard/handlers/elements.ts b/apps/web/src/clipboard/handlers/elements.ts similarity index 94% rename from apps/web/src/lib/clipboard/handlers/elements.ts rename to apps/web/src/clipboard/handlers/elements.ts index 1b58ac78..b2f5f2c3 100644 --- a/apps/web/src/lib/clipboard/handlers/elements.ts +++ b/apps/web/src/clipboard/handlers/elements.ts @@ -1,4 +1,4 @@ -import { PasteCommand } from "@/lib/commands/timeline"; +import { PasteCommand } from "@/commands/timeline"; import type { ClipboardHandler } from "../types"; export const ElementsClipboardHandler = { diff --git a/apps/web/src/lib/clipboard/handlers/index.ts b/apps/web/src/clipboard/handlers/index.ts similarity index 100% rename from apps/web/src/lib/clipboard/handlers/index.ts rename to apps/web/src/clipboard/handlers/index.ts diff --git a/apps/web/src/lib/clipboard/handlers/keyframes.ts b/apps/web/src/clipboard/handlers/keyframes.ts similarity index 93% rename from apps/web/src/lib/clipboard/handlers/keyframes.ts rename to apps/web/src/clipboard/handlers/keyframes.ts index b746c179..ad156e12 100644 --- a/apps/web/src/lib/clipboard/handlers/keyframes.ts +++ b/apps/web/src/clipboard/handlers/keyframes.ts @@ -1,7 +1,7 @@ -import { getKeyframeById } from "@/lib/animation"; -import type { SelectedKeyframeRef } from "@/lib/animation/types"; -import type { TimelineElement } from "@/lib/timeline"; -import { PasteKeyframesCommand } from "@/lib/commands/timeline"; +import { getKeyframeById } from "@/animation"; +import type { SelectedKeyframeRef } from "@/animation/types"; +import type { TimelineElement } from "@/timeline"; +import { PasteKeyframesCommand } from "@/commands/timeline"; import type { ClipboardHandler, KeyframeClipboardCurvePatch, diff --git a/apps/web/src/lib/clipboard/index.ts b/apps/web/src/clipboard/index.ts similarity index 100% rename from apps/web/src/lib/clipboard/index.ts rename to apps/web/src/clipboard/index.ts diff --git a/apps/web/src/lib/clipboard/types.ts b/apps/web/src/clipboard/types.ts similarity index 93% rename from apps/web/src/lib/clipboard/types.ts rename to apps/web/src/clipboard/types.ts index 41e107ef..0d249c9d 100644 --- a/apps/web/src/lib/clipboard/types.ts +++ b/apps/web/src/clipboard/types.ts @@ -5,13 +5,13 @@ import type { AnimationValue, ScalarCurveKeyframePatch, SelectedKeyframeRef, -} from "@/lib/animation/types"; -import type { Command } from "@/lib/commands/base-command"; +} from "@/animation/types"; +import type { Command } from "@/commands/base-command"; import type { CreateTimelineElement, ElementRef, TrackType, -} from "@/lib/timeline"; +} from "@/timeline"; export interface ElementClipboardItem { trackId: string; diff --git a/apps/web/src/lib/commands/base-command.ts b/apps/web/src/commands/base-command.ts similarity index 80% rename from apps/web/src/lib/commands/base-command.ts rename to apps/web/src/commands/base-command.ts index b244f2f3..2ab9af09 100644 --- a/apps/web/src/lib/commands/base-command.ts +++ b/apps/web/src/commands/base-command.ts @@ -1,5 +1,5 @@ -import type { EditorSelectionPatch } from "@/lib/selection/editor-selection"; -import type { ElementRef } from "@/lib/timeline/types"; +import type { EditorSelectionPatch } from "@/selection/editor-selection"; +import type { ElementRef } from "@/timeline/types"; export interface CommandResult { selection?: EditorSelectionPatch; diff --git a/apps/web/src/lib/commands/batch-command.ts b/apps/web/src/commands/batch-command.ts similarity index 100% rename from apps/web/src/lib/commands/batch-command.ts rename to apps/web/src/commands/batch-command.ts diff --git a/apps/web/src/lib/commands/index.ts b/apps/web/src/commands/index.ts similarity index 100% rename from apps/web/src/lib/commands/index.ts rename to apps/web/src/commands/index.ts diff --git a/apps/web/src/lib/commands/media/add-media-asset.ts b/apps/web/src/commands/media/add-media-asset.ts similarity index 91% rename from apps/web/src/lib/commands/media/add-media-asset.ts rename to apps/web/src/commands/media/add-media-asset.ts index 12e0906e..d22241a2 100644 --- a/apps/web/src/lib/commands/media/add-media-asset.ts +++ b/apps/web/src/commands/media/add-media-asset.ts @@ -1,13 +1,13 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; import { toast } from "sonner"; -import type { MediaAsset } from "@/lib/media/types"; +import type { MediaAsset } from "@/media/types"; import { generateUUID } from "@/utils/id"; import { storageService } from "@/services/storage/service"; import type { FrameRate } from "opencut-wasm"; -import { hasMediaId } from "@/lib/timeline/element-utils"; -import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils"; -import { UpdateProjectSettingsCommand } from "@/lib/commands/project"; +import { hasMediaId } from "@/timeline/element-utils"; +import { frameRatesEqual, getHighestImportedVideoFps } from "@/fps/utils"; +import { UpdateProjectSettingsCommand } from "@/commands/project"; export class AddMediaAssetCommand extends Command { private assetId: string; diff --git a/apps/web/src/lib/commands/media/index.ts b/apps/web/src/commands/media/index.ts similarity index 100% rename from apps/web/src/lib/commands/media/index.ts rename to apps/web/src/commands/media/index.ts diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/commands/media/remove-media-asset.ts similarity index 89% rename from apps/web/src/lib/commands/media/remove-media-asset.ts rename to apps/web/src/commands/media/remove-media-asset.ts index 85d187d9..8da62181 100644 --- a/apps/web/src/lib/commands/media/remove-media-asset.ts +++ b/apps/web/src/commands/media/remove-media-asset.ts @@ -1,12 +1,12 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { MediaAsset } from "@/lib/media/types"; -import { buildWaveformSourceKey } from "@/lib/media/waveform-summary"; +import type { MediaAsset } from "@/media/types"; +import { buildWaveformSourceKey } from "@/media/waveform-summary"; import { storageService } from "@/services/storage/service"; import { videoCache } from "@/services/video-cache/service"; import { waveformCache } from "@/services/waveform-cache/service"; -import { hasMediaId } from "@/lib/timeline/element-utils"; -import type { SceneTracks } from "@/lib/timeline"; +import { hasMediaId } from "@/timeline/element-utils"; +import type { SceneTracks } from "@/timeline"; export class RemoveMediaAssetCommand extends Command { private savedAssets: MediaAsset[] | null = null; diff --git a/apps/web/src/lib/commands/preview-tracker.ts b/apps/web/src/commands/preview-tracker.ts similarity index 100% rename from apps/web/src/lib/commands/preview-tracker.ts rename to apps/web/src/commands/preview-tracker.ts diff --git a/apps/web/src/lib/commands/project/index.ts b/apps/web/src/commands/project/index.ts similarity index 100% rename from apps/web/src/lib/commands/project/index.ts rename to apps/web/src/commands/project/index.ts diff --git a/apps/web/src/lib/commands/project/update-project-settings.ts b/apps/web/src/commands/project/update-project-settings.ts similarity index 89% rename from apps/web/src/lib/commands/project/update-project-settings.ts rename to apps/web/src/commands/project/update-project-settings.ts index 32258e12..8d653db2 100644 --- a/apps/web/src/lib/commands/project/update-project-settings.ts +++ b/apps/web/src/commands/project/update-project-settings.ts @@ -1,6 +1,6 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TProject, TProjectSettings } from "@/lib/project/types"; +import type { TProject, TProjectSettings } from "@/project/types"; export class UpdateProjectSettingsCommand extends Command { private savedSettings: TProjectSettings | null = null; diff --git a/apps/web/src/lib/commands/scene/create-scene.ts b/apps/web/src/commands/scene/create-scene.ts similarity index 83% rename from apps/web/src/lib/commands/scene/create-scene.ts rename to apps/web/src/commands/scene/create-scene.ts index 1cc72c0f..b554fe90 100644 --- a/apps/web/src/lib/commands/scene/create-scene.ts +++ b/apps/web/src/commands/scene/create-scene.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { buildDefaultScene } from "@/lib/scenes"; +import type { TScene } from "@/timeline"; +import { buildDefaultScene } from "@/timeline/scenes"; export class CreateSceneCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/delete-scene.ts b/apps/web/src/commands/scene/delete-scene.ts similarity index 88% rename from apps/web/src/lib/commands/scene/delete-scene.ts rename to apps/web/src/commands/scene/delete-scene.ts index ab269b12..92de518f 100644 --- a/apps/web/src/lib/commands/scene/delete-scene.ts +++ b/apps/web/src/commands/scene/delete-scene.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes"; +import type { TScene } from "@/timeline"; +import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes"; export class DeleteSceneCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/index.ts b/apps/web/src/commands/scene/index.ts similarity index 100% rename from apps/web/src/lib/commands/scene/index.ts rename to apps/web/src/commands/scene/index.ts diff --git a/apps/web/src/lib/commands/scene/move-bookmark.ts b/apps/web/src/commands/scene/move-bookmark.ts similarity index 83% rename from apps/web/src/lib/commands/scene/move-bookmark.ts rename to apps/web/src/commands/scene/move-bookmark.ts index 103caa74..a1624eb0 100644 --- a/apps/web/src/lib/commands/scene/move-bookmark.ts +++ b/apps/web/src/commands/scene/move-bookmark.ts @@ -1,8 +1,8 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks"; +import type { TScene } from "@/timeline"; +import { updateSceneInArray } from "@/timeline/scenes"; +import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index"; export class MoveBookmarkCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/remove-bookmark.ts b/apps/web/src/commands/scene/remove-bookmark.ts similarity index 85% rename from apps/web/src/lib/commands/scene/remove-bookmark.ts rename to apps/web/src/commands/scene/remove-bookmark.ts index 5eac920f..ea592556 100644 --- a/apps/web/src/lib/commands/scene/remove-bookmark.ts +++ b/apps/web/src/commands/scene/remove-bookmark.ts @@ -1,11 +1,11 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; +import type { TScene } from "@/timeline"; +import { updateSceneInArray } from "@/timeline/scenes"; import { getFrameTime, removeBookmarkFromArray, -} from "@/lib/timeline/bookmarks"; +} from "@/timeline/bookmarks/index"; export class RemoveBookmarkCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/rename-scene.ts b/apps/web/src/commands/scene/rename-scene.ts similarity index 85% rename from apps/web/src/lib/commands/scene/rename-scene.ts rename to apps/web/src/commands/scene/rename-scene.ts index ccdc85d6..19d6bfd8 100644 --- a/apps/web/src/lib/commands/scene/rename-scene.ts +++ b/apps/web/src/commands/scene/rename-scene.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; +import type { TScene } from "@/timeline"; +import { updateSceneInArray } from "@/timeline/scenes"; export class RenameSceneCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/toggle-bookmark.ts b/apps/web/src/commands/scene/toggle-bookmark.ts similarity index 81% rename from apps/web/src/lib/commands/scene/toggle-bookmark.ts rename to apps/web/src/commands/scene/toggle-bookmark.ts index cb4fa655..4193c76e 100644 --- a/apps/web/src/lib/commands/scene/toggle-bookmark.ts +++ b/apps/web/src/commands/scene/toggle-bookmark.ts @@ -1,8 +1,11 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks"; +import type { TScene } from "@/timeline"; +import { updateSceneInArray } from "@/timeline/scenes"; +import { + getFrameTime, + toggleBookmarkInArray, +} from "@/timeline/bookmarks/index"; export class ToggleBookmarkCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/scene/update-bookmark.ts b/apps/web/src/commands/scene/update-bookmark.ts similarity index 81% rename from apps/web/src/lib/commands/scene/update-bookmark.ts rename to apps/web/src/commands/scene/update-bookmark.ts index 1d1c7283..0d6a0193 100644 --- a/apps/web/src/lib/commands/scene/update-bookmark.ts +++ b/apps/web/src/commands/scene/update-bookmark.ts @@ -1,8 +1,11 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { Bookmark, TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks"; +import type { Bookmark, TScene } from "@/timeline"; +import { updateSceneInArray } from "@/timeline/scenes"; +import { + getFrameTime, + updateBookmarkInArray, +} from "@/timeline/bookmarks/index"; export class UpdateBookmarkCommand extends Command { private savedScenes: TScene[] | null = null; diff --git a/apps/web/src/lib/commands/timeline/clipboard/index.ts b/apps/web/src/commands/timeline/clipboard/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/clipboard/index.ts rename to apps/web/src/commands/timeline/clipboard/index.ts diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts b/apps/web/src/commands/timeline/clipboard/paste-keyframes.ts similarity index 91% rename from apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts rename to apps/web/src/commands/timeline/clipboard/paste-keyframes.ts index 210afbd3..e05b49fa 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts +++ b/apps/web/src/commands/timeline/clipboard/paste-keyframes.ts @@ -4,11 +4,11 @@ import { resolveAnimationTarget, updateScalarKeyframeCurve, upsertPathKeyframe, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { KeyframeClipboardItem } from "@/lib/clipboard"; -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; -import { updateElementInSceneTracks } from "@/lib/timeline"; +} from "@/animation"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { KeyframeClipboardItem } from "@/clipboard"; +import type { SceneTracks, TimelineElement } from "@/timeline"; +import { updateElementInSceneTracks } from "@/timeline"; import { generateUUID } from "@/utils/id"; function pasteKeyframesIntoElement({ diff --git a/apps/web/src/commands/timeline/clipboard/paste.ts b/apps/web/src/commands/timeline/clipboard/paste.ts new file mode 100644 index 00000000..4523566e --- /dev/null +++ b/apps/web/src/commands/timeline/clipboard/paste.ts @@ -0,0 +1,193 @@ +import { + Command, + createElementSelectionResult, + type CommandResult, +} from "@/commands/base-command"; +import { EditorCore } from "@/core"; +import type { SceneTracks, TimelineElement } from "@/timeline"; +import type { ElementClipboardItem } from "@/clipboard"; +import { generateUUID } from "@/utils/id"; +import { + applyPlacement, + resolveTrackPlacement, + enforceMainTrackStart, +} from "@/timeline/placement"; +import { cloneAnimations } from "@/animation"; + +export class PasteCommand extends Command { + private savedState: SceneTracks | null = null; + private pastedElements: { trackId: string; elementId: string }[] = []; + private readonly time: number; + private readonly clipboardItems: ElementClipboardItem[]; + + constructor({ + time, + clipboardItems, + }: { + time: number; + clipboardItems: ElementClipboardItem[]; + }) { + super(); + this.time = time; + this.clipboardItems = clipboardItems; + } + + execute(): CommandResult | undefined { + if (this.clipboardItems.length === 0) return undefined; + + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + this.pastedElements = []; + + const minStart = Math.min( + ...this.clipboardItems.map((item) => item.element.startTime), + ); + + let updatedTracks = this.savedState; + const itemsByTrackId = groupClipboardItemsByTrackId({ + clipboardItems: this.clipboardItems, + }); + + for (const [trackId, items] of itemsByTrackId) { + const elementsToAdd = buildPastedElements({ + items, + minStart, + time: this.time, + }); + + if (elementsToAdd.length === 0) { + continue; + } + + const trackType = items[0].trackType; + const sourceTrackIndex = [ + ...updatedTracks.overlay, + updatedTracks.main, + ...updatedTracks.audio, + ].findIndex((track) => track.id === trackId); + const placementResult = resolveTrackPlacement({ + tracks: updatedTracks, + trackType, + timeSpans: elementsToAdd.map((element) => ({ + startTime: element.startTime, + duration: element.duration, + })), + strategy: { type: "aboveSource", sourceTrackIndex }, + }); + if (!placementResult) { + continue; + } + + let elementsForPlacement = elementsToAdd; + if (placementResult.kind === "existingTrack") { + const targetTrack = + placementResult.trackIndex < updatedTracks.overlay.length + ? updatedTracks.overlay[placementResult.trackIndex] + : placementResult.trackIndex === updatedTracks.overlay.length + ? updatedTracks.main + : updatedTracks.audio[ + placementResult.trackIndex - updatedTracks.overlay.length - 1 + ]; + if (targetTrack?.id === updatedTracks.main.id) { + const earliestElement = elementsToAdd.reduce((earliest, element) => + element.startTime < earliest.startTime ? element : earliest, + ); + const adjustedEarliestStartTime = enforceMainTrackStart({ + tracks: updatedTracks, + targetTrackId: targetTrack.id, + requestedStartTime: earliestElement.startTime, + }); + const delta = adjustedEarliestStartTime - earliestElement.startTime; + + if (delta !== 0) { + elementsForPlacement = elementsToAdd.map((element) => ({ + ...element, + startTime: Math.max(0, element.startTime + delta), + })); + } + } + } + + const applied = applyPlacement({ + tracks: updatedTracks, + placementResult, + elements: elementsForPlacement, + }); + if (!applied) { + continue; + } + + updatedTracks = applied.updatedTracks; + + for (const element of elementsForPlacement) { + this.pastedElements.push({ + trackId: applied.targetTrackId, + elementId: element.id, + }); + } + } + + editor.timeline.updateTracks(updatedTracks); + + if (this.pastedElements.length > 0) { + return createElementSelectionResult(this.pastedElements); + } + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } + + getPastedElements(): { trackId: string; elementId: string }[] { + return this.pastedElements; + } +} + +function groupClipboardItemsByTrackId({ + clipboardItems, +}: { + clipboardItems: ElementClipboardItem[]; +}): Map { + const groupedItems = new Map(); + + for (const item of clipboardItems) { + const existingItems = groupedItems.get(item.trackId) ?? []; + groupedItems.set(item.trackId, [...existingItems, item]); + } + + return groupedItems; +} + +function buildPastedElements({ + items, + minStart, + time, +}: { + items: ElementClipboardItem[]; + minStart: number; + time: number; +}): TimelineElement[] { + const elementsToAdd: TimelineElement[] = []; + + for (const item of items) { + const relativeOffset = item.element.startTime - minStart; + const startTime = Math.max(0, time + relativeOffset); + const newElementId = generateUUID(); + + elementsToAdd.push({ + ...item.element, + id: newElementId, + startTime, + animations: cloneAnimations({ + animations: item.element.animations, + shouldRegenerateKeyframeIds: true, + }), + } as TimelineElement); + } + + return elementsToAdd; +} diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/commands/timeline/element/delete-elements.ts similarity index 90% rename from apps/web/src/lib/commands/timeline/element/delete-elements.ts rename to apps/web/src/commands/timeline/element/delete-elements.ts index 0fc7948f..ad0267b7 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/commands/timeline/element/delete-elements.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks } from "@/timeline"; import { EditorCore } from "@/core"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { TimelineTrack } from "@/timeline"; function removeTrackElements({ track, diff --git a/apps/web/src/commands/timeline/element/duplicate-elements.ts b/apps/web/src/commands/timeline/element/duplicate-elements.ts new file mode 100644 index 00000000..8f65f7c7 --- /dev/null +++ b/apps/web/src/commands/timeline/element/duplicate-elements.ts @@ -0,0 +1,134 @@ +import { + Command, + createElementSelectionResult, + type CommandResult, +} from "@/commands/base-command"; +import type { SceneTracks, TimelineElement } from "@/timeline"; +import { generateUUID } from "@/utils/id"; +import { EditorCore } from "@/core"; +import { applyPlacement, resolveTrackPlacement } from "@/timeline/placement"; +import { cloneAnimations } from "@/animation"; + +interface DuplicateElementsParams { + elements: { trackId: string; elementId: string }[]; +} + +export class DuplicateElementsCommand extends Command { + private duplicatedElements: { trackId: string; elementId: string }[] = []; + private savedState: SceneTracks | null = null; + private elements: DuplicateElementsParams["elements"]; + + constructor({ elements }: DuplicateElementsParams) { + super(); + this.elements = elements; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + this.duplicatedElements = []; + + let updatedTracks = this.savedState; + + for (const track of [ + ...this.savedState.overlay, + this.savedState.main, + ...this.savedState.audio, + ]) { + const elementsToDuplicate = this.elements.filter( + (elementEntry) => elementEntry.trackId === track.id, + ); + + if (elementsToDuplicate.length === 0) { + continue; + } + + const elementIdsToDuplicate = new Set( + elementsToDuplicate.map((element) => element.elementId), + ); + const newTrackElements: TimelineElement[] = []; + + for (const element of track.elements) { + if (!elementIdsToDuplicate.has(element.id)) { + continue; + } + + const newId = generateUUID(); + newTrackElements.push( + buildDuplicateElement({ + element, + id: newId, + startTime: element.startTime, + }), + ); + } + + const placementResult = resolveTrackPlacement({ + tracks: updatedTracks, + trackType: track.type, + timeSpans: [], + strategy: { type: "alwaysNew", position: "highest" }, + }); + if (!placementResult || placementResult.kind !== "newTrack") { + continue; + } + + const applied = applyPlacement({ + tracks: updatedTracks, + placementResult, + elements: newTrackElements, + }); + if (!applied) { + continue; + } + + updatedTracks = applied.updatedTracks; + + for (const element of newTrackElements) { + this.duplicatedElements.push({ + trackId: applied.targetTrackId, + elementId: element.id, + }); + } + } + + editor.timeline.updateTracks(updatedTracks); + + if (this.duplicatedElements.length > 0) { + return createElementSelectionResult(this.duplicatedElements); + } + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } + + getDuplicatedElements(): { trackId: string; elementId: string }[] { + return this.duplicatedElements; + } +} + +function buildDuplicateElement({ + element, + id, + startTime, +}: { + element: TimelineElement; + id: string; + startTime: number; +}): TimelineElement { + return { + ...element, + id, + name: `${element.name} (copy)`, + startTime, + animations: cloneAnimations({ + animations: element.animations, + shouldRegenerateKeyframeIds: true, + }), + }; +} diff --git a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts b/apps/web/src/commands/timeline/element/effects/add-effect.ts similarity index 85% rename from apps/web/src/lib/commands/timeline/element/effects/add-effect.ts rename to apps/web/src/commands/timeline/element/effects/add-effect.ts index 902ca341..be08b433 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts +++ b/apps/web/src/commands/timeline/element/effects/add-effect.ts @@ -1,8 +1,8 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; -import { buildDefaultEffectInstance } from "@/lib/effects"; +import { isVisualElement, updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks, VisualElement } from "@/timeline"; +import { buildDefaultEffectInstance } from "@/effects"; function addEffectToElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/effects/index.ts b/apps/web/src/commands/timeline/element/effects/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/element/effects/index.ts rename to apps/web/src/commands/timeline/element/effects/index.ts diff --git a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts b/apps/web/src/commands/timeline/element/effects/remove-effect.ts similarity index 86% rename from apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts rename to apps/web/src/commands/timeline/element/effects/remove-effect.ts index f453f2cc..e6ef9934 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts +++ b/apps/web/src/commands/timeline/element/effects/remove-effect.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks, VisualElement } from "@/timeline"; function removeEffectFromElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts b/apps/web/src/commands/timeline/element/effects/reorder-effect.ts similarity index 87% rename from apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts rename to apps/web/src/commands/timeline/element/effects/reorder-effect.ts index 28b7c4e1..554804e2 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts +++ b/apps/web/src/commands/timeline/element/effects/reorder-effect.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks, VisualElement } from "@/timeline"; function reorderEffectsOnElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts b/apps/web/src/commands/timeline/element/effects/toggle-effect.ts similarity index 87% rename from apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts rename to apps/web/src/commands/timeline/element/effects/toggle-effect.ts index a703cd94..89149a79 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts +++ b/apps/web/src/commands/timeline/element/effects/toggle-effect.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks, VisualElement } from "@/timeline"; export function toggleEffectOnElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts b/apps/web/src/commands/timeline/element/effects/update-effect-params.ts similarity index 87% rename from apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts rename to apps/web/src/commands/timeline/element/effects/update-effect-params.ts index 5e75c1f0..62b3c937 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts +++ b/apps/web/src/commands/timeline/element/effects/update-effect-params.ts @@ -1,8 +1,8 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { ParamValues } from "@/lib/params"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/timeline"; +import type { ParamValues } from "@/params"; +import type { SceneTracks, VisualElement } from "@/timeline"; function updateEffectParamsOnElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/commands/timeline/element/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/element/index.ts rename to apps/web/src/commands/timeline/element/index.ts diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/commands/timeline/element/insert-element.ts similarity index 93% rename from apps/web/src/lib/commands/timeline/element/insert-element.ts rename to apps/web/src/commands/timeline/element/insert-element.ts index ed05b8c6..49ac6f2d 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/commands/timeline/element/insert-element.ts @@ -1,23 +1,23 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; import type { CreateTimelineElement, SceneTracks, TimelineElement, TrackType, -} from "@/lib/timeline"; +} from "@/timeline"; import { generateUUID } from "@/utils/id"; -import { requiresMediaId } from "@/lib/timeline/element-utils"; -import type { MediaAsset } from "@/lib/media/types"; -import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation"; -import { floatToFrameRate } from "@/lib/fps/utils"; -import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; +import { requiresMediaId } from "@/timeline/element-utils"; +import type { MediaAsset } from "@/media/types"; +import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation"; +import { floatToFrameRate } from "@/fps/utils"; +import { graphicsRegistry, registerDefaultGraphics } from "@/graphics"; import { applyPlacement, canElementGoOnTrack, resolveTrackPlacement, validateElementTrackCompatibility, -} from "@/lib/timeline/placement"; +} from "@/timeline/placement"; type InsertElementPlacement = | { mode: "explicit"; trackId: string } diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/index.ts b/apps/web/src/commands/timeline/element/keyframes/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/element/keyframes/index.ts rename to apps/web/src/commands/timeline/element/keyframes/index.ts diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts similarity index 81% rename from apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts rename to apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts index b1ef4aef..c049d5bb 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts +++ b/apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts @@ -1,9 +1,9 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { SceneTracks } from "@/lib/timeline"; +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"; export class RemoveEffectParamKeyframeCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts similarity index 88% rename from apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts rename to apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts index 3c3d1c6b..ed917406 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ b/apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts @@ -3,11 +3,11 @@ import { hasKeyframesForPath, removeElementKeyframe, resolveAnimationTarget, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +} from "@/animation"; +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"; function removeKeyframeAndPersist({ element, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts similarity index 86% rename from apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts rename to apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts index ad7f5651..ccbdbc5d 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts +++ b/apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts @@ -1,9 +1,9 @@ import { EditorCore } from "@/core"; -import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { AnimationPath } from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; +import { resolveAnimationTarget, 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"; export class RetimeKeyframeCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts b/apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts similarity index 88% rename from apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts rename to apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts index 9a4c2304..4a3db39d 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts +++ b/apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts @@ -2,14 +2,14 @@ import { EditorCore } from "@/core"; import { resolveAnimationTarget, updateScalarKeyframeCurve, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; +} from "@/animation"; +import { Command, type CommandResult } from "@/commands/base-command"; +import { updateElementInSceneTracks } from "@/timeline"; import type { AnimationPath, ScalarCurveKeyframePatch, -} from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; +} from "@/animation/types"; +import type { SceneTracks } from "@/timeline"; export class UpdateScalarKeyframeCurveCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts similarity index 87% rename from apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts rename to apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts index f436721a..eae8f039 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts +++ b/apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts @@ -1,14 +1,14 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { buildEffectParamPath, resolveAnimationTarget, upsertPathKeyframe, -} from "@/lib/animation"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { AnimationInterpolation } from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; +} from "@/animation"; +import { updateElementInSceneTracks } from "@/timeline"; +import { isVisualElement } from "@/timeline/element-utils"; +import type { AnimationInterpolation } from "@/animation/types"; +import type { SceneTracks } from "@/timeline"; export class UpsertEffectParamKeyframeCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts similarity index 88% rename from apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts rename to apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts index f77e69b8..30514146 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts +++ b/apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts @@ -1,13 +1,13 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import { resolveAnimationTarget, upsertPathKeyframe } from "@/animation"; +import { updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks } from "@/timeline"; import type { AnimationPath, AnimationInterpolation, AnimationValue, -} from "@/lib/animation/types"; +} from "@/animation/types"; export class UpsertKeyframeCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts b/apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts similarity index 91% rename from apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts rename to apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts index 346eb8d9..1445b1f8 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts +++ b/apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts @@ -1,12 +1,12 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { getCustomMaskClosedStateAfterPointRemoval, removeCustomMaskPoints, -} from "@/lib/masks/custom-path"; -import type { CustomMask } from "@/lib/masks/types"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { MaskableElement, SceneTracks } from "@/lib/timeline"; +} from "@/masks/custom-path"; +import type { CustomMask } from "@/masks/types"; +import { isMaskableElement, updateElementInSceneTracks } from "@/timeline"; +import type { MaskableElement, SceneTracks } from "@/timeline"; function deletePointsFromCustomMask({ mask, diff --git a/apps/web/src/lib/commands/timeline/element/masks/index.ts b/apps/web/src/commands/timeline/element/masks/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/element/masks/index.ts rename to apps/web/src/commands/timeline/element/masks/index.ts diff --git a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts b/apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts similarity index 90% rename from apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts rename to apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts index 9f837836..58559af6 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts +++ b/apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts @@ -1,10 +1,10 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { insertPointOnCustomMaskSegment } from "@/lib/masks/definitions/custom"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import type { CustomMask } from "@/lib/masks/types"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { MaskableElement, SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +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"; function insertPointIntoCustomMask({ mask, diff --git a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts b/apps/web/src/commands/timeline/element/masks/remove-mask.ts similarity index 90% rename from apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts rename to apps/web/src/commands/timeline/element/masks/remove-mask.ts index 9f6c5f77..b543bce1 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts +++ b/apps/web/src/commands/timeline/element/masks/remove-mask.ts @@ -1,7 +1,7 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, MaskableElement } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import { isMaskableElement, updateElementInSceneTracks } from "@/timeline"; +import type { SceneTracks, MaskableElement } from "@/timeline"; function removeMaskFromElement({ element, diff --git a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts b/apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts similarity index 89% rename from apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts rename to apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts index 2b5c1694..fec9b923 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts +++ b/apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts @@ -1,8 +1,8 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { Mask } from "@/lib/masks/types"; -import type { SceneTracks, MaskableElement } from "@/lib/timeline"; +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"; export function toggleMaskInvertedOnElement({ element, diff --git a/apps/web/src/commands/timeline/element/move-elements.ts b/apps/web/src/commands/timeline/element/move-elements.ts new file mode 100644 index 00000000..3ed31d42 --- /dev/null +++ b/apps/web/src/commands/timeline/element/move-elements.ts @@ -0,0 +1,183 @@ +import { + Command, + createElementSelectionResult, + type CommandResult, +} from "@/commands/base-command"; +import { EditorCore } from "@/core"; +import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline"; +import { + buildEmptyTrack, + validateElementTrackCompatibility, +} from "@/timeline/placement"; +import type { + PlannedElementMove, + PlannedTrackCreation, +} from "@/timeline/group-move"; +import { findTrackInSceneTracks } from "@/timeline/track-element-update"; + +export class MoveElementCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor({ + moves, + createTracks = [], + }: { + moves: PlannedElementMove[]; + createTracks?: PlannedTrackCreation[]; + }) { + super(); + this.moves = moves; + this.createTracks = createTracks; + } + + private readonly moves: PlannedElementMove[]; + private readonly createTracks: PlannedTrackCreation[]; + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + + let tracksToUpdate = this.savedState; + for (const createTrack of [...this.createTracks].sort( + (firstTrack, secondTrack) => firstTrack.index - secondTrack.index, + )) { + tracksToUpdate = insertTrackAtDisplayIndex({ + tracks: tracksToUpdate, + track: buildEmptyTrack({ + id: createTrack.id, + type: createTrack.type, + }), + insertIndex: createTrack.index, + }); + } + + const movedElementsById = new Map(); + for (const move of this.moves) { + const sourceTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: move.sourceTrackId, + }); + const sourceElement = sourceTrack?.elements.find( + (trackElement) => trackElement.id === move.elementId, + ); + if (!sourceTrack || !sourceElement) { + throw new Error("Source track or element not found"); + } + + const targetTrack = findTrackInSceneTracks({ + tracks: tracksToUpdate, + trackId: move.targetTrackId, + }); + if (!targetTrack) { + throw new Error("Target track not found"); + } + + const validation = validateElementTrackCompatibility({ + element: sourceElement, + track: targetTrack, + }); + if (!validation.isValid) { + throw new Error(validation.errorMessage); + } + + movedElementsById.set(move.elementId, { + ...sourceElement, + startTime: move.newStartTime, + }); + } + + const movedElementIds = new Set(this.moves.map((move) => move.elementId)); + const movedElementsByTargetTrackId = new Map(); + for (const move of this.moves) { + const movedElement = movedElementsById.get(move.elementId); + if (!movedElement) { + continue; + } + + const nextTargetElements = + movedElementsByTargetTrackId.get(move.targetTrackId) ?? []; + nextTargetElements.push(movedElement); + movedElementsByTargetTrackId.set(move.targetTrackId, nextTargetElements); + } + + const updatedTracks = mapSceneTracks({ + tracks: tracksToUpdate, + update: (track) => ({ + ...track, + elements: [ + ...track.elements.filter( + (element) => !movedElementIds.has(element.id), + ), + ...(movedElementsByTargetTrackId.get(track.id) ?? []), + ], + }), + }); + + editor.timeline.updateTracks(updatedTracks); + return createElementSelectionResult( + this.moves.map(({ elementId, targetTrackId }) => ({ + trackId: targetTrackId, + elementId, + })), + ); + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} + +function mapSceneTracks({ + tracks, + update, +}: { + tracks: SceneTracks; + update: (track: TTrack) => TTrack; +}): SceneTracks { + return { + overlay: tracks.overlay.map((track) => update(track)), + main: update(tracks.main), + audio: tracks.audio.map((track) => update(track)), + }; +} + +function insertTrackAtDisplayIndex({ + tracks, + track, + insertIndex, +}: { + tracks: SceneTracks; + track: TimelineTrack; + insertIndex: number; +}): SceneTracks { + if (track.type === "audio") { + const audioInsertIndex = Math.max( + 0, + Math.min(insertIndex - tracks.overlay.length - 1, tracks.audio.length), + ); + return { + ...tracks, + audio: [ + ...tracks.audio.slice(0, audioInsertIndex), + track, + ...tracks.audio.slice(audioInsertIndex), + ], + }; + } + + const overlayInsertIndex = Math.max( + 0, + Math.min(insertIndex, tracks.overlay.length), + ); + return { + ...tracks, + overlay: [ + ...tracks.overlay.slice(0, overlayInsertIndex), + track, + ...tracks.overlay.slice(overlayInsertIndex), + ], + }; +} diff --git a/apps/web/src/commands/timeline/element/split-elements.ts b/apps/web/src/commands/timeline/element/split-elements.ts new file mode 100644 index 00000000..11c317f4 --- /dev/null +++ b/apps/web/src/commands/timeline/element/split-elements.ts @@ -0,0 +1,182 @@ +import { + Command, + createElementSelectionResult, + type CommandResult, +} from "@/commands/base-command"; +import type { SceneTracks, TimelineElement } from "@/timeline"; +import { generateUUID } from "@/utils/id"; +import { EditorCore } from "@/core"; +import { isRetimableElement } from "@/timeline"; +import { splitAnimationsAtTime } from "@/animation"; +import { getSourceSpanAtClipTime } from "@/retime"; + +export class SplitElementsCommand extends Command { + private savedState: SceneTracks | null = null; + private rightSideElements: { trackId: string; elementId: string }[] = []; + private readonly elements: { trackId: string; elementId: string }[]; + private readonly splitTime: number; + private readonly retainSide: "both" | "left" | "right"; + + constructor({ + elements, + splitTime, + retainSide = "both", + }: { + elements: { trackId: string; elementId: string }[]; + splitTime: number; + retainSide?: "both" | "left" | "right"; + }) { + super(); + this.elements = elements; + this.splitTime = splitTime; + this.retainSide = retainSide; + } + + getRightSideElements(): { trackId: string; elementId: string }[] { + return this.rightSideElements; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + this.rightSideElements = []; + + const splitTrack = < + TTrack extends { id: string; elements: TimelineElement[] }, + >( + track: TTrack, + ): TTrack => { + const elementsToSplit = this.elements.filter( + (target) => target.trackId === track.id, + ); + + if (elementsToSplit.length === 0) { + return track; + } + + const elements = track.elements.flatMap((element) => { + const shouldSplit = elementsToSplit.some( + (target) => target.elementId === element.id, + ); + + if (!shouldSplit) { + return [element]; + } + + const effectiveStart = element.startTime; + const effectiveEnd = element.startTime + element.duration; + + if ( + this.splitTime <= effectiveStart || + this.splitTime >= effectiveEnd + ) { + return [element]; + } + + const relativeTime = this.splitTime - element.startTime; + const leftVisibleDuration = relativeTime; + const rightVisibleDuration = element.duration - relativeTime; + const retimeRef = isRetimableElement(element) + ? element.retime + : undefined; + const leftSourceSpan = getSourceSpanAtClipTime({ + clipTime: leftVisibleDuration, + retime: retimeRef, + }); + const totalSourceSpan = getSourceSpanAtClipTime({ + clipTime: element.duration, + retime: retimeRef, + }); + const rightSourceSpan = totalSourceSpan - leftSourceSpan; + const { leftAnimations, rightAnimations } = splitAnimationsAtTime({ + animations: element.animations, + splitTime: relativeTime, + shouldIncludeSplitBoundary: true, + }); + let splitResult: TimelineElement[]; + + if (this.retainSide === "left") { + splitResult = [ + { + ...element, + duration: leftVisibleDuration, + trimEnd: element.trimEnd + rightSourceSpan, + name: `${element.name} (left)`, + animations: leftAnimations, + ...(retimeRef !== undefined ? { retime: retimeRef } : {}), + }, + ]; + } else if (this.retainSide === "right") { + const newId = generateUUID(); + this.rightSideElements.push({ + trackId: track.id, + elementId: newId, + }); + splitResult = [ + { + ...element, + id: newId, + startTime: this.splitTime, + duration: rightVisibleDuration, + trimStart: element.trimStart + leftSourceSpan, + name: `${element.name} (right)`, + animations: rightAnimations, + ...(retimeRef !== undefined ? { retime: retimeRef } : {}), + }, + ]; + } else { + // "both" - split into two pieces + const secondElementId = generateUUID(); + this.rightSideElements.push({ + trackId: track.id, + elementId: secondElementId, + }); + splitResult = [ + { + ...element, + duration: leftVisibleDuration, + trimEnd: element.trimEnd + rightSourceSpan, + name: `${element.name} (left)`, + animations: leftAnimations, + ...(retimeRef !== undefined ? { retime: retimeRef } : {}), + }, + { + ...element, + id: secondElementId, + startTime: this.splitTime, + duration: rightVisibleDuration, + trimStart: element.trimStart + leftSourceSpan, + name: `${element.name} (right)`, + animations: rightAnimations, + ...(retimeRef !== undefined ? { retime: retimeRef } : {}), + }, + ]; + } + + return splitResult; + }); + + return { ...track, elements } as TTrack; + }; + + const updatedTracks: SceneTracks = { + overlay: this.savedState.overlay.map((track) => splitTrack(track)), + main: splitTrack(this.savedState.main), + audio: this.savedState.audio.map((track) => splitTrack(track)), + }; + + editor.timeline.updateTracks(updatedTracks); + + if (this.rightSideElements.length > 0) { + return createElementSelectionResult(this.rightSideElements); + } + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts similarity index 92% rename from apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts rename to apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts index 7300f540..36d4cd87 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts +++ b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts @@ -1,20 +1,20 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { buildSeparatedAudioElement, canExtractSourceAudio, isSourceAudioSeparated, -} from "@/lib/timeline/audio-separation"; +} from "@/timeline/audio-separation"; import { applyPlacement, resolveTrackPlacement, -} from "@/lib/timeline/placement"; -import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update"; +} from "@/timeline/placement"; +import { updateElementInSceneTracks } from "@/timeline/track-element-update"; import type { SceneTracks, TimelineElement, VideoElement, -} from "@/lib/timeline/types"; +} from "@/timeline/types"; import { generateUUID } from "@/utils/id"; export class ToggleSourceAudioSeparationCommand extends Command { diff --git a/apps/web/src/lib/commands/timeline/element/update-elements.ts b/apps/web/src/commands/timeline/element/update-elements.ts similarity index 87% rename from apps/web/src/lib/commands/timeline/element/update-elements.ts rename to apps/web/src/commands/timeline/element/update-elements.ts index 54d1ca88..f03f2498 100644 --- a/apps/web/src/lib/commands/timeline/element/update-elements.ts +++ b/apps/web/src/commands/timeline/element/update-elements.ts @@ -1,11 +1,11 @@ import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks, TimelineElement } from "@/timeline"; import { findTrackInSceneTracks, updateElementInSceneTracks, -} from "@/lib/timeline"; -import { applyElementUpdate } from "@/lib/timeline/update-pipeline"; +} from "@/timeline"; +import { applyElementUpdate } from "@/timeline/update-pipeline"; export class UpdateElementsCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/commands/timeline/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/index.ts rename to apps/web/src/commands/timeline/index.ts diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/commands/timeline/track/add-track.ts similarity index 93% rename from apps/web/src/lib/commands/timeline/track/add-track.ts rename to apps/web/src/commands/timeline/track/add-track.ts index a577c693..6b58b687 100644 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ b/apps/web/src/commands/timeline/track/add-track.ts @@ -1,11 +1,11 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks, TrackType } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks, TrackType } from "@/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { buildEmptyTrack, getDefaultInsertIndexForTrack, -} from "@/lib/timeline/placement"; +} from "@/timeline/placement"; export class AddTrackCommand extends Command { private trackId: string; diff --git a/apps/web/src/lib/commands/timeline/track/index.ts b/apps/web/src/commands/timeline/track/index.ts similarity index 100% rename from apps/web/src/lib/commands/timeline/track/index.ts rename to apps/web/src/commands/timeline/track/index.ts diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/commands/timeline/track/remove-track.ts similarity index 86% rename from apps/web/src/lib/commands/timeline/track/remove-track.ts rename to apps/web/src/commands/timeline/track/remove-track.ts index 5fcaa898..13a9ff4e 100644 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ b/apps/web/src/commands/timeline/track/remove-track.ts @@ -1,6 +1,6 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/commands/base-command"; import { EditorCore } from "@/core"; -import type { SceneTracks } from "@/lib/timeline"; +import type { SceneTracks } from "@/timeline"; export class RemoveTrackCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/commands/timeline/track/toggle-track-mute.ts similarity index 85% rename from apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts rename to apps/web/src/commands/timeline/track/toggle-track-mute.ts index 7ffe2fd7..0d1f41cd 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts +++ b/apps/web/src/commands/timeline/track/toggle-track-mute.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks } from "@/timeline"; import { EditorCore } from "@/core"; -import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; +import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline"; export class ToggleTrackMuteCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/commands/timeline/track/toggle-track-visibility.ts similarity index 85% rename from apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts rename to apps/web/src/commands/timeline/track/toggle-track-visibility.ts index 2292c83a..f4160e3f 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts +++ b/apps/web/src/commands/timeline/track/toggle-track-visibility.ts @@ -1,7 +1,7 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks } from "@/timeline"; import { EditorCore } from "@/core"; -import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; +import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline"; export class ToggleTrackVisibilityCommand extends Command { private savedState: SceneTracks | null = null; diff --git a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts b/apps/web/src/commands/timeline/tracks-snapshot.ts similarity index 75% rename from apps/web/src/lib/commands/timeline/tracks-snapshot.ts rename to apps/web/src/commands/timeline/tracks-snapshot.ts index daed6a9e..5b6d871a 100644 --- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts +++ b/apps/web/src/commands/timeline/tracks-snapshot.ts @@ -1,5 +1,5 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; +import { Command, type CommandResult } from "@/commands/base-command"; +import type { SceneTracks } from "@/timeline"; import { EditorCore } from "@/core"; export class TracksSnapshotCommand extends Command { diff --git a/apps/web/src/components/editor/editor-header.tsx b/apps/web/src/components/editor/editor-header.tsx index 44e8af68..6404dc01 100644 --- a/apps/web/src/components/editor/editor-header.tsx +++ b/apps/web/src/components/editor/editor-header.tsx @@ -10,20 +10,20 @@ import { DropdownMenuTrigger, } from "../ui/dropdown-menu"; import Link from "next/link"; -import { RenameProjectDialog } from "./dialogs/rename-project-dialog"; -import { DeleteProjectDialog } from "./dialogs/delete-project-dialog"; +import { RenameProjectDialog } from "@/project/components/rename-project-dialog"; +import { DeleteProjectDialog } from "@/project/components/delete-project-dialog"; import { useRouter } from "next/navigation"; import { FaDiscord } from "react-icons/fa6"; import { ExportButton } from "./export-button"; -import { FeedbackPopover } from "@/lib/feedback/components/feedback-popover"; +import { FeedbackPopover } from "@/feedback/components/feedback-popover"; import { ThemeToggle } from "../theme-toggle"; -import { DEFAULT_LOGO_URL } from "@/lib/site/brand"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { DEFAULT_LOGO_URL } from "@/site/brand"; +import { SOCIAL_LINKS } from "@/site/social"; import { toast } from "sonner"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { CommandIcon, Logout05Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { ShortcutsDialog } from "./dialogs/shortcuts-dialog"; +import { ShortcutsDialog } from "@/actions/components/shortcuts-dialog"; import Image from "next/image"; import { cn } from "@/utils/ui"; diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx index e66067da..e738fb68 100644 --- a/apps/web/src/components/editor/export-button.tsx +++ b/apps/web/src/components/editor/export-button.tsx @@ -18,22 +18,22 @@ import { getExportMimeType, getExportFileExtension, downloadBuffer, -} from "@/lib/export"; +} from "@/export"; import { Check, Copy, Download, RotateCcw } from "lucide-react"; import { EXPORT_FORMAT_VALUES, EXPORT_QUALITY_VALUES, type ExportFormat, type ExportQuality, -} from "@/lib/export"; +} from "@/export"; import { Section, SectionContent, SectionHeader, SectionTitle, } from "@/components/section"; -import { useEditor } from "@/hooks/use-editor"; -import { DEFAULT_EXPORT_OPTIONS } from "@/lib/export/defaults"; +import { useEditor } from "@/editor/use-editor"; +import { DEFAULT_EXPORT_OPTIONS } from "@/export/defaults"; function isExportFormat(value: string): value is ExportFormat { return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value); diff --git a/apps/web/src/components/editor/onboarding.tsx b/apps/web/src/components/editor/onboarding.tsx index 54196c76..bc5533ce 100644 --- a/apps/web/src/components/editor/onboarding.tsx +++ b/apps/web/src/components/editor/onboarding.tsx @@ -3,8 +3,8 @@ import { ArrowRightIcon } from "lucide-react"; import { useState } from "react"; import ReactMarkdown from "react-markdown"; -import { SOCIAL_LINKS } from "@/lib/site/social"; -import { useLocalStorage } from "@/hooks/storage/use-local-storage"; +import { SOCIAL_LINKS } from "@/site/social"; +import { useLocalStorage } from "@/services/storage/use-local-storage"; import { Button } from "../ui/button"; import { Dialog, DialogBody, DialogContent, DialogTitle } from "../ui/dialog"; diff --git a/apps/web/src/stores/assets-panel-store.tsx b/apps/web/src/components/editor/panels/assets/assets-panel-store.tsx similarity index 100% rename from apps/web/src/stores/assets-panel-store.tsx rename to apps/web/src/components/editor/panels/assets/assets-panel-store.tsx diff --git a/apps/web/src/components/editor/panels/assets/draggable-item.tsx b/apps/web/src/components/editor/panels/assets/draggable-item.tsx index 166c81db..2f8bc64a 100644 --- a/apps/web/src/components/editor/panels/assets/draggable-item.tsx +++ b/apps/web/src/components/editor/panels/assets/draggable-item.tsx @@ -10,9 +10,9 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useEditor } from "@/hooks/use-editor"; -import { clearDragData, setDragData } from "@/lib/drag-data"; -import type { TimelineDragData } from "@/lib/timeline/drag"; +import { useEditor } from "@/editor/use-editor"; +import { clearDragData, setDragData } from "@/timeline/drag-data"; +import type { TimelineDragData } from "@/timeline/drag"; import { cn } from "@/utils/ui"; export interface DraggableItemProps { diff --git a/apps/web/src/components/editor/panels/assets/index.tsx b/apps/web/src/components/editor/panels/assets/index.tsx index 1cdbf295..bae73299 100644 --- a/apps/web/src/components/editor/panels/assets/index.tsx +++ b/apps/web/src/components/editor/panels/assets/index.tsx @@ -1,15 +1,15 @@ "use client"; import { Separator } from "@/components/ui/separator"; -import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store"; +import { type Tab, useAssetsPanelStore } from "@/components/editor/panels/assets/assets-panel-store"; import { TabBar } from "./tabbar"; -import { Captions } from "./views/captions"; +import { Captions } from "@/subtitles/components/assets-view"; import { MediaView } from "./views/assets"; import { SettingsView } from "./views/settings"; -import { SoundsView } from "./views/sounds"; -import { StickersView } from "./views/stickers"; -import { TextView } from "./views/text"; -import { EffectsView } from "./views/effects"; +import { SoundsView } from "@/sounds/components/assets-view"; +import { StickersView } from "@/stickers/components/assets-view"; +import { TextView } from "@/text/components/assets-view"; +import { EffectsView } from "@/effects/components/assets-view"; export function AssetsPanel() { const { activeTab } = useAssetsPanelStore(); diff --git a/apps/web/src/components/editor/panels/assets/tabbar.tsx b/apps/web/src/components/editor/panels/assets/tabbar.tsx index da53bed0..3b084c0d 100644 --- a/apps/web/src/components/editor/panels/assets/tabbar.tsx +++ b/apps/web/src/components/editor/panels/assets/tabbar.tsx @@ -12,7 +12,7 @@ import { TAB_KEYS, tabs, useAssetsPanelStore, -} from "@/stores/assets-panel-store"; +} from "@/components/editor/panels/assets/assets-panel-store"; export function TabBar() { const { activeTab, setActiveTab } = useAssetsPanelStore(); diff --git a/apps/web/src/components/editor/panels/assets/views/assets.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx index 33b47c67..4afd22b7 100644 --- a/apps/web/src/components/editor/panels/assets/views/assets.tsx +++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx @@ -25,28 +25,28 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; -import { useEditor } from "@/hooks/use-editor"; -import { useFileUpload } from "@/hooks/use-file-upload"; -import { invokeAction } from "@/lib/actions"; -import { processMediaAssets } from "@/lib/media/processing"; -import { showMediaUploadToast } from "@/lib/media/upload-toast"; +import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation"; +import { TICKS_PER_SECOND } from "@/wasm"; +import { useEditor } from "@/editor/use-editor"; +import { useFileUpload } from "@/media/use-file-upload"; +import { invokeAction } from "@/actions"; +import { processMediaAssets } from "@/media/processing"; +import { showMediaUploadToast } from "@/media/upload-toast"; import { SelectableItem, SelectableSurface, useSelection, useSelectionScope, -} from "@/lib/selection"; -import { buildElementFromMedia } from "@/lib/timeline/element-utils"; +} from "@/selection"; +import { buildElementFromMedia } from "@/timeline/element-utils"; import { type MediaSortKey, type MediaSortOrder, type MediaViewMode, useAssetsPanelStore, -} from "@/stores/assets-panel-store"; -import { MASKABLE_ELEMENT_TYPES } from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; +} from "@/components/editor/panels/assets/assets-panel-store"; +import { MASKABLE_ELEMENT_TYPES } from "@/timeline"; +import type { MediaAsset } from "@/media/types"; import { cn } from "@/utils/ui"; import { CloudUploadIcon, diff --git a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx index 099e335a..9ce34ebb 100644 --- a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx +++ b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx @@ -12,12 +12,12 @@ import { Popover, PopoverTrigger } from "@/components/ui/popover"; import { BACKGROUND_BLUR_INTENSITY_PRESETS, DEFAULT_BACKGROUND_BLUR_INTENSITY, -} from "@/lib/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; +} from "@/background/blur"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; import { patternCraftGradients } from "@/data/colors/pattern-craft"; import { colors } from "@/data/colors/solid"; import { syntaxUIGradients } from "@/data/colors/syntax-ui"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { effectPreviewService } from "@/services/renderer/effect-preview"; import { cn } from "@/utils/ui"; diff --git a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx index 4d87493a..bf3a1c16 100644 --- a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx +++ b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx @@ -9,9 +9,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { FPS_PRESETS } from "@/lib/fps/presets"; -import { floatToFrameRate, frameRateToFloat } from "@/lib/fps/utils"; -import { useEditor } from "@/hooks/use-editor"; +import { FPS_PRESETS } from "@/fps/presets"; +import { floatToFrameRate, frameRateToFloat } from "@/fps/utils"; +import { useEditor } from "@/editor/use-editor"; import { Section, SectionContent, @@ -22,7 +22,7 @@ import { BackgroundContent } from "./background"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { NumberField } from "@/components/ui/number-field"; -import { useEditorStore } from "@/stores/editor-store"; +import { useEditorStore } from "@/editor/editor-store"; import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; import { HugeiconsIcon } from "@hugeicons/react"; import { Tick02Icon } from "@hugeicons/core-free-icons"; @@ -30,7 +30,7 @@ import { cn } from "@/utils/ui"; import { dimensionToAspectRatio } from "@/utils/geometry"; import { formatNumberForDisplay } from "@/utils/math"; import { OcSquarePlusIcon } from "@/components/icons"; -import type { TCanvasSize } from "@/lib/project/types"; +import type { TCanvasSize } from "@/project/types"; type SettingsView = "project-info" | "background"; diff --git a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx b/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx deleted file mode 100644 index 3d96f762..00000000 --- a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import { useEditor } from "@/hooks/use-editor"; -import { findCurrentScene } from "@/lib/scenes"; -import { getBookmarksActiveAtTime } from "@/lib/timeline/bookmarks"; - -export function BookmarkNoteOverlay() { - const editor = useEditor(); - const currentTime = editor.playback.getCurrentTime(); - const activeProject = editor.project.getActive(); - - if (!activeProject) { - return null; - } - - const activeScene = findCurrentScene({ - scenes: activeProject.scenes, - currentSceneId: activeProject.currentSceneId, - }); - - if (!activeScene) { - return null; - } - - const bookmarks = activeScene.bookmarks; - const activeBookmarks = getBookmarksActiveAtTime({ - bookmarks, - time: currentTime, - }); - const bookmarksWithNotes = activeBookmarks.filter( - (bookmark) => bookmark.note != null && bookmark.note.trim() !== "", - ); - - if (bookmarksWithNotes.length === 0) { - return null; - } - - return ( -
- {bookmarksWithNotes.map((bookmark) => ( -
- {bookmark.note} -
- ))} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx b/apps/web/src/components/editor/panels/preview/guide-overlay.tsx deleted file mode 100644 index 4270fcb3..00000000 --- a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { getGuideById } from "@/lib/guides"; -import { usePreviewStore } from "@/stores/preview-store"; -import { usePreviewViewport } from "./preview-viewport"; - -export function GuideOverlay() { - const activeGuide = usePreviewStore((state) => state.activeGuide); - const viewport = usePreviewViewport(); - const guide = getGuideById(activeGuide); - - if (!guide) { - return null; - } - - return ( -
- {guide.renderOverlay({ - width: viewport.sceneWidth, - height: viewport.sceneHeight, - })} -
- ); -} diff --git a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx index 9cc2b02e..c9a9c22e 100644 --- a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx +++ b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx @@ -1,6 +1,6 @@ "use client"; -import type { ParamDefinition, NumberParamDefinition } from "@/lib/params"; +import type { ParamDefinition, NumberParamDefinition } from "@/params"; import { formatNumberForDisplay, getFractionDigitsForStep, diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts index 27300784..967156b3 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts @@ -1,5 +1,5 @@ -import { useEditor } from "@/hooks/use-editor"; -import { getElementLocalTime } from "@/lib/animation"; +import { useEditor } from "@/editor/use-editor"; +import { getElementLocalTime } from "@/animation"; export function useElementPlayhead({ startTime, diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts index a42e422f..734b514f 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts @@ -1,11 +1,11 @@ -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { getKeyframeAtTime, hasKeyframesForPath, upsertElementKeyframe, -} from "@/lib/animation"; -import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types"; -import type { TimelineElement } from "@/lib/timeline"; +} from "@/animation"; +import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types"; +import type { TimelineElement } from "@/timeline"; export function useKeyframedColorProperty({ trackId, diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts index 872d55c0..9dd39768 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts @@ -1,11 +1,11 @@ -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { getKeyframeAtTime, hasKeyframesForPath, upsertElementKeyframe, -} from "@/lib/animation"; -import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types"; -import type { TimelineElement } from "@/lib/timeline"; +} from "@/animation"; +import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types"; +import type { TimelineElement } from "@/timeline"; import { snapToStep } from "@/utils/math"; import { usePropertyDraft } from "./use-property-draft"; diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts index 29b290d9..b0b6f01a 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts @@ -1,6 +1,6 @@ "use client"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { buildGraphicParamPath, coerceAnimationValueForParam, @@ -9,12 +9,12 @@ import { getParamValueKind, hasKeyframesForPath, upsertPathKeyframe, -} from "@/lib/animation"; +} from "@/animation"; import type { ElementAnimations, -} from "@/lib/animation/types"; -import type { ParamDefinition } from "@/lib/params"; -import type { TimelineElement } from "@/lib/timeline"; +} from "@/animation/types"; +import type { ParamDefinition } from "@/params"; +import type { TimelineElement } from "@/timeline"; export interface KeyframedParamPropertyResult { hasAnimatedKeyframes: boolean; diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx index 75319ef3..52fa42b5 100644 --- a/apps/web/src/components/editor/panels/properties/index.tsx +++ b/apps/web/src/components/editor/panels/properties/index.tsx @@ -8,8 +8,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useEditor } from "@/hooks/use-editor"; -import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; +import { useEditor } from "@/editor/use-editor"; +import { useElementSelection } from "@/timeline/hooks/element/use-element-selection"; import { usePropertiesStore } from "./stores/properties-store"; import { getPropertiesConfig } from "./registry"; import { cn } from "@/utils/ui"; diff --git a/apps/web/src/components/editor/panels/properties/registry.tsx b/apps/web/src/components/editor/panels/properties/registry.tsx index df6c0e52..f9e40ab3 100644 --- a/apps/web/src/components/editor/panels/properties/registry.tsx +++ b/apps/web/src/components/editor/panels/properties/registry.tsx @@ -11,8 +11,8 @@ import type { VideoElement, AudioElement, TimelineElement, -} from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; +} from "@/timeline"; +import type { MediaAsset } from "@/media/types"; import { HugeiconsIcon } from "@hugeicons/react"; import { TextFontIcon, @@ -22,14 +22,14 @@ import { MagicWand05Icon, DashboardSpeed02Icon, } from "@hugeicons/core-free-icons"; -import { TransformTab } from "./tabs/transform-tab"; -import { BlendingTab } from "./tabs/blending-tab"; -import { AudioTab } from "./tabs/audio-tab"; -import { TextTab } from "./tabs/text-tab"; -import { ClipEffectsTab, StandaloneEffectTab } from "./tabs/effects-tab"; -import { MasksTab } from "./tabs/masks-tab"; -import { SpeedTab } from "./tabs/speed-tab"; -import { GraphicTab } from "./tabs/graphic-tab"; +import { TransformTab } from "@/rendering/components/transform-tab"; +import { BlendingTab } from "@/rendering/components/blending-tab"; +import { AudioTab } from "@/timeline/components/audio-tab"; +import { TextTab } from "@/text/components/text-tab"; +import { ClipEffectsTab, StandaloneEffectTab } from "@/effects/components/effects-tab"; +import { MasksTab } from "@/masks/components/masks-tab"; +import { SpeedTab } from "@/speed/components/speed-tab"; +import { GraphicTab } from "@/graphics/components/graphic-tab"; import { OcShapesIcon } from "@/components/icons"; export type TabContentProps = { diff --git a/apps/web/src/components/editor/scenes-view.tsx b/apps/web/src/components/editor/scenes-view.tsx index 34efeaff..b0f487a4 100644 --- a/apps/web/src/components/editor/scenes-view.tsx +++ b/apps/web/src/components/editor/scenes-view.tsx @@ -21,9 +21,9 @@ import { DialogFooter, DialogTrigger, } from "@/components/ui/dialog"; -import { canDeleteScene, getMainScene } from "@/lib/scenes"; +import { canDeleteScene, getMainScene } from "@/timeline/scenes"; import { toast } from "sonner"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; export function ScenesView({ children }: { children: React.ReactNode }) { const editor = useEditor(); diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 21334267..b56f8c6c 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -2,8 +2,8 @@ import Link from "next/link"; import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri"; import { FaGithub } from "react-icons/fa6"; import Image from "next/image"; -import { DEFAULT_LOGO_URL } from "@/lib/site/brand"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { DEFAULT_LOGO_URL } from "@/site/brand"; +import { SOCIAL_LINKS } from "@/site/social"; import { capitalizeFirstLetter } from "@/utils/string"; type Category = "resources" | "company"; diff --git a/apps/web/src/components/gitHub-contribute-section.tsx b/apps/web/src/components/gitHub-contribute-section.tsx index 0239e366..6978b81a 100644 --- a/apps/web/src/components/gitHub-contribute-section.tsx +++ b/apps/web/src/components/gitHub-contribute-section.tsx @@ -1,6 +1,6 @@ import { Button } from "./ui/button"; import Link from "next/link"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { SOCIAL_LINKS } from "@/site/social"; import { GithubIcon, Link04Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 66fec0ea..99ba1730 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -16,8 +16,8 @@ import { } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { cn } from "@/utils/ui"; -import { DEFAULT_LOGO_URL, SITE_URL } from "@/lib/site/brand"; -import { SOCIAL_LINKS } from "@/lib/site/social"; +import { DEFAULT_LOGO_URL, SITE_URL } from "@/site/brand"; +import { SOCIAL_LINKS } from "@/site/social"; import { ContextMenu, ContextMenuContent, diff --git a/apps/web/src/components/providers/editor-provider.tsx b/apps/web/src/components/providers/editor-provider.tsx index 98e6d4c6..fdc84af0 100644 --- a/apps/web/src/components/providers/editor-provider.tsx +++ b/apps/web/src/components/providers/editor-provider.tsx @@ -4,12 +4,12 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { EditorCore } from "@/core"; -import { useEditor } from "@/hooks/use-editor"; -import { useKeybindingsListener } from "@/hooks/use-keybindings"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { useEditorActions } from "@/hooks/actions/use-editor-actions"; -import { loadFontAtlas } from "@/lib/fonts/google-fonts"; +import { useEditor } from "@/editor/use-editor"; +import { useKeybindingsListener } from "@/actions/use-keybindings"; +import { useKeybindingsStore } from "@/actions/keybindings-store"; +import { useTimelineStore } from "@/timeline/timeline-store"; +import { useEditorActions } from "@/actions/use-editor-actions"; +import { loadFontAtlas } from "@/fonts/google-fonts"; import { initializeGpuRenderer, isGpuAvailable, diff --git a/apps/web/src/components/storage-provider.tsx b/apps/web/src/components/storage-provider.tsx index fc7ae121..185a44ef 100644 --- a/apps/web/src/components/storage-provider.tsx +++ b/apps/web/src/components/storage-provider.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { storageService } from "@/services/storage/service"; interface StorageContextType { diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx index 297d58a7..27ea4bdb 100644 --- a/apps/web/src/components/ui/font-picker.tsx +++ b/apps/web/src/components/ui/font-picker.tsx @@ -9,10 +9,10 @@ import { } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { loadFullFont } from "@/lib/fonts/google-fonts"; -import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts"; -import type { FontAtlas, FontAtlasEntry } from "@/lib/fonts/types"; -import { useFontAtlas } from "@/hooks/use-font-atlas"; +import { loadFullFont } from "@/fonts/google-fonts"; +import { SYSTEM_FONTS } from "@/fonts/system-fonts"; +import type { FontAtlas, FontAtlasEntry } from "@/fonts/types"; +import { useFontAtlas } from "@/fonts/use-font-atlas"; import { cn } from "@/utils/ui"; import { ChevronDown, Search } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react"; diff --git a/apps/web/src/components/ui/use-overlay-open-change.ts b/apps/web/src/components/ui/use-overlay-open-change.ts index dbc46cfc..b9cb87d8 100644 --- a/apps/web/src/components/ui/use-overlay-open-change.ts +++ b/apps/web/src/components/ui/use-overlay-open-change.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useId, useRef } from "react"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; +import { useKeybindingsStore } from "@/actions/keybindings-store"; export function useOverlayOpenChange({ open, diff --git a/apps/web/src/core/index.ts b/apps/web/src/core/index.ts index 483a26c1..3e55e1d6 100644 --- a/apps/web/src/core/index.ts +++ b/apps/web/src/core/index.ts @@ -10,9 +10,9 @@ import { AudioManager } from "./managers/audio-manager"; import { SelectionManager } from "./managers/selection-manager"; import { ClipboardManager } from "./managers/clipboard-manager"; import { DiagnosticsManager } from "./managers/diagnostics-manager"; -import { registerDefaultEffects } from "@/lib/effects"; -import { registerDefaultMasks } from "@/lib/masks"; -import { registerTranscriptionDiagnostics } from "@/lib/transcription/diagnostics"; +import { registerDefaultEffects } from "@/effects"; +import { registerDefaultMasks } from "@/masks"; +import { registerTranscriptionDiagnostics } from "@/transcription/diagnostics"; export class EditorCore { private static instance: EditorCore | null = null; diff --git a/apps/web/src/core/managers/audio-manager.ts b/apps/web/src/core/managers/audio-manager.ts index 076b998d..545565be 100644 --- a/apps/web/src/core/managers/audio-manager.ts +++ b/apps/web/src/core/managers/audio-manager.ts @@ -1,21 +1,21 @@ import type { EditorCore } from "@/core"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; import { clampRetimeRate, shouldMaintainPitch, -} from "@/lib/retime/rate"; -import type { AudioClipSource } from "@/lib/media/audio"; -import { createAudioContext, collectAudioClips } from "@/lib/media/audio"; +} from "@/retime/rate"; +import type { AudioClipSource } from "@/media/audio"; +import { createAudioContext, collectAudioClips } from "@/media/audio"; import { buildAudioGainAutomation, hasAnimatedVolume, -} from "@/lib/timeline/audio-state"; -import { createAudioMasteringChain } from "@/lib/media/audio-mastering"; +} from "@/timeline/audio-state"; +import { createAudioMasteringChain } from "@/media/audio-mastering"; import { getClipTimeAtSourceTime, getSourceTimeAtClipTime, renderRetimedBuffer, -} from "@/lib/retime"; +} from "@/retime"; import { ALL_FORMATS, AudioBufferSink, diff --git a/apps/web/src/core/managers/clipboard-manager.ts b/apps/web/src/core/managers/clipboard-manager.ts index b19343df..8a2a51c9 100644 --- a/apps/web/src/core/managers/clipboard-manager.ts +++ b/apps/web/src/core/managers/clipboard-manager.ts @@ -5,7 +5,7 @@ import { type ClipboardEntry, type CopyContext, type PasteContext, -} from "@/lib/clipboard"; +} from "@/clipboard"; export class ClipboardManager { private entry: ClipboardEntry | null = null; diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts index 7cdd6373..9082db10 100644 --- a/apps/web/src/core/managers/commands.ts +++ b/apps/web/src/core/managers/commands.ts @@ -1,8 +1,8 @@ import type { EditorCore } from "@/core"; -import type { Command, CommandResult } from "@/lib/commands"; -import type { EditorSelectionSnapshot } from "@/lib/selection/editor-selection"; -import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple"; -import type { SceneTracks } from "@/lib/timeline/types"; +import type { Command, CommandResult } from "@/commands"; +import type { EditorSelectionSnapshot } from "@/selection/editor-selection"; +import { applyRippleAdjustments, computeRippleAdjustments } from "@/ripple"; +import type { SceneTracks } from "@/timeline/types"; interface CommandHistoryEntry { command: Command; diff --git a/apps/web/src/core/managers/diagnostics-manager.ts b/apps/web/src/core/managers/diagnostics-manager.ts index d022a350..0a32fab5 100644 --- a/apps/web/src/core/managers/diagnostics-manager.ts +++ b/apps/web/src/core/managers/diagnostics-manager.ts @@ -1,5 +1,5 @@ import type { EditorCore } from "@/core"; -import type { DiagnosticDefinition } from "@/lib/diagnostics/types"; +import type { DiagnosticDefinition } from "@/diagnostics/types"; interface DiagnosticRegistration extends DiagnosticDefinition { check: (editor: EditorCore) => boolean; diff --git a/apps/web/src/core/managers/media-manager.ts b/apps/web/src/core/managers/media-manager.ts index e43340a7..553d1557 100644 --- a/apps/web/src/core/managers/media-manager.ts +++ b/apps/web/src/core/managers/media-manager.ts @@ -1,11 +1,11 @@ import type { EditorCore } from "@/core"; import { toast } from "sonner"; -import type { MediaAsset } from "@/lib/media/types"; +import type { MediaAsset } from "@/media/types"; import { storageService } from "@/services/storage/service"; import { generateUUID } from "@/utils/id"; import { videoCache } from "@/services/video-cache/service"; import { waveformCache } from "@/services/waveform-cache/service"; -import { BatchCommand, RemoveMediaAssetCommand } from "@/lib/commands"; +import { BatchCommand, RemoveMediaAssetCommand } from "@/commands"; export class MediaManager { private assets: MediaAsset[] = []; diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts index 341a0eae..1ea09826 100644 --- a/apps/web/src/core/managers/playback-manager.ts +++ b/apps/web/src/core/managers/playback-manager.ts @@ -1,5 +1,5 @@ import type { EditorCore } from "@/core"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; import { roundToFrame } from "opencut-wasm"; export class PlaybackManager { diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts index 8555786d..b38fd50d 100644 --- a/apps/web/src/core/managers/project-manager.ts +++ b/apps/web/src/core/managers/project-manager.ts @@ -6,16 +6,16 @@ import type { TProjectSortOption, TProjectSettings, TTimelineViewState, -} from "@/lib/project/types"; -import type { ExportOptions, ExportResult, ExportState } from "@/lib/export"; +} from "@/project/types"; +import type { ExportOptions, ExportResult, ExportState } from "@/export"; import { storageService } from "@/services/storage/service"; import { toast } from "sonner"; import { generateUUID } from "@/utils/id"; -import { UpdateProjectSettingsCommand } from "@/lib/commands/project"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes"; -import { DEFAULT_FPS } from "@/lib/fps/defaults"; -import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes"; +import { UpdateProjectSettingsCommand } from "@/commands/project"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; +import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; +import { DEFAULT_FPS } from "@/fps/defaults"; +import { buildDefaultScene, getProjectDurationFromScenes } from "@/timeline/scenes"; import { buildScene } from "@/services/renderer/scene-builder"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; import { @@ -24,11 +24,11 @@ import { runStorageMigrations, type MigrationProgress, } from "@/services/storage/migrations"; -import { loadFonts } from "@/lib/fonts/google-fonts"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { getElementFontFamilies } from "@/lib/timeline/element-utils"; -import { getRaisedProjectFpsForImportedMedia } from "@/lib/fps/utils"; -import type { MediaAsset } from "@/lib/media/types"; +import { loadFonts } from "@/fonts/google-fonts"; +import { DEFAULTS } from "@/timeline/defaults"; +import { getElementFontFamilies } from "@/timeline/element-utils"; +import { getRaisedProjectFpsForImportedMedia } from "@/fps/utils"; +import type { MediaAsset } from "@/media/types"; export interface MigrationState { isMigrating: boolean; diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts index 495292e6..7984655c 100644 --- a/apps/web/src/core/managers/renderer-manager.ts +++ b/apps/web/src/core/managers/renderer-manager.ts @@ -1,12 +1,12 @@ import type { EditorCore } from "@/core"; import type { RootNode } from "@/services/renderer/nodes/root-node"; -import type { ExportOptions, ExportResult } from "@/lib/export"; +import type { ExportOptions, ExportResult } from "@/export"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; import { SceneExporter } from "@/services/renderer/scene-exporter"; import { buildScene } from "@/services/renderer/scene-builder"; -import { createTimelineAudioBuffer } from "@/lib/media/audio"; +import { createTimelineAudioBuffer } from "@/media/audio"; import { formatTimecode } from "opencut-wasm"; -import { frameRateToFloat } from "@/lib/fps/utils"; +import { frameRateToFloat } from "@/fps/utils"; import { downloadBlob } from "@/utils/browser"; type SnapshotResult = diff --git a/apps/web/src/core/managers/scenes-manager.ts b/apps/web/src/core/managers/scenes-manager.ts index 3508a066..13f251a1 100644 --- a/apps/web/src/core/managers/scenes-manager.ts +++ b/apps/web/src/core/managers/scenes-manager.ts @@ -1,320 +1,320 @@ -import type { EditorCore } from "@/core"; -import type { SceneTracks, TScene } from "@/lib/timeline"; -import { storageService } from "@/services/storage/service"; -import { - getMainScene, - ensureMainScene, - canDeleteScene, - findCurrentScene, -} from "@/lib/scenes"; -import { - getBookmarkAtTime, - getFrameTime, - isBookmarkAtTime, -} from "@/lib/timeline/bookmarks"; -import { - CreateSceneCommand, - DeleteSceneCommand, - MoveBookmarkCommand, - RemoveBookmarkCommand, - RenameSceneCommand, - ToggleBookmarkCommand, - UpdateBookmarkCommand, -} from "@/lib/commands/scene"; - -export class ScenesManager { - private active: TScene | null = null; - private list: TScene[] = []; - private listeners = new Set<() => void>(); - - constructor(private editor: EditorCore) {} - - async createScene({ - name, - isMain = false, - }: { - name: string; - isMain: boolean; - }): Promise { - if (!this.editor.project.getActive()) { - throw new Error("No active project"); - } - - const command = new CreateSceneCommand(name, isMain); - this.editor.command.execute({ command }); - return command.getSceneId(); - } - - async deleteScene({ sceneId }: { sceneId: string }): Promise { - const sceneToDelete = this.list.find((s) => s.id === sceneId); - - if (!sceneToDelete) { - throw new Error("Scene not found"); - } - - const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete }); - if (!canDelete) { - throw new Error(reason); - } - - if (!this.editor.project.getActive()) { - throw new Error("No active project"); - } - - const command = new DeleteSceneCommand(sceneId); - this.editor.command.execute({ command }); - } - - async renameScene({ - sceneId, - name, - }: { - sceneId: string; - name: string; - }): Promise { - if (!this.editor.project.getActive()) { - throw new Error("No active project"); - } - - const command = new RenameSceneCommand(sceneId, name); - this.editor.command.execute({ command }); - } - - async switchToScene({ sceneId }: { sceneId: string }): Promise { - const targetScene = this.list.find((s) => s.id === sceneId); - - if (!targetScene) { - throw new Error("Scene not found"); - } - - const activeProject = this.editor.project.getActive(); - - if (activeProject) { - const updatedProject = { - ...activeProject, - currentSceneId: sceneId, - metadata: { - ...activeProject.metadata, - updatedAt: new Date(), - }, - }; - - this.editor.project.setActiveProject({ project: updatedProject }); - } - - this.active = targetScene; - this.notify(); - } - - async toggleBookmark({ time }: { time: number }): Promise { - const command = new ToggleBookmarkCommand(time); - this.editor.command.execute({ command }); - } - - isBookmarked({ time }: { time: number }): boolean { - const activeScene = this.getActiveScene(); - const activeProject = this.editor.project.getActive(); - - if (!activeScene || !this.active || !activeProject) return false; - - const frameTime = getFrameTime({ - time, - fps: activeProject.settings.fps, - }); - - return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime }); - } - - async removeBookmark({ time }: { time: number }): Promise { - const command = new RemoveBookmarkCommand(time); - this.editor.command.execute({ command }); - } - - async updateBookmark({ - time, - updates, - }: { - time: number; - updates: Partial<{ note: string; color: string; duration: number }>; - }): Promise { - const command = new UpdateBookmarkCommand(time, updates); - this.editor.command.execute({ command }); - } - - async moveBookmark({ - fromTime, - toTime, - }: { - fromTime: number; - toTime: number; - }): Promise { - const command = new MoveBookmarkCommand(fromTime, toTime); - this.editor.command.execute({ command }); - } - - getBookmarkAtTime({ time }: { time: number }) { - const activeScene = this.active; - const activeProject = this.editor.project.getActive(); - - if (!activeScene || !activeProject) return null; - - const frameTime = getFrameTime({ - time, - fps: activeProject.settings.fps, - }); - - return getBookmarkAtTime({ - bookmarks: activeScene.bookmarks, - frameTime, - }); - } - - async loadProjectScenes({ projectId }: { projectId: string }): Promise { - try { - const result = await storageService.loadProject({ id: projectId }); - if (result?.project.scenes) { - const ensuredScenes = result.project.scenes ?? []; - const currentScene = findCurrentScene({ - scenes: ensuredScenes, - currentSceneId: result.project.currentSceneId, - }); - - this.list = ensuredScenes; - this.active = currentScene; - this.notify(); - } - } catch (error) { - console.error("Failed to load project scenes:", error); - this.list = []; - this.active = null; - this.notify(); - } - } - - initializeScenes({ - scenes, - currentSceneId, - }: { - scenes: TScene[]; - currentSceneId?: string; - }): void { - const ensuredScenes = ensureMainScene({ scenes }); - const currentScene = currentSceneId - ? ensuredScenes.find((s) => s.id === currentSceneId) - : null; - - const fallbackScene = getMainScene({ scenes: ensuredScenes }); - - this.list = ensuredScenes; - this.active = currentScene || fallbackScene; - this.notify(); - - const hasAddedMainScene = ensuredScenes.length > scenes.length; - if (hasAddedMainScene) { - const activeProject = this.editor.project.getActive(); - - if (activeProject) { - const updatedProject = { - ...activeProject, - scenes: ensuredScenes, - metadata: { - ...activeProject.metadata, - updatedAt: new Date(), - }, - }; - - this.editor.project.setActiveProject({ project: updatedProject }); - this.editor.save.markDirty({ force: true }); - } - } - } - - clearScenes(): void { - this.list = []; - this.active = null; - this.notify(); - } - - getActiveScene(): TScene { - if (!this.active) { - throw new Error("No active scene."); - } - return this.active; - } - - getActiveSceneOrNull(): TScene | null { - return this.active; - } - - getScenes(): TScene[] { - return this.list; - } - - setScenes({ - scenes, - activeSceneId, - }: { - scenes: TScene[]; - activeSceneId?: string; - }): void { - this.list = scenes; - const nextActiveSceneId = activeSceneId ?? this.active?.id ?? null; - this.active = nextActiveSceneId - ? (scenes.find((scene) => scene.id === nextActiveSceneId) ?? null) - : null; - this.notify(); - - const activeProject = this.editor.project.getActive(); - if (activeProject) { - const updatedProject = { - ...activeProject, - scenes, - metadata: { - ...activeProject.metadata, - updatedAt: new Date(), - }, - }; - this.editor.project.setActiveProject({ project: updatedProject }); - } - } - - subscribe(listener: () => void): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - private notify(): void { - this.listeners.forEach((fn) => { - fn(); - }); - } - - updateSceneTracks({ tracks }: { tracks: SceneTracks }): void { - if (!this.active) return; - - const updatedScene: TScene = { - ...this.active, - tracks, - updatedAt: new Date(), - }; - - this.list = this.list.map((s) => - s.id === this.active?.id ? updatedScene : s, - ); - this.active = updatedScene; - this.notify(); - - const activeProject = this.editor.project.getActive(); - if (activeProject) { - const updatedProject = { - ...activeProject, - scenes: this.list, - metadata: { - ...activeProject.metadata, - updatedAt: new Date(), - }, - }; - this.editor.project.setActiveProject({ project: updatedProject }); - } - } -} +import type { EditorCore } from "@/core"; +import type { SceneTracks, TScene } from "@/timeline"; +import { storageService } from "@/services/storage/service"; +import { + getMainScene, + ensureMainScene, + canDeleteScene, + findCurrentScene, +} from "@/timeline/scenes"; +import { + getBookmarkAtTime, + getFrameTime, + isBookmarkAtTime, +} from "@/timeline/bookmarks/index"; +import { + CreateSceneCommand, + DeleteSceneCommand, + MoveBookmarkCommand, + RemoveBookmarkCommand, + RenameSceneCommand, + ToggleBookmarkCommand, + UpdateBookmarkCommand, +} from "@/commands/scene"; + +export class ScenesManager { + private active: TScene | null = null; + private list: TScene[] = []; + private listeners = new Set<() => void>(); + + constructor(private editor: EditorCore) {} + + async createScene({ + name, + isMain = false, + }: { + name: string; + isMain: boolean; + }): Promise { + if (!this.editor.project.getActive()) { + throw new Error("No active project"); + } + + const command = new CreateSceneCommand(name, isMain); + this.editor.command.execute({ command }); + return command.getSceneId(); + } + + async deleteScene({ sceneId }: { sceneId: string }): Promise { + const sceneToDelete = this.list.find((s) => s.id === sceneId); + + if (!sceneToDelete) { + throw new Error("Scene not found"); + } + + const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete }); + if (!canDelete) { + throw new Error(reason); + } + + if (!this.editor.project.getActive()) { + throw new Error("No active project"); + } + + const command = new DeleteSceneCommand(sceneId); + this.editor.command.execute({ command }); + } + + async renameScene({ + sceneId, + name, + }: { + sceneId: string; + name: string; + }): Promise { + if (!this.editor.project.getActive()) { + throw new Error("No active project"); + } + + const command = new RenameSceneCommand(sceneId, name); + this.editor.command.execute({ command }); + } + + async switchToScene({ sceneId }: { sceneId: string }): Promise { + const targetScene = this.list.find((s) => s.id === sceneId); + + if (!targetScene) { + throw new Error("Scene not found"); + } + + const activeProject = this.editor.project.getActive(); + + if (activeProject) { + const updatedProject = { + ...activeProject, + currentSceneId: sceneId, + metadata: { + ...activeProject.metadata, + updatedAt: new Date(), + }, + }; + + this.editor.project.setActiveProject({ project: updatedProject }); + } + + this.active = targetScene; + this.notify(); + } + + async toggleBookmark({ time }: { time: number }): Promise { + const command = new ToggleBookmarkCommand(time); + this.editor.command.execute({ command }); + } + + isBookmarked({ time }: { time: number }): boolean { + const activeScene = this.getActiveScene(); + const activeProject = this.editor.project.getActive(); + + if (!activeScene || !this.active || !activeProject) return false; + + const frameTime = getFrameTime({ + time, + fps: activeProject.settings.fps, + }); + + return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime }); + } + + async removeBookmark({ time }: { time: number }): Promise { + const command = new RemoveBookmarkCommand(time); + this.editor.command.execute({ command }); + } + + async updateBookmark({ + time, + updates, + }: { + time: number; + updates: Partial<{ note: string; color: string; duration: number }>; + }): Promise { + const command = new UpdateBookmarkCommand(time, updates); + this.editor.command.execute({ command }); + } + + async moveBookmark({ + fromTime, + toTime, + }: { + fromTime: number; + toTime: number; + }): Promise { + const command = new MoveBookmarkCommand(fromTime, toTime); + this.editor.command.execute({ command }); + } + + getBookmarkAtTime({ time }: { time: number }) { + const activeScene = this.active; + const activeProject = this.editor.project.getActive(); + + if (!activeScene || !activeProject) return null; + + const frameTime = getFrameTime({ + time, + fps: activeProject.settings.fps, + }); + + return getBookmarkAtTime({ + bookmarks: activeScene.bookmarks, + frameTime, + }); + } + + async loadProjectScenes({ projectId }: { projectId: string }): Promise { + try { + const result = await storageService.loadProject({ id: projectId }); + if (result?.project.scenes) { + const ensuredScenes = result.project.scenes ?? []; + const currentScene = findCurrentScene({ + scenes: ensuredScenes, + currentSceneId: result.project.currentSceneId, + }); + + this.list = ensuredScenes; + this.active = currentScene; + this.notify(); + } + } catch (error) { + console.error("Failed to load project scenes:", error); + this.list = []; + this.active = null; + this.notify(); + } + } + + initializeScenes({ + scenes, + currentSceneId, + }: { + scenes: TScene[]; + currentSceneId?: string; + }): void { + const ensuredScenes = ensureMainScene({ scenes }); + const currentScene = currentSceneId + ? ensuredScenes.find((s) => s.id === currentSceneId) + : null; + + const fallbackScene = getMainScene({ scenes: ensuredScenes }); + + this.list = ensuredScenes; + this.active = currentScene || fallbackScene; + this.notify(); + + const hasAddedMainScene = ensuredScenes.length > scenes.length; + if (hasAddedMainScene) { + const activeProject = this.editor.project.getActive(); + + if (activeProject) { + const updatedProject = { + ...activeProject, + scenes: ensuredScenes, + metadata: { + ...activeProject.metadata, + updatedAt: new Date(), + }, + }; + + this.editor.project.setActiveProject({ project: updatedProject }); + this.editor.save.markDirty({ force: true }); + } + } + } + + clearScenes(): void { + this.list = []; + this.active = null; + this.notify(); + } + + getActiveScene(): TScene { + if (!this.active) { + throw new Error("No active scene."); + } + return this.active; + } + + getActiveSceneOrNull(): TScene | null { + return this.active; + } + + getScenes(): TScene[] { + return this.list; + } + + setScenes({ + scenes, + activeSceneId, + }: { + scenes: TScene[]; + activeSceneId?: string; + }): void { + this.list = scenes; + const nextActiveSceneId = activeSceneId ?? this.active?.id ?? null; + this.active = nextActiveSceneId + ? (scenes.find((scene) => scene.id === nextActiveSceneId) ?? null) + : null; + this.notify(); + + const activeProject = this.editor.project.getActive(); + if (activeProject) { + const updatedProject = { + ...activeProject, + scenes, + metadata: { + ...activeProject.metadata, + updatedAt: new Date(), + }, + }; + this.editor.project.setActiveProject({ project: updatedProject }); + } + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private notify(): void { + this.listeners.forEach((fn) => { + fn(); + }); + } + + updateSceneTracks({ tracks }: { tracks: SceneTracks }): void { + if (!this.active) return; + + const updatedScene: TScene = { + ...this.active, + tracks, + updatedAt: new Date(), + }; + + this.list = this.list.map((s) => + s.id === this.active?.id ? updatedScene : s, + ); + this.active = updatedScene; + this.notify(); + + const activeProject = this.editor.project.getActive(); + if (activeProject) { + const updatedProject = { + ...activeProject, + scenes: this.list, + metadata: { + ...activeProject.metadata, + updatedAt: new Date(), + }, + }; + this.editor.project.setActiveProject({ project: updatedProject }); + } + } +} diff --git a/apps/web/src/core/managers/selection-manager.ts b/apps/web/src/core/managers/selection-manager.ts index 5da4fa60..cfc46ccc 100644 --- a/apps/web/src/core/managers/selection-manager.ts +++ b/apps/web/src/core/managers/selection-manager.ts @@ -1,12 +1,12 @@ import type { EditorCore } from "@/core"; -import type { SelectedKeyframeRef } from "@/lib/animation/types"; +import type { SelectedKeyframeRef } from "@/animation/types"; import type { EditorSelectionKind, EditorSelectionPatch, EditorSelectionSnapshot, SelectedMaskPointSelection, -} from "@/lib/selection/editor-selection"; -import type { ElementRef } from "@/lib/timeline/types"; +} from "@/selection/editor-selection"; +import type { ElementRef } from "@/timeline/types"; export class SelectionManager { private selectedElements: ElementRef[] = []; diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts index c58128fb..dbc3332b 100644 --- a/apps/web/src/core/managers/timeline-manager.ts +++ b/apps/web/src/core/managers/timeline-manager.ts @@ -1,33 +1,33 @@ import type { EditorCore } from "@/core"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import type { ParamValues } from "@/lib/params"; +import type { ElementBounds } from "@/preview/element-bounds"; +import type { ParamValues } from "@/params"; import type { SceneTracks, TrackType, TimelineTrack, TimelineElement, RetimeConfig, -} from "@/lib/timeline"; -import { calculateTotalDuration } from "@/lib/timeline"; -import { findTrackInSceneTracks } from "@/lib/timeline/track-element-update"; +} from "@/timeline"; +import { calculateTotalDuration } from "@/timeline"; +import { findTrackInSceneTracks } from "@/timeline/track-element-update"; import { canElementBeHidden, canElementHaveAudio, -} from "@/lib/timeline/element-utils"; +} from "@/timeline/element-utils"; import type { AnimationPath, AnimationInterpolation, AnimationValue, AnimationValueForPath, ScalarCurveKeyframePatch, -} from "@/lib/animation/types"; +} from "@/animation/types"; import { getElementLocalTime, resolveAnimationTarget, resolveAnimationPathValueAtTime, -} from "@/lib/animation"; +} from "@/animation"; import { lastFrameTime } from "opencut-wasm"; -import { BatchCommand } from "@/lib/commands"; +import { BatchCommand } from "@/commands"; import { AddTrackCommand, RemoveTrackCommand, @@ -56,12 +56,12 @@ import { UpsertEffectParamKeyframeCommand, RemoveEffectParamKeyframeCommand, ToggleSourceAudioSeparationCommand, -} from "@/lib/commands/timeline"; -import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element"; +} from "@/commands/timeline"; +import type { InsertElementParams } from "@/commands/timeline/element/insert-element"; import type { PlannedElementMove, PlannedTrackCreation, -} from "@/lib/timeline/group-move"; +} from "@/timeline/group-move"; export class TimelineManager { private listeners = new Set<() => void>(); diff --git a/apps/web/src/lib/db/index.ts b/apps/web/src/db/index.ts similarity index 90% rename from apps/web/src/lib/db/index.ts rename to apps/web/src/db/index.ts index b1ffd119..29e3ffda 100644 --- a/apps/web/src/lib/db/index.ts +++ b/apps/web/src/db/index.ts @@ -1,7 +1,7 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; -import { webEnv } from "@/lib/env/web"; +import { webEnv } from "@/env/web"; let _db: ReturnType | null = null; diff --git a/apps/web/src/lib/db/schema.ts b/apps/web/src/db/schema.ts similarity index 100% rename from apps/web/src/lib/db/schema.ts rename to apps/web/src/db/schema.ts diff --git a/apps/web/src/lib/diagnostics/types.ts b/apps/web/src/diagnostics/types.ts similarity index 100% rename from apps/web/src/lib/diagnostics/types.ts rename to apps/web/src/diagnostics/types.ts diff --git a/apps/web/src/lib/cancel-interaction.ts b/apps/web/src/editor/cancel-interaction.ts similarity index 100% rename from apps/web/src/lib/cancel-interaction.ts rename to apps/web/src/editor/cancel-interaction.ts diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/editor/editor-store.ts similarity index 83% rename from apps/web/src/stores/editor-store.ts rename to apps/web/src/editor/editor-store.ts index 07cb433e..6b05e8bd 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/editor/editor-store.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; -import { DEFAULT_CANVAS_PRESETS } from "@/lib/canvas/sizes"; -import type { TCanvasSize } from "@/lib/project/types"; +import { DEFAULT_CANVAS_PRESETS } from "@/canvas/sizes"; +import type { TCanvasSize } from "@/project/types"; interface EditorState { isInitializing: boolean; diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/editor/panel-store.ts similarity index 93% rename from apps/web/src/stores/panel-store.ts rename to apps/web/src/editor/panel-store.ts index 757a37a6..2cb768a1 100644 --- a/apps/web/src/stores/panel-store.ts +++ b/apps/web/src/editor/panel-store.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { PANEL_CONFIG } from "@/lib/panels/layout"; +import { PANEL_CONFIG } from "@/panels/layout"; export interface PanelSizes { tools: number; diff --git a/apps/web/src/hooks/use-editor.ts b/apps/web/src/editor/use-editor.ts similarity index 100% rename from apps/web/src/hooks/use-editor.ts rename to apps/web/src/editor/use-editor.ts diff --git a/apps/web/src/hooks/use-menu-preview.ts b/apps/web/src/editor/use-menu-preview.ts similarity index 87% rename from apps/web/src/hooks/use-menu-preview.ts rename to apps/web/src/editor/use-menu-preview.ts index a3c14489..ca42124e 100644 --- a/apps/web/src/hooks/use-menu-preview.ts +++ b/apps/web/src/editor/use-menu-preview.ts @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; export function useMenuPreview() { const editor = useEditor(); diff --git a/apps/web/src/components/editor/panels/assets/views/effects.tsx b/apps/web/src/effects/components/assets-view.tsx similarity index 92% rename from apps/web/src/components/editor/panels/assets/views/effects.tsx rename to apps/web/src/effects/components/assets-view.tsx index 74128df6..000af7e6 100644 --- a/apps/web/src/components/editor/panels/assets/views/effects.tsx +++ b/apps/web/src/effects/components/assets-view.tsx @@ -3,11 +3,11 @@ import { useEffect, useRef, useCallback } from "react"; import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; -import { effectsRegistry, EFFECT_TARGET_ELEMENT_TYPES } from "@/lib/effects"; +import { effectsRegistry, EFFECT_TARGET_ELEMENT_TYPES } from "@/effects"; import { effectPreviewService } from "@/services/renderer/effect-preview"; -import { useEditor } from "@/hooks/use-editor"; -import { buildEffectElement } from "@/lib/timeline/element-utils"; -import type { EffectDefinition } from "@/lib/effects/types"; +import { useEditor } from "@/editor/use-editor"; +import { buildEffectElement } from "@/timeline/element-utils"; +import type { EffectDefinition } from "@/effects/types"; export function EffectsView() { const effects = effectsRegistry.getAll(); diff --git a/apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx b/apps/web/src/effects/components/effects-tab.tsx similarity index 93% rename from apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx rename to apps/web/src/effects/components/effects-tab.tsx index 1b94ee2d..102d818a 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx +++ b/apps/web/src/effects/components/effects-tab.tsx @@ -1,12 +1,12 @@ "use client"; import { useState } from "react"; -import type { ParamValues } from "@/lib/params"; -import type { Effect } from "@/lib/effects/types"; -import type { EffectElement, VisualElement } from "@/lib/timeline"; -import { effectsRegistry } from "@/lib/effects"; -import { useEditor } from "@/hooks/use-editor"; -import { useElementPreview } from "@/hooks/use-element-preview"; +import type { ParamValues } from "@/params"; +import type { Effect } from "@/effects/types"; +import type { EffectElement, VisualElement } from "@/timeline"; +import { effectsRegistry } from "@/effects"; +import { useEditor } from "@/editor/use-editor"; +import { useElementPreview } from "@/timeline/hooks/use-element-preview"; import { Section, SectionContent, @@ -14,7 +14,7 @@ import { SectionTitle, SectionFields, } from "@/components/section"; -import { PropertyParamField } from "../components/property-param-field"; +import { PropertyParamField } from "@/components/editor/panels/properties/components/property-param-field"; import { Button } from "@/components/ui/button"; import { HugeiconsIcon } from "@hugeicons/react"; import { @@ -25,7 +25,7 @@ import { } from "@hugeicons/core-free-icons"; import { cn } from "@/utils/ui"; import { Separator } from "@/components/ui/separator"; -import { useAssetsPanelStore } from "@/stores/assets-panel-store"; +import { useAssetsPanelStore } from "@/components/editor/panels/assets/assets-panel-store"; export function StandaloneEffectTab({ element, diff --git a/apps/web/src/lib/effects/definitions/blur.ts b/apps/web/src/effects/definitions/blur.ts similarity index 97% rename from apps/web/src/lib/effects/definitions/blur.ts rename to apps/web/src/effects/definitions/blur.ts index b464db3a..194196e9 100644 --- a/apps/web/src/lib/effects/definitions/blur.ts +++ b/apps/web/src/effects/definitions/blur.ts @@ -1,4 +1,4 @@ -import type { EffectDefinition, EffectPass } from "@/lib/effects/types"; +import type { EffectDefinition, EffectPass } from "@/effects/types"; export const GAUSSIAN_BLUR_SHADER = "gaussian-blur"; diff --git a/apps/web/src/lib/effects/definitions/index.ts b/apps/web/src/effects/definitions/index.ts similarity index 100% rename from apps/web/src/lib/effects/definitions/index.ts rename to apps/web/src/effects/definitions/index.ts diff --git a/apps/web/src/lib/effects/index.ts b/apps/web/src/effects/index.ts similarity index 80% rename from apps/web/src/lib/effects/index.ts rename to apps/web/src/effects/index.ts index 92484ee2..eebb8cbc 100644 --- a/apps/web/src/lib/effects/index.ts +++ b/apps/web/src/effects/index.ts @@ -1,9 +1,9 @@ import { generateUUID } from "@/utils/id"; -import { buildDefaultParamValues } from "@/lib/registry"; +import { buildDefaultParamValues } from "@/params/registry"; import { effectsRegistry } from "./registry"; -import type { ParamValues } from "@/lib/params"; -import type { Effect, EffectDefinition, EffectPass } from "@/lib/effects/types"; -import { VISUAL_ELEMENT_TYPES } from "@/lib/timeline"; +import type { ParamValues } from "@/params"; +import type { Effect, EffectDefinition, EffectPass } from "@/effects/types"; +import { VISUAL_ELEMENT_TYPES } from "@/timeline"; export { effectsRegistry } from "./registry"; export { registerDefaultEffects } from "./definitions"; diff --git a/apps/web/src/lib/effects/registry.ts b/apps/web/src/effects/registry.ts similarity index 59% rename from apps/web/src/lib/effects/registry.ts rename to apps/web/src/effects/registry.ts index 29cc3202..610bca02 100644 --- a/apps/web/src/lib/effects/registry.ts +++ b/apps/web/src/effects/registry.ts @@ -1,5 +1,5 @@ -import { DefinitionRegistry } from "@/lib/registry"; -import type { EffectDefinition } from "@/lib/effects/types"; +import { DefinitionRegistry } from "@/params/registry"; +import type { EffectDefinition } from "@/effects/types"; export class EffectsRegistry extends DefinitionRegistry { constructor() { diff --git a/apps/web/src/lib/effects/types.ts b/apps/web/src/effects/types.ts similarity index 92% rename from apps/web/src/lib/effects/types.ts rename to apps/web/src/effects/types.ts index 1862b440..6b85e743 100644 --- a/apps/web/src/lib/effects/types.ts +++ b/apps/web/src/effects/types.ts @@ -1,4 +1,4 @@ -import type { ParamDefinition, ParamValues } from "@/lib/params"; +import type { ParamDefinition, ParamValues } from "@/params"; export interface Effect { id: string; diff --git a/apps/web/src/lib/env/web.ts b/apps/web/src/env/web.ts similarity index 100% rename from apps/web/src/lib/env/web.ts rename to apps/web/src/env/web.ts diff --git a/apps/web/src/lib/export/defaults.ts b/apps/web/src/export/defaults.ts similarity index 100% rename from apps/web/src/lib/export/defaults.ts rename to apps/web/src/export/defaults.ts diff --git a/apps/web/src/lib/export/index.ts b/apps/web/src/export/index.ts similarity index 100% rename from apps/web/src/lib/export/index.ts rename to apps/web/src/export/index.ts diff --git a/apps/web/src/lib/export/mime-types.ts b/apps/web/src/export/mime-types.ts similarity index 100% rename from apps/web/src/lib/export/mime-types.ts rename to apps/web/src/export/mime-types.ts diff --git a/apps/web/src/lib/feedback/components/feedback-popover.tsx b/apps/web/src/feedback/components/feedback-popover.tsx similarity index 100% rename from apps/web/src/lib/feedback/components/feedback-popover.tsx rename to apps/web/src/feedback/components/feedback-popover.tsx diff --git a/apps/web/src/lib/feedback/index.ts b/apps/web/src/feedback/index.ts similarity index 100% rename from apps/web/src/lib/feedback/index.ts rename to apps/web/src/feedback/index.ts diff --git a/apps/web/src/lib/feedback/queries.ts b/apps/web/src/feedback/queries.ts similarity index 90% rename from apps/web/src/lib/feedback/queries.ts rename to apps/web/src/feedback/queries.ts index 6962e71f..121c6fe9 100644 --- a/apps/web/src/lib/feedback/queries.ts +++ b/apps/web/src/feedback/queries.ts @@ -1,4 +1,4 @@ -import { db, feedback } from "@/lib/db"; +import { db, feedback } from "@/db"; import { generateUUID } from "@/utils/id"; import type { FeedbackEntry, SubmitFeedbackInput } from "./types"; diff --git a/apps/web/src/lib/feedback/types.ts b/apps/web/src/feedback/types.ts similarity index 100% rename from apps/web/src/lib/feedback/types.ts rename to apps/web/src/feedback/types.ts diff --git a/apps/web/src/lib/fonts/google-fonts.ts b/apps/web/src/fonts/google-fonts.ts similarity index 95% rename from apps/web/src/lib/fonts/google-fonts.ts rename to apps/web/src/fonts/google-fonts.ts index 8ac55a08..36fca068 100644 --- a/apps/web/src/lib/fonts/google-fonts.ts +++ b/apps/web/src/fonts/google-fonts.ts @@ -1,5 +1,5 @@ -import type { FontAtlas } from "@/lib/fonts/types"; -import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts"; +import type { FontAtlas } from "@/fonts/types"; +import { SYSTEM_FONTS } from "@/fonts/system-fonts"; const GOOGLE_FONTS_CSS = "https://fonts.googleapis.com/css2"; const FONT_ATLAS_PATH = "/fonts/font-atlas.json"; diff --git a/apps/web/src/lib/fonts/system-fonts.ts b/apps/web/src/fonts/system-fonts.ts similarity index 100% rename from apps/web/src/lib/fonts/system-fonts.ts rename to apps/web/src/fonts/system-fonts.ts diff --git a/apps/web/src/lib/fonts/types.ts b/apps/web/src/fonts/types.ts similarity index 100% rename from apps/web/src/lib/fonts/types.ts rename to apps/web/src/fonts/types.ts diff --git a/apps/web/src/hooks/use-font-atlas.ts b/apps/web/src/fonts/use-font-atlas.ts similarity index 84% rename from apps/web/src/hooks/use-font-atlas.ts rename to apps/web/src/fonts/use-font-atlas.ts index aa48f3b4..153eebd4 100644 --- a/apps/web/src/hooks/use-font-atlas.ts +++ b/apps/web/src/fonts/use-font-atlas.ts @@ -3,9 +3,9 @@ import { getCachedFontAtlas, loadFontAtlas, clearFontAtlasCache, -} from "@/lib/fonts/google-fonts"; -import type { FontAtlas } from "@/lib/fonts/types"; -import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts"; +} from "@/fonts/google-fonts"; +import type { FontAtlas } from "@/fonts/types"; +import { SYSTEM_FONTS } from "@/fonts/system-fonts"; type Status = "idle" | "loading" | "error"; diff --git a/apps/web/src/lib/fps/__tests__/fps.test.ts b/apps/web/src/fps/__tests__/fps.test.ts similarity index 98% rename from apps/web/src/lib/fps/__tests__/fps.test.ts rename to apps/web/src/fps/__tests__/fps.test.ts index 10eed8d9..e025a188 100644 --- a/apps/web/src/lib/fps/__tests__/fps.test.ts +++ b/apps/web/src/fps/__tests__/fps.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { getHighestImportedVideoFps, getRaisedProjectFpsForImportedMedia, -} from "@/lib/fps/utils"; +} from "@/fps/utils"; describe("getHighestImportedVideoFps", () => { test("returns the highest valid video fps", () => { diff --git a/apps/web/src/lib/fps/defaults.ts b/apps/web/src/fps/defaults.ts similarity index 100% rename from apps/web/src/lib/fps/defaults.ts rename to apps/web/src/fps/defaults.ts diff --git a/apps/web/src/lib/fps/presets.ts b/apps/web/src/fps/presets.ts similarity index 100% rename from apps/web/src/lib/fps/presets.ts rename to apps/web/src/fps/presets.ts diff --git a/apps/web/src/lib/fps/utils.ts b/apps/web/src/fps/utils.ts similarity index 98% rename from apps/web/src/lib/fps/utils.ts rename to apps/web/src/fps/utils.ts index 932788c4..e30e263a 100644 --- a/apps/web/src/lib/fps/utils.ts +++ b/apps/web/src/fps/utils.ts @@ -1,5 +1,5 @@ import type { FrameRate } from "opencut-wasm"; -import type { MediaAsset } from "@/lib/media/types"; +import type { MediaAsset } from "@/media/types"; type MediaAssetFpsInput = Pick; diff --git a/apps/web/src/lib/gradients/canvas.ts b/apps/web/src/gradients/canvas.ts similarity index 100% rename from apps/web/src/lib/gradients/canvas.ts rename to apps/web/src/gradients/canvas.ts diff --git a/apps/web/src/lib/gradients/index.ts b/apps/web/src/gradients/index.ts similarity index 100% rename from apps/web/src/lib/gradients/index.ts rename to apps/web/src/gradients/index.ts diff --git a/apps/web/src/lib/gradients/parser.ts b/apps/web/src/gradients/parser.ts similarity index 100% rename from apps/web/src/lib/gradients/parser.ts rename to apps/web/src/gradients/parser.ts diff --git a/apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx b/apps/web/src/graphics/components/graphic-tab.tsx similarity index 79% rename from apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx rename to apps/web/src/graphics/components/graphic-tab.tsx index c61c9cd7..60599772 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx +++ b/apps/web/src/graphics/components/graphic-tab.tsx @@ -1,17 +1,17 @@ "use client"; import { useRef } from "react"; -import { useElementPlayhead } from "../hooks/use-element-playhead"; +import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead"; import { useKeyframedParamProperty, type KeyframedParamPropertyResult, -} from "../hooks/use-keyframed-param-property"; -import { resolveGraphicParamsAtTime } from "@/lib/animation"; -import type { ParamDefinition, ParamValues } from "@/lib/params"; -import type { GraphicElement } from "@/lib/timeline"; -import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; -import { useElementPreview } from "@/hooks/use-element-preview"; -import { useEditor } from "@/hooks/use-editor"; +} from "@/components/editor/panels/properties/hooks/use-keyframed-param-property"; +import { resolveGraphicParamsAtTime } from "@/animation"; +import type { ParamDefinition, ParamValues } from "@/params"; +import type { GraphicElement } from "@/timeline"; +import { graphicsRegistry, registerDefaultGraphics } from "@/graphics"; +import { useElementPreview } from "@/timeline/hooks/use-element-preview"; +import { useEditor } from "@/editor/use-editor"; import { Section, SectionContent, @@ -19,7 +19,7 @@ import { SectionHeader, SectionTitle, } from "@/components/section"; -import { PropertyParamField } from "../components/property-param-field"; +import { PropertyParamField } from "@/components/editor/panels/properties/components/property-param-field"; import { Button } from "@/components/ui/button"; import { HugeiconsIcon } from "@hugeicons/react"; import { MinusSignIcon, PlusSignIcon } from "@hugeicons/core-free-icons"; @@ -78,9 +78,7 @@ export function GraphicTab({ - {hasStrokeParams && ( - - )} + {hasStrokeParams && } ); } @@ -206,21 +204,23 @@ function AnimatedGraphicParamField({ isPlayheadWithinElementRange: boolean; resolvedParams: ParamValues; }) { - const animatedParam: KeyframedParamPropertyResult = useKeyframedParamProperty({ - param, - trackId, - elementId: element.id, - animations: element.animations, - localTime, - isPlayheadWithinElementRange, - resolvedValue: resolvedParams[param.key] ?? param.default, - buildBaseUpdates: ({ value }) => ({ - params: { - ...element.params, - [param.key]: value, - }, - }), - }); + const animatedParam: KeyframedParamPropertyResult = useKeyframedParamProperty( + { + param, + trackId, + elementId: element.id, + animations: element.animations, + localTime, + isPlayheadWithinElementRange, + resolvedValue: resolvedParams[param.key] ?? param.default, + buildBaseUpdates: ({ value }) => ({ + params: { + ...element.params, + [param.key]: value, + }, + }), + }, + ); return ( { diff --git a/apps/web/src/lib/graphics/stroke.ts b/apps/web/src/graphics/stroke.ts similarity index 100% rename from apps/web/src/lib/graphics/stroke.ts rename to apps/web/src/graphics/stroke.ts diff --git a/apps/web/src/lib/graphics/types.ts b/apps/web/src/graphics/types.ts similarity index 83% rename from apps/web/src/lib/graphics/types.ts rename to apps/web/src/graphics/types.ts index 22bfc5ac..1c6cc10e 100644 --- a/apps/web/src/lib/graphics/types.ts +++ b/apps/web/src/graphics/types.ts @@ -1,4 +1,4 @@ -import type { ParamDefinition, ParamValues } from "@/lib/params"; +import type { ParamDefinition, ParamValues } from "@/params"; export const DEFAULT_GRAPHIC_SOURCE_SIZE = 512; diff --git a/apps/web/src/lib/guides/definitions/custom.tsx b/apps/web/src/guides/definitions/custom.tsx similarity index 89% rename from apps/web/src/lib/guides/definitions/custom.tsx rename to apps/web/src/guides/definitions/custom.tsx index fe7944cb..a5bede0b 100644 --- a/apps/web/src/lib/guides/definitions/custom.tsx +++ b/apps/web/src/guides/definitions/custom.tsx @@ -1,7 +1,7 @@ import { PlusSignIcon, RulerIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Button } from "@/components/ui/button"; -import type { GuideDefinition } from "@/lib/guides/types"; +import type { GuideDefinition } from "@/guides/types"; function CustomGuideOptions() { return ( diff --git a/apps/web/src/lib/guides/definitions/grid.tsx b/apps/web/src/guides/definitions/grid.tsx similarity index 95% rename from apps/web/src/lib/guides/definitions/grid.tsx rename to apps/web/src/guides/definitions/grid.tsx index 7677a1d9..7df6425d 100644 --- a/apps/web/src/lib/guides/definitions/grid.tsx +++ b/apps/web/src/guides/definitions/grid.tsx @@ -9,11 +9,11 @@ import { GRID_MIN, GRID_MAX, DEFAULT_GRID_CONFIG, -} from "@/lib/guides/grid"; -import { usePreviewStore } from "@/stores/preview-store"; +} from "@/guides/grid"; +import { usePreviewStore } from "@/preview/preview-store"; import { clampRound } from "@/utils/math"; import { cn } from "@/utils/ui"; -import type { GuideDefinition } from "@/lib/guides/types"; +import type { GuideDefinition } from "@/guides/types"; function GridLines({ rows, diff --git a/apps/web/src/lib/guides/definitions/platforms.tsx b/apps/web/src/guides/definitions/platforms.tsx similarity index 91% rename from apps/web/src/lib/guides/definitions/platforms.tsx rename to apps/web/src/guides/definitions/platforms.tsx index 8c403eb1..b2471a74 100644 --- a/apps/web/src/lib/guides/definitions/platforms.tsx +++ b/apps/web/src/guides/definitions/platforms.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import type { GuideDefinition } from "@/lib/guides/types"; +import type { GuideDefinition } from "@/guides/types"; import { TikTokLayout } from "./tiktok-layout"; function PlatformLogo({ diff --git a/apps/web/src/lib/guides/definitions/tiktok-layout.tsx b/apps/web/src/guides/definitions/tiktok-layout.tsx similarity index 100% rename from apps/web/src/lib/guides/definitions/tiktok-layout.tsx rename to apps/web/src/guides/definitions/tiktok-layout.tsx diff --git a/apps/web/src/lib/guides/grid.ts b/apps/web/src/guides/grid.ts similarity index 100% rename from apps/web/src/lib/guides/grid.ts rename to apps/web/src/guides/grid.ts diff --git a/apps/web/src/lib/guides/index.ts b/apps/web/src/guides/index.ts similarity index 75% rename from apps/web/src/lib/guides/index.ts rename to apps/web/src/guides/index.ts index 9ea75631..a01d3d24 100644 --- a/apps/web/src/lib/guides/index.ts +++ b/apps/web/src/guides/index.ts @@ -1,13 +1,14 @@ -import { GUIDE_REGISTRY } from "./registry"; -import type { GuideDefinition } from "@/lib/guides/types"; - -export { GUIDE_REGISTRY, isGuideId } from "./registry"; -export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry"; - -export function getGuideById(guideId: string | null): GuideDefinition | null { - if (!guideId) { - return null; - } - - return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null; -} +import { GUIDE_REGISTRY } from "./registry"; +import type { GuideDefinition } from "@/guides/types"; + +export { GUIDE_REGISTRY, isGuideId } from "./registry"; +export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry"; +export { getGuidePreviewOverlaySource } from "./preview-overlay"; + +export function getGuideById(guideId: string | null): GuideDefinition | null { + if (!guideId) { + return null; + } + + return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null; +} diff --git a/apps/web/src/guides/preview-overlay.tsx b/apps/web/src/guides/preview-overlay.tsx new file mode 100644 index 00000000..d71adb12 --- /dev/null +++ b/apps/web/src/guides/preview-overlay.tsx @@ -0,0 +1,33 @@ +import { getGuideById, type GuideId } from "@/guides"; +import { + EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT, + type PreviewOverlaySourceResult, +} from "@/preview/overlays"; + +export function getGuidePreviewOverlaySource({ + guideId, +}: { + guideId: GuideId | null; +}): PreviewOverlaySourceResult { + const guide = getGuideById(guideId); + if (!guide) { + return EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT; + } + + return { + definitions: [], + instances: [ + { + id: `guide-${guide.id}`, + mount: { kind: "scene" }, + plane: "under-interaction", + pointerEvents: "none", + render: ({ sceneHeight, sceneWidth }) => + guide.renderOverlay({ + width: sceneWidth, + height: sceneHeight, + }), + }, + ], + }; +} diff --git a/apps/web/src/lib/guides/registry.tsx b/apps/web/src/guides/registry.tsx similarity index 79% rename from apps/web/src/lib/guides/registry.tsx rename to apps/web/src/guides/registry.tsx index 8000d989..d1c6d652 100644 --- a/apps/web/src/lib/guides/registry.tsx +++ b/apps/web/src/guides/registry.tsx @@ -1,4 +1,4 @@ -import type { GuideDefinition } from "@/lib/guides/types"; +import type { GuideDefinition } from "@/guides/types"; import { gridGuide } from "./definitions/grid"; // import { customGuide } from "./definitions/custom"; import { @@ -8,7 +8,7 @@ import { spotlightGuide, } from "./definitions/platforms"; -export type { GuideDefinition, GuideRenderProps } from "@/lib/guides/types"; +export type { GuideDefinition, GuideRenderProps } from "@/guides/types"; export const GUIDE_REGISTRY = [ gridGuide, diff --git a/apps/web/src/lib/guides/types.ts b/apps/web/src/guides/types.ts similarity index 100% rename from apps/web/src/lib/guides/types.ts rename to apps/web/src/guides/types.ts diff --git a/apps/web/src/lib/selection/index.ts b/apps/web/src/lib/selection/index.ts deleted file mode 100644 index b1ded820..00000000 --- a/apps/web/src/lib/selection/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SelectableItem } from "@/lib/selection/selectable-item"; -export { SelectableSurface } from "@/lib/selection/selectable-surface"; -export { useSelection } from "@/lib/selection/context"; -export { useSelectionScope } from "@/lib/selection/hooks/use-selection-scope"; diff --git a/apps/web/src/lib/timeline/snap-utils.ts b/apps/web/src/lib/timeline/snap-utils.ts deleted file mode 100644 index b2be95a9..00000000 --- a/apps/web/src/lib/timeline/snap-utils.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { Bookmark, SceneTracks } from "@/lib/timeline"; -import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale"; -import { getElementKeyframes } from "@/lib/animation"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; - -export interface SnapPoint { - time: number; - type: "element-start" | "element-end" | "playhead" | "bookmark" | "keyframe"; - elementId?: string; - trackId?: string; -} - -export interface SnapResult { - snappedTime: number; - snapPoint: SnapPoint | null; - snapDistance: number; -} - -const DEFAULT_SNAP_THRESHOLD_PX = 10; - -export function findSnapPoints({ - tracks, - playheadTime, - excludeElementId, - excludeElementIds, - bookmarks = [], - excludeBookmarkTime, - enableElementSnapping = true, - enablePlayheadSnapping = true, - enableBookmarkSnapping = true, - enableKeyframeSnapping = true, -}: { - tracks: SceneTracks; - playheadTime: number; - excludeElementId?: string; - excludeElementIds?: Set; - bookmarks?: Array; - excludeBookmarkTime?: number; - enableElementSnapping?: boolean; - enablePlayheadSnapping?: boolean; - enableBookmarkSnapping?: boolean; - enableKeyframeSnapping?: boolean; -}): SnapPoint[] { - const snapPoints: SnapPoint[] = []; - const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio]; - const blockedElementIds = new Set(excludeElementIds); - if (excludeElementId) { - blockedElementIds.add(excludeElementId); - } - - for (const track of orderedTracks) { - for (const element of track.elements) { - if (blockedElementIds.has(element.id)) continue; - - if (enableElementSnapping) { - snapPoints.push( - { - time: element.startTime, - type: "element-start", - elementId: element.id, - trackId: track.id, - }, - { - time: element.startTime + element.duration, - type: "element-end", - elementId: element.id, - trackId: track.id, - }, - ); - } - - if (enableKeyframeSnapping) { - for (const keyframe of getElementKeyframes({ - animations: element.animations, - })) { - snapPoints.push({ - time: element.startTime + keyframe.time, - type: "keyframe", - elementId: element.id, - trackId: track.id, - }); - } - } - } - } - - if (enablePlayheadSnapping) { - snapPoints.push({ time: playheadTime, type: "playhead" }); - } - - if (enableBookmarkSnapping) { - for (const bookmark of bookmarks) { - if ( - excludeBookmarkTime != null && - bookmark.time === excludeBookmarkTime - ) { - continue; - } - snapPoints.push({ time: bookmark.time, type: "bookmark" }); - } - } - - return snapPoints; -} - -export function snapToNearestPoint({ - targetTime, - snapPoints, - zoomLevel, - snapThreshold = DEFAULT_SNAP_THRESHOLD_PX, -}: { - targetTime: number; - snapPoints: Array; - zoomLevel: number; - snapThreshold?: number; -}): SnapResult { - const pixelsPerSecond = BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel; - const thresholdInTicks = (snapThreshold / pixelsPerSecond) * TICKS_PER_SECOND; - - let closestSnapPoint: SnapPoint | null = null; - let closestDistance = Infinity; - - for (const snapPoint of snapPoints) { - const distance = Math.abs(targetTime - snapPoint.time); - if (distance < thresholdInTicks && distance < closestDistance) { - closestDistance = distance; - closestSnapPoint = snapPoint; - } - } - - return { - snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime, - snapPoint: closestSnapPoint, - snapDistance: closestDistance, - }; -} - -export function snapElementEdge({ - targetTime, - elementDuration, - tracks, - playheadTime, - zoomLevel, - excludeElementId, - excludeElementIds, - snapToStart = true, - bookmarks = [], -}: { - targetTime: number; - elementDuration: number; - tracks: SceneTracks; - playheadTime: number; - zoomLevel: number; - excludeElementId?: string; - excludeElementIds?: Set; - snapToStart?: boolean; - bookmarks?: Array; -}): SnapResult { - const snapPoints = findSnapPoints({ - tracks, - playheadTime, - excludeElementId, - excludeElementIds, - bookmarks, - }); - - const effectiveTargetTime = snapToStart - ? targetTime - : targetTime + elementDuration; - - const snapResult = snapToNearestPoint({ - targetTime: effectiveTargetTime, - snapPoints, - zoomLevel, - }); - - if (!snapToStart && snapResult.snapPoint) { - snapResult.snappedTime = snapResult.snappedTime - elementDuration; - } - - return snapResult; -} diff --git a/apps/web/src/lib/masks/__tests__/snap.test.ts b/apps/web/src/masks/__tests__/snap.test.ts similarity index 96% rename from apps/web/src/lib/masks/__tests__/snap.test.ts rename to apps/web/src/masks/__tests__/snap.test.ts index e2f9cafb..2f33615f 100644 --- a/apps/web/src/lib/masks/__tests__/snap.test.ts +++ b/apps/web/src/masks/__tests__/snap.test.ts @@ -4,23 +4,23 @@ import { getCustomMaskClosedStateAfterPointRemoval, insertPointIntoCustomMaskSegment, removeCustomMaskPoints, -} from "@/lib/masks/custom-path"; +} from "@/masks/custom-path"; import { appendPointToCustomMask, customMaskDefinition, insertPointOnCustomMaskSegment, -} from "@/lib/masks/definitions/custom"; -import { getSplitMaskStrokeSegment } from "@/lib/masks/definitions/split"; -import { textMaskDefinition } from "@/lib/masks/definitions/text"; -import { getMaskSnapGeometry } from "@/lib/masks/geometry"; -import { snapMaskInteraction } from "@/lib/masks/snap"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; +} from "@/masks/definitions/custom"; +import { getSplitMaskStrokeSegment } from "@/masks/definitions/split"; +import { textMaskDefinition } from "@/masks/definitions/text"; +import { getMaskSnapGeometry } from "@/masks/geometry"; +import { snapMaskInteraction } from "@/masks/snap"; +import type { ElementBounds } from "@/preview/element-bounds"; import type { CustomMaskParams, RectangleMaskParams, SplitMaskParams, TextMaskParams, -} from "@/lib/masks/types"; +} from "@/masks/types"; const bounds: ElementBounds = { cx: 200, diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/masks/components/masks-tab.tsx similarity index 97% rename from apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx rename to apps/web/src/masks/components/masks-tab.tsx index 090fce64..97fba3ab 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx +++ b/apps/web/src/masks/components/masks-tab.tsx @@ -1,16 +1,13 @@ "use client"; -import type { MaskableElement } from "@/lib/timeline"; -import type { Mask, MaskType, TextMask } from "@/lib/masks/types"; -import type { - NumberParamDefinition, - SelectParamDefinition, -} from "@/lib/params"; -import { masksRegistry, buildDefaultMaskInstance } from "@/lib/masks"; -import { useEditor } from "@/hooks/use-editor"; -import { useElementPreview } from "@/hooks/use-element-preview"; -import { useMenuPreview } from "@/hooks/use-menu-preview"; -import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; +import type { MaskableElement } from "@/timeline"; +import type { Mask, MaskType, TextMask } from "@/masks/types"; +import type { NumberParamDefinition, SelectParamDefinition } from "@/params"; +import { masksRegistry, buildDefaultMaskInstance } from "@/masks"; +import { useEditor } from "@/editor/use-editor"; +import { useElementPreview } from "@/timeline/hooks/use-element-preview"; +import { useMenuPreview } from "@/editor/use-menu-preview"; +import { getVisibleElementsWithBounds } from "@/preview/element-bounds"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowExpandIcon, @@ -58,7 +55,7 @@ import { SectionHeader, SectionTitle, } from "@/components/section"; -import { usePropertyDraft } from "../hooks/use-property-draft"; +import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; import { OcMirrorIcon, OcShapesIcon, diff --git a/apps/web/src/lib/masks/custom-path.ts b/apps/web/src/masks/custom-path.ts similarity index 99% rename from apps/web/src/lib/masks/custom-path.ts rename to apps/web/src/masks/custom-path.ts index 94a9043c..0349df63 100644 --- a/apps/web/src/lib/masks/custom-path.ts +++ b/apps/web/src/masks/custom-path.ts @@ -1,4 +1,4 @@ -import type { ElementBounds } from "@/lib/preview/element-bounds"; +import type { ElementBounds } from "@/preview/element-bounds"; export interface CustomMaskPathPoint { id: string; diff --git a/apps/web/src/lib/masks/definitions/box-like.ts b/apps/web/src/masks/definitions/box-like.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/box-like.ts rename to apps/web/src/masks/definitions/box-like.ts index fe0f1a85..7db51df7 100644 --- a/apps/web/src/lib/masks/definitions/box-like.ts +++ b/apps/web/src/masks/definitions/box-like.ts @@ -1,7 +1,7 @@ import { DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO, MIN_MASK_DIMENSION, -} from "@/lib/masks/dimensions"; +} from "@/masks/dimensions"; import { computeFeatherUpdate } from "../param-update"; import type { BaseMaskParams, @@ -10,13 +10,13 @@ import type { MaskInteractionDefinition, MaskParamUpdateArgs, RectangleMaskParams, -} from "@/lib/masks/types"; -import type { NumberParamDefinition, ParamDefinition } from "@/lib/params"; +} from "@/masks/types"; +import type { NumberParamDefinition, ParamDefinition } from "@/params"; import { getBoxMaskHandlePositions, getBoxMaskOverlays, -} from "@/lib/masks/handle-positions"; -import { snapMaskInteraction } from "@/lib/masks/snap"; +} from "@/masks/handle-positions"; +import { snapMaskInteraction } from "@/masks/snap"; const PERCENTAGE_DISPLAY: Pick< NumberParamDefinition, diff --git a/apps/web/src/lib/masks/definitions/cinematic-bars.ts b/apps/web/src/masks/definitions/cinematic-bars.ts similarity index 99% rename from apps/web/src/lib/masks/definitions/cinematic-bars.ts rename to apps/web/src/masks/definitions/cinematic-bars.ts index ef9c6e00..36b31328 100644 --- a/apps/web/src/lib/masks/definitions/cinematic-bars.ts +++ b/apps/web/src/masks/definitions/cinematic-bars.ts @@ -2,7 +2,7 @@ import type { MaskDefaultContext, MaskDefinition, RectangleMaskParams, -} from "@/lib/masks/types"; +} from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/custom.ts b/apps/web/src/masks/definitions/custom.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/custom.ts rename to apps/web/src/masks/definitions/custom.ts index 0fc69929..cd6e2055 100644 --- a/apps/web/src/lib/masks/definitions/custom.ts +++ b/apps/web/src/masks/definitions/custom.ts @@ -1,7 +1,7 @@ import { generateUUID } from "@/utils/id"; -import type { ParamDefinition } from "@/lib/params"; -import { PEN_CURSOR } from "@/components/editor/panels/preview/cursors"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; +import type { ParamDefinition } from "@/params"; +import { PEN_CURSOR } from "@/preview/components/cursors"; +import type { ElementBounds } from "@/preview/element-bounds"; import type { CustomMask, CustomMaskParams, @@ -9,7 +9,7 @@ import type { MaskHandlePosition, MaskOverlay, MaskParamUpdateArgs, -} from "@/lib/masks/types"; +} from "@/masks/types"; import { buildCustomMaskPath2D, buildCustomMaskSvgPath, @@ -23,18 +23,18 @@ import { parseCustomMaskHandleId, recenterCustomMaskPath, type CustomMaskPathPoint, -} from "@/lib/masks/custom-path"; -import { getBoxMaskHandlePositions } from "@/lib/masks/handle-positions"; -import { computeFeatherUpdate } from "@/lib/masks/param-update"; +} from "@/masks/custom-path"; +import { getBoxMaskHandlePositions } from "@/masks/handle-positions"; +import { computeFeatherUpdate } from "@/masks/param-update"; import { setMaskLocalCenter, toGlobalMaskSnapLines, -} from "@/lib/masks/geometry"; +} from "@/masks/geometry"; import { snapPosition, snapRotation, snapScale, -} from "@/lib/preview/preview-snap"; +} from "@/preview/preview-snap"; const PERCENTAGE_DISPLAY = { displayMultiplier: 100, diff --git a/apps/web/src/lib/masks/definitions/diamond.ts b/apps/web/src/masks/definitions/diamond.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/diamond.ts rename to apps/web/src/masks/definitions/diamond.ts index 56930678..037aee70 100644 --- a/apps/web/src/lib/masks/definitions/diamond.ts +++ b/apps/web/src/masks/definitions/diamond.ts @@ -1,4 +1,4 @@ -import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types"; +import type { MaskDefinition, RectangleMaskParams } from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/ellipse.ts b/apps/web/src/masks/definitions/ellipse.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/ellipse.ts rename to apps/web/src/masks/definitions/ellipse.ts index 2eb85f0c..d0567817 100644 --- a/apps/web/src/lib/masks/definitions/ellipse.ts +++ b/apps/web/src/masks/definitions/ellipse.ts @@ -1,4 +1,4 @@ -import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types"; +import type { MaskDefinition, RectangleMaskParams } from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/heart.ts b/apps/web/src/masks/definitions/heart.ts similarity index 97% rename from apps/web/src/lib/masks/definitions/heart.ts rename to apps/web/src/masks/definitions/heart.ts index 429c5d98..d15c09e6 100644 --- a/apps/web/src/lib/masks/definitions/heart.ts +++ b/apps/web/src/masks/definitions/heart.ts @@ -1,4 +1,4 @@ -import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types"; +import type { MaskDefinition, RectangleMaskParams } from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/index.ts b/apps/web/src/masks/definitions/index.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/index.ts rename to apps/web/src/masks/definitions/index.ts index fd00cab1..9cee1020 100644 --- a/apps/web/src/lib/masks/definitions/index.ts +++ b/apps/web/src/masks/definitions/index.ts @@ -1,4 +1,4 @@ -import type { BaseMaskParams, MaskDefinition } from "@/lib/masks/types"; +import type { BaseMaskParams, MaskDefinition } from "@/masks/types"; import { masksRegistry, type MaskIconProps } from "../registry"; import { cinematicBarsMaskDefinition } from "./cinematic-bars"; import { customMaskDefinition } from "./custom"; diff --git a/apps/web/src/lib/masks/definitions/rectangle.ts b/apps/web/src/masks/definitions/rectangle.ts similarity index 96% rename from apps/web/src/lib/masks/definitions/rectangle.ts rename to apps/web/src/masks/definitions/rectangle.ts index 832b8307..7bf1a7fd 100644 --- a/apps/web/src/lib/masks/definitions/rectangle.ts +++ b/apps/web/src/masks/definitions/rectangle.ts @@ -1,4 +1,4 @@ -import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types"; +import type { MaskDefinition, RectangleMaskParams } from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/split.ts b/apps/web/src/masks/definitions/split.ts similarity index 98% rename from apps/web/src/lib/masks/definitions/split.ts rename to apps/web/src/masks/definitions/split.ts index c1a14d90..d50a2571 100644 --- a/apps/web/src/lib/masks/definitions/split.ts +++ b/apps/web/src/masks/definitions/split.ts @@ -3,13 +3,13 @@ import type { MaskDefinition, MaskParamUpdateArgs, SplitMaskParams, -} from "@/lib/masks/types"; +} from "@/masks/types"; import { halfPlaneSign, lineEdgeIntersection } from "../utils"; import { getLineMaskHandlePositions, getLineMaskOverlay, -} from "@/lib/masks/handle-positions"; -import { snapMaskInteraction } from "@/lib/masks/snap"; +} from "@/masks/handle-positions"; +import { snapMaskInteraction } from "@/masks/snap"; // cos(Ï€/2) returns ~6e-17 in JS, not 0. Values below this threshold are snapped // to exactly 0 to prevent opposite-sign float noise on canvas corners that lie diff --git a/apps/web/src/lib/masks/definitions/star.ts b/apps/web/src/masks/definitions/star.ts similarity index 97% rename from apps/web/src/lib/masks/definitions/star.ts rename to apps/web/src/masks/definitions/star.ts index d311ab80..a7f8819c 100644 --- a/apps/web/src/lib/masks/definitions/star.ts +++ b/apps/web/src/masks/definitions/star.ts @@ -1,4 +1,4 @@ -import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types"; +import type { MaskDefinition, RectangleMaskParams } from "@/masks/types"; import { BOX_LIKE_MASK_PARAMS, buildBoxMaskInteraction, diff --git a/apps/web/src/lib/masks/definitions/text.ts b/apps/web/src/masks/definitions/text.ts similarity index 94% rename from apps/web/src/lib/masks/definitions/text.ts rename to apps/web/src/masks/definitions/text.ts index b774ed28..c285f1c8 100644 --- a/apps/web/src/lib/masks/definitions/text.ts +++ b/apps/web/src/masks/definitions/text.ts @@ -1,34 +1,34 @@ -import type { ParamDefinition } from "@/lib/params"; +import type { ParamDefinition } from "@/params"; import type { MaskDefinition, MaskParamUpdateArgs, TextMask, TextMaskParams, -} from "@/lib/masks/types"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/lib/text/typography"; +} from "@/masks/types"; +import { DEFAULTS } from "@/timeline/defaults"; +import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/text/typography"; import { drawMeasuredTextLayout, measureTextLayout, strokeMeasuredTextLayout, -} from "@/lib/text/primitives"; -import { getTextMeasurementContext } from "@/lib/text/measure-element"; -import { getTextVisualRect } from "@/lib/text/layout"; +} from "@/text/primitives"; +import { getTextMeasurementContext } from "@/text/measure-element"; +import { getTextVisualRect } from "@/text/layout"; import { getBoxMaskHandlePositions, getBoxMaskRectOverlay, -} from "@/lib/masks/handle-positions"; -import { computeFeatherUpdate } from "@/lib/masks/param-update"; +} from "@/masks/handle-positions"; +import { computeFeatherUpdate } from "@/masks/param-update"; import { setMaskLocalCenter, toGlobalMaskSnapLines, -} from "@/lib/masks/geometry"; +} from "@/masks/geometry"; import { snapPosition, snapRotation, snapScale, type ScaleEdgePreference, -} from "@/lib/preview/preview-snap"; +} from "@/preview/preview-snap"; const PERCENTAGE_DISPLAY = { displayMultiplier: 100, diff --git a/apps/web/src/lib/masks/dimensions.ts b/apps/web/src/masks/dimensions.ts similarity index 100% rename from apps/web/src/lib/masks/dimensions.ts rename to apps/web/src/masks/dimensions.ts diff --git a/apps/web/src/lib/masks/feather.ts b/apps/web/src/masks/feather.ts similarity index 100% rename from apps/web/src/lib/masks/feather.ts rename to apps/web/src/masks/feather.ts diff --git a/apps/web/src/lib/masks/geometry.ts b/apps/web/src/masks/geometry.ts similarity index 90% rename from apps/web/src/lib/masks/geometry.ts rename to apps/web/src/masks/geometry.ts index a5b05030..007dfa49 100644 --- a/apps/web/src/lib/masks/geometry.ts +++ b/apps/web/src/masks/geometry.ts @@ -1,7 +1,7 @@ -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import type { SnapLine } from "@/lib/preview/preview-snap"; -import { MIN_MASK_DIMENSION } from "@/lib/masks/dimensions"; -import type { RectangleMaskParams } from "@/lib/masks/types"; +import type { ElementBounds } from "@/preview/element-bounds"; +import type { SnapLine } from "@/preview/preview-snap"; +import { MIN_MASK_DIMENSION } from "@/masks/dimensions"; +import type { RectangleMaskParams } from "@/masks/types"; type CenterMaskParams = { centerX: number; diff --git a/apps/web/src/lib/masks/handle-positions.ts b/apps/web/src/masks/handle-positions.ts similarity index 98% rename from apps/web/src/lib/masks/handle-positions.ts rename to apps/web/src/masks/handle-positions.ts index d32b8424..75668959 100644 --- a/apps/web/src/lib/masks/handle-positions.ts +++ b/apps/web/src/masks/handle-positions.ts @@ -1,5 +1,5 @@ -import { FEATHER_HANDLE_SCALE } from "@/lib/masks/feather"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; +import { FEATHER_HANDLE_SCALE } from "@/masks/feather"; +import type { ElementBounds } from "@/preview/element-bounds"; import type { MaskFeatures, MaskHandlePosition, @@ -8,7 +8,7 @@ import type { MaskRectOverlay, RectangleMaskParams, MaskShapeOverlay, -} from "@/lib/masks/types"; +} from "@/masks/types"; const LINE_HANDLE_OFFSET_SCREEN_PX = 20; const BOX_HANDLE_OFFSET_SCREEN_PX = 20; diff --git a/apps/web/src/lib/masks/index.ts b/apps/web/src/masks/index.ts similarity index 84% rename from apps/web/src/lib/masks/index.ts rename to apps/web/src/masks/index.ts index 43871b44..3544e239 100644 --- a/apps/web/src/lib/masks/index.ts +++ b/apps/web/src/masks/index.ts @@ -1,4 +1,4 @@ -import type { Mask, MaskDefaultContext, MaskType } from "@/lib/masks/types"; +import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types"; import { masksRegistry } from "./registry"; import { generateUUID } from "@/utils/id"; diff --git a/apps/web/src/lib/masks/param-update.ts b/apps/web/src/masks/param-update.ts similarity index 85% rename from apps/web/src/lib/masks/param-update.ts rename to apps/web/src/masks/param-update.ts index 4f8693eb..e5c7f5b7 100644 --- a/apps/web/src/lib/masks/param-update.ts +++ b/apps/web/src/masks/param-update.ts @@ -1,4 +1,4 @@ -import { FEATHER_HANDLE_SCALE, MAX_FEATHER } from "@/lib/masks/feather"; +import { FEATHER_HANDLE_SCALE, MAX_FEATHER } from "@/masks/feather"; export function computeFeatherUpdate({ startFeather, diff --git a/apps/web/src/lib/masks/registry.ts b/apps/web/src/masks/registry.ts similarity index 93% rename from apps/web/src/lib/masks/registry.ts rename to apps/web/src/masks/registry.ts index ae8ddd20..00b276c8 100644 --- a/apps/web/src/lib/masks/registry.ts +++ b/apps/web/src/masks/registry.ts @@ -1,5 +1,5 @@ -import { MAX_FEATHER } from "@/lib/masks/feather"; -import type { ParamDefinition } from "@/lib/params"; +import { MAX_FEATHER } from "@/masks/feather"; +import type { ParamDefinition } from "@/params"; import type { BaseMaskParams, MaskDefaultContext, @@ -8,9 +8,9 @@ import type { MaskSnapArgs, MaskSnapResult, MaskType, -} from "@/lib/masks/types"; +} from "@/masks/types"; import type { HugeiconsIconProps } from "@hugeicons/react"; -import { DefinitionRegistry } from "@/lib/registry"; +import { DefinitionRegistry } from "@/params/registry"; export type MaskIconProps = { icon: HugeiconsIconProps["icon"]; diff --git a/apps/web/src/lib/masks/snap.ts b/apps/web/src/masks/snap.ts similarity index 96% rename from apps/web/src/lib/masks/snap.ts rename to apps/web/src/masks/snap.ts index 7396dd82..cff62ebb 100644 --- a/apps/web/src/lib/masks/snap.ts +++ b/apps/web/src/masks/snap.ts @@ -1,5 +1,5 @@ -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import { MIN_MASK_DIMENSION } from "@/lib/masks/dimensions"; +import type { ElementBounds } from "@/preview/element-bounds"; +import { MIN_MASK_DIMENSION } from "@/masks/dimensions"; import { snapPosition, snapRotation, @@ -7,8 +7,8 @@ import { snapScaleAxes, type ScaleEdgePreference, type SnapLine, -} from "@/lib/preview/preview-snap"; -import type { RectangleMaskParams, SplitMaskParams } from "@/lib/masks/types"; +} from "@/preview/preview-snap"; +import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types"; import { isRectangleMaskParams, getMaskSnapGeometry, diff --git a/apps/web/src/lib/masks/types.ts b/apps/web/src/masks/types.ts similarity index 95% rename from apps/web/src/lib/masks/types.ts rename to apps/web/src/masks/types.ts index 5a0d126d..4f6c90aa 100644 --- a/apps/web/src/lib/masks/types.ts +++ b/apps/web/src/masks/types.ts @@ -1,12 +1,12 @@ -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import type { SnapLine } from "@/lib/preview/preview-snap"; -import type { ParamDefinition } from "@/lib/params"; -import type { CustomMaskPathPoint } from "@/lib/masks/custom-path"; +import type { ElementBounds } from "@/preview/element-bounds"; +import type { SnapLine } from "@/preview/preview-snap"; +import type { ParamDefinition } from "@/params"; +import type { CustomMaskPathPoint } from "@/masks/custom-path"; import type { TextDecoration, TextFontStyle, TextFontWeight, -} from "@/lib/text/primitives"; +} from "@/text/primitives"; export type MaskType = | "split" diff --git a/apps/web/src/hooks/use-mask-handles.ts b/apps/web/src/masks/use-mask-handles.ts similarity index 96% rename from apps/web/src/hooks/use-mask-handles.ts rename to apps/web/src/masks/use-mask-handles.ts index 917a0677..b81de4f4 100644 --- a/apps/web/src/hooks/use-mask-handles.ts +++ b/apps/web/src/masks/use-mask-handles.ts @@ -1,25 +1,25 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import { useEditor } from "@/editor/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; -import { masksRegistry } from "@/lib/masks"; +import { masksRegistry } from "@/masks"; import { parseCustomMaskHandleId, parseCustomMaskSegmentHandleId, -} from "@/lib/masks/custom-path"; -import { appendPointToCustomMask } from "@/lib/masks/definitions/custom"; +} from "@/masks/custom-path"; +import { appendPointToCustomMask } from "@/masks/definitions/custom"; import { getVisibleElementsWithBounds, type ElementBounds, -} from "@/lib/preview/element-bounds"; +} from "@/preview/element-bounds"; import { SNAP_THRESHOLD_SCREEN_PIXELS, type SnapLine, -} from "@/lib/preview/preview-snap"; -import type { SelectedMaskPointSelection } from "@/lib/selection/editor-selection"; -import type { Mask, MaskInteractionResult } from "@/lib/masks/types"; -import type { MaskableElement } from "@/lib/timeline"; -import { registerCanceller } from "@/lib/cancel-interaction"; +} from "@/preview/preview-snap"; +import type { SelectedMaskPointSelection } from "@/selection/editor-selection"; +import type { Mask, MaskInteractionResult } from "@/masks/types"; +import type { MaskableElement } from "@/timeline"; +import { registerCanceller } from "@/editor/cancel-interaction"; interface DragState { trackId: string; diff --git a/apps/web/src/lib/masks/utils.ts b/apps/web/src/masks/utils.ts similarity index 100% rename from apps/web/src/lib/masks/utils.ts rename to apps/web/src/masks/utils.ts diff --git a/apps/web/src/lib/media/audio-mastering.ts b/apps/web/src/media/audio-mastering.ts similarity index 100% rename from apps/web/src/lib/media/audio-mastering.ts rename to apps/web/src/media/audio-mastering.ts diff --git a/apps/web/src/lib/media/audio.ts b/apps/web/src/media/audio.ts similarity index 96% rename from apps/web/src/lib/media/audio.ts rename to apps/web/src/media/audio.ts index 675cc3a5..42756d49 100644 --- a/apps/web/src/lib/media/audio.ts +++ b/apps/web/src/media/audio.ts @@ -4,26 +4,26 @@ import type { LibraryAudioElement, RetimeConfig, SceneTracks, -} from "@/lib/timeline"; -import { shouldMaintainPitch } from "@/lib/retime/rate"; -import type { MediaAsset } from "@/lib/media/types"; -import { applyAudioMasteringToBuffer } from "@/lib/media/audio-mastering"; -import type { AudioCapableElement } from "@/lib/timeline/audio-state"; +} from "@/timeline"; +import { shouldMaintainPitch } from "@/retime/rate"; +import type { MediaAsset } from "@/media/types"; +import { applyAudioMasteringToBuffer } from "@/media/audio-mastering"; +import type { AudioCapableElement } from "@/timeline/audio-state"; import { hasAnimatedVolume, resolveEffectiveAudioGain, -} from "@/lib/timeline/audio-state"; -import { doesElementHaveEnabledAudio } from "@/lib/timeline/audio-separation"; -import { canElementHaveAudio, hasMediaId } from "@/lib/timeline/element-utils"; -import { canTrackHaveAudio } from "@/lib/timeline"; -import { mediaSupportsAudio } from "@/lib/media/media-utils"; -import { getSourceTimeAtClipTime, renderRetimedBuffer } from "@/lib/retime"; +} from "@/timeline/audio-state"; +import { doesElementHaveEnabledAudio } from "@/timeline/audio-separation"; +import { canElementHaveAudio, hasMediaId } from "@/timeline/element-utils"; +import { canTrackHaveAudio } from "@/timeline"; +import { mediaSupportsAudio } from "@/media/media-utils"; +import { getSourceTimeAtClipTime, renderRetimedBuffer } from "@/retime"; import { Input, ALL_FORMATS, BlobSource, AudioBufferSink } from "mediabunny"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; import { computeRmsBuckets, type SampleBucket, -} from "@/lib/media/waveform-summary"; +} from "@/media/waveform-summary"; const MAX_AUDIO_CHANNELS = 2; const EXPORT_SAMPLE_RATE = 44100; diff --git a/apps/web/src/lib/media/media-utils.ts b/apps/web/src/media/media-utils.ts similarity index 85% rename from apps/web/src/lib/media/media-utils.ts rename to apps/web/src/media/media-utils.ts index dc73da23..dd59bf31 100644 --- a/apps/web/src/lib/media/media-utils.ts +++ b/apps/web/src/media/media-utils.ts @@ -1,4 +1,4 @@ -import type { MediaAsset, MediaType } from "@/lib/media/types"; +import type { MediaAsset, MediaType } from "@/media/types"; export const SUPPORTS_AUDIO: readonly MediaType[] = ["audio", "video"]; diff --git a/apps/web/src/lib/media/mediabunny.ts b/apps/web/src/media/mediabunny.ts similarity index 95% rename from apps/web/src/lib/media/mediabunny.ts rename to apps/web/src/media/mediabunny.ts index 1e97fc42..47ffaeb1 100644 --- a/apps/web/src/lib/media/mediabunny.ts +++ b/apps/web/src/media/mediabunny.ts @@ -1,8 +1,8 @@ import { Input, ALL_FORMATS, BlobSource } from "mediabunny"; -import { createTimelineAudioBuffer } from "@/lib/media/audio"; -import type { SceneTracks } from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { createTimelineAudioBuffer } from "@/media/audio"; +import type { SceneTracks } from "@/timeline"; +import type { MediaAsset } from "@/media/types"; +import { TICKS_PER_SECOND } from "@/wasm"; export async function getVideoInfo({ videoFile, diff --git a/apps/web/src/lib/media/processing.ts b/apps/web/src/media/processing.ts similarity index 98% rename from apps/web/src/lib/media/processing.ts rename to apps/web/src/media/processing.ts index 547e7ed8..d8d483f8 100644 --- a/apps/web/src/lib/media/processing.ts +++ b/apps/web/src/media/processing.ts @@ -1,9 +1,9 @@ import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny"; import { toast } from "sonner"; -import { getMediaTypeFromFile } from "@/lib/media/media-utils"; +import { getMediaTypeFromFile } from "@/media/media-utils"; import { formatStorageBytes } from "@/services/storage/quota"; import { storageService } from "@/services/storage/service"; -import type { MediaAsset } from "@/lib/media/types"; +import type { MediaAsset } from "@/media/types"; import { getVideoInfo } from "./mediabunny"; export interface ProcessedMediaAsset extends Omit {} diff --git a/apps/web/src/lib/media/types.ts b/apps/web/src/media/types.ts similarity index 100% rename from apps/web/src/lib/media/types.ts rename to apps/web/src/media/types.ts diff --git a/apps/web/src/lib/media/upload-toast.ts b/apps/web/src/media/upload-toast.ts similarity index 100% rename from apps/web/src/lib/media/upload-toast.ts rename to apps/web/src/media/upload-toast.ts diff --git a/apps/web/src/hooks/use-file-upload.ts b/apps/web/src/media/use-file-upload.ts similarity index 97% rename from apps/web/src/hooks/use-file-upload.ts rename to apps/web/src/media/use-file-upload.ts index d174e002..1ee30ad9 100644 --- a/apps/web/src/hooks/use-file-upload.ts +++ b/apps/web/src/media/use-file-upload.ts @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { hasDragData } from "@/lib/drag-data"; +import { hasDragData } from "@/timeline/drag-data"; interface UseFileUploadOptions { accept?: string; diff --git a/apps/web/src/hooks/use-paste-media.ts b/apps/web/src/media/use-paste-media.ts similarity index 83% rename from apps/web/src/hooks/use-paste-media.ts rename to apps/web/src/media/use-paste-media.ts index 95020a04..3055920e 100644 --- a/apps/web/src/hooks/use-paste-media.ts +++ b/apps/web/src/media/use-paste-media.ts @@ -1,15 +1,15 @@ import { useEffect } from "react"; -import { useEditor } from "@/hooks/use-editor"; -import { processMediaAssets } from "@/lib/media/processing"; -import { showMediaUploadToast } from "@/lib/media/upload-toast"; -import { buildElementFromMedia } from "@/lib/timeline/element-utils"; -import { AddMediaAssetCommand } from "@/lib/commands/media"; -import { InsertElementCommand } from "@/lib/commands/timeline"; -import { BatchCommand } from "@/lib/commands"; -import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { useEditor } from "@/editor/use-editor"; +import { processMediaAssets } from "@/media/processing"; +import { showMediaUploadToast } from "@/media/upload-toast"; +import { buildElementFromMedia } from "@/timeline/element-utils"; +import { AddMediaAssetCommand } from "@/commands/media"; +import { InsertElementCommand } from "@/commands/timeline"; +import { BatchCommand } from "@/commands"; +import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation"; +import { TICKS_PER_SECOND } from "@/wasm"; import { isTypableDOMElement } from "@/utils/browser"; -import type { MediaType } from "@/lib/media/types"; +import type { MediaType } from "@/media/types"; const MEDIA_MIME_PREFIXES: MediaType[] = ["image", "video", "audio"]; diff --git a/apps/web/src/lib/media/waveform-summary.ts b/apps/web/src/media/waveform-summary.ts similarity index 97% rename from apps/web/src/lib/media/waveform-summary.ts rename to apps/web/src/media/waveform-summary.ts index 522d6e04..03a0059c 100644 --- a/apps/web/src/lib/media/waveform-summary.ts +++ b/apps/web/src/media/waveform-summary.ts @@ -1,7 +1,7 @@ "use client"; -import { getSourceTimeAtClipTime } from "@/lib/retime"; -import type { RetimeConfig } from "@/lib/timeline"; +import { getSourceTimeAtClipTime } from "@/retime"; +import type { RetimeConfig } from "@/timeline"; const RMS_ANALYSIS_WINDOW_SECONDS = 0.02; const DEFAULT_SOURCE_WAVEFORM_BUCKET_SIZE = 128; diff --git a/apps/web/src/lib/panels/layout.ts b/apps/web/src/panels/layout.ts similarity index 100% rename from apps/web/src/lib/panels/layout.ts rename to apps/web/src/panels/layout.ts diff --git a/apps/web/src/lib/params.ts b/apps/web/src/params/index.ts similarity index 100% rename from apps/web/src/lib/params.ts rename to apps/web/src/params/index.ts diff --git a/apps/web/src/lib/registry.ts b/apps/web/src/params/registry.ts similarity index 88% rename from apps/web/src/lib/registry.ts rename to apps/web/src/params/registry.ts index f1c92f67..d9fcc266 100644 --- a/apps/web/src/lib/registry.ts +++ b/apps/web/src/params/registry.ts @@ -1,4 +1,4 @@ -import type { ParamDefinition, ParamValues } from "@/lib/params"; +import type { ParamDefinition, ParamValues } from "@/params"; export function buildDefaultParamValues( params: ParamDefinition[], diff --git a/apps/web/src/components/editor/panels/preview/context-menu.tsx b/apps/web/src/preview/components/context-menu.tsx similarity index 64% rename from apps/web/src/components/editor/panels/preview/context-menu.tsx rename to apps/web/src/preview/components/context-menu.tsx index 677f35db..fca55d71 100644 --- a/apps/web/src/components/editor/panels/preview/context-menu.tsx +++ b/apps/web/src/preview/components/context-menu.tsx @@ -1,73 +1,85 @@ -"use client"; - -import { - ContextMenuCheckboxItem, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, -} from "@/components/ui/context-menu"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; -import { usePreviewStore } from "@/stores/preview-store"; -import { toast } from "sonner"; - -export function PreviewContextMenu({ - onToggleFullscreen, - containerRef, -}: { - onToggleFullscreen: () => void; - containerRef: React.RefObject; -}) { - const editor = useEditor(); - const viewport = usePreviewViewport(); - const { overlays, setOverlayVisibility } = usePreviewStore(); - - const handleCopySnapshot = async () => { - const result = await editor.renderer.copySnapshot(); - - if (!result.success) { - toast.error("Failed to copy snapshot", { - description: result.error ?? "Please try again", - }); - return; - } - }; - - const handleSaveSnapshot = async () => { - const result = await editor.renderer.saveSnapshot(); - - if (!result.success) { - toast.error("Failed to save snapshot", { - description: result.error ?? "Please try again", - }); - return; - } - }; - - return ( - - - Fit to screen - - - - Full screen - - - Save snapshot - - - Copy snapshot - - - - setOverlayVisibility({ overlay: "bookmarks", isVisible: !!checked }) - } - > - Show bookmark notes - - - ); -} +"use client"; + +import { + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, +} from "@/components/ui/context-menu"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import { useEditor } from "@/editor/use-editor"; +import type { PreviewOverlayControl } from "@/preview/overlays"; +import { toast } from "sonner"; + +export function PreviewContextMenu({ + onToggleFullscreen, + containerRef, + overlayControls, + onOverlayVisibilityChange, +}: { + onToggleFullscreen: () => void; + containerRef: React.RefObject; + overlayControls: PreviewOverlayControl[]; + onOverlayVisibilityChange: (params: { + overlayId: string; + isVisible: boolean; + }) => void; +}) { + const editor = useEditor(); + const viewport = usePreviewViewport(); + + const handleCopySnapshot = async () => { + const result = await editor.renderer.copySnapshot(); + + if (!result.success) { + toast.error("Failed to copy snapshot", { + description: result.error ?? "Please try again", + }); + return; + } + }; + + const handleSaveSnapshot = async () => { + const result = await editor.renderer.saveSnapshot(); + + if (!result.success) { + toast.error("Failed to save snapshot", { + description: result.error ?? "Please try again", + }); + return; + } + }; + + return ( + + + Fit to screen + + + + Full screen + + + Save snapshot + + + Copy snapshot + + {overlayControls.length > 0 ? : null} + {overlayControls.map((overlayControl) => ( + + onOverlayVisibilityChange({ + overlayId: overlayControl.id, + isVisible: !!checked, + }) + } + > + {overlayControl.label} + + ))} + + ); +} diff --git a/apps/web/src/components/editor/panels/preview/cursors.ts b/apps/web/src/preview/components/cursors.ts similarity index 100% rename from apps/web/src/components/editor/panels/preview/cursors.ts rename to apps/web/src/preview/components/cursors.ts diff --git a/apps/web/src/components/editor/panels/preview/guide-popover.tsx b/apps/web/src/preview/components/guide-popover.tsx similarity index 91% rename from apps/web/src/components/editor/panels/preview/guide-popover.tsx rename to apps/web/src/preview/components/guide-popover.tsx index bcac8075..5bfa92ce 100644 --- a/apps/web/src/components/editor/panels/preview/guide-popover.tsx +++ b/apps/web/src/preview/components/guide-popover.tsx @@ -1,8 +1,8 @@ "use client"; import { AnimatePresence, motion } from "motion/react"; -import { GUIDE_REGISTRY, getGuideById } from "@/lib/guides"; -import { usePreviewStore } from "@/stores/preview-store"; +import { GUIDE_REGISTRY, getGuideById } from "@/guides"; +import { usePreviewStore } from "@/preview/preview-store"; import { Popover, PopoverContent, diff --git a/apps/web/src/components/editor/panels/preview/handle-primitives.tsx b/apps/web/src/preview/components/handle-primitives.tsx similarity index 100% rename from apps/web/src/components/editor/panels/preview/handle-primitives.tsx rename to apps/web/src/preview/components/handle-primitives.tsx diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/preview/components/index.tsx similarity index 81% rename from apps/web/src/components/editor/panels/preview/index.tsx rename to apps/web/src/preview/components/index.tsx index 369d854c..d3cb2a49 100644 --- a/apps/web/src/components/editor/panels/preview/index.tsx +++ b/apps/web/src/preview/components/index.tsx @@ -2,19 +2,21 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; import useDeepCompareEffect from "use-deep-compare-effect"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { useRafLoop } from "@/hooks/use-raf-loop"; import { useContainerSize } from "@/hooks/use-container-size"; import { useFullscreen } from "@/hooks/use-fullscreen"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; import type { RootNode } from "@/services/renderer/nodes/root-node"; import { buildScene } from "@/services/renderer/scene-builder"; +import { PreviewOverlayLayer } from "./overlay-layer"; import { PreviewInteractionOverlay } from "./preview-interaction-overlay"; -import { BookmarkNoteOverlay } from "./bookmark-note-overlay"; -import { GuideOverlay } from "./guide-overlay"; import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; -import { usePreviewStore } from "@/stores/preview-store"; +import type { + PreviewOverlayControl, + PreviewOverlayInstance, +} from "@/preview/overlays"; import { PreviewContextMenu } from "./context-menu"; import { PreviewToolbar } from "./toolbar"; import { @@ -53,7 +55,18 @@ function normalizeWheelDelta({ return delta; } -export function PreviewPanel() { +export function PreviewPanel({ + overlayControls, + overlayInstances, + onOverlayVisibilityChange, +}: { + overlayControls: PreviewOverlayControl[]; + overlayInstances: PreviewOverlayInstance[]; + onOverlayVisibilityChange: (params: { + overlayId: string; + isVisible: boolean; + }) => void; +}) { const containerRef = useRef(null); const { toggleFullscreen } = useFullscreen({ containerRef }); @@ -65,6 +78,9 @@ export function PreviewPanel() { @@ -103,9 +119,18 @@ function RenderTreeController() { function PreviewCanvas({ containerRef, onToggleFullscreen, + overlayControls, + overlayInstances, + onOverlayVisibilityChange, }: { containerRef: React.RefObject; onToggleFullscreen: () => void; + overlayControls: PreviewOverlayControl[]; + overlayInstances: PreviewOverlayInstance[]; + onOverlayVisibilityChange: (params: { + overlayId: string; + isVisible: boolean; + }) => void; }) { const canvasRef = useRef(null); const viewportRef = useRef(null); @@ -117,7 +142,6 @@ function PreviewCanvas({ const editor = useEditor(); const activeProject = useEditor((e) => e.project.getActive()); const renderTree = useEditor((e) => e.renderer.getRenderTree()); - const { overlays } = usePreviewStore(); const viewport = usePreviewViewportState({ canvasHeight: nativeHeight, canvasWidth: nativeWidth, @@ -132,16 +156,18 @@ function PreviewCanvas({ height: nativeHeight, fps: activeProject.settings.fps, }); - }, [nativeWidth, nativeHeight, activeProject.settings.fps.numerator, activeProject.settings.fps.denominator]); + }, [nativeWidth, nativeHeight, activeProject.settings.fps]); const render = useCallback(() => { if (canvasRef.current && renderTree && !renderingRef.current) { - const renderTime = Math.min( - editor.playback.getCurrentTime(), - editor.timeline.getLastFrameTime(), - ); - const ticksPerFrame = Math.round(TICKS_PER_SECOND * renderer.fps.denominator / renderer.fps.numerator); - const frame = Math.floor(renderTime / ticksPerFrame); + const renderTime = Math.min( + editor.playback.getCurrentTime(), + editor.timeline.getLastFrameTime(), + ); + const ticksPerFrame = Math.round( + (TICKS_PER_SECOND * renderer.fps.denominator) / renderer.fps.numerator, + ); + const frame = Math.floor(renderTime / ticksPerFrame); if ( frame !== lastFrameRef.current || @@ -161,7 +187,7 @@ function PreviewCanvas({ }); } } - }, [renderer, renderTree, editor.playback]); + }, [renderer, renderTree, editor.playback, editor.timeline]); useRafLoop(render); @@ -276,14 +302,22 @@ function PreviewCanvas({ : activeProject?.settings.background.color, }} /> - + - {overlays.bookmarks && } + diff --git a/apps/web/src/components/editor/panels/preview/mask-handles.tsx b/apps/web/src/preview/components/mask-handles.tsx similarity index 96% rename from apps/web/src/components/editor/panels/preview/mask-handles.tsx rename to apps/web/src/preview/components/mask-handles.tsx index d32b6ff9..340cab0a 100644 --- a/apps/web/src/components/editor/panels/preview/mask-handles.tsx +++ b/apps/web/src/preview/components/mask-handles.tsx @@ -1,9 +1,9 @@ "use client"; -import { PEN_CURSOR } from "@/components/editor/panels/preview/cursors"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useMaskHandles } from "@/hooks/use-mask-handles"; -import type { SnapLine } from "@/lib/preview/preview-snap"; +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 { CornerHandle, CircleHandle, diff --git a/apps/web/src/preview/components/overlay-layer.tsx b/apps/web/src/preview/components/overlay-layer.tsx new file mode 100644 index 00000000..eb730f6e --- /dev/null +++ b/apps/web/src/preview/components/overlay-layer.tsx @@ -0,0 +1,197 @@ +"use client"; + +import type { + PreviewOverlayHudAnchor, + PreviewOverlayInstance, + PreviewOverlayPlane, +} from "@/preview/overlays"; +import { usePreviewViewport } from "./preview-viewport"; + +const HUD_ANCHOR_CLASS_NAMES: Record = { + "top-left": "absolute top-2 left-2 flex flex-col items-start gap-1.5", + "top-right": "absolute top-2 right-2 flex flex-col items-end gap-1.5", + "bottom-left": + "absolute bottom-2 left-2 flex flex-col-reverse items-start gap-1.5", + "bottom-right": + "absolute right-2 bottom-2 flex flex-col-reverse items-end gap-1.5", +}; + +function getPositionedMountStyle({ + instance, + baseStyle, + sceneLeft, + sceneTop, + sceneWidth, + sceneHeight, +}: { + instance: PreviewOverlayInstance; + baseStyle: { + pointerEvents: "none" | "auto"; + zIndex: number | undefined; + }; + sceneLeft: number; + sceneTop: number; + sceneWidth: number; + sceneHeight: number; +}) { + if (instance.mount.kind === "hud") { + return baseStyle; + } + + const isSceneMount = instance.mount.kind === "scene"; + const mount = instance.mount; + const hasExplicitBounds = + mount.x != null || + mount.y != null || + mount.width != null || + mount.height != null; + + return { + ...baseStyle, + left: (isSceneMount ? sceneLeft : 0) + (mount.x ?? 0), + top: (isSceneMount ? sceneTop : 0) + (mount.y ?? 0), + ...(hasExplicitBounds + ? { + ...(mount.width != null ? { width: mount.width } : {}), + ...(mount.height != null ? { height: mount.height } : {}), + } + : { + width: isSceneMount ? sceneWidth : "100%", + height: isSceneMount ? sceneHeight : "100%", + }), + } as const; +} + +export function PreviewOverlayLayer({ + instances, + plane, +}: { + instances: PreviewOverlayInstance[]; + plane: PreviewOverlayPlane; +}) { + const viewport = usePreviewViewport(); + const visibleInstances = instances + .filter((instance) => (instance.plane ?? "over-interaction") === plane) + .sort((left, right) => (left.zIndex ?? 0) - (right.zIndex ?? 0)); + + if (visibleInstances.length === 0) { + return null; + } + + const hudInstancesByAnchor = visibleInstances.reduce< + Record + >( + (groups, instance) => { + if (instance.mount.kind === "hud") { + groups[instance.mount.anchor].push(instance); + } + return groups; + }, + { + "top-left": [], + "top-right": [], + "bottom-left": [], + "bottom-right": [], + }, + ); + + return ( + <> + {visibleInstances.map((instance) => { + if (instance.mount.kind === "hud") { + return null; + } + + const pointerEvents = instance.pointerEvents ?? "none"; + const baseStyle = { + pointerEvents, + zIndex: instance.zIndex, + } as const; + const content = instance.render({ + sceneHeight: viewport.sceneHeight, + sceneWidth: viewport.sceneWidth, + }); + + switch (instance.mount.kind) { + case "scene": + return ( +
+ {content} +
+ ); + + case "viewport": + return ( +
+ {content} +
+ ); + + default: + return null; + } + })} + {( + Object.entries(hudInstancesByAnchor) as Array< + [PreviewOverlayHudAnchor, PreviewOverlayInstance[]] + > + ).map(([anchor, anchorInstances]) => { + if (anchorInstances.length === 0) { + return null; + } + + const sortedAnchorInstances = [...anchorInstances].sort( + (left, right) => + (left.mount.kind === "hud" ? (left.mount.order ?? 0) : 0) - + (right.mount.kind === "hud" ? (right.mount.order ?? 0) : 0), + ); + + return ( +
+ {sortedAnchorInstances.map((instance) => { + const pointerEvents = instance.pointerEvents ?? "none"; + const content = instance.render({ + sceneHeight: viewport.sceneHeight, + sceneWidth: viewport.sceneWidth, + }); + + return ( +
+ {content} +
+ ); + })} +
+ ); + })} + + ); +} diff --git a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx b/apps/web/src/preview/components/preview-interaction-overlay.tsx similarity index 87% rename from apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx rename to apps/web/src/preview/components/preview-interaction-overlay.tsx index 6ee4b584..e16a31d2 100644 --- a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx +++ b/apps/web/src/preview/components/preview-interaction-overlay.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { usePreviewInteraction } from "@/hooks/use-preview-interaction"; -import type { SnapLine } from "@/lib/preview/preview-snap"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import { usePreviewInteraction } from "@/preview/hooks/use-preview-interaction"; +import type { SnapLine } from "@/preview/preview-snap"; import { TransformHandles } from "./transform-handles"; import { MaskHandles } from "./mask-handles"; import { SnapGuides } from "./snap-guides"; import { TextEditOverlay } from "./text-edit-overlay"; -import { usePropertiesStore } from "../properties/stores/properties-store"; -import { useEditor } from "@/hooks/use-editor"; +import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store"; +import { useEditor } from "@/editor/use-editor"; export function PreviewInteractionOverlay() { const [snapLines, setSnapLines] = useState([]); diff --git a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx b/apps/web/src/preview/components/preview-viewport.tsx similarity index 94% rename from apps/web/src/components/editor/panels/preview/preview-viewport.tsx rename to apps/web/src/preview/components/preview-viewport.tsx index 2ac6bd92..6dc9f084 100644 --- a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx +++ b/apps/web/src/preview/components/preview-viewport.tsx @@ -15,9 +15,9 @@ import { positionToOverlay, screenPixelsToLogicalThreshold, screenToCanvas, -} from "@/lib/preview/preview-coords"; +} from "@/preview/preview-coords"; import { clamp, isNearlyEqual } from "@/utils/math"; -import { PREVIEW_ZOOM } from "@/lib/preview/zoom"; +import { PREVIEW_ZOOM } from "@/preview/zoom"; const MIDDLE_MOUSE_BUTTON = 1; const IS_AT_FIT_EPSILON = 0.001; diff --git a/apps/web/src/components/editor/panels/preview/snap-guides.tsx b/apps/web/src/preview/components/snap-guides.tsx similarity index 83% rename from apps/web/src/components/editor/panels/preview/snap-guides.tsx rename to apps/web/src/preview/components/snap-guides.tsx index ced5e921..5c2e1b9f 100644 --- a/apps/web/src/components/editor/panels/preview/snap-guides.tsx +++ b/apps/web/src/preview/components/snap-guides.tsx @@ -1,7 +1,7 @@ "use client"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import type { SnapLine } from "@/lib/preview/preview-snap"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import type { SnapLine } from "@/preview/preview-snap"; export function SnapGuides({ lines }: { lines: SnapLine[] }) { const viewport = usePreviewViewport(); diff --git a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx b/apps/web/src/preview/components/text-edit-overlay.tsx similarity index 92% rename from apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx rename to apps/web/src/preview/components/text-edit-overlay.tsx index eabbde8a..bc27c724 100644 --- a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx +++ b/apps/web/src/preview/components/text-edit-overlay.tsx @@ -1,15 +1,15 @@ "use client"; import { useCallback, useEffect, useRef } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; -import type { TextElement } from "@/lib/timeline"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import { useEditor } from "@/editor/use-editor"; +import type { TextElement } from "@/timeline"; +import { DEFAULTS } from "@/timeline/defaults"; import { getElementLocalTime, resolveTransformAtTime, -} from "@/lib/animation"; -import { resolveTextLayout } from "@/lib/text/primitives"; +} from "@/animation"; +import { resolveTextLayout } from "@/text/primitives"; export function TextEditOverlay({ trackId, diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/preview/components/toolbar.tsx similarity index 93% rename from apps/web/src/components/editor/panels/preview/toolbar.tsx rename to apps/web/src/preview/components/toolbar.tsx index 9ee6c822..b44e42ef 100644 --- a/apps/web/src/components/editor/panels/preview/toolbar.tsx +++ b/apps/web/src/preview/components/toolbar.tsx @@ -1,9 +1,9 @@ "use client"; import { useState, useEffect } from "react"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { formatTimecode } from "opencut-wasm"; -import { invokeAction } from "@/lib/actions"; +import { invokeAction } from "@/actions"; import { EditableTimecode } from "@/components/editable-timecode"; import { Button } from "@/components/ui/button"; import { @@ -13,7 +13,7 @@ import { PlayIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { getGuideById } from "@/lib/guides"; +import { getGuideById } from "@/guides"; import { Separator } from "@/components/ui/separator"; import { Select, @@ -22,10 +22,10 @@ import { SelectItem, SelectSeparator, } from "@/components/ui/select"; -import { PREVIEW_ZOOM_PRESETS } from "@/lib/preview/zoom"; +import { PREVIEW_ZOOM_PRESETS } from "@/preview/zoom"; import { usePreviewViewport } from "./preview-viewport"; import { GridPopover } from "./guide-popover"; -import { usePreviewStore } from "@/stores/preview-store"; +import { usePreviewStore } from "@/preview/preview-store"; export function PreviewToolbar({ onToggleFullscreen, diff --git a/apps/web/src/components/editor/panels/preview/transform-handles.tsx b/apps/web/src/preview/components/transform-handles.tsx similarity index 91% rename from apps/web/src/components/editor/panels/preview/transform-handles.tsx rename to apps/web/src/preview/components/transform-handles.tsx index 2b81b557..ad496cbc 100644 --- a/apps/web/src/components/editor/panels/preview/transform-handles.tsx +++ b/apps/web/src/preview/components/transform-handles.tsx @@ -1,16 +1,16 @@ "use client"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useTransformHandles } from "@/hooks/use-transform-handles"; -import { isVisualElement } from "@/lib/timeline/element-utils"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import { useTransformHandles } from "@/preview/hooks/use-transform-handles"; +import { isVisualElement } from "@/timeline/element-utils"; import { getCornerPosition, getEdgeHandlePosition, ROTATION_HANDLE_OFFSET, type Corner, type Edge, -} from "@/lib/preview/element-bounds"; -import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction"; +} from "@/preview/element-bounds"; +import type { OnSnapLinesChange } from "@/preview/hooks/use-preview-interaction"; import { BoundingBoxOutline, CornerHandle, diff --git a/apps/web/src/lib/preview/element-bounds.ts b/apps/web/src/preview/element-bounds.ts similarity index 91% rename from apps/web/src/lib/preview/element-bounds.ts rename to apps/web/src/preview/element-bounds.ts index 39595c33..ac556181 100644 --- a/apps/web/src/lib/preview/element-bounds.ts +++ b/apps/web/src/preview/element-bounds.ts @@ -1,12 +1,12 @@ -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size"; -import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics"; -import { measureTextElement } from "@/lib/text/measure-element"; +import type { SceneTracks, TimelineElement } from "@/timeline"; +import type { MediaAsset } from "@/media/types"; +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; +import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics"; +import { measureTextElement } from "@/text/measure-element"; import { getElementLocalTime, resolveTransformAtTime, -} from "@/lib/animation"; +} from "@/animation"; export interface ElementBounds { cx: number; diff --git a/apps/web/src/lib/preview/hit-test.ts b/apps/web/src/preview/hit-test.ts similarity index 97% rename from apps/web/src/lib/preview/hit-test.ts rename to apps/web/src/preview/hit-test.ts index 2552922b..0f5fd45e 100644 --- a/apps/web/src/lib/preview/hit-test.ts +++ b/apps/web/src/preview/hit-test.ts @@ -1,5 +1,5 @@ import type { ElementWithBounds } from "./element-bounds"; -import type { ElementRef } from "@/lib/timeline/types"; +import type { ElementRef } from "@/timeline/types"; function pointInRotatedRect({ px, diff --git a/apps/web/src/hooks/use-preview-interaction.ts b/apps/web/src/preview/hooks/use-preview-interaction.ts similarity index 95% rename from apps/web/src/hooks/use-preview-interaction.ts rename to apps/web/src/preview/hooks/use-preview-interaction.ts index cfb3d527..78212e74 100644 --- a/apps/web/src/hooks/use-preview-interaction.ts +++ b/apps/web/src/preview/hooks/use-preview-interaction.ts @@ -1,25 +1,25 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import type { Transform } from "@/lib/rendering"; -import type { ElementRef, TextElement } from "@/lib/timeline"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import type { Transform } from "@/rendering"; +import type { ElementRef, TextElement } from "@/timeline"; import { getVisibleElementsWithBounds, type ElementWithBounds, -} from "@/lib/preview/element-bounds"; +} from "@/preview/element-bounds"; import { getHitElements, hitTest, resolvePreferredHit, -} from "@/lib/preview/hit-test"; -import { isVisualElement } from "@/lib/timeline/element-utils"; +} from "@/preview/hit-test"; +import { isVisualElement } from "@/timeline/element-utils"; import { SNAP_THRESHOLD_SCREEN_PIXELS, snapPosition, type SnapLine, -} from "@/lib/preview/preview-snap"; -import { registerCanceller } from "@/lib/cancel-interaction"; +} from "@/preview/preview-snap"; +import { registerCanceller } from "@/editor/cancel-interaction"; export type OnSnapLinesChange = (lines: SnapLine[]) => void; diff --git a/apps/web/src/hooks/use-transform-handles.ts b/apps/web/src/preview/hooks/use-transform-handles.ts similarity index 93% rename from apps/web/src/hooks/use-transform-handles.ts rename to apps/web/src/preview/hooks/use-transform-handles.ts index 7495a387..030e026c 100644 --- a/apps/web/src/hooks/use-transform-handles.ts +++ b/apps/web/src/preview/hooks/use-transform-handles.ts @@ -1,12 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction"; -import { useEditor } from "@/hooks/use-editor"; +import { usePreviewViewport } from "@/preview/components/preview-viewport"; +import type { OnSnapLinesChange } from "@/preview/hooks/use-preview-interaction"; +import { useEditor } from "@/editor/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; import { getVisibleElementsWithBounds, type ElementWithBounds, -} from "@/lib/preview/element-bounds"; +} from "@/preview/element-bounds"; import { MIN_SCALE, SNAP_THRESHOLD_SCREEN_PIXELS, @@ -15,27 +15,23 @@ import { snapScaleAxes, type ScaleEdgePreference, type SnapLine, -} from "@/lib/preview/preview-snap"; -import { isVisualElement } from "@/lib/timeline/element-utils"; +} from "@/preview/preview-snap"; +import { isVisualElement } from "@/timeline/element-utils"; import { getElementLocalTime, hasKeyframesForPath, resolveTransformAtTime, setChannel, -} from "@/lib/animation"; -import type { Transform } from "@/lib/rendering"; -import type { ElementAnimations } from "@/lib/animation/types"; -import { registerCanceller } from "@/lib/cancel-interaction"; +} from "@/animation"; +import type { Transform } from "@/rendering"; +import type { ElementAnimations } from "@/animation/types"; +import { registerCanceller } from "@/editor/cancel-interaction"; type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; type Edge = "right" | "left" | "bottom"; type HandleType = Corner | Edge | "rotation"; -function getPreferredEdge({ - edge, -}: { - edge: Edge; -}): ScaleEdgePreference { +function getPreferredEdge({ edge }: { edge: Edge }): ScaleEdgePreference { return edge === "right" ? { right: true } : edge === "left" @@ -191,7 +187,12 @@ export function useTransformHandles({ releaseCapturedPointer(); }, }); - }, [activeHandle, clearActiveHandleState, editor.timeline, releaseCapturedPointer]); + }, [ + activeHandle, + clearActiveHandleState, + editor.timeline, + releaseCapturedPointer, + ]); const handleCornerPointerDown = useCallback( ({ event, corner }: { event: React.PointerEvent; corner: Corner }) => { @@ -333,11 +334,10 @@ export function useTransformHandles({ edge === "right" || edge === "left" ? "transform.scaleX" : "transform.scaleY"; - const shouldClearScaleAnimation = - hasKeyframesForPath({ - animations: element.animations, - propertyPath, - }); + const shouldClearScaleAnimation = hasKeyframesForPath({ + animations: element.animations, + propertyPath, + }); const animationsWithoutScale = shouldClearScaleAnimation ? setChannel({ animations: element.animations, @@ -605,18 +605,16 @@ export function useTransformHandles({ ); const handlePointerUp = useCallback(() => { - if ( - scaleStateRef.current || - rotationStateRef.current || - edgeScaleStateRef.current - ) { - editor.timeline.commitPreview(); - clearActiveHandleState(); - } - releaseCapturedPointer(); - }, - [clearActiveHandleState, editor, releaseCapturedPointer], - ); + if ( + scaleStateRef.current || + rotationStateRef.current || + edgeScaleStateRef.current + ) { + editor.timeline.commitPreview(); + clearActiveHandleState(); + } + releaseCapturedPointer(); + }, [clearActiveHandleState, editor, releaseCapturedPointer]); return { selectedWithBounds, diff --git a/apps/web/src/preview/overlays.ts b/apps/web/src/preview/overlays.ts new file mode 100644 index 00000000..6e5b0816 --- /dev/null +++ b/apps/web/src/preview/overlays.ts @@ -0,0 +1,108 @@ +import type { ReactNode } from "react"; + +export type PreviewOverlayHudAnchor = + | "top-left" + | "top-right" + | "bottom-left" + | "bottom-right"; + +export type PreviewOverlayMount = + | { + kind: "hud"; + anchor: PreviewOverlayHudAnchor; + order?: number; + } + | { + kind: "scene"; + x?: number; + y?: number; + width?: number; + height?: number; + } + | { + kind: "viewport"; + x?: number; + y?: number; + width?: number; + height?: number; + }; + +export type PreviewOverlayPlane = "under-interaction" | "over-interaction"; + +export interface PreviewOverlayRenderContext { + sceneHeight: number; + sceneWidth: number; +} + +export interface PreviewOverlayInstance { + id: string; + mount: PreviewOverlayMount; + plane?: PreviewOverlayPlane; + pointerEvents?: "none" | "auto"; + zIndex?: number; + render: (context: PreviewOverlayRenderContext) => ReactNode; +} + +export interface PreviewOverlayDefinition { + id: string; + label: string; + defaultVisible?: boolean; +} + +export interface PreviewOverlayControl extends PreviewOverlayDefinition { + isVisible: boolean; +} + +export interface PreviewOverlaySourceResult { + definitions: PreviewOverlayDefinition[]; + instances: PreviewOverlayInstance[]; +} + +export const EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT: PreviewOverlaySourceResult = { + definitions: [], + instances: [], +}; + +export function isPreviewOverlayVisible({ + overlay, + overlays, +}: { + overlay: PreviewOverlayDefinition; + overlays: Record; +}): boolean { + return overlays[overlay.id] ?? overlay.defaultVisible ?? true; +} + +export function createPreviewOverlayControl({ + overlay, + overlays, +}: { + overlay: PreviewOverlayDefinition; + overlays: Record; +}): PreviewOverlayControl { + return { + ...overlay, + isVisible: isPreviewOverlayVisible({ overlay, overlays }), + }; +} + +export function mergePreviewOverlaySources({ + sources, +}: { + sources: PreviewOverlaySourceResult[]; +}): PreviewOverlaySourceResult { + const definitionsById = new Map(); + const instances: PreviewOverlayInstance[] = []; + + for (const source of sources) { + for (const definition of source.definitions) { + definitionsById.set(definition.id, definition); + } + instances.push(...source.instances); + } + + return { + definitions: [...definitionsById.values()], + instances, + }; +} diff --git a/apps/web/src/lib/preview/preview-coords.ts b/apps/web/src/preview/preview-coords.ts similarity index 100% rename from apps/web/src/lib/preview/preview-coords.ts rename to apps/web/src/preview/preview-coords.ts diff --git a/apps/web/src/lib/preview/preview-snap.ts b/apps/web/src/preview/preview-snap.ts similarity index 100% rename from apps/web/src/lib/preview/preview-snap.ts rename to apps/web/src/preview/preview-snap.ts diff --git a/apps/web/src/stores/preview-store.ts b/apps/web/src/preview/preview-store.ts similarity index 71% rename from apps/web/src/stores/preview-store.ts rename to apps/web/src/preview/preview-store.ts index b5957cb5..0cfcbdff 100644 --- a/apps/web/src/stores/preview-store.ts +++ b/apps/web/src/preview/preview-store.ts @@ -1,112 +1,104 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { isGuideId, type GuideId } from "@/lib/guides"; -import { DEFAULT_GRID_CONFIG } from "@/lib/guides/grid"; -import type { GridConfig } from "@/lib/guides/types"; - -interface PreviewOverlaysState { - bookmarks: boolean; -} - -interface PersistedPreviewState { - activeGuide?: string | null; - layoutGuide?: { - platform?: string | null; - }; - overlays?: PreviewOverlaysState; - gridConfig?: GridConfig; -} - -interface PreviewState { - activeGuide: GuideId | null; - overlays: PreviewOverlaysState; - gridConfig: GridConfig; - toggleGuide: (guideId: GuideId) => void; - setGridConfig: (config: Partial) => void; - setOverlayVisibility: ({ - overlay, - isVisible, - }: { - overlay: keyof PreviewOverlaysState; - isVisible: boolean; - }) => void; - toggleOverlayVisibility: ({ - overlay, - }: { - overlay: keyof PreviewOverlaysState; - }) => void; -} - -const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = { - bookmarks: true, -}; - -function getPersistedActiveGuide( - state: PersistedPreviewState | undefined, -): GuideId | null { - const persistedGuide = - state?.activeGuide ?? state?.layoutGuide?.platform ?? null; - - if (typeof persistedGuide !== "string") { - return null; - } - - return isGuideId(persistedGuide) ? persistedGuide : null; -} - -export const usePreviewStore = create()( - persist( - (set) => ({ - activeGuide: null, - overlays: DEFAULT_PREVIEW_OVERLAYS, - gridConfig: DEFAULT_GRID_CONFIG, - toggleGuide: (guideId) => { - set((state) => ({ - activeGuide: state.activeGuide === guideId ? null : guideId, - })); - }, - setGridConfig: (config) => { - set((state) => ({ - gridConfig: { ...state.gridConfig, ...config }, - })); - }, - setOverlayVisibility: ({ overlay, isVisible }) => { - set((state) => ({ - overlays: { - ...state.overlays, - [overlay]: isVisible, - }, - })); - }, - toggleOverlayVisibility: ({ overlay }) => { - set((state) => ({ - overlays: { - ...state.overlays, - [overlay]: !state.overlays[overlay], - }, - })); - }, - }), - { - name: "preview-settings", - version: 4, - migrate: (persistedState) => { - const state = persistedState as PersistedPreviewState | undefined; - - return { - activeGuide: getPersistedActiveGuide(state), - overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS, - gridConfig: { - rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows, - cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols, - }, - }; - }, - partialize: (state) => ({ - activeGuide: state.activeGuide, - overlays: state.overlays, - gridConfig: state.gridConfig, - }), - }, - ), -); +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { isGuideId, type GuideId } from "@/guides"; +import { DEFAULT_GRID_CONFIG } from "@/guides/grid"; +import type { GridConfig } from "@/guides/types"; + +type PreviewOverlaysState = Record; + +interface PersistedPreviewState { + activeGuide?: string | null; + layoutGuide?: { + platform?: string | null; + }; + overlays?: PreviewOverlaysState; + gridConfig?: GridConfig; +} + +interface PreviewState { + activeGuide: GuideId | null; + overlays: PreviewOverlaysState; + gridConfig: GridConfig; + toggleGuide: (guideId: GuideId) => void; + setGridConfig: (config: Partial) => void; + setOverlayVisibility: ({ + overlayId, + isVisible, + }: { + overlayId: string; + isVisible: boolean; + }) => void; + toggleOverlayVisibility: ({ overlayId }: { overlayId: string }) => void; +} + +const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {}; + +function getPersistedActiveGuide( + state: PersistedPreviewState | undefined, +): GuideId | null { + const persistedGuide = + state?.activeGuide ?? state?.layoutGuide?.platform ?? null; + + if (typeof persistedGuide !== "string") { + return null; + } + + return isGuideId(persistedGuide) ? persistedGuide : null; +} + +export const usePreviewStore = create()( + persist( + (set) => ({ + activeGuide: null, + overlays: DEFAULT_PREVIEW_OVERLAYS, + gridConfig: DEFAULT_GRID_CONFIG, + toggleGuide: (guideId) => { + set((state) => ({ + activeGuide: state.activeGuide === guideId ? null : guideId, + })); + }, + setGridConfig: (config) => { + set((state) => ({ + gridConfig: { ...state.gridConfig, ...config }, + })); + }, + setOverlayVisibility: ({ overlayId, isVisible }) => { + set((state) => ({ + overlays: { + ...state.overlays, + [overlayId]: isVisible, + }, + })); + }, + toggleOverlayVisibility: ({ overlayId }) => { + set((state) => ({ + overlays: { + ...state.overlays, + [overlayId]: !state.overlays[overlayId], + }, + })); + }, + }), + { + name: "preview-settings", + version: 6, + migrate: (persistedState) => { + const state = persistedState as PersistedPreviewState | undefined; + + return { + activeGuide: getPersistedActiveGuide(state), + overlays: DEFAULT_PREVIEW_OVERLAYS, + gridConfig: { + rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows, + cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols, + }, + }; + }, + partialize: (state) => ({ + activeGuide: state.activeGuide, + overlays: state.overlays, + gridConfig: state.gridConfig, + }), + }, + ), +); diff --git a/apps/web/src/lib/preview/zoom.ts b/apps/web/src/preview/zoom.ts similarity index 100% rename from apps/web/src/lib/preview/zoom.ts rename to apps/web/src/preview/zoom.ts diff --git a/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx b/apps/web/src/project/components/delete-project-dialog.tsx similarity index 100% rename from apps/web/src/components/editor/dialogs/delete-project-dialog.tsx rename to apps/web/src/project/components/delete-project-dialog.tsx diff --git a/apps/web/src/components/editor/dialogs/migration-dialog.tsx b/apps/web/src/project/components/migration-dialog.tsx similarity index 96% rename from apps/web/src/components/editor/dialogs/migration-dialog.tsx rename to apps/web/src/project/components/migration-dialog.tsx index 4c9550c2..4aac26d5 100644 --- a/apps/web/src/components/editor/dialogs/migration-dialog.tsx +++ b/apps/web/src/project/components/migration-dialog.tsx @@ -7,7 +7,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { Loader2 } from "lucide-react"; export function MigrationDialog() { diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/project/components/project-info-dialog.tsx similarity index 97% rename from apps/web/src/components/editor/dialogs/project-info-dialog.tsx rename to apps/web/src/project/components/project-info-dialog.tsx index ed4eb990..f0ea364e 100644 --- a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx +++ b/apps/web/src/project/components/project-info-dialog.tsx @@ -6,7 +6,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import type { TProjectMetadata } from "@/lib/project/types"; +import type { TProjectMetadata } from "@/project/types"; import { formatDate } from "@/utils/date"; import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm"; import { Button } from "@/components/ui/button"; diff --git a/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx b/apps/web/src/project/components/rename-project-dialog.tsx similarity index 100% rename from apps/web/src/components/editor/dialogs/rename-project-dialog.tsx rename to apps/web/src/project/components/rename-project-dialog.tsx diff --git a/apps/web/src/lib/project/types.ts b/apps/web/src/project/types.ts similarity index 95% rename from apps/web/src/lib/project/types.ts rename to apps/web/src/project/types.ts index 0a454e07..42e844f6 100644 --- a/apps/web/src/lib/project/types.ts +++ b/apps/web/src/project/types.ts @@ -1,5 +1,5 @@ import type { FrameRate } from "opencut-wasm"; -import type { TScene } from "@/lib/timeline/types"; +import type { TScene } from "@/timeline/types"; export type TBackground = | { diff --git a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx b/apps/web/src/rendering/components/blending-tab.tsx similarity index 89% rename from apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx rename to apps/web/src/rendering/components/blending-tab.tsx index f8e5397d..3e0db46c 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx +++ b/apps/web/src/rendering/components/blending-tab.tsx @@ -1,9 +1,9 @@ -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { clamp } from "@/utils/math"; import { NumberField } from "@/components/ui/number-field"; import { OcCheckerboardIcon } from "@/components/icons"; import { Fragment, useRef } from "react"; -import { useMenuPreview } from "@/hooks/use-menu-preview"; +import { useMenuPreview } from "@/editor/use-menu-preview"; import { Section, SectionContent, @@ -19,16 +19,16 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import type { BlendMode } from "@/lib/rendering"; -import type { ElementType } from "@/lib/timeline"; -import type { ElementAnimations } from "@/lib/animation/types"; +import type { BlendMode } from "@/rendering"; +import type { ElementType } from "@/timeline"; +import type { ElementAnimations } from "@/animation/types"; import { HugeiconsIcon } from "@hugeicons/react"; import { RainDropIcon } from "@hugeicons/core-free-icons"; -import { KeyframeToggle } from "../components/keyframe-toggle"; -import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; -import { useElementPlayhead } from "../hooks/use-element-playhead"; -import { resolveOpacityAtTime } from "@/lib/animation"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle"; +import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property"; +import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead"; +import { resolveOpacityAtTime } from "@/animation"; +import { DEFAULTS } from "@/timeline/defaults"; import { isPropertyAtDefault } from "./transform-tab"; type BlendingElement = { diff --git a/apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx b/apps/web/src/rendering/components/transform-tab.tsx similarity index 95% rename from apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx rename to apps/web/src/rendering/components/transform-tab.tsx index c3cef2d6..60d4c7df 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx +++ b/apps/web/src/rendering/components/transform-tab.tsx @@ -1,7 +1,7 @@ import { NumberField } from "@/components/ui/number-field"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { clamp, isNearlyEqual } from "@/utils/math"; -import type { VisualElement } from "@/lib/timeline"; +import type { VisualElement } from "@/timeline"; import { Section, SectionContent, @@ -21,12 +21,12 @@ import { getGroupKeyframesAtTime, hasGroupKeyframeAtTime, resolveTransformAtTime, -} from "@/lib/animation"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { useElementPlayhead } from "../hooks/use-element-playhead"; -import { KeyframeToggle } from "../components/keyframe-toggle"; -import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; -import { usePropertiesStore } from "../stores/properties-store"; +} from "@/animation"; +import { DEFAULTS } from "@/timeline/defaults"; +import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead"; +import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle"; +import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property"; +import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store"; export function parseNumericInput({ input }: { input: string }): number | null { const parsed = parseFloat(input); diff --git a/apps/web/src/lib/rendering.ts b/apps/web/src/rendering/index.ts similarity index 100% rename from apps/web/src/lib/rendering.ts rename to apps/web/src/rendering/index.ts diff --git a/apps/web/src/lib/retime/__tests__/resolve.test.ts b/apps/web/src/retime/__tests__/resolve.test.ts similarity index 92% rename from apps/web/src/lib/retime/__tests__/resolve.test.ts rename to apps/web/src/retime/__tests__/resolve.test.ts index 2fbd6675..bb7b9a6d 100644 --- a/apps/web/src/lib/retime/__tests__/resolve.test.ts +++ b/apps/web/src/retime/__tests__/resolve.test.ts @@ -4,8 +4,8 @@ import { getEffectiveRateAt, getSourceTimeAtClipTime, getTimelineDurationForSourceSpan, -} from "@/lib/retime"; -import type { RetimeConfig } from "@/lib/timeline"; +} from "@/retime"; +import type { RetimeConfig } from "@/timeline"; const twoX: RetimeConfig = { rate: 2 }; const halfX: RetimeConfig = { rate: 0.5 }; diff --git a/apps/web/src/lib/retime/__tests__/split.test.ts b/apps/web/src/retime/__tests__/split.test.ts similarity index 90% rename from apps/web/src/lib/retime/__tests__/split.test.ts rename to apps/web/src/retime/__tests__/split.test.ts index a00eadac..3c7bef63 100644 --- a/apps/web/src/lib/retime/__tests__/split.test.ts +++ b/apps/web/src/retime/__tests__/split.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test"; import { getSourceSpanAtClipTime, splitRetimeAtClipTime, -} from "@/lib/retime"; -import type { RetimeConfig } from "@/lib/timeline"; +} from "@/retime"; +import type { RetimeConfig } from "@/timeline"; describe("retime split", () => { test("measures source span at a clip time", () => { diff --git a/apps/web/src/lib/retime/audio-stretch.ts b/apps/web/src/retime/audio-stretch.ts similarity index 93% rename from apps/web/src/lib/retime/audio-stretch.ts rename to apps/web/src/retime/audio-stretch.ts index 92902dff..7df67f36 100644 --- a/apps/web/src/lib/retime/audio-stretch.ts +++ b/apps/web/src/retime/audio-stretch.ts @@ -1,6 +1,6 @@ import { PitchShifter } from "soundtouchjs"; -import { clampRetimeRate, shouldMaintainPitch } from "@/lib/retime/rate"; -import type { RetimeConfig } from "@/lib/timeline"; +import { clampRetimeRate, shouldMaintainPitch } from "@/retime/rate"; +import type { RetimeConfig } from "@/timeline"; import { getSourceTimeAtClipTime } from "./resolve"; const RATE_EPSILON = 1e-6; diff --git a/apps/web/src/lib/retime/index.ts b/apps/web/src/retime/index.ts similarity index 100% rename from apps/web/src/lib/retime/index.ts rename to apps/web/src/retime/index.ts diff --git a/apps/web/src/lib/retime/presets.ts b/apps/web/src/retime/presets.ts similarity index 62% rename from apps/web/src/lib/retime/presets.ts rename to apps/web/src/retime/presets.ts index fbd391ff..816c8675 100644 --- a/apps/web/src/lib/retime/presets.ts +++ b/apps/web/src/retime/presets.ts @@ -1,5 +1,5 @@ -import type { RetimeConfig } from "@/lib/timeline"; -import { clampRetimeRate } from "@/lib/retime/rate"; +import type { RetimeConfig } from "@/timeline"; +import { clampRetimeRate } from "@/retime/rate"; export function buildConstantRetime({ rate, diff --git a/apps/web/src/lib/retime/rate.ts b/apps/web/src/retime/rate.ts similarity index 100% rename from apps/web/src/lib/retime/rate.ts rename to apps/web/src/retime/rate.ts diff --git a/apps/web/src/lib/retime/resolve.ts b/apps/web/src/retime/resolve.ts similarity index 85% rename from apps/web/src/lib/retime/resolve.ts rename to apps/web/src/retime/resolve.ts index 5e7236d5..0dea9dd8 100644 --- a/apps/web/src/lib/retime/resolve.ts +++ b/apps/web/src/retime/resolve.ts @@ -1,5 +1,5 @@ -import type { RetimeConfig } from "@/lib/timeline"; -import { clampRetimeRate } from "@/lib/retime/rate"; +import type { RetimeConfig } from "@/timeline"; +import { clampRetimeRate } from "@/retime/rate"; function getSafeRate({ rate }: { rate: number }): number { return clampRetimeRate({ rate }); diff --git a/apps/web/src/lib/retime/split.ts b/apps/web/src/retime/split.ts similarity index 88% rename from apps/web/src/lib/retime/split.ts rename to apps/web/src/retime/split.ts index 2635590e..1b7360cc 100644 --- a/apps/web/src/lib/retime/split.ts +++ b/apps/web/src/retime/split.ts @@ -1,4 +1,4 @@ -import type { RetimeConfig } from "@/lib/timeline"; +import type { RetimeConfig } from "@/timeline"; import { getSourceTimeAtClipTime } from "./resolve"; export function getSourceSpanAtClipTime({ diff --git a/apps/web/src/lib/ripple/apply.ts b/apps/web/src/ripple/apply.ts similarity index 96% rename from apps/web/src/lib/ripple/apply.ts rename to apps/web/src/ripple/apply.ts index f96e4600..4ad83529 100644 --- a/apps/web/src/lib/ripple/apply.ts +++ b/apps/web/src/ripple/apply.ts @@ -1,4 +1,4 @@ -import type { SceneTracks, TimelineTrack } from "@/lib/timeline/types"; +import type { SceneTracks, TimelineTrack } from "@/timeline/types"; import { rippleShiftElements } from "./shift"; export interface RippleAdjustment { diff --git a/apps/web/src/lib/ripple/diff.ts b/apps/web/src/ripple/diff.ts similarity index 99% rename from apps/web/src/lib/ripple/diff.ts rename to apps/web/src/ripple/diff.ts index 49238738..e450d5ae 100644 --- a/apps/web/src/lib/ripple/diff.ts +++ b/apps/web/src/ripple/diff.ts @@ -1,4 +1,4 @@ -import type { SceneTracks, TimelineElement, TimelineTrack } from "@/lib/timeline/types"; +import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline/types"; import type { RippleAdjustment } from "./apply"; interface Interval { diff --git a/apps/web/src/lib/ripple/index.ts b/apps/web/src/ripple/index.ts similarity index 100% rename from apps/web/src/lib/ripple/index.ts rename to apps/web/src/ripple/index.ts diff --git a/apps/web/src/lib/ripple/shift.ts b/apps/web/src/ripple/shift.ts similarity index 85% rename from apps/web/src/lib/ripple/shift.ts rename to apps/web/src/ripple/shift.ts index 03ab4a67..a88d2b2d 100644 --- a/apps/web/src/lib/ripple/shift.ts +++ b/apps/web/src/ripple/shift.ts @@ -1,4 +1,4 @@ -import type { TimelineElement } from "@/lib/timeline/types"; +import type { TimelineElement } from "@/timeline/types"; export function rippleShiftElements({ elements, diff --git a/apps/web/src/lib/selection/attributes.ts b/apps/web/src/selection/attributes.ts similarity index 100% rename from apps/web/src/lib/selection/attributes.ts rename to apps/web/src/selection/attributes.ts diff --git a/apps/web/src/lib/selection/context.tsx b/apps/web/src/selection/context.tsx similarity index 100% rename from apps/web/src/lib/selection/context.tsx rename to apps/web/src/selection/context.tsx diff --git a/apps/web/src/lib/selection/editor-selection.ts b/apps/web/src/selection/editor-selection.ts similarity index 84% rename from apps/web/src/lib/selection/editor-selection.ts rename to apps/web/src/selection/editor-selection.ts index e9563b97..21ab6466 100644 --- a/apps/web/src/lib/selection/editor-selection.ts +++ b/apps/web/src/selection/editor-selection.ts @@ -1,5 +1,5 @@ -import type { SelectedKeyframeRef } from "@/lib/animation/types"; -import type { ElementRef } from "@/lib/timeline/types"; +import type { SelectedKeyframeRef } from "@/animation/types"; +import type { ElementRef } from "@/timeline/types"; export interface SelectedMaskPointSelection { trackId: string; diff --git a/apps/web/src/lib/selection/hit-testing.ts b/apps/web/src/selection/hit-testing.ts similarity index 100% rename from apps/web/src/lib/selection/hit-testing.ts rename to apps/web/src/selection/hit-testing.ts diff --git a/apps/web/src/lib/selection/hooks/use-box-select.ts b/apps/web/src/selection/hooks/use-box-select.ts similarity index 91% rename from apps/web/src/lib/selection/hooks/use-box-select.ts rename to apps/web/src/selection/hooks/use-box-select.ts index ab8464ec..90a584b5 100644 --- a/apps/web/src/lib/selection/hooks/use-box-select.ts +++ b/apps/web/src/selection/hooks/use-box-select.ts @@ -1,170 +1,170 @@ -"use client"; - -import { useCallback, useEffect, useRef, useState } from "react"; -import type { - BoxSelectionSnapshot, - ResolveIntersections, -} from "@/lib/selection/types"; - -interface SelectionBoxState extends BoxSelectionSnapshot { - startPos: { x: number; y: number }; - currentPos: { x: number; y: number }; - isActive: boolean; - isAdditive: boolean; -} - -export function useBoxSelect({ - containerRef, - resolveIntersections, - selectedIds, - anchorId, - onSelectionChange, - shouldStartSelection, - getIsAdditiveSelection, - isEnabled = true, -}: { - containerRef: React.RefObject; - resolveIntersections: ResolveIntersections; - selectedIds: TId[]; - anchorId: TId | null; - onSelectionChange: (state: { - intersectedIds: TId[]; - initialSelectedIds: TId[]; - initialAnchorId: TId | null; - isAdditive: boolean; - }) => void; - shouldStartSelection?: (event: React.MouseEvent) => boolean; - getIsAdditiveSelection?: (event: React.MouseEvent) => boolean; - isEnabled?: boolean; -}) { - const [selectionBox, setSelectionBox] = useState | null>( - null, - ); - const justFinishedSelectingRef = useRef(false); - const shouldStartSelectionCheck = shouldStartSelection ?? (() => true); - const getIsAdditiveSelectionCheck = - getIsAdditiveSelection ?? - ((event: React.MouseEvent) => event.ctrlKey || event.metaKey); - - const handleMouseDown = useCallback( - (event: React.MouseEvent) => { - const canStartSelection = shouldStartSelectionCheck(event); - if (!isEnabled || event.button !== 0 || !canStartSelection) { - return; - } - - setSelectionBox({ - startPos: { x: event.clientX, y: event.clientY }, - currentPos: { x: event.clientX, y: event.clientY }, - isActive: false, - isAdditive: getIsAdditiveSelectionCheck(event), - initialSelectedIds: selectedIds, - initialAnchorId: anchorId, - }); - }, - [ - anchorId, - getIsAdditiveSelectionCheck, - isEnabled, - selectedIds, - shouldStartSelectionCheck, - ], - ); - - const updateSelection = useCallback( - ({ - startPos, - currentPos, - isAdditive, - initialSelectedIds, - initialAnchorId, - }: SelectionBoxState) => { - const intersectedIds = resolveIntersections({ - startPos, - currentPos, - }); - onSelectionChange({ - intersectedIds, - initialSelectedIds, - initialAnchorId, - isAdditive, - }); - }, - [onSelectionChange, resolveIntersections], - ); - - useEffect(() => { - if (!selectionBox) { - return; - } - - const handleMouseMove = ({ clientX, clientY }: MouseEvent) => { - const deltaX = Math.abs(clientX - selectionBox.startPos.x); - const deltaY = Math.abs(clientY - selectionBox.startPos.y); - const nextSelectionBox = { - ...selectionBox, - currentPos: { x: clientX, y: clientY }, - isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive, - }; - - setSelectionBox(nextSelectionBox); - - if (!nextSelectionBox.isActive) { - return; - } - - updateSelection(nextSelectionBox); - }; - - const handleMouseUp = () => { - if (selectionBox.isActive) { - justFinishedSelectingRef.current = true; - requestAnimationFrame(() => { - justFinishedSelectingRef.current = false; - }); - } - - setSelectionBox(null); - }; - - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); - }; - }, [selectionBox, updateSelection]); - - useEffect(() => { - if (!selectionBox) { - return; - } - - const previousBodyUserSelect = document.body.style.userSelect; - const previousContainerUserSelect = containerRef.current?.style.userSelect ?? ""; - - document.body.style.userSelect = "none"; - if (containerRef.current) { - containerRef.current.style.userSelect = "none"; - } - - return () => { - document.body.style.userSelect = previousBodyUserSelect; - if (containerRef.current) { - containerRef.current.style.userSelect = previousContainerUserSelect; - } - }; - }, [containerRef, selectionBox]); - - const shouldIgnoreClick = useCallback(() => { - return justFinishedSelectingRef.current; - }, []); - - return { - selectionBox, - handleMouseDown, - isSelecting: selectionBox?.isActive ?? false, - shouldIgnoreClick, - }; -} +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import type { + BoxSelectionSnapshot, + ResolveIntersections, +} from "@/selection/types"; + +interface SelectionBoxState extends BoxSelectionSnapshot { + startPos: { x: number; y: number }; + currentPos: { x: number; y: number }; + isActive: boolean; + isAdditive: boolean; +} + +export function useBoxSelect({ + containerRef, + resolveIntersections, + selectedIds, + anchorId, + onSelectionChange, + shouldStartSelection, + getIsAdditiveSelection, + isEnabled = true, +}: { + containerRef: React.RefObject; + resolveIntersections: ResolveIntersections; + selectedIds: TId[]; + anchorId: TId | null; + onSelectionChange: (state: { + intersectedIds: TId[]; + initialSelectedIds: TId[]; + initialAnchorId: TId | null; + isAdditive: boolean; + }) => void; + shouldStartSelection?: (event: React.MouseEvent) => boolean; + getIsAdditiveSelection?: (event: React.MouseEvent) => boolean; + isEnabled?: boolean; +}) { + const [selectionBox, setSelectionBox] = + useState | null>(null); + const justFinishedSelectingRef = useRef(false); + const shouldStartSelectionCheck = shouldStartSelection ?? (() => true); + const getIsAdditiveSelectionCheck = + getIsAdditiveSelection ?? + ((event: React.MouseEvent) => event.ctrlKey || event.metaKey); + + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + const canStartSelection = shouldStartSelectionCheck(event); + if (!isEnabled || event.button !== 0 || !canStartSelection) { + return; + } + + setSelectionBox({ + startPos: { x: event.clientX, y: event.clientY }, + currentPos: { x: event.clientX, y: event.clientY }, + isActive: false, + isAdditive: getIsAdditiveSelectionCheck(event), + initialSelectedIds: selectedIds, + initialAnchorId: anchorId, + }); + }, + [ + anchorId, + getIsAdditiveSelectionCheck, + isEnabled, + selectedIds, + shouldStartSelectionCheck, + ], + ); + + const updateSelection = useCallback( + ({ + startPos, + currentPos, + isAdditive, + initialSelectedIds, + initialAnchorId, + }: SelectionBoxState) => { + const intersectedIds = resolveIntersections({ + startPos, + currentPos, + }); + onSelectionChange({ + intersectedIds, + initialSelectedIds, + initialAnchorId, + isAdditive, + }); + }, + [onSelectionChange, resolveIntersections], + ); + + useEffect(() => { + if (!selectionBox) { + return; + } + + const handleMouseMove = ({ clientX, clientY }: MouseEvent) => { + const deltaX = Math.abs(clientX - selectionBox.startPos.x); + const deltaY = Math.abs(clientY - selectionBox.startPos.y); + const nextSelectionBox = { + ...selectionBox, + currentPos: { x: clientX, y: clientY }, + isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive, + }; + + setSelectionBox(nextSelectionBox); + + if (!nextSelectionBox.isActive) { + return; + } + + updateSelection(nextSelectionBox); + }; + + const handleMouseUp = () => { + if (selectionBox.isActive) { + justFinishedSelectingRef.current = true; + requestAnimationFrame(() => { + justFinishedSelectingRef.current = false; + }); + } + + setSelectionBox(null); + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [selectionBox, updateSelection]); + + useEffect(() => { + if (!selectionBox) { + return; + } + + const previousBodyUserSelect = document.body.style.userSelect; + const previousContainerUserSelect = + containerRef.current?.style.userSelect ?? ""; + + document.body.style.userSelect = "none"; + if (containerRef.current) { + containerRef.current.style.userSelect = "none"; + } + + return () => { + document.body.style.userSelect = previousBodyUserSelect; + if (containerRef.current) { + containerRef.current.style.userSelect = previousContainerUserSelect; + } + }; + }, [containerRef, selectionBox]); + + const shouldIgnoreClick = useCallback(() => { + return justFinishedSelectingRef.current; + }, []); + + return { + selectionBox, + handleMouseDown, + isSelecting: selectionBox?.isActive ?? false, + shouldIgnoreClick, + }; +} diff --git a/apps/web/src/lib/selection/hooks/use-selection-scope.ts b/apps/web/src/selection/hooks/use-selection-scope.ts similarity index 80% rename from apps/web/src/lib/selection/hooks/use-selection-scope.ts rename to apps/web/src/selection/hooks/use-selection-scope.ts index a7090c5d..ad4d2ce8 100644 --- a/apps/web/src/lib/selection/hooks/use-selection-scope.ts +++ b/apps/web/src/selection/hooks/use-selection-scope.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; -import { useSelectionContext } from "@/lib/selection/context"; -import { activateScope, type ScopeEntry } from "@/lib/selection/scope"; +import { useSelectionContext } from "@/selection/context"; +import { activateScope, type ScopeEntry } from "@/selection/scope"; export function useSelectionScope() { const { selectedIds, clearSelection } = useSelectionContext(); diff --git a/apps/web/src/selection/index.ts b/apps/web/src/selection/index.ts new file mode 100644 index 00000000..9d62731b --- /dev/null +++ b/apps/web/src/selection/index.ts @@ -0,0 +1,4 @@ +export { SelectableItem } from "@/selection/selectable-item"; +export { SelectableSurface } from "@/selection/selectable-surface"; +export { useSelection } from "@/selection/context"; +export { useSelectionScope } from "@/selection/hooks/use-selection-scope"; diff --git a/apps/web/src/lib/selection/scope.ts b/apps/web/src/selection/scope.ts similarity index 100% rename from apps/web/src/lib/selection/scope.ts rename to apps/web/src/selection/scope.ts diff --git a/apps/web/src/lib/selection/selectable-item.tsx b/apps/web/src/selection/selectable-item.tsx similarity index 87% rename from apps/web/src/lib/selection/selectable-item.tsx rename to apps/web/src/selection/selectable-item.tsx index 03264753..cc9e1dbf 100644 --- a/apps/web/src/lib/selection/selectable-item.tsx +++ b/apps/web/src/selection/selectable-item.tsx @@ -1,9 +1,9 @@ "use client"; import { forwardRef, useCallback } from "react"; -import { useSelectionContext } from "@/lib/selection/context"; -import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes"; -import type { SelectableItemProps } from "@/lib/selection/types"; +import { useSelectionContext } from "@/selection/context"; +import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes"; +import type { SelectableItemProps } from "@/selection/types"; import { cn } from "@/utils/ui"; function setForwardedRef(ref: React.ForwardedRef, value: T | null) { diff --git a/apps/web/src/lib/selection/selectable-surface.tsx b/apps/web/src/selection/selectable-surface.tsx similarity index 89% rename from apps/web/src/lib/selection/selectable-surface.tsx rename to apps/web/src/selection/selectable-surface.tsx index 012bc486..24166daf 100644 --- a/apps/web/src/lib/selection/selectable-surface.tsx +++ b/apps/web/src/selection/selectable-surface.tsx @@ -1,10 +1,10 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { SelectionBox } from "@/lib/selection/selection-box"; -import { SelectionContext } from "@/lib/selection/context"; -import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes"; -import { resolveElementIntersections } from "@/lib/selection/hit-testing"; +import { SelectionBox } from "@/selection/selection-box"; +import { SelectionContext } from "@/selection/context"; +import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes"; +import { resolveElementIntersections } from "@/selection/hit-testing"; import { applyBoxSelection, clearSelection, @@ -13,12 +13,12 @@ import { replaceSelection, selectRange, toggleSelection, -} from "@/lib/selection/state"; +} from "@/selection/state"; import type { SelectableSurfaceProps, SelectionState, -} from "@/lib/selection/types"; -import { useBoxSelect } from "@/lib/selection/hooks/use-box-select"; +} from "@/selection/types"; +import { useBoxSelect } from "@/selection/hooks/use-box-select"; import { cn } from "@/utils/ui"; export function SelectableSurface({ diff --git a/apps/web/src/lib/selection/selection-box.tsx b/apps/web/src/selection/selection-box.tsx similarity index 100% rename from apps/web/src/lib/selection/selection-box.tsx rename to apps/web/src/selection/selection-box.tsx diff --git a/apps/web/src/lib/selection/state.ts b/apps/web/src/selection/state.ts similarity index 94% rename from apps/web/src/lib/selection/state.ts rename to apps/web/src/selection/state.ts index b0ba3089..b44578a1 100644 --- a/apps/web/src/lib/selection/state.ts +++ b/apps/web/src/selection/state.ts @@ -1,7 +1,7 @@ import type { BoxSelectionChange, SelectionState, -} from "@/lib/selection/types"; +} from "@/selection/types"; function dedupeIds({ ids }: { ids: string[] }) { return [...new Set(ids)]; diff --git a/apps/web/src/lib/selection/types.ts b/apps/web/src/selection/types.ts similarity index 100% rename from apps/web/src/lib/selection/types.ts rename to apps/web/src/selection/types.ts diff --git a/apps/web/src/services/renderer/compositor/frame-descriptor.ts b/apps/web/src/services/renderer/compositor/frame-descriptor.ts index f93eae27..bae9203f 100644 --- a/apps/web/src/services/renderer/compositor/frame-descriptor.ts +++ b/apps/web/src/services/renderer/compositor/frame-descriptor.ts @@ -1,5 +1,5 @@ -import { drawCssBackground } from "@/lib/gradients"; -import { masksRegistry } from "@/lib/masks"; +import { drawCssBackground } from "@/gradients"; +import { masksRegistry } from "@/masks"; import type { AnyBaseNode } from "../nodes/base-node"; import type { CanvasRenderer } from "../canvas-renderer"; import { createOffscreenCanvas } from "../canvas-utils"; @@ -19,7 +19,7 @@ import type { LayerMaskDescriptor, QuadTransformDescriptor, } from "./types"; -import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics"; +import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics"; export type TextureUploadDescriptor = { id: string; diff --git a/apps/web/src/services/renderer/compositor/types.ts b/apps/web/src/services/renderer/compositor/types.ts index 5d9322e8..af5a58c2 100644 --- a/apps/web/src/services/renderer/compositor/types.ts +++ b/apps/web/src/services/renderer/compositor/types.ts @@ -1,5 +1,5 @@ -import type { BlendMode } from "@/lib/rendering"; -import type { EffectPass } from "@/lib/effects/types"; +import type { BlendMode } from "@/rendering"; +import type { EffectPass } from "@/effects/types"; export type FrameDescriptor = { width: number; diff --git a/apps/web/src/services/renderer/effect-preview.ts b/apps/web/src/services/renderer/effect-preview.ts index 06e64d0e..c4cacde0 100644 --- a/apps/web/src/services/renderer/effect-preview.ts +++ b/apps/web/src/services/renderer/effect-preview.ts @@ -1,7 +1,7 @@ import { createOffscreenCanvas } from "./canvas-utils"; -import { effectsRegistry, resolveEffectPasses } from "@/lib/effects"; -import { buildDefaultParamValues } from "@/lib/registry"; -import type { ParamValues } from "@/lib/params"; +import { effectsRegistry, resolveEffectPasses } from "@/effects"; +import { buildDefaultParamValues } from "@/params/registry"; +import type { ParamValues } from "@/params"; import { gpuRenderer } from "./gpu-renderer"; const PREVIEW_SIZE = 160; diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts index c11c3015..c61b6b98 100644 --- a/apps/web/src/services/renderer/gpu-renderer.ts +++ b/apps/web/src/services/renderer/gpu-renderer.ts @@ -3,7 +3,7 @@ import { applyMaskFeather as applyMaskFeatherWasm, initializeGpu, } from "opencut-wasm"; -import type { EffectPass, EffectUniformValue } from "@/lib/effects/types"; +import type { EffectPass, EffectUniformValue } from "@/effects/types"; let gpuAvailable = false; let initPromise: Promise | null = null; diff --git a/apps/web/src/services/renderer/nodes/blur-background-node.ts b/apps/web/src/services/renderer/nodes/blur-background-node.ts index d20e2533..d83c1cf5 100644 --- a/apps/web/src/services/renderer/nodes/blur-background-node.ts +++ b/apps/web/src/services/renderer/nodes/blur-background-node.ts @@ -1,5 +1,5 @@ -import type { EffectPass } from "@/lib/effects/types"; -import type { RetimeConfig } from "@/lib/timeline"; +import type { EffectPass } from "@/effects/types"; +import type { RetimeConfig } from "@/timeline"; import { BaseNode } from "./base-node"; export type BlurBackgroundNodeParams = { diff --git a/apps/web/src/services/renderer/nodes/effect-layer-node.ts b/apps/web/src/services/renderer/nodes/effect-layer-node.ts index d6e70fcf..d1b51fa3 100644 --- a/apps/web/src/services/renderer/nodes/effect-layer-node.ts +++ b/apps/web/src/services/renderer/nodes/effect-layer-node.ts @@ -1,5 +1,5 @@ -import type { EffectPass } from "@/lib/effects/types"; -import type { ParamValues } from "@/lib/params"; +import type { EffectPass } from "@/effects/types"; +import type { ParamValues } from "@/params"; import { BaseNode } from "./base-node"; export type EffectLayerNodeParams = { diff --git a/apps/web/src/services/renderer/nodes/graphic-node.ts b/apps/web/src/services/renderer/nodes/graphic-node.ts index 494d2179..2d022510 100644 --- a/apps/web/src/services/renderer/nodes/graphic-node.ts +++ b/apps/web/src/services/renderer/nodes/graphic-node.ts @@ -3,8 +3,8 @@ import { DEFAULT_GRAPHIC_SOURCE_SIZE, getGraphicDefinition, registerDefaultGraphics, -} from "@/lib/graphics"; -import type { ParamValues } from "@/lib/params"; +} from "@/graphics"; +import type { ParamValues } from "@/params"; import { VisualNode, type ResolvedVisualNodeState, diff --git a/apps/web/src/services/renderer/nodes/sticker-node.ts b/apps/web/src/services/renderer/nodes/sticker-node.ts index 3c155cb6..3d22799e 100644 --- a/apps/web/src/services/renderer/nodes/sticker-node.ts +++ b/apps/web/src/services/renderer/nodes/sticker-node.ts @@ -1,4 +1,4 @@ -import { resolveStickerId } from "@/lib/stickers"; +import { resolveStickerId } from "@/stickers"; import { VisualNode, type ResolvedVisualSourceNodeState, diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts index 86384328..d5755315 100644 --- a/apps/web/src/services/renderer/nodes/text-node.ts +++ b/apps/web/src/services/renderer/nodes/text-node.ts @@ -1,11 +1,11 @@ import { BaseNode } from "./base-node"; -import type { TextElement } from "@/lib/timeline"; -import type { EffectPass } from "@/lib/effects/types"; -import type { Transform } from "@/lib/rendering"; +import type { TextElement } from "@/timeline"; +import type { EffectPass } from "@/effects/types"; +import type { Transform } from "@/rendering"; import { drawMeasuredTextLayout, -} from "@/lib/text/primitives"; -import type { MeasuredTextElement } from "@/lib/text/measure-element"; +} from "@/text/primitives"; +import type { MeasuredTextElement } from "@/text/measure-element"; export type TextNodeParams = TextElement & { canvasCenter: { x: number; y: number }; diff --git a/apps/web/src/services/renderer/nodes/visual-node.ts b/apps/web/src/services/renderer/nodes/visual-node.ts index 010f146e..995e598d 100644 --- a/apps/web/src/services/renderer/nodes/visual-node.ts +++ b/apps/web/src/services/renderer/nodes/visual-node.ts @@ -1,8 +1,8 @@ import { BaseNode } from "./base-node"; -import type { Effect, EffectPass } from "@/lib/effects/types"; -import type { Mask } from "@/lib/masks/types"; -import type { BlendMode, Transform } from "@/lib/rendering"; -import type { RetimeConfig, VisualElement } from "@/lib/timeline"; +import type { Effect, EffectPass } from "@/effects/types"; +import type { Mask } from "@/masks/types"; +import type { BlendMode, Transform } from "@/rendering"; +import type { RetimeConfig, VisualElement } from "@/timeline"; export interface VisualNodeParams { duration: number; diff --git a/apps/web/src/services/renderer/resolve.ts b/apps/web/src/services/renderer/resolve.ts index 3f30f0d5..578c9603 100644 --- a/apps/web/src/services/renderer/resolve.ts +++ b/apps/web/src/services/renderer/resolve.ts @@ -5,20 +5,20 @@ import { resolveGraphicParamsAtTime, resolveOpacityAtTime, resolveTransformAtTime, -} from "@/lib/animation"; -import { resolveEffectParamsAtTime } from "@/lib/animation/effect-param-channel"; +} from "@/animation"; +import { resolveEffectParamsAtTime } from "@/animation/effect-param-channel"; import { buildGaussianBlurPasses, intensityToSigma, -} from "@/lib/effects/definitions/blur"; -import { effectsRegistry, resolveEffectPasses } from "@/lib/effects"; -import type { Effect, EffectPass } from "@/lib/effects/types"; -import { getSourceTimeAtClipTime } from "@/lib/retime"; -import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics"; +} from "@/effects/definitions/blur"; +import { effectsRegistry, resolveEffectPasses } from "@/effects"; +import type { Effect, EffectPass } from "@/effects/types"; +import { getSourceTimeAtClipTime } from "@/retime"; +import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics"; import { getTextMeasurementContext, measureTextElement, -} from "@/lib/text/measure-element"; +} from "@/text/measure-element"; import { videoCache } from "@/services/video-cache/service"; import type { CanvasRenderer } from "./canvas-renderer"; import type { AnyBaseNode } from "./nodes/base-node"; diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts index 47e6139c..e79c62d4 100644 --- a/apps/web/src/services/renderer/scene-builder.ts +++ b/apps/web/src/services/renderer/scene-builder.ts @@ -1,5 +1,5 @@ -import type { SceneTracks, TimelineTrack } from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; +import type { SceneTracks, TimelineTrack } from "@/timeline"; +import type { MediaAsset } from "@/media/types"; import { RootNode } from "./nodes/root-node"; import { VideoNode } from "./nodes/video-node"; import { ImageNode } from "./nodes/image-node"; @@ -10,8 +10,8 @@ import { ColorNode } from "./nodes/color-node"; import { BlurBackgroundNode } from "./nodes/blur-background-node"; import { EffectLayerNode } from "./nodes/effect-layer-node"; import type { AnyBaseNode } from "./nodes/base-node"; -import type { TBackground, TCanvasSize } from "@/lib/project/types"; -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur"; +import type { TBackground, TCanvasSize } from "@/project/types"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; const PREVIEW_MAX_IMAGE_SIZE = 2048; diff --git a/apps/web/src/services/renderer/scene-exporter.ts b/apps/web/src/services/renderer/scene-exporter.ts index b19005cb..1b3267a7 100644 --- a/apps/web/src/services/renderer/scene-exporter.ts +++ b/apps/web/src/services/renderer/scene-exporter.ts @@ -14,10 +14,10 @@ import { } from "mediabunny"; import type { FrameRate } from "opencut-wasm"; import { mediaTimeToSeconds } from "opencut-wasm"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; -import { frameRateToFloat } from "@/lib/fps/utils"; +import { TICKS_PER_SECOND } from "@/wasm"; +import { frameRateToFloat } from "@/fps/utils"; import type { RootNode } from "./nodes/root-node"; -import type { ExportFormat, ExportQuality } from "@/lib/export"; +import type { ExportFormat, ExportQuality } from "@/export"; import { CanvasRenderer } from "./canvas-renderer"; type ExportParams = { diff --git a/apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx similarity index 89% rename from apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx rename to apps/web/src/services/storage/components/storage-persistence-dialog.tsx index 0e854a93..4544ef1d 100644 --- a/apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx +++ b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx @@ -9,7 +9,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useStoragePersistence } from "@/hooks/use-storage-persistence"; +import { useStoragePersistence } from "@/services/storage/use-storage-persistence"; export function StoragePersistenceDialog() { const { showDialog, onConfirm, onDismiss } = useStoragePersistence(); diff --git a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts index 9cea9a2e..6d0c37fb 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; +import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; const DEFAULT_FPS = 30; import type { MediaAssetData } from "@/services/storage/types"; import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2"; diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts index a19269cd..c4b64e50 100644 --- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts @@ -1,6 +1,6 @@ -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; +import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; const DEFAULT_FPS = 30; import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; import type { MediaAssetData } from "@/services/storage/types"; diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts index 36e3d9a8..5f3c01e9 100644 --- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts @@ -1,4 +1,4 @@ -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size"; +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts index 5dcf6feb..3ef6cde9 100644 --- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts @@ -1,6 +1,6 @@ import type { MigrationResult, ProjectRecord } from "./types"; -import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; -import { clampDb } from "@/lib/timeline/audio-state"; +import { VOLUME_DB_MIN } from "@/timeline/audio-constants"; +import { clampDb } from "@/timeline/audio-state"; import { getProjectId, isRecord } from "./utils"; export function transformProjectV17ToV18({ diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts index f5194bda..6c30e5c8 100644 --- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts +++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts @@ -1,4 +1,4 @@ -import { INTENSITY_TO_SIGMA_DIVISOR } from "@/lib/effects/definitions/blur"; +import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts index cde1294e..5e3adc33 100644 --- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts +++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts @@ -1,7 +1,7 @@ import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; const ARBITRARY_FPS_DENOMINATOR = 1_000_000; const STANDARD_FRAME_RATES = [ { value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 }, diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts index 186ae1e9..820b0087 100644 --- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts +++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts @@ -1,4 +1,4 @@ -import { parseCustomMaskPath } from "@/lib/masks/custom-path"; +import { parseCustomMaskPath } from "@/masks/custom-path"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts index 1aea0297..8ef77846 100644 --- a/apps/web/src/services/storage/service.ts +++ b/apps/web/src/services/storage/service.ts @@ -1,6 +1,6 @@ -import type { TProject, TProjectMetadata } from "@/lib/project/types"; -import { getProjectDurationFromScenes } from "@/lib/scenes"; -import type { MediaAsset } from "@/lib/media/types"; +import type { TProject, TProjectMetadata } from "@/project/types"; +import { getProjectDurationFromScenes } from "@/timeline/scenes"; +import type { MediaAsset } from "@/media/types"; import { IndexedDBAdapter } from "./indexeddb-adapter"; import { OPFSAdapter } from "./opfs-adapter"; import { @@ -16,12 +16,12 @@ import type { SerializedProject, SerializedScene, } from "./types"; -import type { SavedSoundsData, SavedSound, SoundEffect } from "@/lib/sounds/types"; +import type { SavedSoundsData, SavedSound, SoundEffect } from "@/sounds/types"; import { migrations, runStorageMigrations, } from "@/services/storage/migrations"; -import type { Bookmark, SceneTracks, TScene } from "@/lib/timeline"; +import type { Bookmark, SceneTracks, TScene } from "@/timeline"; function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] { if (!Array.isArray(raw)) return []; diff --git a/apps/web/src/services/storage/types.ts b/apps/web/src/services/storage/types.ts index 1dcf6545..58c14b49 100644 --- a/apps/web/src/services/storage/types.ts +++ b/apps/web/src/services/storage/types.ts @@ -1,10 +1,10 @@ -import type { MediaType } from "@/lib/media/types"; +import type { MediaType } from "@/media/types"; import type { TProject, TProjectMetadata, TTimelineViewState, -} from "@/lib/project/types"; -import type { TScene } from "@/lib/timeline"; +} from "@/project/types"; +import type { TScene } from "@/timeline"; export interface StorageAdapter { get(key: string): Promise; diff --git a/apps/web/src/hooks/storage/use-local-storage.ts b/apps/web/src/services/storage/use-local-storage.ts similarity index 100% rename from apps/web/src/hooks/storage/use-local-storage.ts rename to apps/web/src/services/storage/use-local-storage.ts diff --git a/apps/web/src/hooks/use-storage-persistence.ts b/apps/web/src/services/storage/use-storage-persistence.ts similarity index 100% rename from apps/web/src/hooks/use-storage-persistence.ts rename to apps/web/src/services/storage/use-storage-persistence.ts diff --git a/apps/web/src/services/transcription/service.ts b/apps/web/src/services/transcription/service.ts index 443a4230..9fa17a87 100644 --- a/apps/web/src/services/transcription/service.ts +++ b/apps/web/src/services/transcription/service.ts @@ -3,11 +3,11 @@ import type { TranscriptionResult, TranscriptionProgress, TranscriptionModelId, -} from "@/lib/transcription/types"; +} from "@/transcription/types"; import { DEFAULT_TRANSCRIPTION_MODEL, TRANSCRIPTION_MODELS, -} from "@/lib/transcription/models"; +} from "@/transcription/models"; import type { WorkerMessage, WorkerResponse } from "./worker"; type ProgressCallback = (progress: TranscriptionProgress) => void; diff --git a/apps/web/src/services/transcription/worker.ts b/apps/web/src/services/transcription/worker.ts index 928b580e..0ced6484 100644 --- a/apps/web/src/services/transcription/worker.ts +++ b/apps/web/src/services/transcription/worker.ts @@ -3,11 +3,11 @@ import { type AutomaticSpeechRecognitionPipeline, type AutomaticSpeechRecognitionOutput, } from "@huggingface/transformers"; -import type { TranscriptionSegment } from "@/lib/transcription/types"; +import type { TranscriptionSegment } from "@/transcription/types"; import { DEFAULT_CHUNK_LENGTH_SECONDS, DEFAULT_STRIDE_SECONDS, -} from "@/lib/transcription/audio"; +} from "@/transcription/audio"; export type WorkerMessage = | { type: "init"; modelId: string } diff --git a/apps/web/src/services/waveform-cache/service.ts b/apps/web/src/services/waveform-cache/service.ts index 3b7d34f3..f37cf98a 100644 --- a/apps/web/src/services/waveform-cache/service.ts +++ b/apps/web/src/services/waveform-cache/service.ts @@ -1,10 +1,10 @@ "use client"; -import { createAudioContext } from "@/lib/media/audio"; +import { createAudioContext } from "@/media/audio"; import { buildSourceWaveformSummary, type SourceWaveformSummary, -} from "@/lib/media/waveform-summary"; +} from "@/media/waveform-summary"; interface GetSourceWaveformSummaryArgs { sourceKey: string; diff --git a/apps/web/src/lib/site/brand.ts b/apps/web/src/site/brand.ts similarity index 100% rename from apps/web/src/lib/site/brand.ts rename to apps/web/src/site/brand.ts diff --git a/apps/web/src/lib/site/external-tools.ts b/apps/web/src/site/external-tools.ts similarity index 100% rename from apps/web/src/lib/site/external-tools.ts rename to apps/web/src/site/external-tools.ts diff --git a/apps/web/src/lib/site/social.ts b/apps/web/src/site/social.ts similarity index 100% rename from apps/web/src/lib/site/social.ts rename to apps/web/src/site/social.ts diff --git a/apps/web/src/lib/site/sponsors.ts b/apps/web/src/site/sponsors.ts similarity index 100% rename from apps/web/src/lib/site/sponsors.ts rename to apps/web/src/site/sponsors.ts diff --git a/apps/web/src/components/editor/panels/assets/views/sounds.tsx b/apps/web/src/sounds/components/assets-view.tsx similarity index 94% rename from apps/web/src/components/editor/panels/assets/views/sounds.tsx rename to apps/web/src/sounds/components/assets-view.tsx index c7a1029c..fc7528c8 100644 --- a/apps/web/src/components/editor/panels/assets/views/sounds.tsx +++ b/apps/web/src/sounds/components/assets-view.tsx @@ -22,9 +22,9 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; -import { useSoundSearch } from "@/hooks/use-sound-search"; -import { useSoundsStore } from "@/stores/sounds-store"; -import type { SavedSound, SoundEffect } from "@/lib/sounds/types"; +import { useSoundSearch } from "@/sounds/use-sound-search"; +import { useSoundsStore } from "@/sounds/sounds-store"; +import type { SavedSound, SoundEffect } from "@/sounds/types"; import { cn } from "@/utils/ui"; import { FavouriteIcon, diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/sounds/sounds-store.ts similarity index 98% rename from apps/web/src/stores/sounds-store.ts rename to apps/web/src/sounds/sounds-store.ts index b4b5772b..b9b3f070 100644 --- a/apps/web/src/stores/sounds-store.ts +++ b/apps/web/src/sounds/sounds-store.ts @@ -1,9 +1,9 @@ import { create } from "zustand"; -import type { SoundEffect, SavedSound } from "@/lib/sounds/types"; +import type { SoundEffect, SavedSound } from "@/sounds/types"; import { storageService } from "@/services/storage/service"; import { toast } from "sonner"; import { EditorCore } from "@/core"; -import { buildLibraryAudioElement } from "@/lib/timeline/element-utils"; +import { buildLibraryAudioElement } from "@/timeline/element-utils"; interface SoundsStore { topSoundEffects: SoundEffect[]; diff --git a/apps/web/src/lib/sounds/types.ts b/apps/web/src/sounds/types.ts similarity index 100% rename from apps/web/src/lib/sounds/types.ts rename to apps/web/src/sounds/types.ts diff --git a/apps/web/src/hooks/use-sound-search.ts b/apps/web/src/sounds/use-sound-search.ts similarity index 93% rename from apps/web/src/hooks/use-sound-search.ts rename to apps/web/src/sounds/use-sound-search.ts index 263a01e2..79bc3110 100644 --- a/apps/web/src/hooks/use-sound-search.ts +++ b/apps/web/src/sounds/use-sound-search.ts @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useSoundsStore } from "@/stores/sounds-store"; +import { useSoundsStore } from "@/sounds/sounds-store"; export function useSoundSearch({ query, diff --git a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx b/apps/web/src/speed/components/speed-tab.tsx similarity index 89% rename from apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx rename to apps/web/src/speed/components/speed-tab.tsx index cb10aac3..2f9231d6 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx +++ b/apps/web/src/speed/components/speed-tab.tsx @@ -1,18 +1,18 @@ import { useRef } from "react"; -import { useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/editor/use-editor"; import { NumberField } from "@/components/ui/number-field"; import { Switch } from "@/components/ui/switch"; import { HugeiconsIcon } from "@hugeicons/react"; import { DashboardSpeed02Icon } from "@hugeicons/core-free-icons"; -import { buildConstantRetime } from "@/lib/retime"; +import { buildConstantRetime } from "@/retime"; import { DEFAULT_RETIME_RATE, MIN_RETIME_RATE, MAX_RETIME_RATE, clampRetimeRate, canMaintainPitch, -} from "@/lib/retime/rate"; -import type { AudioElement, VideoElement } from "@/lib/timeline"; +} from "@/retime/rate"; +import type { AudioElement, VideoElement } from "@/timeline"; import { Section, SectionContent, @@ -21,7 +21,7 @@ import { SectionHeader, SectionTitle, } from "@/components/section"; -import { usePropertyDraft } from "../hooks/use-property-draft"; +import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; import { formatNumberForDisplay, getFractionDigitsForStep, diff --git a/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts b/apps/web/src/stickers/__tests__/sticker-id.test.ts similarity index 100% rename from apps/web/src/lib/stickers/__tests__/sticker-id.test.ts rename to apps/web/src/stickers/__tests__/sticker-id.test.ts diff --git a/apps/web/src/lib/stickers/categories.ts b/apps/web/src/stickers/categories.ts similarity index 100% rename from apps/web/src/lib/stickers/categories.ts rename to apps/web/src/stickers/categories.ts diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/stickers/components/assets-view.tsx similarity index 95% rename from apps/web/src/components/editor/panels/assets/views/stickers.tsx rename to apps/web/src/stickers/components/assets-view.tsx index 318aaccb..aab84c12 100644 --- a/apps/web/src/components/editor/panels/assets/views/stickers.tsx +++ b/apps/web/src/stickers/components/assets-view.tsx @@ -9,22 +9,22 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useEditor } from "@/hooks/use-editor"; -import { resolveStickerIntrinsicSize } from "@/lib/stickers"; +import { useEditor } from "@/editor/use-editor"; +import { resolveStickerIntrinsicSize } from "@/stickers"; import { buildGraphicElement, buildStickerElement, -} from "@/lib/timeline/element-utils"; -import { STICKER_CATEGORIES } from "@/lib/stickers/categories"; -import { getRegionLabel, resolveQueryToRegions } from "@/lib/stickers"; -import { parseShapeStickerId } from "@/lib/stickers/providers/shapes"; -import type { TimelineDragData } from "@/lib/timeline/drag"; +} from "@/timeline/element-utils"; +import { STICKER_CATEGORIES } from "@/stickers/categories"; +import { getRegionLabel, resolveQueryToRegions } from "@/stickers"; +import { parseShapeStickerId } from "@/stickers/providers/shapes"; +import type { TimelineDragData } from "@/timeline/drag"; import type { StickerBrowseSection, StickerCategory, StickerItem as StickerData, -} from "@/lib/stickers"; -import { useStickersStore } from "@/stores/stickers-store"; +} from "@/stickers"; +import { useStickersStore } from "@/stickers/stickers-store"; import { cn } from "@/utils/ui"; import { HappyIcon, diff --git a/apps/web/src/lib/stickers/index.ts b/apps/web/src/stickers/index.ts similarity index 97% rename from apps/web/src/lib/stickers/index.ts rename to apps/web/src/stickers/index.ts index 2d71d097..d41e277d 100644 --- a/apps/web/src/lib/stickers/index.ts +++ b/apps/web/src/stickers/index.ts @@ -1,8 +1,8 @@ import { STICKER_CATEGORIES, -} from "@/lib/stickers/categories"; -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size"; -import type { StickerCategory } from "@/lib/stickers/types"; +} from "@/stickers/categories"; +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; +import type { StickerCategory } from "@/stickers/types"; import { stickersRegistry } from "./registry"; import { resolveStickerId } from "./resolver"; import { registerDefaultStickerProviders } from "./providers"; diff --git a/apps/web/src/lib/stickers/intrinsic-size.ts b/apps/web/src/stickers/intrinsic-size.ts similarity index 100% rename from apps/web/src/lib/stickers/intrinsic-size.ts rename to apps/web/src/stickers/intrinsic-size.ts diff --git a/apps/web/src/lib/stickers/providers/countries-data.ts b/apps/web/src/stickers/providers/countries-data.ts similarity index 100% rename from apps/web/src/lib/stickers/providers/countries-data.ts rename to apps/web/src/stickers/providers/countries-data.ts diff --git a/apps/web/src/lib/stickers/providers/flags.ts b/apps/web/src/stickers/providers/flags.ts similarity index 100% rename from apps/web/src/lib/stickers/providers/flags.ts rename to apps/web/src/stickers/providers/flags.ts diff --git a/apps/web/src/lib/stickers/providers/index.ts b/apps/web/src/stickers/providers/index.ts similarity index 87% rename from apps/web/src/lib/stickers/providers/index.ts rename to apps/web/src/stickers/providers/index.ts index 830a1129..ca23963c 100644 --- a/apps/web/src/lib/stickers/providers/index.ts +++ b/apps/web/src/stickers/providers/index.ts @@ -1,5 +1,5 @@ import { stickersRegistry } from "../registry"; -import type { StickerProvider } from "@/lib/stickers/types"; +import type { StickerProvider } from "@/stickers/types"; import { flagsProvider } from "./flags"; import { logosProvider } from "./logos"; import { shapesProvider } from "./shapes"; diff --git a/apps/web/src/lib/stickers/providers/logos.ts b/apps/web/src/stickers/providers/logos.ts similarity index 100% rename from apps/web/src/lib/stickers/providers/logos.ts rename to apps/web/src/stickers/providers/logos.ts diff --git a/apps/web/src/lib/stickers/providers/shapes.ts b/apps/web/src/stickers/providers/shapes.ts similarity index 97% rename from apps/web/src/lib/stickers/providers/shapes.ts rename to apps/web/src/stickers/providers/shapes.ts index a23f14a7..e3137425 100644 --- a/apps/web/src/lib/stickers/providers/shapes.ts +++ b/apps/web/src/stickers/providers/shapes.ts @@ -1,5 +1,5 @@ -import { buildGraphicPreviewUrl, buildDefaultGraphicInstance, graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; -import type { ParamValues } from "@/lib/params"; +import { buildGraphicPreviewUrl, buildDefaultGraphicInstance, graphicsRegistry, registerDefaultGraphics } from "@/graphics"; +import type { ParamValues } from "@/params"; import { buildStickerId, parseStickerId } from "../sticker-id"; import type { StickerBrowseResult, diff --git a/apps/web/src/lib/stickers/registry.ts b/apps/web/src/stickers/registry.ts similarity index 62% rename from apps/web/src/lib/stickers/registry.ts rename to apps/web/src/stickers/registry.ts index 7148c09d..a7d79df0 100644 --- a/apps/web/src/lib/stickers/registry.ts +++ b/apps/web/src/stickers/registry.ts @@ -1,5 +1,5 @@ -import type { StickerProvider } from "@/lib/stickers/types"; -import { DefinitionRegistry } from "@/lib/registry"; +import type { StickerProvider } from "@/stickers/types"; +import { DefinitionRegistry } from "@/params/registry"; export class StickersRegistry extends DefinitionRegistry { constructor() { diff --git a/apps/web/src/lib/stickers/resolver.ts b/apps/web/src/stickers/resolver.ts similarity index 87% rename from apps/web/src/lib/stickers/resolver.ts rename to apps/web/src/stickers/resolver.ts index 0002e716..a7825bbc 100644 --- a/apps/web/src/lib/stickers/resolver.ts +++ b/apps/web/src/stickers/resolver.ts @@ -1,7 +1,7 @@ import { stickersRegistry } from "./registry"; import { parseStickerId } from "./sticker-id"; import { registerDefaultStickerProviders } from "./providers"; -import type { StickerResolveOptions } from "@/lib/stickers/types"; +import type { StickerResolveOptions } from "@/stickers/types"; export function resolveStickerId({ stickerId, diff --git a/apps/web/src/lib/stickers/sticker-id.ts b/apps/web/src/stickers/sticker-id.ts similarity index 100% rename from apps/web/src/lib/stickers/sticker-id.ts rename to apps/web/src/stickers/sticker-id.ts diff --git a/apps/web/src/stores/stickers-store.ts b/apps/web/src/stickers/stickers-store.ts similarity index 94% rename from apps/web/src/stores/stickers-store.ts rename to apps/web/src/stickers/stickers-store.ts index 191050dd..aacc5a79 100644 --- a/apps/web/src/stores/stickers-store.ts +++ b/apps/web/src/stickers/stickers-store.ts @@ -5,13 +5,13 @@ import { browseCategory, searchAll, searchStickers as searchStickersFromProviders, -} from "@/lib/stickers"; -import type { StickerBrowseResult, StickerSearchResult } from "@/lib/stickers"; -import { STICKER_CATEGORIES } from "@/lib/stickers/categories"; -import type { StickerCategory } from "@/lib/stickers/types"; -import { registerDefaultStickerProviders } from "@/lib/stickers/providers"; -import { stickersRegistry } from "@/lib/stickers/registry"; -import { parseStickerId } from "@/lib/stickers/sticker-id"; +} from "@/stickers"; +import type { StickerBrowseResult, StickerSearchResult } from "@/stickers"; +import { STICKER_CATEGORIES } from "@/stickers/categories"; +import type { StickerCategory } from "@/stickers/types"; +import { registerDefaultStickerProviders } from "@/stickers/providers"; +import { stickersRegistry } from "@/stickers/registry"; +import { parseStickerId } from "@/stickers/sticker-id"; const MAX_RECENT_STICKERS = 50; let browseRequestVersion = 0; diff --git a/apps/web/src/lib/stickers/types.ts b/apps/web/src/stickers/types.ts similarity index 94% rename from apps/web/src/lib/stickers/types.ts rename to apps/web/src/stickers/types.ts index bc7dd490..86aa4299 100644 --- a/apps/web/src/lib/stickers/types.ts +++ b/apps/web/src/stickers/types.ts @@ -1,4 +1,4 @@ -import type { STICKER_CATEGORIES } from "@/lib/stickers/categories"; +import type { STICKER_CATEGORIES } from "@/stickers/categories"; export type StickerCategory = keyof typeof STICKER_CATEGORIES; diff --git a/apps/web/src/stores/properties-store.ts b/apps/web/src/stores/properties-store.ts deleted file mode 100644 index f45f176d..00000000 --- a/apps/web/src/stores/properties-store.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { create } from "zustand"; - -interface ClipEffectsTarget { - elementId: string; - trackId: string; -} - -interface PropertiesState { - clipEffectsTarget: ClipEffectsTarget | null; - openClipEffects: ({ elementId, trackId }: ClipEffectsTarget) => void; - closeClipEffects: () => void; -} - -export const usePropertiesStore = create()((set) => ({ - clipEffectsTarget: null, - openClipEffects: ({ elementId, trackId }) => - set({ clipEffectsTarget: { elementId, trackId } }), - closeClipEffects: () => set({ clipEffectsTarget: null }), -})); diff --git a/apps/web/src/lib/subtitles/ass.ts b/apps/web/src/subtitles/ass.ts similarity index 100% rename from apps/web/src/lib/subtitles/ass.ts rename to apps/web/src/subtitles/ass.ts diff --git a/apps/web/src/lib/subtitles/build-subtitle-text-element.ts b/apps/web/src/subtitles/build-subtitle-text-element.ts similarity index 97% rename from apps/web/src/lib/subtitles/build-subtitle-text-element.ts rename to apps/web/src/subtitles/build-subtitle-text-element.ts index 698310a6..c4ac21ac 100644 --- a/apps/web/src/lib/subtitles/build-subtitle-text-element.ts +++ b/apps/web/src/subtitles/build-subtitle-text-element.ts @@ -1,12 +1,12 @@ -import { FONT_SIZE_SCALE_REFERENCE } from "@/lib/text/typography"; +import { FONT_SIZE_SCALE_REFERENCE } from "@/text/typography"; import { getTextVisualRect, measureTextBlock, setCanvasLetterSpacing, -} from "@/lib/text/layout"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; -import type { CreateTextElement } from "@/lib/timeline"; +} from "@/text/layout"; +import { DEFAULTS } from "@/timeline/defaults"; +import { TICKS_PER_SECOND } from "@/wasm"; +import type { CreateTextElement } from "@/timeline"; import type { SubtitleCue, SubtitleStyleOverrides } from "./types"; const SUBTITLE_MAX_WIDTH_RATIO = 0.8; diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/subtitles/components/assets-view.tsx similarity index 92% rename from apps/web/src/components/editor/panels/assets/views/captions.tsx rename to apps/web/src/subtitles/components/assets-view.tsx index 040df749..7e2ddad9 100644 --- a/apps/web/src/components/editor/panels/assets/views/captions.tsx +++ b/apps/web/src/subtitles/components/assets-view.tsx @@ -8,21 +8,21 @@ import { SelectValue, } from "@/components/ui/select"; import { useReducer, useRef, useState } from "react"; -import { extractTimelineAudio } from "@/lib/media/mediabunny"; -import { useEditor } from "@/hooks/use-editor"; -import { TRANSCRIPTION_DIAGNOSTICS_SCOPE } from "@/lib/transcription/diagnostics"; -import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE } from "@/lib/transcription/audio"; -import { TRANSCRIPTION_LANGUAGES } from "@/lib/transcription/supported-languages"; +import { extractTimelineAudio } from "@/media/mediabunny"; +import { useEditor } from "@/editor/use-editor"; +import { TRANSCRIPTION_DIAGNOSTICS_SCOPE } from "@/transcription/diagnostics"; +import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE } from "@/transcription/audio"; +import { TRANSCRIPTION_LANGUAGES } from "@/transcription/supported-languages"; import type { CaptionChunk, TranscriptionLanguage, TranscriptionProgress, -} from "@/lib/transcription/types"; +} from "@/transcription/types"; import { transcriptionService } from "@/services/transcription/service"; -import { decodeAudioToFloat32 } from "@/lib/media/audio"; -import { buildCaptionChunks } from "@/lib/transcription/caption"; -import { insertCaptionChunksAsTextTrack } from "@/lib/subtitles/insert"; -import { parseSubtitleFile } from "@/lib/subtitles/parse"; +import { decodeAudioToFloat32 } from "@/media/audio"; +import { buildCaptionChunks } from "@/transcription/caption"; +import { insertCaptionChunksAsTextTrack } from "@/subtitles/insert"; +import { parseSubtitleFile } from "@/subtitles/parse"; import { Spinner } from "@/components/ui/spinner"; import { Section, @@ -38,7 +38,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import type { DiagnosticSeverity } from "@/lib/diagnostics/types"; +import type { DiagnosticSeverity } from "@/diagnostics/types"; const DIAGNOSTIC_BUTTON_VARIANT: Record< DiagnosticSeverity, diff --git a/apps/web/src/lib/subtitles/insert.ts b/apps/web/src/subtitles/insert.ts similarity index 97% rename from apps/web/src/lib/subtitles/insert.ts rename to apps/web/src/subtitles/insert.ts index 1bb04fc8..50d6c40b 100644 --- a/apps/web/src/lib/subtitles/insert.ts +++ b/apps/web/src/subtitles/insert.ts @@ -3,7 +3,7 @@ import { AddTrackCommand, BatchCommand, InsertElementCommand, -} from "@/lib/commands"; +} from "@/commands"; import { buildSubtitleTextElement } from "./build-subtitle-text-element"; import type { SubtitleCue } from "./types"; diff --git a/apps/web/src/lib/subtitles/parse.ts b/apps/web/src/subtitles/parse.ts similarity index 100% rename from apps/web/src/lib/subtitles/parse.ts rename to apps/web/src/subtitles/parse.ts diff --git a/apps/web/src/lib/subtitles/srt.ts b/apps/web/src/subtitles/srt.ts similarity index 100% rename from apps/web/src/lib/subtitles/srt.ts rename to apps/web/src/subtitles/srt.ts diff --git a/apps/web/src/lib/subtitles/types.ts b/apps/web/src/subtitles/types.ts similarity index 91% rename from apps/web/src/lib/subtitles/types.ts rename to apps/web/src/subtitles/types.ts index 86f6525d..e4f5db3c 100644 --- a/apps/web/src/lib/subtitles/types.ts +++ b/apps/web/src/subtitles/types.ts @@ -1,5 +1,5 @@ -import type { TextBackground, TextElement } from "@/lib/timeline"; -import type { CaptionChunk } from "@/lib/transcription/types"; +import type { TextBackground, TextElement } from "@/timeline"; +import type { CaptionChunk } from "@/transcription/types"; export interface SubtitlePlacementStyle { verticalAlign?: "top" | "middle" | "bottom"; diff --git a/apps/web/src/lib/text/background.ts b/apps/web/src/text/background.ts similarity index 100% rename from apps/web/src/lib/text/background.ts rename to apps/web/src/text/background.ts diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/text/components/assets-view.tsx similarity index 84% rename from apps/web/src/components/editor/panels/assets/views/text.tsx rename to apps/web/src/text/components/assets-view.tsx index da295da3..25a49893 100644 --- a/apps/web/src/components/editor/panels/assets/views/text.tsx +++ b/apps/web/src/text/components/assets-view.tsx @@ -1,8 +1,8 @@ import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; -import { useEditor } from "@/hooks/use-editor"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { buildTextElement } from "@/lib/timeline/element-utils"; +import { useEditor } from "@/editor/use-editor"; +import { DEFAULTS } from "@/timeline/defaults"; +import { buildTextElement } from "@/timeline/element-utils"; export function TextView() { const editor = useEditor(); diff --git a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx b/apps/web/src/text/components/text-tab.tsx similarity index 95% rename from apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx rename to apps/web/src/text/components/text-tab.tsx index d37379ea..bf14efba 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx +++ b/apps/web/src/text/components/text-tab.tsx @@ -2,7 +2,7 @@ import { Textarea } from "@/components/ui/textarea"; import { FontPicker } from "@/components/ui/font-picker"; -import type { TextElement } from "@/lib/timeline"; +import type { TextElement } from "@/timeline"; import { NumberField } from "@/components/ui/number-field"; import { useRef } from "react"; import { @@ -17,23 +17,20 @@ import { ColorPicker } from "@/components/ui/color-picker"; import { Button } from "@/components/ui/button"; import { uppercase } from "@/utils/string"; import { clamp, formatNumberForDisplay } from "@/utils/math"; -import { useEditor } from "@/hooks/use-editor"; -import { - CORNER_RADIUS_MAX, - CORNER_RADIUS_MIN, -} from "@/lib/text/background"; +import { useEditor } from "@/editor/use-editor"; +import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN } from "@/text/background"; import { DEFAULT_TEXT_COLOR, MAX_FONT_SIZE, MIN_FONT_SIZE, -} from "@/lib/text/typography"; -import { usePropertyDraft } from "../hooks/use-property-draft"; -import { useKeyframedColorProperty } from "../hooks/use-keyframed-color-property"; -import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; -import { useElementPlayhead } from "../hooks/use-element-playhead"; -import { KeyframeToggle } from "../components/keyframe-toggle"; -import { isPropertyAtDefault } from "./transform-tab"; -import { resolveColorAtTime, resolveNumberAtTime } from "@/lib/animation"; +} from "@/text/typography"; +import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; +import { useKeyframedColorProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-color-property"; +import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property"; +import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead"; +import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle"; +import { isPropertyAtDefault } from "@/rendering/components/transform-tab"; +import { resolveColorAtTime, resolveNumberAtTime } from "@/animation"; import { HugeiconsIcon } from "@hugeicons/react"; import { MinusSignIcon, @@ -41,7 +38,7 @@ import { TextFontIcon, } from "@hugeicons/core-free-icons"; import { OcTextHeightIcon, OcTextWidthIcon } from "@/components/icons"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import { DEFAULTS } from "@/timeline/defaults"; import { cn } from "@/utils/ui"; export function TextTab({ diff --git a/apps/web/src/lib/text/layout.ts b/apps/web/src/text/layout.ts similarity index 97% rename from apps/web/src/lib/text/layout.ts rename to apps/web/src/text/layout.ts index a3badfdc..aabf5ca9 100644 --- a/apps/web/src/lib/text/layout.ts +++ b/apps/web/src/text/layout.ts @@ -1,5 +1,5 @@ -import type { TextBackground, TextElement } from "@/lib/timeline"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import type { TextBackground, TextElement } from "@/timeline"; +import { DEFAULTS } from "@/timeline/defaults"; type TextRect = { left: number; diff --git a/apps/web/src/lib/text/measure-element.ts b/apps/web/src/text/measure-element.ts similarity index 93% rename from apps/web/src/lib/text/measure-element.ts rename to apps/web/src/text/measure-element.ts index 0b107587..3a49696f 100644 --- a/apps/web/src/lib/text/measure-element.ts +++ b/apps/web/src/text/measure-element.ts @@ -1,7 +1,7 @@ -import { CORNER_RADIUS_MIN } from "@/lib/text/background"; -import { resolveNumberAtTime } from "@/lib/animation"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import type { TextBackground, TextElement } from "@/lib/timeline"; +import { CORNER_RADIUS_MIN } from "@/text/background"; +import { resolveNumberAtTime } from "@/animation"; +import { DEFAULTS } from "@/timeline/defaults"; +import type { TextBackground, TextElement } from "@/timeline"; import { getTextVisualRect, } from "./layout"; diff --git a/apps/web/src/lib/text/primitives.ts b/apps/web/src/text/primitives.ts similarity index 98% rename from apps/web/src/lib/text/primitives.ts rename to apps/web/src/text/primitives.ts index 20674c3c..9a9262a4 100644 --- a/apps/web/src/lib/text/primitives.ts +++ b/apps/web/src/text/primitives.ts @@ -1,5 +1,5 @@ -import type { TextCanvasContext, TextBlockMeasurement } from "@/lib/text/layout"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import type { TextCanvasContext, TextBlockMeasurement } from "@/text/layout"; +import { DEFAULTS } from "@/timeline/defaults"; import { clamp } from "@/utils/math"; import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN } from "./background"; import { diff --git a/apps/web/src/lib/text/typography.ts b/apps/web/src/text/typography.ts similarity index 100% rename from apps/web/src/lib/text/typography.ts rename to apps/web/src/text/typography.ts diff --git a/apps/web/src/lib/timeline/audio-constants.ts b/apps/web/src/timeline/audio-constants.ts similarity index 100% rename from apps/web/src/lib/timeline/audio-constants.ts rename to apps/web/src/timeline/audio-constants.ts diff --git a/apps/web/src/lib/timeline/audio-display.ts b/apps/web/src/timeline/audio-display.ts similarity index 100% rename from apps/web/src/lib/timeline/audio-display.ts rename to apps/web/src/timeline/audio-display.ts diff --git a/apps/web/src/lib/timeline/audio-separation/index.ts b/apps/web/src/timeline/audio-separation/index.ts similarity index 93% rename from apps/web/src/lib/timeline/audio-separation/index.ts rename to apps/web/src/timeline/audio-separation/index.ts index 849dbda3..beb488de 100644 --- a/apps/web/src/lib/timeline/audio-separation/index.ts +++ b/apps/web/src/timeline/audio-separation/index.ts @@ -1,7 +1,7 @@ -import { cloneAnimations } from "@/lib/animation"; -import type { ElementAnimations } from "@/lib/animation/types"; -import type { MediaAsset } from "@/lib/media/types"; -import { DEFAULTS } from "@/lib/timeline/defaults"; +import { cloneAnimations } from "@/animation"; +import type { ElementAnimations } from "@/animation/types"; +import type { MediaAsset } from "@/media/types"; +import { DEFAULTS } from "@/timeline/defaults"; import type { CreateUploadAudioElement, TimelineElement, diff --git a/apps/web/src/lib/timeline/audio-state.ts b/apps/web/src/timeline/audio-state.ts similarity index 93% rename from apps/web/src/lib/timeline/audio-state.ts rename to apps/web/src/timeline/audio-state.ts index 165ece94..4ef559ea 100644 --- a/apps/web/src/lib/timeline/audio-state.ts +++ b/apps/web/src/timeline/audio-state.ts @@ -1,5 +1,5 @@ -import { hasKeyframesForPath } from "@/lib/animation/keyframe-query"; -import { resolveNumberAtTime } from "@/lib/animation/resolve"; +import { hasKeyframesForPath } from "@/animation/keyframe-query"; +import { resolveNumberAtTime } from "@/animation/resolve"; import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "./audio-constants"; import type { TimelineElement } from "./types"; const DEFAULT_STEP_SECONDS = 1 / 60; @@ -32,7 +32,7 @@ export function hasAnimatedVolume({ }); } -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; export function resolveEffectiveAudioGain({ element, diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/timeline/bookmarks/components/bookmarks.tsx similarity index 95% rename from apps/web/src/components/editor/panels/timeline/bookmarks.tsx rename to apps/web/src/timeline/bookmarks/components/bookmarks.tsx index c432b7d4..20e22c57 100644 --- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx +++ b/apps/web/src/timeline/bookmarks/components/bookmarks.tsx @@ -2,20 +2,18 @@ import { useEffect, useState } from "react"; import type { EditorCore } from "@/core"; -import { useEditor } from "@/hooks/use-editor"; -import type { BookmarkDragState } from "@/hooks/timeline/use-bookmark-drag"; -import { - DEFAULT_TIMELINE_BOOKMARK_COLOR, -} from "./theme"; -import { TIMELINE_BOOKMARK_ROW_HEIGHT_PX } from "./layout"; -import { DEFAULT_FPS } from "@/lib/fps/defaults"; +import { useEditor } from "@/editor/use-editor"; +import type { BookmarkDragState } from "../hooks/use-bookmark-drag"; +import { DEFAULT_TIMELINE_BOOKMARK_COLOR } from "@/timeline/components/theme"; +import { TIMELINE_BOOKMARK_ROW_HEIGHT_PX } from "@/timeline/components/layout"; +import { DEFAULT_FPS } from "@/fps/defaults"; import { snappedSeekTime } from "opencut-wasm"; import { ArrowTurnBackwardIcon, Delete02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import type { Bookmark } from "@/lib/timeline"; +import type { Bookmark } from "@/timeline"; import { Popover, PopoverAnchor, @@ -27,10 +25,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { uppercase } from "@/utils/string"; import { clamp, formatNumberForDisplay } from "@/utils/math"; -import { - timelineTimeToPixels, - timelineTimeToSnappedPixels, -} from "@/lib/timeline"; +import { timelineTimeToPixels, timelineTimeToSnappedPixels } from "@/timeline"; const MIN_BOOKMARK_WIDTH_PX = 2; const BOOKMARK_MARKER_WIDTH_PX = 12; @@ -360,7 +355,10 @@ function BookmarkPopoverContent({ /> {bookmark.color && bookmark.color.replace(/^#/, "").toUpperCase() !== - DEFAULT_TIMELINE_BOOKMARK_COLOR.replace(/^#/, "").toUpperCase() && ( + DEFAULT_TIMELINE_BOOKMARK_COLOR.replace( + /^#/, + "", + ).toUpperCase() && (