From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/editor/cancel-interaction.ts | 24 ++++++++ apps/web/src/editor/editor-store.ts | 24 ++++++++ apps/web/src/editor/panel-store.ts | 93 +++++++++++++++++++++++++++++++ apps/web/src/editor/use-editor.ts | 60 ++++++++++++++++++++ apps/web/src/editor/use-menu-preview.ts | 26 +++++++++ 5 files changed, 227 insertions(+) create mode 100644 apps/web/src/editor/cancel-interaction.ts create mode 100644 apps/web/src/editor/editor-store.ts create mode 100644 apps/web/src/editor/panel-store.ts create mode 100644 apps/web/src/editor/use-editor.ts create mode 100644 apps/web/src/editor/use-menu-preview.ts (limited to 'apps/web/src/editor') 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(); + +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; +} + +export const useEditorStore = create()((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) => void; + resetPanels: () => void; +} + +export const usePanelStore = create()( + 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 | 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(selector: (editor: EditorCore) => T): T; +export function useEditor( + selector?: (editor: EditorCore) => T, +): EditorCore | T { + const editor = useMemo(() => EditorCore.getInstance(), []); + const selectorRef = useRef(selector); + selectorRef.current = selector; + + const snapshotCacheRef = useRef(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 }; +} -- cgit v1.3.1