diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/editor | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/editor')
| -rw-r--r-- | apps/web/src/editor/cancel-interaction.ts | 24 | ||||
| -rw-r--r-- | apps/web/src/editor/editor-store.ts | 24 | ||||
| -rw-r--r-- | apps/web/src/editor/panel-store.ts | 93 | ||||
| -rw-r--r-- | apps/web/src/editor/use-editor.ts | 60 | ||||
| -rw-r--r-- | apps/web/src/editor/use-menu-preview.ts | 26 |
5 files changed, 227 insertions, 0 deletions
diff --git a/apps/web/src/editor/cancel-interaction.ts b/apps/web/src/editor/cancel-interaction.ts new file mode 100644 index 00000000..297a2062 --- /dev/null +++ b/apps/web/src/editor/cancel-interaction.ts @@ -0,0 +1,24 @@ +type CancelFn = () => void;
+
+const cancellers = new Set<CancelFn>();
+
+export function registerCanceller({ fn }: { fn: CancelFn }): () => void {
+ cancellers.add(fn);
+
+ return () => {
+ cancellers.delete(fn);
+ };
+}
+
+export function cancelInteraction(): boolean {
+ if (cancellers.size === 0) return false;
+
+ const activeCancellers = Array.from(cancellers);
+ cancellers.clear();
+
+ for (const cancel of activeCancellers) {
+ cancel();
+ }
+
+ return true;
+}
diff --git a/apps/web/src/editor/editor-store.ts b/apps/web/src/editor/editor-store.ts new file mode 100644 index 00000000..6b05e8bd --- /dev/null +++ b/apps/web/src/editor/editor-store.ts @@ -0,0 +1,24 @@ +import { create } from "zustand";
+import { DEFAULT_CANVAS_PRESETS } from "@/canvas/sizes";
+import type { TCanvasSize } from "@/project/types";
+
+interface EditorState {
+ isInitializing: boolean;
+ isPanelsReady: boolean;
+ canvasPresets: TCanvasSize[];
+ setInitializing: (loading: boolean) => void;
+ setPanelsReady: (ready: boolean) => void;
+ initializeApp: () => Promise<void>;
+}
+
+export const useEditorStore = create<EditorState>()((set) => ({
+ isInitializing: true,
+ isPanelsReady: false,
+ canvasPresets: DEFAULT_CANVAS_PRESETS,
+ setInitializing: (loading) => set({ isInitializing: loading }),
+ setPanelsReady: (ready) => set({ isPanelsReady: ready }),
+ initializeApp: async () => {
+ set({ isInitializing: true, isPanelsReady: false });
+ set({ isPanelsReady: true, isInitializing: false });
+ },
+}));
diff --git a/apps/web/src/editor/panel-store.ts b/apps/web/src/editor/panel-store.ts new file mode 100644 index 00000000..2cb768a1 --- /dev/null +++ b/apps/web/src/editor/panel-store.ts @@ -0,0 +1,93 @@ +import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { PANEL_CONFIG } from "@/panels/layout";
+
+export interface PanelSizes {
+ tools: number;
+ preview: number;
+ properties: number;
+ mainContent: number;
+ timeline: number;
+}
+
+export type PanelId = keyof PanelSizes;
+
+interface PanelState {
+ panels: PanelSizes;
+ setPanel: (panel: PanelId, size: number) => void;
+ setPanels: (sizes: Partial<PanelSizes>) => void;
+ resetPanels: () => void;
+}
+
+export const usePanelStore = create<PanelState>()(
+ persist(
+ (set) => ({
+ ...PANEL_CONFIG,
+ setPanel: (panel, size) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ [panel]: size,
+ },
+ })),
+ setPanels: (sizes) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ ...sizes,
+ },
+ })),
+ resetPanels: () => set({ ...PANEL_CONFIG }),
+ }),
+ {
+ name: "panel-sizes",
+ version: 2,
+ migrate: (persistedState) => {
+ const state = persistedState as
+ | {
+ panels?: Partial<PanelSizes> | null;
+ toolsPanel?: number;
+ previewPanel?: number;
+ propertiesPanel?: number;
+ mainContent?: number;
+ timeline?: number;
+ tools?: number;
+ preview?: number;
+ properties?: number;
+ }
+ | undefined
+ | null;
+
+ if (!state) return { panels: { ...PANEL_CONFIG.panels } };
+
+ if (state.panels && typeof state.panels === "object") {
+ return {
+ panels: {
+ ...PANEL_CONFIG.panels,
+ ...state.panels,
+ },
+ };
+ }
+
+ return {
+ panels: {
+ tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
+ preview:
+ state.preview ??
+ state.previewPanel ??
+ PANEL_CONFIG.panels.preview,
+ properties:
+ state.properties ??
+ state.propertiesPanel ??
+ PANEL_CONFIG.panels.properties,
+ mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
+ timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
+ },
+ };
+ },
+ partialize: (state) => ({
+ panels: state.panels,
+ }),
+ },
+ ),
+);
diff --git a/apps/web/src/editor/use-editor.ts b/apps/web/src/editor/use-editor.ts new file mode 100644 index 00000000..ce48e036 --- /dev/null +++ b/apps/web/src/editor/use-editor.ts @@ -0,0 +1,60 @@ +import { useCallback, useMemo, useRef, useSyncExternalStore } from "react"; +import { EditorCore } from "@/core"; + +function isShallowEqual(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true; + if (!Array.isArray(a) || !Array.isArray(b)) return false; + if (a.length !== b.length) return false; + return a.every((item, i) => Object.is(item, b[i])); +} + +const subscribeNone = () => () => {}; + +export function useEditor(): EditorCore; +export function useEditor<T>(selector: (editor: EditorCore) => T): T; +export function useEditor<T>( + selector?: (editor: EditorCore) => T, +): EditorCore | T { + const editor = useMemo(() => EditorCore.getInstance(), []); + const selectorRef = useRef(selector); + selectorRef.current = selector; + + const snapshotCacheRef = useRef<unknown>(undefined); + + const subscribeAll = useCallback( + (onChange: () => void) => { + const unsubscribers = [ + editor.playback.subscribe(onChange), + editor.timeline.subscribe(onChange), + editor.scenes.subscribe(onChange), + editor.project.subscribe(onChange), + editor.media.subscribe(onChange), + editor.renderer.subscribe(onChange), + editor.selection.subscribe(onChange), + editor.clipboard.subscribe(onChange), + editor.diagnostics.subscribe(onChange), + ]; + return () => { + unsubscribers.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }, + [editor], + ); + + const getSnapshot = useCallback(() => { + const next = selectorRef.current ? selectorRef.current(editor) : editor; + if (isShallowEqual(snapshotCacheRef.current, next)) { + return snapshotCacheRef.current; + } + snapshotCacheRef.current = next; + return next; + }, [editor]); + + return useSyncExternalStore( + selector ? subscribeAll : subscribeNone, + getSnapshot, + getSnapshot, + ) as EditorCore | T; +} diff --git a/apps/web/src/editor/use-menu-preview.ts b/apps/web/src/editor/use-menu-preview.ts new file mode 100644 index 00000000..ca42124e --- /dev/null +++ b/apps/web/src/editor/use-menu-preview.ts @@ -0,0 +1,26 @@ +import { useRef } from "react";
+import { useEditor } from "@/editor/use-editor";
+
+export function useMenuPreview() {
+ const editor = useEditor();
+ const didCommitRef = useRef(false);
+
+ const discard = () => {
+ if (!didCommitRef.current && editor.timeline.isPreviewActive()) {
+ editor.timeline.discardPreview();
+ }
+ };
+
+ const markCommitted = () => {
+ didCommitRef.current = true;
+ };
+
+ const onOpenChange = (isOpen: boolean) => {
+ if (!isOpen) {
+ discard();
+ didCommitRef.current = false;
+ }
+ };
+
+ return { onPointerLeave: discard, onOpenChange, markCommitted };
+}
|
